Git 版本控制

Git 是一个专业的版本控制软件,既可以管理个人文档,也可以进行团队协作。本文尽可能介绍概念性的原理以便理解,以供有需要的同学们快速查阅。

所谓版本控制,实际上就是阶段性备份。习惯比较好的同学,在进行文档修改时会创建副本,这样,当修改后的版本不好,或者出错、误删,都可以用备份好的副本来继续工作,这就是最简单的版本控制。这种"人肉"版本控制的优点显然是简单,只要有好习惯就可以,不需要使用额外的工具。但是缺点也是显而易见的,一方面是随着修改次数的增加,副本会越来越多使得文件夹显得非常凌乱;另一方面,当需要以前的版本时,仅从副本的文件名很难获得足够的信息来精确定位所需的版本。为了更好地进行版本控制,Git 是一个非常不错的选择。

在使用 Git 之前首先介绍一些基本概念,或许会对理解 Git 的工作流程有所帮助。

在正式介绍 Git 之前,首先简要说明三个相关的概念:

  • 工作区:是能够看到的目录,例如 ~/LearnGit/
  • 暂存区:用于临时保存工作区的修改,一般不可见;
  • 版本库:有时候也叫仓库,是工作目录下的 .git/ 隐藏文件夹,一般不可见。

工作区的文件被改动后,需要先提交到暂存区,暂存区所记录的修改通常需要提供一定的注释或说明才能提交到版本库。一旦文件提交到了版本库,Git 就会生成一个哈希值来标识当前版本。这种哈希值由40位16进制的字符组成,形如 78b6157cdbc031530c24b003fd72d283fab30d44 。通常情况下哈希值差异很大,在不引起矛盾的情况下,我们可以只使用哈希值的前几位来表示相应的版本。

文件提交到版本库后就可以为所欲为了,下面的一系列图片将会帮助理解 Git 的开发流程。

假设工作过程中对文件进行了各种修改,并提交到仓库,提交历史将如下图所示:

Git 提交历史示意图

其中,蓝色的方块代表一次提交,其值就是该提交的哈希值前几位。由于 Git 储存数据的方式,新的提交会指向旧的提交,因而也将较新的提交称为"上游",对应将之前的提交称为"下游",蓝色方块串起来的提交历史就是一个分支。图中标有 master 字样的黄色方块就是分支默认的名字,其实际上是一个指针,指向分支的最新提交。在 Git 中,一个分支就是一个指针,反过来,一个普通的指针就对应一个分支。标有 HEAD 字样的红色方块是一个特殊的指针,它只能指向分支,也就是说,它是指针的指针。HEAD 的位置决定了当前工作目录显示的是哪个提交,也决定了新的提交会在哪个分支。

注意
本文初稿形成时 Git 的默认分支名为 master ,这有一定的历史原因。新版本 Git 提倡使用 main 作为默认分支名,一方面其具有跟明确的含义,另一方面也避免 master/slave 这种略有侮辱性的词语。建议大家遵循新的约定。

为了进一步说明 HEAD 的影响,假设在 97bbf1b 位置出新建了一个名叫 dev 的指针,并让 HEAD 移动到 dev 的位置,如下图所示。

Git 分支切换

此时如果你观察工作目录,你会惊奇地发现"穿越"到了过去:工作目录的样子跟当时 97bbf1b 提交时候的样子一模一样,后面新的修改不见了、添加的文件不见了、删除的文件复活了。但是如果把 HEAD 分支重新移动到 master 的位置,工作空间又会变成 1e08a24 的样子,各种修改又回来了。可见,文件提交到版本库后不会丢失,而工作空间显示的文件结构由 HEAD 所指向的分支决定。

HEAD 还决定新的提交的位置。为了理解这个,可以打一个非常有意思的比方:把文件的提交,或者说仓库的向上游的扩展看作是修路,那么 master 就代表一个没有包工头的施工队,而 HEAD 就是包工头。包工头带领哪个施工队,工程就会在那个施工队的后面延续。而当有多个施工队时,其他的施工队就处于窝工状态。考察下面这种情况,当 HEAD 指向 dev 分支时,当修改的文件提交后,仓库是在 A 处延续还是在 B 处呢?

Git 提交的延拓

答案是在 B 处延续。进一步,由于产生了新的提交,而前面提到代表分支的指针指向最新的提交,又有 HEAD 指向分支的指针,我们就可以得到这样的结论:新的提交产生后,分支指针和 HEAD 指针将同时移动,指向最新的提交。例如在 dev 分支上又做了两次修改,整个历史将如下图所示:

不同分支上的提交

可以看到开发进程在 97bbf1b 处分叉,这也印证了"分支"名字的由来。

在不同分支上进行不同的修改,而后将修改以某种方式整合到主分支上,称之为分支的合并。例如,将上述 dev 分支的提交合并到 master 分支后,整个提交历史如下图所示:

Git 分支合并

刚刚提到"主分支",实际上 Git 中的分支都是平权的,不存在所谓的主分支,只是习惯上会将默认的 master 分支看作主分支。Git 的分支非常适合团队协作,例如,在 97bbf1b 处新建多个分支分配给团队的各个小组:dev 分支进行进一步开发、doc 分支进行帮助和说明文档的编写、debug 分支进行详细的调试等,一轮进行完成后合并到 master 分支,并由此重新生成各个分支进行下一轮开发。

下面简要介绍部分常用命令的使用,当然 Git 的使用不局限于命令行,已有很多的图形界面程序可以使 Git 操作通过点击鼠标完成,这里就不作推荐了。

对于 Windows 系统,从 Git 官方网站下载安装包之后,直接双击即可实现 Git 的安装,一路基本都采用默认设置即可。在安装过程中会出现下面这样选择默认编辑器的页面,由于默认的 vim 编辑器对新手不友好,可以根据习惯另外选择(可能需要另外安装)。

Windows 下 Git 的安装设置

安装完成后,在桌面或者资源浏览器内右键会有 Git Bash Here 的选项,我们打开它进行初始化设置。

初始化设置主要是设置用户名和邮箱,分别运行以下两个命令:

bash

git config --global user.name <username>
git config --global user.email <emailaddress>

其中 <> 内部的命令由用户自己指定,输入时不需要输入尖括号。后面再介绍命令使用时也将采用这种表述。

这两条命令运行后不会出现任何可见的效果,因为 Git 认为简单的最好,而最简单的莫过于什么都不显示了~

为了使用 Git 进行版本控制,需要将工作目录进行初始化,在工作目录下右键点击 Git Bash Here ,输入如下命令即可:

bash

git init

将文件添加到暂存区、再将暂存区所记录的修改提交到版本库,依次使用以下命令(# 是 Git 的注释符):

bash

git add <filename>      ## 将工作区修改到暂存区
git commit              ## 将暂存区修改提交到版本库

若需要将工作目录下的所有修改或新增的文件添加到暂存区,可以使用 . 来代替所有的文件名。提交到版本库时, Git 会根据安装时设置的默认编辑器弹出编辑界面,在其中编写此次提交的备注,保存并关闭后即完成了 commit 命令。编写备注中, # 开头的行将作为注释忽略。一般情况下第一行简要说明此次修改的内容,空一行之后从第三行对此次修改进行详细说明,以便后期需要版本回退时精确定位回退的版本。

初始化仓库时会自从创建名为 master 的分支,若需要创建分支,可以使用这个命令:

bash

git branch <branchname> [<hash>]

上面的中括号表示可选参数,用于指定新分支所指向的提交(用哈希值表示),当留空时默认为当前所在的提交。

移动 HEAD 所指向的分支,可以使用 switch 命令,如下:

bash

git switch <branchname>

将某分支合并到当前分支,采用如下命令:

bash

git merge <branchname>

合并时,若某文件在两个分支不相同,Git 会提示存在冲突,并尝试进行差异对比。所有冲突解决之后会要求编写备注以产生新的提交。

使用时经常需要查看 Git 当前的状态,或者查看当前分支的整个开发历史,可以使用以下命令:

bash

git status          ## 查看当前状态
git log             ## 查看当前分支的历史提交

当历史太多又想掌握全部状态时,可以使用带参数的 log 命令,如本人常用以下命令查看整个仓库最近20次的提交:

bash

git log --all --oneline --graph --decorate -20

而为了简化这个命令,将整个命令取个别名:

bash

git config --global alias.logs 'log --all --oneline --graph --decorate -20'

以后就可以用 logs 这个别名查看 Git 最近20次提交的历史。

收集一些 Git 的使用技巧,看到了就随手记下来,或有用或没用,不定时更新。

随着提交越来越多,仓库体积也会越来越大。对于个人维护的项目,如果想要清理过去所有提交以减小仓库体积,可以使用下面这种 简单粗暴的方法

  1. 创建孤立分支并将当前提交的文件检出到新分支:git checkout --orphan <newBranch>
  2. 添加所有文件:git add .
  3. 提交更改:git commit
  4. 删除原分支:git branch -D master
  5. 重命名新分支:git branch -m master
  6. 强制提交到远程分支:git push -f origin master
警告
这种方法简单粗暴,会不可恢复地删除过往历史,在执行该操作前请做好备份。

可以采取下面的命令将特定版本中的某个文件提取出来,并写入到一个新文件中:

bash

git show <comment-id>:<filename> > <newfilename>        ## 用法
git show 7926ba:spdoc.rst > spdoc.temp.rst              ## 示例

这种方法需要准确知道待提取的文件在对应版本下的名字。对文件夹内的文件,应当以相对路径的形式给出。

如果使用 HTTP 的方式克隆仓库,在每次提交代码到远程仓库时都需要提供帐号和密码,这无疑是个繁琐的操作。为了将密码保存在本地,可以使用 储存凭证 的功能:

bash

git config --global credential.helper store