跳到主要内容

Git 使用指南:理解 Git 工作原理

· 阅读需 12 分钟
Josh Cena

这篇文章是从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 的指针。 该项目目前的树状结构如下:

Git tree 1

语言学家小明发现了这个仓库。他对此很感兴趣,但他对小丽还在用古早的 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 FrenchAdd XiaoMing's name to Contributors.txt,而它们的哈希值分别为 C1C2。注意,他不一定是按顺序做出这些改动的,但 Git 把 C2 当作 C1 的继承者,因为它是后来的 commit。现在,XiaoMing/change-output-language 这一分支就指向了 C2 commit。该仓库现在的 Git 树如下:

Git tree 2

此时他的改动还只是本地的——没人能在 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 tree 3

而事实上,以上就是所有 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 树如下:

Git tree 4

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

Git tree 5

但却发现不能直接合并小红的。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:

Git tree 6

此时,小丽、小明、小红三人就成功地完成了一次在 GitHub 上的开源项目合作。

如果想要学习更多 Git 有关的知识,并深入研究它的树状结构,可以访问这个网站:Learn Git Branching

拓展阅读

如果想要了解 pull, push, commit, add, 和 checkout 时具体发生了什么,可以参考以下网站:

除此之外,如果你不喜欢阅读长篇大论,你可以观看下面这个长约 82 分钟的 YouTube 视频:

也可以在这个可视化的页面上尝试各类 Git 指令:

Footnotes

  1. 中文大致意思是“提交”——这也是我不喜欢用中文写作的原因,中英混杂总是难以避免。