代码托管与版本控制的实际使用

有很多常用的版本控制系统,如 Git、SVN 等。

我们经常听到的代码托管平台有 GitHub、Gitee、GitLab 等,从他们的名称可以看出,这些平台是基于 Git 这一版本控制系统的。关于 Git 的历史本篇文章不多展开,这里主要介绍结合代码托管平台网站介绍一下 Git 的使用和基本的协作开发。

以实例讲解

接下来我们用实例的方式进行内容的讲解。

假设小 A 要开始编写一本介绍自己校园生活的书。经过一段时间的思考,小 A 构思好了此书大概的章节、目录规划。

接下来,小 A 需要思考自己该使用何种工具和平台来实现这个事情。他希望自己的书能够被更多人看到,因此很可能会通过计算机和互联网技术来实现这一愿望。

固然,小 A 可以采用纸笔的形式写草稿。不过,传统纸张书写的内容进入计算机,还存在数字化这一过程。为了省去这一过程,同时提升自己的计算机能力,小 A 决定全程采用计算机编写内容。

以往,小 A 可能会选择使用 Microsoft Word 之类的办公(排版)软件来书写。不过,经过之前章节的学习,小 A 发现自己也可以使用标记语言来编写文稿,使用一些现有的工具,自己可以方便地构建出现代网页,并且应用任何自己喜欢的样式。

小 A 发现有一款类似 gitbook 的工具 mdBook 比较适合自己的需求。这样的工具可以根据用户提供的描述文档结构的文件,生成文档的目录。小 A 发现书籍的目录结构可以和文件系统中的目录的结构完美的对应起来。

关于这款工具的使用这里不详细介绍。假设小 A 的工作目录为“campus-guide”。下面为小 A 工作目录的结构。

campus-guide
├── book.toml   // 告诉 mdBook 如何生成书籍产物,如格式等可选项
├── book/       // mdBook 生成的网页默认会输出在这个目录
└── src/        // 用来生成书籍的源文件目录
    ├── SUMMARY.md   // 描述目录结构的文件
    ├── chapt-1/     // 存放第 1 章节的目录
    |   ├── 1-A.md
    |   ├── 1-B.md
    |   └── README.md   // 章节开始介绍性的文档
    └── chapt-2/     // 存放第 2 章节的目录
        ├── 2-A.md
        ├── 2-B.md
        └── README.md   // 章节开始介绍性的文档

相应的,小 A 的 SUMMARY.md 可能为如下内容:

# Summary

- [Chapter 1](./chapt-1/README.md)
    - [1-A](./chapt-1/1-A.md)
    - [1-B](./chapt-1/1-B.md)

- [Chapter 2](./chapt-2/README.md)
    - [2-A](./chapt-2/2-A.md)
    - [2-B](./chapt-2/2-B.md)

当然,小 A 也可以使用更为具体的目录名。则对应的 SUMMARY.md 文件可以为:

# Summary

- [基础设施](./basic-facilities/README.md)
    - [南教学楼](./basic-facilities/south-teaching-building.md)
    - [食堂](./basic-facilities/student-dinning-hall.md)

- [食在校园](./dinning-on-campus/README.md)
    - [食堂就餐流程](./dinning-on-campus/dinning-procedure.md)
    - [食堂特色菜品](./dinning-on-campus/specialties.md)

相应的的目录结构:

campus-guide
├── book.toml
├── book/
└── src/
    ├── SUMMARY.md
    ├── basic-facilities/
    |   ├── README.md
    |   ├── south-teaching-building.md
    |   └── student-dinning-hall.md
    └── dinning-on-campus/
        ├── dinning-procedure.md
        ├── README.md
        └── specialties.md

mdBook 可以根据 SUMMARY.md 文件直接生成缺失的目录和文件。因此用户只需写好 SUMMARY.md 即可,执行一次 mdbook build 即可实现目录和文件的新建。

现在目录结构已经完成,小 A 可以开始编写了。完成了一定的内容后,只需要在 campus-guide 目录下运行 mdbook build 即可生成静态网页。(需要将 mdbook 可执行文件所在的目录添加进 PATH 环境变量)

开始工程后,小 A 想用版本快照记录自己编辑的历史,以便可以在自己不满意时回退到之前的版本;此外,由于书籍编写量巨大,小 A 希望和其他人一同编写。以上这两点都可以使用版本控制系统来解决。

如果没有版本控制系统,小 A 可能需要定期将自己的工程打包,存储在一个目录下,并且在需要回退时将这个压缩包解压。这种方法不容易查看每次快照的状态,更不方便比较两次版本之间的差异。此外,小 A 的协作者要想将自己编写的内容和小 A 的汇总,也需要小 A 进行手动的比较和合并,效率较低且容易出错;当协作者的数量增加时,事情会变得更复杂;如果想查看某一内容曾经的编辑者,也几乎是不可能的事。

这里有一个秩事,1998 年,KDE 开始开发自己的浏览器渲染引擎 KHTML,不久,Apple 发现了这一开源项目,并创建了自己的分支 WebKit。根据 KHTML 采用的 LGPL 协议,Apple 必须公布自己修改后的源码。但是 Apple 使用了极其阴险的方式:他们公开的是没有修改记录(版本历史)的最终源码包,并且里面混杂了大量的 Apple 专有代码。这使得 KDE 无法利用 VCS(版本控制系统)摘取有用的代码修改,浪费了大量人力,并且导致 WebKit 的改进无法被 KHTML 上游吸收,最终这两个渲染引擎分道扬镳。

经过一番调查后,小 A 决定使用 Git 管理自己的项目。

获得 Git

用户可以在 Git 官方的下载页面 Git - Downloads - git-scm.com 下载 Git 的二进制版本。

在 Windows 下,安装 Git 会带来一个精简、定制的 MSYS2 环境,其中包含一些基础的 GNU 工具。如果用户已经安装了 MSYS2 环境,也可以尝试使用其中提供的 Git。

Git 自带有一个简易的图形化界面客户端。此外,也有许多 第三方为 Git 设计的图形化客户端。对于一般用户来说,使用客户端可以免去记忆和输入命令的苦恼。

关于 Git 的具体使用,读者可以参考 Git - OI WikiGit 的官方文档Atlassian Git Tutorial,以及 Pro Git 一书。

本地和远程仓库的建立

小 A 可以将项目现有的内容作为自己第一次快照的内容,也可以只选择若干的文件进行提交(commit),并且决定是否将其余的修改也提交到版本控制系统。

好习惯是为每次提交填写“提交信息(commit messgae)”,一般是说明这次提交的目的(相较于上一次的变动)——做了什么,或者说解决(fix)了哪些问题、新增了哪些功能(feat)。

项目中有些内容是小 A 不应该提交到版本控制系统中的。比如,小 A 使用的 mdBook 工具会将构建产物输出在项目同级的 book 目录下。对于使用 Git 的小 A,可以通过在目录下的 .gitognore 文件中标记出需要被版本控制系统忽略的某些目录或某些文件。

用户可能还需要配置 .gitattributes 文件:

git - What is the purpose of text=auto in .gitattributes file? - Stack Overflow

什么是 .gitattributes ? - 知乎 (zhihu.com)

现在,小 A 完成了本地仓库的初始化。而如果要和小 B 一起合作,还需要建立一个“远程仓库”。

对于小 A 这样的普通用户,它可以选择使用现有的、基于 Git 的代码托管平台,比如 GitHubGitLab 或者 BitBucket。注册对应网站的用户后,小 A 可以创建一个属于自己的仓库,也可以创建一个组织(比如“本书编写组”),并将仓库建立在组织名下。

小 A 需要给自己的仓库起一个名字。名称可以是任意的,但为了清晰表示仓库的内容和目的,小 A 决定采用“Campus-Guide-Book”作为仓库的名称。

由于仓库是公开的,为了别人更好的了解仓库中的内容,小 A 可能还需要为自己的项目添加一个 README 文件(自述文件)进行说明。根据对应托管平台的情况,小 A 可以使用托管平台支持的标记语言——如 Markdown——或者纯文本文件,来书写项目的自述文件。

除此之外,为了保护自己的知识产权,小 A 可能还需要为自己的仓库添加许可证(License)。

用户也可以事先在代码托管平台上建立好一个远程仓库。

推送修改

小 A 在本地安装好客户端时,需要进行一定的设置,其中一项就是自己的称谓和电子邮箱。这样,版本控制系统就能记录修改的人员以及对应的改动。

远端仓库建立好时,内部一般是空的,用户可以设定本地仓库对应的远端仓库地址,便可以向远端仓库进行推送(push)。

不过,远端仓库中也可能已经有一些内容,这时建议用户先将远端仓库“clone”到本地,再修改这个仓库。不然,直接从远端仓库的拉取代码到本地仓库,可能会发生冲突,需要手动解决。如果本地有未提交的修改,还需要先将本地仓库中的修改暂存起来。

小 A 要向远端仓库推送修改,需要向远程服务器证明自己的身份。这个过程可以使用用户名和密码,也可以使用 SSH 密钥。远端仓库验证小 A 的身份后,才会允许小 A 的推送。

简单来讲,用户需要使用一定的程序生成 SSH 密钥,在将该密钥添加到远端仓库所在对应平台后,在本地计算机上使用该密钥与服务器建立通信,就能完成用户身份的验证。

参考链接:Using Git with SSH keys - Linux Kamarada

当小 A 在自己的计算机上完成一定阶段的修改后,可以在本地进行“commit(提交)”操作。可以连续进行若干次“commit”,并在确认无误后,将这几次“commit”一并推送到远端仓库(push origin)。

如果小 B 也想向远端仓库提交修改,需要将仓库“clone”到自己的计算机上。此外,还需要远端仓库的所有者小 A 为小 B 分配相应的权限(小 B 也需要在对应的平台上注册账号)。之后,小 B 可以进行修改、提交,并将修改推送到远端仓库。

使用分支

现在,小 A 和小 B 同时拥有对一个仓库的访问权限,二人交替的向远端仓库提交修改。

如果二人修改的内容互不冲突,那么理论上可以向同一个分支提交修改。为了保持与远端仓库最新版本的同步,参与者都要定时执行“pull origin”的操作将代码拉取到本地。

事实上,同时修改的若干方很难保证彼此的修改之间不存在冲突,这会导致每次从远端仓库拉取(pull)更新的时候都需要先解决冲突。此外,这样交替修改所形成的版本时间线也不易于查看和分析。

因此,小 A 和小 B 应该事先约定好分工的部分,并在不同的分支上进行各自的修改,并在需要汇入时从该分支向主分支发起“pull request”,简称“PR”,检查(review)无冲突后,即可将修改合并入主分支。

在汇入主分支时,有几种不同的方式。一般,如果为了保持主分支版本迭代线的整洁,会对待合并分支进行 rebase 操作之后合并。

一般来说,对于一个较为稳定的项目,任何参与者每次针对某个问题的修改,都应新建一个分支进行操作。

比如,每当小 A 或小 B 准备开始一个新的章节时,都可以新建一个分支,并在完成后汇入主分支。

如果小 A 和小 B 的工作之间存在互相引用的情况,则需要实现设计好引用的接口。

外部人员的参与

为了能够帮助更多的人、同时邀请到更多的人参与进来,小 A 将自己在代码托管平台上的仓库设置为了公开仓库。

小 C 对于这个项目很感兴趣。他也想参与进来。

假如小 C 对项目持有问题或建议,可以在“issue”中与项目管理者以及其他用户讨论这一点。在发起一个新的“issue”前,最好搜索是否已经存在类似的“issue”(包括已经解决的)。当“issue”对应的问题解决后,小 C 或者仓库维护者应关闭该“issue”。

假如小 C 想向项目中贡献内容,还应查看项目的参与规则,这一般会在项目根目录的 CONTRIBUTING 文件中注明。如果找不到这样的文件,那么小 C 最好发起一个“issue”来向仓库所有者询问对应的时候情况。

小 C 也应该明白自己的贡献将会以何种形式被使用。这是仓库持有者和参与者都应该考虑的问题。

了解清楚情况后,可以将小 A 的仓库“fork”一份到自己的名下。由于仓库是公开的,任何人都可以访问这个仓库,不需要小 A 的同意。之后,小 C 可以将“fork”得到的这个仓库“clone”到本地,并且建立分支、进行修改、“commit”,并将修改推送到这个自己“fork”得到的远端仓库。

如果小 C 只是想使用小 A 的内容,则可以选择“clone”这个项目,或者从项目的 Release 中下载某个版本的代码或构建产物。而如果要进行修改,因为不具有其远端仓库的写入权限,因此需要先将仓库“fork”一份在自己的名下,并将修改推送到该“fork”得到的远端仓库。

如果小 C 完成了自己的修改,希望将修改合并到小 A 的仓库,则需要在代码托管平台向小 A 的仓库发起“PR”。

当小 A 或其他仓库维护者完成对于小 C 发起的“PR”中的内容的检查(即“review”)后,可以通过小 C 的“PR”请求。如果系统通过比较,发现“PR”来源分支的状态和目标分支的状态存在冲突(即无法完成自动合并),则需要小 A(仓库管理者)或者小 B(请求发起者)解决出现的冲突之后再进行合并。

Wiki

(待补充)

Release

(待补充)

参考