Git 快速入门与最佳实践

Git/版本控制
405
0
0
2022-09-14
标签   Git基础

Git 快速入门与最佳实践Git 的使用已经成为程序员必会的基本技能之一了。

除了我们使用的 ADD、Commit “常识”命令,还有一些更加实用的:Stash、Rebase、Cherry-Pick、Reset、Revert 等等更高级的命令,充分利用这些能极大的提高我们的生产力。

但是,做程序员这么多年了,你真的会用 Git 么?本次内容分享,我们邀请了 Coding 高级开发工程师谭贺贺从“原理”级别的视角去解读 Git,帮助我们更全面的了解它 ,让它融入我们的开发生活。

以下是分享正文:

我叫谭贺贺,目前在 Coding 主要负责 WebIDE 与 Codeinsight 的开发。我今天带来的主要内容是 Git 的原理与应用。

谈起 Git,大家的第一印象无非是和 SVN 一样的版本控制系统,但其实他们有着非常大的不同,至少 SVN没有像 Git 一样这么多的玩法。

下面我举几个例子,简略的说一下。

1

1.1 搭建博客

阮一峰将写博客的人分成三个阶段:

  • 使用免费空间,比如 CSDN、博客园。
  • 发现免费空间限制太多,于是自己购买域名和空间,搭建独立博客。
  • 独立博客管理太麻烦,最好在保留控制权的前提下,让别人来管,自己负责写文章。

其实第三种阶段指的就是使用 Pages 服务。很多公司比如 Coding、Github 等代码托管平台都推出了 Pages 服务,可以用来搭建个人博客。

Pages 服务不需要复杂的配置,就可以完成博客的搭建。

在使用 Pages 的过程中,通过使用标记语言完成博客的编写,推送到服务器上,就可以看到新发布的博客了。不需要管理服务器,降低了搭建博客的门槛,同时又保持了用户对博客的高度定制权。

1

1.2 写书

很多牛人喜欢写博客,博客写多了,然后汇集起来就出了本书。比如 Matrix67《思考的乐趣》、阮一峰《如何变得有思想》就是这样的例子。

其实出书距离我们也并不遥远,为什么?因为有 Gitbook 这样的服务。

对于 Git + Pages 服务的用户,Gitbook 很容易上手,因为使用 Gitbook 就是使用 Git 与 Markdown。

你完全可以将你 Markdown 的博客复制,汇集起来,形成一本书籍。内容的排版 Gitbook 会帮你做,我们只负责内容就可以了。编写好内容,我们就能立刻获得 HTML、PDF、EPUB、MOBI 四个版本的电子书。这是 HTML 版的预览:

Git 快速入门与最佳实践

在 Gitbook 上有 Explore 频道,上面列出了所有公开的书籍(当然也可以直接搜索)。

Git 快速入门与最佳实践

实际上,除了写书,还可以连同其他人一起进行外文资料的翻译。比如说《The Swift Programming Language》中文版,将英文版分成几个部分,然后在开源项目中由参与者认领翻译,每个人贡献一份自己的力量,这样以非常快的响应速度完成了跟随官方文档更新的操作。

如果你喜欢的一门语言或者技术中文资料缺乏,大家可以发起这样的活动,完成外文资料的翻译。

1

1.3 人才招聘

人才招聘这一块,至今还并没有形成一定的规模。但仍旧有很多的公司选择在代码托管平台上(比如 Coding、Github)上寻找中意的开发者。

有一些开发者看准了这一块,专门开发了这样的网站,比如 Githuber.cn、Github-awards.com。

拿 Githuber 举例,该网站主要提供两个功能,第一个是星榜,说白了就是将将所有用户按照语言分类,然后根据星标数进行排序。

Git 快速入门与最佳实践

我们可以很容易的看到排行榜上前几位的用户,他们的开源项目,这在一定程度上能代表这门语言的发展趋势。比如我对 JAVA 比较感兴趣,然后我看了一下前十名,发现大部分都是 Android 开发,由此可见 Android 开发的火爆程度。当然你也可以看到你的排名,会让你有打怪升级的快感。

第二个功能是搜索,输入筛选条件,搜搜程序员!

Git 快速入门与最佳实践

1

1.4 WebIDE

Coding WebIDE 是 Coding 自主研发的在线集成开发环境 (IDE)。只要你的项目在代码托管平台存放,就可以导入到 WebIDE。之后就可以在线开发。

Git 快速入门与最佳实践

WebIDE 还提供 Web Terminal 功能,用户可以远程操作Docker容器,自由安装喜欢的软件包、方便折腾。

看起来是不是还挺好玩的,如果想把这些都玩转,Git 是肯定要好好学的。接下来,我们就看一下 Git 的基本原理。

2

Git 原理

我们可以现在想一下,如果我们自己来设计,应该怎么设计。传统的设计方案我们可以简单的分成两块:工作目录,远程仓库。

Git 快速入门与最佳实践

但是作为一个目标明确的分布式版本控制系统,首先要做的就是添加一个本地仓库。

Git 快速入门与最佳实践

接着我们选择在工作目录与远程仓库中间加一个缓冲区域,叫做暂存区。

Git 快速入门与最佳实践

加入暂存区的原因有以下几点:

1. 为了能够实现部分提交
2. 为了不在工作区创建状态文件、会污染工作区。
3. 暂存区记录文件的修改时间等信息,提高文件比较的效率。

至此就我们本地而言有三个重要的区域:工作区、暂存区、本地仓库。

接下来我们想一下本地仓库是如何存放项目历史版本。

2

2.1 快照

Git 快速入门与最佳实践

这是项目的三个版本,版本 1 中有两个文件 A 和 B,然后修改了 A,变成了 A1,形成了版本 2,接着又修改了 B 变为 B1,形成了版本 3。

如果我们把项目的每个版本都保存到本地仓库,需要保存至少 6 个文件,而实际上,只有 4 个不同的文件 A、A1、B、B1。为了节省存储的空间,我们要像一个方法将同样的文件只需要保存一份。这就引入了 SHA-1 算法。

可以使用 Git 命令计算文件的 SHA-1 值。

Git 快速入门与最佳实践

SHA-1 算法将文件中的内容通过计算生成一个 40 位的 Hash 值。

SHA-1 算法的特点:

  • 由文件内容计算出的 Hash 值;
  • Hash 值相同,文件内容相同。

对于上图中的内容,无论我们执行多少次,都会得到相同的结果。因此,文件的 SHA-1 值是可以作为文件的唯一 ID 。同时,它还有一个额外的功能,校验文件完整性。

有了 SHA-1 的帮助,我们可以对项目版本的存储方式做一下调整。

Git 快速入门与最佳实践

2

2.1.1 数据库中存储的数据内容

实际上,现在就与 Git 实际存储的结构一致了。我们可以预览一下实际存储在 .Git 下的文件。

Git 快速入门与最佳实践

我们可以看到,在 Objects 目录下,存放了很多文件,他们都使用 SHA-1 的前两位创建了文件夹,剩下的 38 位为文件名。我们先称呼这些文件为 Obj 文件。

对于这么多的 Obj 文件,就保存了我们代码提交的所有记录。对于这些 Obj 文件,其实分为四种类型,分别是 Blob、Tree、Commit、Tag。接下来,我们分别来看一下。

1. Blob

首先 A、A1、B、B1 就是 Blob 类型的 Obj。

Blob: 用来存放项目文件的内容,但是不包括文件的路径、名字、格式等其它描述信息。项目的任意文件的任意版本都是以 Blob 的形式存放的。

2. Tree

Tree 用来表示目录。我们知道项目就是一个目录,目录中有文件、有子目录。因此 Tree 中有 Blob、子 Tree,且都是使用 SHA-1值引用的。这是与目录对应的。从顶层的 Tree 纵览整个树状的结构,叶子结点就是 Blob,表示文件的内容,非叶子结点表示项目的目录,顶层的 Tree 对象就代表了当前项目的快照。

3. Commit

Commit:表示一次提交,有 Parent 字段,用来引用父提交。指向了一个顶层 Tree,表示了项目的快照,还有一些其它的信息,比如上一个提交 Committer、Author、Message 等信息。

2

2.2 暂存区

暂存区是一个文件,路径为: .Git/index

Git 快速入门与最佳实践

它是一个二进制文件,但是我们可以使用命令来查看其中的内容。

这里我们关注第二列和第四列就可以了,第四列是文件名,第二列指的是文件的 Blob。这个 Blob 存放了文件暂存时的内容。

第二列就是 SHA-1 Hash 值,相当于内容的外键,指向了实际存储文件内容的 Blob。第三列是文件的冲突状态,这个后面会讲,第四列是文件的路径名。

我们操作暂存区的场景是这样的,每当编辑好一个或几个文件后,把它加入到暂存区,然后接着修改其他文件,改好后放入暂存区,循环反复。直到修改完毕,最后使用 Commit 命令,将暂存区的内容永久保存到本地仓库。

这个过程其实就是构建项目快照的过程,当我们提交时 Git 会使用暂存区的这些信息生成 Tree 对象,也就是项目快照,永久保存到数据库中。因此也可以说暂存区是用来构建项目快照的区域。

2

2.3 文件状态

有了工作区、暂存区、本地仓库,就可以来定义文件的状态了。

Git 快速入门与最佳实践

文件的状态可以分为两类。一类是暂存区与本地仓库比较得出的状态,另一类是工作区与暂存区比较得出的状态。为什么要分成两类的愿意也很简单,因为第一类状态在提交时,会直接写入本地仓库。而第二种则不会。一个文件可以同时拥有两种状态。

比如一个文件可能既有上面的 Modified 状态,又有下面 Modified 状态,但其实他们表示了不同的状态,Git 会使用绿色和红色把这两中 Modified 状态区分开来。

2

2.4 分支

接下来,看一个很重要的概念,分支。

Git 快速入门与最佳实践

分支的目的是让我们可以并行的进行开发。比如我们当前正在开发功能,但是需要修复一个紧急 BUG,我们不可能在这个项目正在修改的状态下修复 BUG,因为这样会引入更多的 BUG。

有了分支的概念,我们就可以新建一个分支,修复 BUG,使新功能与 BUG 修复同步进行。

分支的实现其实很简单,我们可以先看一下 .Git/HEAD 文件,它保存了当前的分支。

Git 快速入门与最佳实践

其实这个 ref 表示的就是一个分支,它也是一个文件,我们可以继续看一下这个文件的内容:

Git 快速入门与最佳实践

可以看到分支存储了一个 Object,我们可以使用 cat-file 命令继续查看该 Object 的内容。

Git 快速入门与最佳实践

从上面的内容,我们知道了分支指向了一次提交。为什么分支指向一个提交的原因,其实也是 Git 中的分支为什么这么轻量的答案。

因为分支就是指向了一个 Commit 的指针,当我们提交新的 Commit,这个分支的指向只需要跟着更新就可以了,而创建分支仅仅是创建一个指针。

3

3. 高层命令

在 Git 中分为两种类型的命令,一种是完成底层工作的工具集,称为底层命令,另一种是对用户更友好的高层命令。一条高层命令,往往是由多条底层命令组成的。

不知道的人可能一听高层感觉很厉害的样子,其实就是指的是那些我们最常使用的 Git 命令。

3

3.1 Add & Commit

Add 和 Commit 应该可以说是我们使用频率最高的高层命令了。

Git 快速入门与最佳实践

Touch 指的是创建一个文件,代表了我们对项目文件内容的修改,Add 操作是将修改保存到暂存区,Commit 是将暂存区的内容永久保存到本地仓库。

每当将修改的文件加入到暂存区,Git 都会根据文件的内容计算出 SHA-1,并将内容转换成 Blob,写入数据库。然后使用 SHA-1 值更新该列表中的文件项。

在暂存区的文件列表中,每一个文件名,都会对应一个 SHA-1 值,用于指向文件的实际内容。最后提交的那一刻,Git 会将这个列表信息转换为项目的快照,也就是 Tree 对象。写入数据库,并再构建一个 Commit 对象,写入数据库。然后更新分支指向。

3

3.2.1 Conflicts

Git 中的分支十分轻量,因此我们在使用 Git 的时候会频繁的用到分支。不可不免的需要将新创建的分支合并。

在 Git 中合并分支有两种选择:Merge 和 Rebase。但是,无论哪一种,都有可能产生冲突。因此我们先来看一下冲突的产生。

Git 快速入门与最佳实践

图上的情况,并不是移动分支指针就能解决问题的,它需要一种合并策略。首先,我们需要明确的是谁和谁的合并,是 2、3 与 4、5、6 的合并吗?说到分支,我们总会联想到线,就会认为是线的合并。其实不是的,真实合并的是 3 和 6。因为每一次提交都包含了项目完整的快照,即合并只是 Tree 与 Tree 的合并。

我们可以先想一个简单的算法。用来比较 3 和 6。但是我们还需要一个比较的标准,如果只是 3 和 6 比较,那么 3 与 6 相比,添加了一个文件,也可以说成是 6 与 3 比删除了一个文件,这无法确切表示当前的冲突状态。因此我们选取他们的两个分支的分歧点(Merge Base)作为参考点,进行比较。

比较时,相对于 Merge Base(提交1)进行比较。

首先把 1、3、6 中所有的文件做一个列表,然后依次遍历这个列表中的文件。现在我们拿列表中的一个文件进行举例,把在提交 1、3、6 中的该文件分别称为版本1、版本3、版本6。

1. 版本1、版本3、版本6的 SHA-1 值完全相同,这种情况表明没有冲突;

2. 版本3或6至少一个与版本1状态相同(指的是SHA-1值相同或都不存在),这种情况可以自动合并。比如1中存在一个文件,3对该文件进行修改,而6中删除了这个文件,则以6为准就可以了;

3. 版本3或版本6都与版本1的状态不同,情况复杂一些,自动合并策略很难生效,需要手动解决。我们来看一下这种状态的定义。

冲突状态定义:

1 and 3: DELETED_BY_THEM;

1 and 6:DELETED_BY_US;

3 and 6:BOTH_ADDED;

1 and 3 and 6:BOTH_MODIFIED

我们拿第一种情况举例,文件有两种状态 1 和 3,1 表示该文件存在于 Commit 1(也就是MERGE_BASE),3 表示该文件在 Commit 3 (Master 分支)中被修改了没有 6,也就是该文件在 Commit 6(Feature 分支)被删除了,总结来说这种状态就是 DELETED_BY_THEM。

可以再看一下第四种情况,文件有三种状态 1、3、6,1 表示 Commit 1(MERGE_BASE)中存在,3 表示 Commit 3(Master 分支)进行了修改,6 表示(Feature 分支)也进行了修改,总结来说就是 BOTH_MODIFIED(双方修改)。

遇到不可自动合并冲突时,Git 会将这些状态写入到暂存区。与我们讨论不同的是,Git 使用 1、2、3 标记文件,1 表示文件的 Base 版本,2 表示当前的分支的版本,3 表示要合并分支的版本。

3

3.2.2 Merge

在解决完冲突后,我们可以将修改的内容提交为一个新的提交。这就是 Merge。

Git 快速入门与最佳实践

Merge 之后仍可以做出新的提交。

Git 快速入门与最佳实践

可以看到 Merge 是一种不修改分支历史提交记录的方式,这也是我们常用的方式。但是这种方式在某些情况下使用起来不太方便,比如当我们创建了 PR、MR 或者将修改补丁发送给管理者,管理者在合并操作中产生了冲突,还需要去解决冲突,这无疑增加了他人的负担。

使用 Rebase 可以解决这种问题。

3

3.2.3 Rebase

假设我们的分支结构如下:

Rebase 会把从 Merge Base 以来的所有提交,以补丁的形式一个一个重新达到目标分支上。这使得目标分支合并该分支的时候会直接 Fast Forward,即不会产生任何冲突。提交历史是一条线,这对强迫症患者可谓是一大福音。

Git 快速入门与最佳实践

如果我们想要看 Rebase 实际上做了什么,有一个方法,那就是用“慢镜头”来看 Rebase 的整个操作过程。Rebase 提供了交互式选项(参数 -i),我们可以针对每一个 Patch,选择你要进行的操作。

通过这个交互式选项,我们可以“单步调试” Rebase 操作。

经过测试,其实 Rebase 主要在 .Git/Rebase-Merge 下生成了两个文件,分别为 Git-Rebase-todo 和 Done 文件,这两个文件的作用光看名字就可以看得出来。Git-Rebase-todo 存放了 Rebase 将要操作的 Commit。而 Done 存放正在操作或已经操作完毕的 Commit。

比如我们这里,Git-Rebase-todo 存放了 4、5、6,三个提交。

Git 快速入门与最佳实践

首先 Git 将 SHA-1 为 4 的 Commit 放入 Done。表示正在操作 4,然后将 4 以补丁的形式打到 3 上,形成了新的提交 4’。这一步是可能产生冲突的,如果有冲突,需要解决完冲突之后才能继续操作。

Git 快速入门与最佳实践

接着讲 SHA-1 为 5 的提交放入 Done 文件,然后将 5 以补丁的形式打到 4’ 上,形成 5’。

Git 快速入门与最佳实践

再接着将 SHA-1 为 6 的提交放入 Done 文件,然后将 6 以补丁的形式打到 5’ 上,形成 6’。最后移动分支指针,使其指向最新的提交 6’ 上。这就完成了 Rebase 的操作。

Git 快速入门与最佳实践

我们看一下真实的 Rebase 文件。

Git 快速入门与最佳实践

该文件一共有三列,第一列表示要进行的操作,所有可以进行的操作,在下面注释里都列了出来,比如 Pick 表示使用该提交,Reword 表示使用该提交,但修改其提交的 Message、Edit 表示使用该提交,但是要对该提交进行一些修改,其它的就不一一说了。

而 Done 文件的形式如下,和 Git-Rebase-todo 是一样的:

Git 快速入门与最佳实践

从刚才的图中,我们就可以看到 Rebase 的一个缺点,那就是修改了分支的历史提交。如果已经将分支推送到了远程仓库,会导致无法将修改后的分支推送上去,必须使用 -f 参数(Force)强行推送。

所以使用 Rebase 最好不要在公共分支上进行操作。

3

3.3.1 Checkout

对于 Checkout,我们一般不会陌生。因为使用它的频率非常高,经常用来切换分支、或者切换到某一次提交。

这里我们以切换分支为例,从 Git 的工作区、暂存区、本地仓库分别来看 Checkout 所做的事情。Checkout 前的状态如下:

Git 快速入门与最佳实践

首先 Checkout 找到目标提交(Commit),目标提交中的快照也就是 Tree 对象就是我们要检出的项目版本。

Checkout 首先根据Tree生成暂存区的内容,再根据 Tree 与其包含的 Blob 转换成我们的项目文件。然后修改 HEAD 的指向,表示切换分支。

Git 快速入门与最佳实践

可以看到 Checkout 并没有修改提交的历史记录。只是将对应版本的项目内容提取出来。

3

3.3.2 Revert

如果我们想要用一个用一个反向提交恢复项目的某个版本,那就需要 Revert 来协助我们完成了。什么是反向提交呢,就是旧版本添加了的内容,要在新版本中删除,旧版本中删除了的内容,要在新版本中添加。这在分支已经推送到远程仓库的情境下非常有用。

Revert 之前:

Git 快速入门与最佳实践

Revert 也不会修改历史提交记录,实际的操作相当于是检出目标提交的项目快照到工作区与暂存区,然后用一个新的提交完成版本的“回退”。

Revert 之后:

Git 快速入门与最佳实践

3

3.3.3 Reset

Reset 操作与 Revert 很像,用来在当前分支进行版本的“回退”,不同的是,Reset 是会修改历史提交记录的。

Reset 常用的选项有三个,分别是 —Soft, —Mixed, —Hard。他们的作用域依次增大。

我们分别来看。

Soft 会仅仅修改分支指向。而不修改工作区与暂存区的内容,我们可以接着做一次提交,形成一个新的 Commit。这在我们撤销临时提交的场景下显得比较有用。

使用 Reset --Soft 前:

Git 快速入门与最佳实践

使用 Reset --Soft 后:

Git 快速入门与最佳实践

Mixed 比 Soft 的作用域多了一个 暂存区。实际上 Mixed 选项与 Soft 只差了一个 Add 操作。

使用 Reset --Mixed 前:

Git 快速入门与最佳实践

使用 Reset --Mixed 后:

Git 快速入门与最佳实践

Hard 会比 Mixed作用域又多了一个工作区。

使用 Reset --Hard 前:

Git 快速入门与最佳实践

使用 Reset --Hard 后:

Git 快速入门与最佳实践

Hard 选项会导致工作区内容“丢失”。

在使用 Hard 选项时,一定要确保知道自己在做什么,不要在迷糊的时候使用这条选项。如果真的误操作了,也不要慌,因为只要 Git 一般不会主动删除本地仓库中的内容,根据你丢失的情况,可以进行找回,比如在丢失后可以使用 Git Reset --Hard ORIG_HEAD 立即恢复,或者使用 reflog 命令查看之前分支的引用。

3

3.4 Stash

有时,我们在一个分支上做了一些工作,修改了很多代码,而这时需要切换到另一个分支干点别的事。但又不想将只做了一半的工作提交。在曾经这样做过,将当前的修改做一次提交,Message 填写 Half of work,然后切换另一个分支去做工作,完成工作后,切换回来使用 Reset —Soft 或者是 Commit Amend。

Git 为了帮我们解决这种需求,提供了 Stash 命令。

Stash 将工作区与暂存区中的内容做一个提交,保存起来,然后使用Reset Hard 选项恢复工作区与暂存区内容。我们可以随时使用 Stash Apply 将修改应用回来。

Stash 实现思路将我们的修改提交到本地仓库,使用特殊的分支指针(.Git/refs/Stash)引用该提交,然后在恢复的时候,将该提交恢复即可。我们可以更进一步,看看 Stash 做的提交是什么样的结构。

Git 快速入门与最佳实践

如图所示,如果我们提供了 Include-Untracked 选项,Git 会将 Untracked 文件做一个提交,但是该提交是一个游离的状态,接着将暂存区的内容做一个提交。最后将工作区的修改做一个提交,并以Untracked 的提交、暂存区 的提交、基础提交为父提交。

搞这么复杂,是为了提供更灵活地选项,我们可以选择性的恢复其中的内容。比如恢复 Stash 时,可以选择是否重建 Index,即与 Stash 操作时完全一致的状态。

3

3.5 Bisect

最后要讲到一个曾经把我从“火坑”中救出来的功能。

项目发布到线上的项目出现了 BUG,而经过排查却找不到问 BUG的源头。我们还有一种方法,那就是先找到上一次好的版本,从上一次到本次之间的所有提交依次尝试,一一排查。直到找到出现问题的那一次提交,然后分析 BUG原因。

Git 为我们想到了这样的场景,同样是刚才的思路,但是使用二分法进行查找。这就是 Bisect 命令。

使用该命令很简单:

Git 快速入门与最佳实践

Git 会计算中间的一个提交,然后我们进行测试。

Git 快速入门与最佳实践

根据测试结果,使用 Git Bisect Good or Bad 进行标记,Git 会自动切换到下一个提交。不断的重复这个步骤,直到找到最初引入 BUG的那一次提交。

Git 快速入门与最佳实践

我们知道二分法的效率是很高的,2 的 10 次方就已经 1024 了,因此我们测试一般最多是 10 次,再多就是 11 次、12 次。

其实这就要求我们优化测试的方法,使得简单的操作就能使 BUG重现。如果重新的操作非常简单,简单到我们可以使用脚本就能测试,那就更轻松了,可以使用 Git Bisect run ./test.sh,一步到位。

如某一提交代码跑不起来,可用 Git Bisect Skip 跳过当前提交或使用 Visualize 在 Git 给出的列表中手动指定一个提交进行测试。

往期技术分享:

扫描下方二维码入群

入群请完整备注“姓名-公司-职务”

- FIN -

Git 快速入门与最佳实践