这篇文章是从C 社的新成员练手 repo 的 README.md 的第一节转移过来的,添加了一些内容,并做了相应的翻译。(原文用英文的原因,应该是 sy 大佬和我都更习惯用英语写技术相关的内容吧……)
添加文件时,你既可以用带图形用户界面(GUI)的 GitHub Desktop,也可以用命令行。你可以从 GUI 入手,但你会有一天意识到命令行功能的强大,开始用它的。另外,Visual Studio Code 的用户们也可以试试其自带的源代码管理工具。
我们觉得有必要给你解释你每一步究竟在做什么,而不是让你机械地重复我们写好的教程。这对你尤其有帮助,因为教程通常都把事情想得很完美,但现实则充满了各种意外和变数。阅读时,我们并不要求你事先懂得任何 Git 操作。
你可以把 Git 理解成一个版本控制系统。它能记录每个文件的创建、更改、和删除,而仓库管理者可以在各个版本(commit1)间自由切换,就好像游戏中的若干存档一样。
基础操作:clone, branch, commit, push, pull request
假设现在 GitHub 上有这样一个叫 Hello
的仓库,是由小丽创建的,装着可能是历史上最著名的一段代码。它的目录长这个样子:
.
├── Hello.cpp
│
├── Contributors.txt
│
└── README.md
// Hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Hello world!" << endl;
}
// Contributors.txt
XiaoLi
// README.md
# Hello world
A piece of C++ code that prints `Hello world!`
你可以把每个 commit 看作是整个仓库的一个快照,一个拷贝。实际上它要比一份完整的拷贝轻量得多,但原理上它保存了那一时刻仓库的全部信息。比如上面的版本就可以看作 C0
commit。(每个 commit 都有一个独有的哈希值,但长到人类无法阅读。因此用 C0
来指代就好了。)我们也管这个 commit 叫 master
分支。一个分支即一系列有线性发展关系的 commit,而 master
分支则作为主分支。实际上,一个分支就是一个指向某个 commit 的指针。 该项目目前的树状结构如下:

语言学家小明发现了这个仓库。他对此很感兴趣,但他对小丽还在用古早的 Hello world!
非常不满,因此决定做点贡献。为此,他需要先把这个文件夹(也就是仓库)下载下来。这基本就是 克隆仓库 (clone the repo) 做的事情。
小明在自己的机器上有了一份完整的拷贝后,就可以像本地项目一样作编辑了。他对三个文件都作了更新:
// Hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Bonjour le monde!" << endl;
}
// Contributors.txt
XiaoLi
XiaoMing
// README.md
# Hello world
A piece of C++ code that prints `Bonjour le monde!`
现在他的直觉告诉他,他需要把这些代码发回 GitHub。要做到这一点,他可以直接提交一个 commit。但这里有个严峻的问题:小丽对小明的行为完全没有控制。 事实上,大多数公开仓库(包括 C 社的)都限制了其他人直接向 master
提交 commit,因为没有哪个环节可以做安全验证。一旦成功 commit,小丽就会惊讶地发现她 GitHub 上的代码变成了法语。并且无论如何,这也是非常恶劣的行为:在任何合作项目中,都永远不要直接向 master 分支提交 commit。(除 非你还有一周就要辞职了) 向 master
提交 commit 还有一个问题,我们马上就会看到。
因此为了解决这一问题,小明 新建了一个分支 (create a branch) ,并把它命名为 XiaoMing/change-output-language
。我们可以认为,小明是在一个和 master
相同但独立的文件夹里工作,而他做出的任何改动都不会影响 master
。这不仅能明确他的目的,确定他分支作者的地位,还能避免冲突和混乱。
现在这些变更都会被记录在 XiaoMing/change-output-language
中。但当他准备 commit 时,他记起了另一条准则:一个 commit 只应该实现一个功能。 重新审视自己的改动,他觉得改变语言和在 Contributors.txt
里添加自己的名字应该是独立的改动。(这里的区别微乎其微,但在实际的项目中还是非常容易判断的。)因此他 上传了两个 commit (make a commit) ,分别命名为 Change output to French
和 Add XiaoMing's name to Contributors.txt
,而它们的哈希值分别为 C1
和 C2
。注意,他不一定是按顺序做出这些改动的,但 Git 把 C2
当作 C1
的继承者,因为它是后来的 commit。现在,XiaoMing/change-output-language
这一分支就指向了 C2
commit。该仓库现在的 Git 树如下:

此时他的改动还只是本地的——没人能在 GitHub 网页上看到它们。因此他接下来 发布了分支并将其推送至源 (publish branch and push to origin) 。这会把 XiaoMing/change-output-language
这一分支及其包含的所有 commit 上传到 GitHub 远程终端。
当他发布了分支之后,其后在该分支上 做出的任何 commit 都会自动被同步到 GitHub 上。
紧接着,他 发布了拉取请求 (create pull request) ,请求代码拥有者(也就是小丽)合并这一分支。分支被合并后,所有的改变都会在 master
中体现出来。
只有一个分支时,事情非常简单,因为此时分支上所有的 commit 都是 master
的继承者,在合并分支时,Git 只需要把所有东西都加到 master
,也就是 C0
上,就可以了。因此,它会移动 master
指针:

而事实上,以上就是所有 Git 新用户需要明白的操作。但作为未来的 GitHub 代码管理者,你应当思考得更深入一点。
解决冲突
为了让事情更有趣一点,我们又同时请来了守旧派小红。她对小丽忘记在 cpp 文件结尾加 return 0;
非常不满,因此决定修正这一问题。同样地,她复制了仓库,在 master
上新建了一个叫 XiaoHong/improve-code-style
的分支,而由于此时小明还没有提交分支,master
仍然指向 C0
。文件改动如下(C3
):
// Hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Hello world!" << endl;
return 0;
}
// Contributors.txt
XiaoLi
XiaoHong
// README.md
# Hello world
A piece of C++ code that prints `Hello world!`
她发布了分支并提交了合并请求。此时,Git 树如下:

小丽午休结束之后,回到 GitHub 页面上,发现多了两个合并请求。她开心地合并了小明的请求:

但却发 现不能直接合并小红的。GitHub 警告说,该分支和 master
间有冲突,因为有同时作出的修改,Git 不知道要留下哪一个,因此需要她手动解决冲突。解决页面有如下的内容:
// Hello.cpp
#include <iostream>
using namespace std;
int main() {
<<<<<<< master
cout << "Bonjour le monde!" << endl;
=======
cout << "Hello world!" << endl;
return 0;
>>>>>>> XiaoHong/improve-code-style
}
// Contributors.txt
XiaoLi
<<<<<<< master
XiaoMing
=======
XiaoHong
>>>>>>> XiaoHong/improve-code-style
注意到 README.md 文件不需要解决冲突,因为只有小明作了修改;Git 还是能意识到这一点的。怎么解决上述问题对小丽这样的人 类还是非常显然的:只要同时留下两人所做的改动即可。因此,她删去了多余的内容,解决了冲突,并合并了小红的分支。文件现在的样子(C4
):
// Hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Bonjour le monde!" << endl;
return 0;
}
// Contributors.txt
XiaoLi
XiaoMing
XiaoHong
// README.md
# Hello world
A piece of C++ code that prints `Bonjour le monde!`
而在合并一个并不是自己的继承者的分支时,Git 会新建一个 commit:

此时,小丽、小明、小红三人就成功地完成了一次在 GitHub 上的开源项目合作。
如果想要学习更多 Git 有关的知识,并深入研究它的树状结构,可以访问这个网站:Learn Git Branching
拓展阅读
如果想要了解 pull
, push
, commit
, add
, 和 checkout
时具体发生了什么,可以参考以下网站:
- Git SCM - Getting Started: Git Basics
- Git SCM - Git Branching: Branches in a Nutshell
- Git SCM - Git Branching: Branching Workflows
- Stack Overflow - What's the difference between HEAD, working tree and index, in Git?
- Understanding Git: Data Model
- Understanding Git: Branching
- Understanding Git: Index
除此之外,如果你不喜欢阅读长篇大论,你可以观看下面这个长约 82 分钟的 YouTube 视频:
也可以在这个可视化的页面上尝试各类 Git 指令:
Footnotes
-
中文大致意思是“提交”——这也是我不喜欢用中文写作的原因,中英混杂总是难以避免。 ↩