版本管理工具Git 的主要特点
2022-07-11石庆冬
石庆冬
(英特尔(中国)有限公司 北京市 100013)
为了代替BitKeeper,Linux 之父Linus Torvalds 在2005年4 月开发出了Git,用于Linux 内核2.6.12 版本的管理。同年12 月,Git 1.0 正式发布。当初,Git 只是作为替代品,用来管理内核代码。目前,Git 已经成为开源软件项目版本管理工具的首选。Git 通常被认为是Global Information Tracker 的缩写,Git 官网的解释是傻瓜式内容跟踪器(the stupid content tracker)。在Linux 内核目录里运行如下命令,可以看见Git 用于内核版本管理的第一次提交:
1 免费并开源
Git 是免费的,在MacOS,Windows 和Linux/Unix 等主流操作系统下,可以任意下载和安装Git。安装包和安装方法详见:
https://git-scm.com/downloads
Git 的源代码是公开的,如果愿意,可以参与到Git 的开发与维护当中。用如下任意一条克隆命令可得到Git 的代码库:
Git 的源代码,就是用Git 做版本控制的。也可以从源代码安装Git,就是先对源代码进行编译然后再安装,这需要相应的编译环境,通常是在Linux 下进行。
2 易学
把Git 学精比较难,但入门很容易,掌握十条左右的Git 基本命令就可以开展工作了。了解Git 的三个区和相应的三个文件状态,可以更容易学习基本命令。用git clone 命令把代码仓库取到本地后,即可进入仓库目录开始修改文件。被修改过的文件处于工作区,状态是已修改(modified);通过git add 命令将修改好的文件放入暂存区,文件的状态变为已暂存(staged);再使用git commit 命令将文件放入交付区(本地仓库),文件的状态变为已提交(committed),如图1。
图1:Git 的三个区
命令git restore --staged
在多数情况下,能接触到的状态是以上三种。文件的状态还可以是未跟踪(untracked),新增(new file),被删除(deleted)和被改名(renamed)等。
打个比方,车间是工作区,成品仓库是交付区,仓库门口是暂存区。暂放在门口的产品经过检查、清点和记录后方可入库。在这个过程中如果发现产品问题,可以退回。
很多时候不需要使用Git 的命令行,通过简单易用的图形界面(GUI)工具就能够完成多数任务。例如,Windows下的SourceTree,Linux 下的gitk 和gitg 等。在各种主流操作系统下常用的Git 图形界面工具在官网有介绍:
https://git-scm.com/downloads/guis/
在命令行模式下,可以执行到Git 所有的命令及其所有的功能。而图形界面工具,从功能上说,是命令行的子集,图形工具不可能把命令行的功能全部实现,真是那样的话,图形工具将被做得非常复杂并且令人眼花缭乱,失去了图形界面让人感觉易学易用的初衷。学会了命令行,再学习相应的GUI 中的功能时,基本上不会有困难,反之就不一定了。对于初学者,命令行和图形界面工具可以一起相辅相成地学习。
3 分布式
版本管理工具分为集中式(Centralized)和分布式(Distributed)两种。比较常见的集中式工具有CVS,Subversion 和Perforce。在集中式的版本管理模式下,有一台机器作为中央服务器,存放版本库的全部内容(分支、标签、修改历史和版本库的相关配置等),服务器的管理员可以轻松掌控每个账户的权限。开发人员工作在处于客户端的机器上,开发者拿到的代码,是按照某种配置规则得到自己权限范围内的某套版本代码的快照。如果开发人员想查看某个文件在另一个分支的内容,或者需要把修改好的文件提交,必须与服务器保持联网。如果网络或者服务器出现故障,开发人员无法提交更新,无法与他人协同工作。如果服务器的磁盘发生损坏并且没有事先备份,很可能某些数据将永远丢失,即使把每位开发人员的快照都拿到、也很难恢复原本的数据库。而分布式则不然。
在分布式模式下,开发人员一旦拿到代码,不是仅提取到最新版本的、或者某种配置规则下的文件快照,而是将整个版本库复制到本地,版本库中所有的分支、标签、版本库从创建到当前所有的修改历史(每次提交的信息)一应俱全。本地机器有了版本库后,开发者可以离线工作,本地机器既是客户机也是服务器。一起协同工作的任何一台机器发生故障,都不用担心数据会丢失,因为这时可以从其他机器得到完整数据库。当然,无论是分布式还是集中式,平时都要做好数据库的备份工作。
如果网络中断,使用Git 的开发者仍然可以继续交互。git bundle 命令可以对一段时间的工作成果进行打包,打包成一个文件,然后用U 盘拷贝给其他人。git bundle 命令也可以针对某个分支或者整个仓库进行打包。拿到包的开发者,用git fetch 命令即可得到需要的内容。还可以使用命令git format-patch 生成补丁文本文件,再将补丁提供给需要的人。开发者拿到补丁后,用git am 命令可以将补丁打在自己的分支上。总之,在分布式模式下,即便是网络中断了,团队的协作开发也不被完全终止。
4 并行开发十分方便
一个Git 仓库在建立之初默认只有主分支master,通过git branch 命令建立多个分支可以实现并行软件开发。如图2,工程师在工作告一段落后,可将本地新增的内容推送到远程的公共仓库,也可以随时把远程仓库的新增内容同步到本地,使用git switch 命令进行分支的切换,就可以看见其他分支的变化。使用git merge 和git rebase 命令可以实现分支之间的合并和代码的同步。
图2:本地仓库与远程仓库
使用git cherry-pick 命令可以直接从其他分支上拿到某次提交的内容应用到当前分支,正如字面意思,该命令用于筛选自己所需的内容。
在并行开发过程中几乎都会遇到代码冲突的问题,而且不可能完全避免。当同一个文件有两个或以上的人修改,或者本地代码长时间保持孤立时,就容易产生冲突。如果同一个项目组的每个人都经常和远程仓库同步,修改同一个模块的人经常在一起走读和相互审核代码,则可以大大降低冲突出现的概率。
GitFlow 工作流定义了一个围绕项目开发的严格分支模型,它为不同的分支分配了明确的角色,并定义分支之间何时以及如何进行交互,分支命名也有规范。master 分支只存放稳定版本的代码,用作软件的正式对外发布,不能直接向master 分支推送,它的内容只能来自其他分支的合并。develop 分支为主开发分支,基于master 分支克隆。release 分支用于软件对内发布并提交测试。feature 分支基于develop 分支克隆,主要用于新需求和新功能的开发。可以有多个feature 分支,因为可能有多个新功能同时开发。hotfix 分支用于修复具体的问题,它的名字一般包含缺陷跟踪号,修复完成并验证后合并到其他种类的分支上。一个项目组遵从统一的分支管理流程,可以提高开发效率并更容易解决或者减少代码的冲突。
5 未完成的工作可以暂存
当文件修改到一半还没有提交到版本库时,如果遇到一个紧急的任务,这时可使用git stash 或者git stash push 命令把未完成的工作现场保存起来(压栈)。等紧急任务完成后,再使用git stash pop 命令恢复刚才的工作现场(出栈)。
暂存的任务可以有多个,用git stash list 命令查看它们,编号从0 开始。git stash pop 命令默认恢复最后一个入栈的任务,也可将任务编号作为参数来恢复指定的任务。用git stash drop 命令丢弃不再需要的任务。
git stash 命令不是必须使用的。当遇到紧急任务时,完全可以建立一个新的目录,在新目录里重新克隆一份代码,建立一个新分支,在新目录里的新分支上完成紧急任务即可。如果希望紧急任务和老任务处在同一个工作目录里,以方便参考或合并,就应该使用git stash 命令。
6 强大的查询功能
在软件的开发过程中,不变的是,代码一直在变,需求随时会变,尤其是多人协同开发的大型软件项目。大开发团队的工程师常常来自不同的部门,不同的城市,甚至不同的国家。某工程师休假归来甚至一觉醒来,有可能发现了大量新代码,这时,查询一下代码的历史对理解新内容很有帮助。
git log 命令的功能十分强大,可以查看一个文件从创建到当前所有的修改记录,包括每一次的修改时间、修改人、提交人、修改内容等。git log 可以按照作者查询,可以查询指定时间段的修改内容,也能针对某个目录或者整个仓库查询。例如,在Linux 内核代码中查询作者为Eric 的最近5 次提交信息:
命令git grep 用于在代码中搜索字符串,可以是具体的字符串,也可以是正则表达式。例如,在Linux 内核代码中列出包含字符串tty_add_file 的文件:
假设发现某软件版本有一个问题,仅从历史信息不容易确定是哪一次提交引起的,这时可使用命令git bisect,该命令使用二分法,可以帮助迅速地定位引起问题的是哪一次提交。
命令git blame 是针对文件的,它可以列出文件每一行的最后一次提交信息,包括SHA1 值,作者名,提交时间,行号,该行的具体内容等。就是说,通过git blame 命令,每一行代码的是谁写的,什么时候写的,一目了然。
git diff命令可用来比较不同分支或不同标签的内容差异,也可以比较工作区和暂存区、暂存区和本地仓库的不同,还可以比较某两次提交的区别。利用git diff命令也可以生成补丁文件。
7 有多个支持Git的托管平台
为防止意外发生,代码仓库一定不能只放在本地机器上。第三方托管平台是一个很好的选择,比较有名的平台有github,gitlab,gitee(码云),coding 和bitbucket 等,网址分别为:https://github.com/;https://about.gitlab.com/;https://gitee.com/;https://coding.net/和https://bitbucket.org/。
使用托管平台可以快速地建立并开展项目,节省了服务器的搭建与维护工作。托管平台不是仅仅提供了存放代码的空间,还可以用来在线编辑文件、查看文件修改历史、软件缺陷跟踪、代码审核、代码合并、版本比较和权限管理等。免费的、收费的、开放给个人的和开放给公司的等各种类型的服务,托管平台都具备,选择适合自己的就好。
托管平台通常以网页的形式使用,也可以安装其客户端。例如,在https://desktop.github.com/ 可以下载github在Windows 上的客户端。Github 还有手机客户端,地址为https://github.com/mobile,安装后,在手机和平板电脑上也可以查看代码。
8 Repo扩展了Git
Linux 内核代码的Git 仓库地址为:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux
内核的代码量很大,但一个Git 仓库已经足够管理它。现在几乎人手一部安卓智能手机,安卓的代码量非常巨大,它是一个多方合作的项目,有很多模块,不同的模块由不同的组织或公司维护。最新的安卓版本包含的Git 仓库超过了1000 个。是否要运行多次git clone 命令才能取到全部的安卓代码?当然不是。安卓的Manifest 是一种xml 格式的文本文件,它包含了相应版本所需的各个仓库的配置信息(实际上Manifest 还包含其他信息,并非简单地把所需仓库的地址堆放在一起)。如下命令可取得安卓12.0.0_r27 版本的Manifest 文件,内含1051 个Git 仓库的信息:
repo init -u https://android.googlesource.com/platform/manifest -b android-12.0.0_r27
再运行repo sync 命令,等待一段时间,即得到该版本的全部代码。Repo 是一个用Python 脚本写的工具,它可以管理多个Git 仓库。这里的一条repo sync 命令,相当于1051 条git clone 命令。可以说,基于Manifest 文件,repo命令扩展了git 命令。
9 权限控制能力弱
使用Git 取代码时,整个仓库都被取到。如果希望仓库的某个分支或某个目录不对特定用户开放,缺省配置是做不到的,Git 自身遵循开源的原则,基本上没有权限控制能力。Git具备钩子(Hook)机制,Hook可以做一些权限控制的事情,但这不是Hook 的主要作用。使用git submodule 命令设置子模块,可以控制子模块的权限。总之,Git 的权限控制能力不强。
Git 通常需要借助其他工具完善权限的控制。例如,Gitlab 把用户分为不同的角色, 有Guest、Reporter、Developer、Maintainer 和Owner。简单地说,Guest(访客)权限最小,只能提交问题和发表评论;Reporter(问题报告人)可以克隆代码;Developer(开发者)可以修改代码;Maintainer(项目管理员)可以改写或删除标签、保护分支、添加项目成员、编辑项目等;Owner(系统管理员)权限最高,可以删除项目,设置账户的角色等。
10 历史可改写
现实生活中已经发生的事情不可能被改变,但Git 可“篡改历史”。如果要舍弃最后一次提交,可以用 git reset --hard HEAD^1 命令。如果想修改最后一次提交信息,可以用带选项--amend 的git commit 命令。带选项-i 的git rebase 命令,可用来拆分、合并、去掉提交信息,或者调整提交顺序。当然,这些动作可能会引起问题,因为代码的修改常常是有继承性的。
git filter-branch 命令可以以脚本的方式修改大量的历史条目。例如,你的电子邮箱变了,如果希望以前的历史记录当中都显示新邮箱,就可以使用这条命令重写历史。
当本地仓库某个分支的历史被改写后,用git push 命令基本上是无法上传到远程仓库的,因为本地分支与远程仓库的相应分支是有冲突的(历史有差别),加上选项-f 可强行推送到远程仓库、并完全覆盖原有的远程分支。
文件的历史可改写,这很灵活,某些人认为是优点,他们的理由是:谁会关心空调说明书的第一版草稿的内容?但是,一般地,选项-f 不推荐使用,毕竟没有尊重历史。业界的建议是,在自己充分满意和充分验证之前,不要推送你的工作成果。
如果发现是某次历史提交引起了新问题,可以针对具体的问题做修改,或者用git revert 命令反向提交来冲抵引发问题的内容(回滚),不应首选“篡改历史”。有的公司或组织,针对特定的分支是禁用强制推送的。
11 基于和支持Git的工具多种多样
代码审核是软件开发过程中的重要一环。Gerrit 是一个免费、开源、建立在Git 之上、基于网页界面的代码审查工具。很多安卓代码使用Gerrit 作为审核工具,工程师修改好的代码不是直接推送到远程仓库,而是先上传到Gerrit 服务器,审查通过后才能进入远程仓库的集成分支。
Git 仓库并非只能用来存储代码,也可以存放二进制文件。二进制文件往往比较大,每改变一次,对于仓库来说会增加很大的体积,动辄就是几个G,仓库的总体积会增加很快。采用Git LFS(Large File Storage)机制,将大文件存放在专门的服务器,在仓库中放置追踪大文件的、只有几行内容的文件文件,相当于保存了指向大文件的指针,大文件有变化时就改变指针信息,避免了仓库的体积增长过快。仅当需要用到大文件时,运行git lfs pull命令,大文件就会被下载。LFS 机制的引入,极大地节省了空间和提高了仓库拉取速度。
很多流行的持续集成和持续交付工具支持Git,或者说能够很好地与Git 配合。例如,Jenkins,TeamCity 和Buildbot 等。
如果项目中有一个库是用SVN(Subversion)管理的,如果你打算使用Git 又不想转变SVN 版本库,可以使用git svn 命令,该命令相当于Git 与SVN 的一个双向桥接。注意,安装完Git 之后默认是没有git svn 命令的,需要单独安装。例如,Ubuntu 系统中的安装命令是sudo apt-get install gitsvn。
随着Python 语言的普及,使用集成开发环境PyCharm的人越来越多,在PyCharm 中安装Git 插件之后,通过PyCharm 就可以直接克隆Python 代码到本地。同样,也可以在Visual Studio 中使用Git,详见https://code.visualstudio.com/Docs/editor/versioncontrol。
Git 的源代码中有一个文件git-completion.bash,把它添加到.bashrc 之后,在Linux 的bash 环境下使用Git 时,命令可以自动补齐。
可以在Java 程序中使用Git,JGit 是Git 版本控制系统的轻量级的纯Java 库实现,包括存储库访问例程、网络协议和核心版本控制算法。在Java 社区中JGit 被广泛使用。
总之,在很多软件工具的使用过程中会发现Git 的“身影”,Git 已经无处不在。
12 命令有高层底层之分
Git 的常用命令大多为“高层命令”,Git 还为用户提供了“底层命令”。普通用户一般只需要知道高层命令,如果想更深入地了解Git,最好学习一下它的底层命令。
Git 是直接记录快照,而非像SVN 那样做文件的差异比较。Git 可以做到时刻保持数据完整性。Git 实际上是一套内容寻址文件系统,采用的是哈希的方式进行存储和查找,也就是说,Git 只是通过简单的存储键值对(key-value)的方式来实现内容寻址的,key 就是文件的哈希值,value 就是经过压缩后的文件内容。下面举例说明两个底层命令的使用。
先产生一个文本文件,内容为test,然后用git hashobject 查看其哈希值:
这时进入目录.git/objects/里面,可以发现一个子目录,子目录的名字就是刚才的哈希值的前两位,子目录里面有个文件,文件名就是该哈希值的后38 位:
带选项-p 的git cat-file 命令可查看哈希值对应的内容,换为选项-t 时显示相应的类型,blob 一般就是指文件:
13 结语
Git 并非完美无瑕,它有优点也有缺点。Git 的命令很多,命令的选项也非常多,一篇文章很难涵盖它的所有特点。平均一个季度左右,Git 官网会有新版本发布,其功能日臻成熟和完善。从事软件相关工作的人员,和程序设计相关专业的在校师生,了解和学习Git 都会受益匪浅。