diff --git a/basic/06-drive-your-computer-2.md b/basic/06-drive-your-computer-2.md index 3af1049..5528f30 100644 --- a/basic/06-drive-your-computer-2.md +++ b/basic/06-drive-your-computer-2.md @@ -4,10 +4,14 @@ 作者:000lbh 状态:未审阅的草稿版本 + +许可:CC BY-NC-SA 3.0 ::: ## 版本控制概览 +你是否正在编写项目,希望反复尝试不同的代码的效果?你是否曾经为了调整功能删除过大段代码,后悔时却发现无法找回?你是否想和其他人合作开发项目,却发现代码难以同步?本章你将学习 Git 这一版本控制系统,可以解决以上问题。 + 版本控制系统(Version Control System, VCS)用来管理和追踪一个软件的源文件版本的系统,同时也可以提供协作、备份等功能。其可以分为中心化和去中心化两种工作方式。 ### 中心化版本控制 @@ -18,13 +22,13 @@ 而去中心化的工作方式中,每个人都有完整的版本记录,可以存在中心服务器用于交换各个客户端的提交,但是即使服务器下线或者不存在,两个人之间也可以通过互相交换信息来完成版本同步。代表作有 Git。请注意,Git 和 GitHub,GitLab 并不是同一个东西,前者是 VCS,后者是使用 Git 作为 VCS 的代码托管平台。 -## Git 的故事和基本抽象 +## Git 的基本理念 -在介绍 Git 使用之前,我们先讲一点故事。当时 Linux 内核开发完全依赖于 Linus 一个人手工检查并合并全世界发来的补丁,这样工作量非常大。于是,Linus 的一个朋友介绍了 BitMover 公司开发的商业 VCS 软件 BitKeeper 免费授权给 Linux 开发团队使用。此举招致了 FSF 的 RMS 等人的批评,认为在自由软件开发中使用非自由软件是“道德上有污点”的行为,但是作为实用主义者的 Linus 并不在意这些事情,BitKeeper 作为去中心化的 VCS,满足了 Linus 的需求。然而好景不长,有 Linux 内核开发者逆向了 BitKeeper 的协议,致使 BitMover 公司决定收回其授权。Git 就是在这种条件下诞生的,据说第一版 Git 是 Linus 利用 1 周休假时间完成的。 +在介绍 Git 基本理念之前,我们先讲一点故事。2002 年以前,Linux 内核开发完全依赖于 Linus 一个人手工检查并合并全世界发来的补丁,这样工作量非常大。于是,Linus 的一个朋友介绍了 BitMover 公司开发的商业 VCS 软件 BitKeeper 免费授权给 Linux 开发团队使用。此举招致了 FSF 的 RMS 等人的批评,认为在自由软件开发中使用非自由软件是“道德上有污点”的行为,但是作为实用主义者的 Linus 并不在意这些事情,BitKeeper 作为去中心化的 VCS,满足了 Linus 的需求。然而好景不长,有 Linux 内核开发者逆向了 BitKeeper 的协议,致使 BitMover 公司在 2005 年决定收回其授权。Git 就是在这种条件下诞生的,据说第一版 Git 是 Linus 利用 1 周休假时间完成的。 -Git 的设计出于这样一种基本抽象:一个项目的历史记录可以被看作是一个有向无环图(DAG),每个提交是一个节点,每个节点有一个或多个父节点,代表这个提交是由哪些提交衍生出来的。Git 的基本操作就是在这个图上进行操作,比如创建新的节点,删除节点,合并节点等等。或许同学们不知道什么叫什么叫有向无环图,我们举个例子:你的家谱就可以类比为一个有向无环图,每个人是一个节点,每个人有父母,父母又有父母,但是不可能有一个人的父母是他自己,这样就构成了一个有向无环图。 +Git 的设计出于这样一种基本抽象:一个项目的历史记录可以被看作是一个有向无环图(DAG),每个提交是一个节点,每个节点有一个或多个父节点,代表这个提交是由哪些提交衍生出来的。Git 的基本操作就是在这个图上进行操作,比如创建新的节点,删除节点,合并节点等等。或许同学们不熟悉有向无环图这个概念,我们举个例子:家谱就可以类比为一个有向无环图,每个人是一个节点,每个人有父母,父母又有父母,但是不可能有一个人的父母是他自己,也不会有一个人的父母是他的后代,这样就构成了一个有向无环图。 -这样的抽象是自然的:你写的东西大概率是基于别人写的东西,你写完之后,又会有其他人基于你的东西写东西,具有继承性。这样的抽象也是实用的,每一次提交(一个节点)都可以看作是一个快照,你可以随时回到这个快照,查看这个快照的内容,或者基于这个快照进行修改。你也能知道目前的状态是如何从过去的状态演变而来的,这样你就可以知道每一次修改的意图,也可以知道每一次修改的后果。 +这样的抽象是自然的:人们写的代码大概率是基于之前写过的一份或多份代码,写完之后,又会有其他人基于这份代码继续开发,具有继承性。这样的抽象也是实用的,每一次提交(一个节点)都可以看作是一个快照,你可以随时回到这个快照,查看这个快照的内容,或者基于这个快照进行修改。你也能知道目前的状态是如何从过去的状态演变而来的,这样你就可以知道每一次修改的意图,也可以知道每一次修改的后果。 对于 Git 来说,有三个目录:工作区(Working Directory),暂存区(Staging Area)和版本库(Repository)。工作区就是你的项目目录,你可以随意改动,直到你决定记录你的修改。版本库是 Git 存储有向无环图的地方。暂存区可能不那么好理解,暂存区是一个缓冲区,你可以把你的修改放到暂存区,然后一次性提交到版本库,差不多就是这样: @@ -352,7 +356,7 @@ git commit -m "add .gitignore" 有时候我们会想同时开发新功能,并且调优以前的代码,这样可能就需要两条线进行开发,此时分支相关的功能就会很有帮助。分支的英文是 branch,其实就像树枝一样,你可以在树干上开出一个新的树枝,然后在这个树枝上进行开发,多条树枝之间不会影响,同时树枝也可以合并到树干上(这个有点不符合自然常理,不过你可以做一些想象)。 -接下来的例子,我们将演示如何创建分支、合并分支,变基分支以及冲突解决。 +接下来的例子,我们将演示如何创建分支、变基分支、合并分支以及冲突解决。 首先我们执行: @@ -429,6 +433,9 @@ git checkout master #### 合并分支与冲突解决 +合并分支是一种比较有意思的操作,因为其会产生一种叫做合并提交的提交。 +合并提交本身的特别性在于,其具有多于一个的父提交,因此可以将两个分支合并到一起。 + 我们将 master 的 HEAD 设置到刚刚 rebase 后的分支的顶部,然后我们新建一个分支: ```shell @@ -445,6 +452,8 @@ git commit -m "Prepare to merge" git merge merge-example ``` +由于合并的两个分支涉及同一行的修改,git 没有办法决定如何应用这些修改,因此需要手动介入解决。 +变基操作也会出现冲突,感兴趣的同学可以尝试一下如何解决。 如果不出意外,你应该看到: ```plain @@ -501,17 +510,43 @@ git clone url://path/to/be/cloned git pull ``` -如果本地有远端不存在的提交,则拉取代码不能以默认的 “fast-forward” 方式进行,因此需要指定 `--no-ff` 参数进行合并拉取或者指定 `--rebase` 进行变基拉取。在特别有必要时,也可以直接 hard reset 到远端 HEAD 处。 +事实上,pull 子命令同时执行了 fetch 然后将当前分支的 HEAD 指针指向远端对应分支的 HEAD 指针。 + +如果本地有远端不存在的提交,则拉取代码不能以默认的 “fast-forward” 方式进行,因此需要指定 `--no-ff` 参数进行合并拉取或者指定 `--rebase` 进行变基拉取。在特别有必要时,也可以直接 hard reset 到远端 HEAD 处,丢弃本地未上传的提交。 + +由于本地的提交可以很方便的进行变基,不用担心散列值重新计算带来的合作上的冲突,建议出现拉取冲突时使用变基的方式解决,而合并会引入不必要的非线性历史,可能会让历史记录不太简洁。 + +::: danger 警告 +再次提醒,请谨慎使用 hard reset。如果你已经弄丢了一些提交,可以通过提交 hash 找回你的提交。 +::: #### 推送代码 在工作完成,提交完成之后,可以用这个子命令将修改推送至远端。若有远端有本地没有的提交,需要先进行拉取,才能推送,或者 `--force` 强制推送,此时不一致的提交会被本地提交代替。 ::: danger 警告 -使用 `--force` 参数前请三思,仔细检查你将要提交的内容! +使用 `--force` 参数前请三思,仔细检查你将要提交的内容!远端可能配置了自动 gc,在这种情况下,你的提交再也无法找回! ::: -#### 图形化工具的使用 +#### 分叉与合并请求/拉取请求 + +如果你没有一个远程仓库的写权限而又想贡献代码,可以考虑通过代码托管平台提供的分叉(fork)功能,分叉一份代码进行开发,分叉的仓库与原来的仓库独立,一般来说,如果原仓库被删除,分叉的仓库会失去与原仓库的联系,但是并不会被删除。一般情况下,在分叉的仓库中进行开发时也不要使用默认分支(一般叫 master 或者 main),部分代码托管平台拒绝分叉中的默认分支的合并请求被自动合并。 + +开发完成之后,你可以发起合并请求(Merge Request, MR)/拉取请求(Pull Request, PR),向原仓库提交你的修改。原仓库的作者可能会经过一系列审查之后,选择合并你的代码,提出修改意见或者拒绝并关闭你的请求,不过一般直接关闭一个合并请求并不是很礼貌,所以原仓库的作者更可能提出拒绝的意见,此时你可以说服他或者自行关闭请求。 + +除了分叉可以发起合并请求/拉取请求,项目仓库内存在的分支也可以发起向其他分支的合并请求。这一般适用于对仓库有写权限的人,为了审查讨论代码修改,避免意外,不直接将修改放入默认分支而是先使用其他分支进行开发,通过合并请求/拉取请求再将代码放入默认分支。 + +如果你是仓库的所有者,你可能会收到合并请求/拉取请求,一般的代码托管平台提供三种合并方式: + +- 合并(merge):创建一个基于提出请求的分支和目标分支的合并提交(见前面合并分支部分的说明) +- 变基(rebase):将提出请求的分支中的相关提交的修改内容依次应用到目标分支上 +- 压缩(squash):将提出请求的分支中的相关提交的修改内容作为一个提交应用到目标分支上 + +请注意,变基的合并方式不会把提出请求的分支中的提交原样纳入到目标分支上,因为变基需要重新计算每个提交的散列值(部分代码托管平台对于可以快进的情况变基不会重新计算散列值),因此基于散列值的内容,比如提交签名,可能会失效。 + +如果需要线性历史,建议不使用合并的请求合并方式。 + +### 图形化工具的使用 1. VSCode @@ -525,6 +560,6 @@ git pull KDE 桌面的 git 管理软件 -#### 附加:对提交签名 +## 结语 -多人合作中,验证你的提交来自于你是很重要的,因此你可以对提交签名以确保这一点(具体操作暂略) +Git 部分的介绍就暂告一段段落了。Git 本身有上百个子命令,本教程肯定无法完全覆盖,很多高级用法自然也没有办法介绍。更多信息可以访问[Git Book](https://git-scm.com/book/en/v2)学习,也可以参考 man 手册。