04、 git存储原理以及分支创建与合并

大家好,我们在上上节留了三个问题,git的版本回退和gitignore在上一节做个简单阐述,本节我们针对git分支管理展开介绍一下。
本节内容预告:

  1. 分支解决了什么问题
  2. 分支常用命令介绍
  3. git存储原理
  4. 分支合并以及可能冲突解决
  5. 总结回顾

1.分支解决了什么问题?

看过我前面博客的话,相信对这个问题并不陌生,甚至已经有自己的答案。这里还是简单说一下,因为带着问题看东西,效率会相对来说高一点,学技术之前我们先要明白为什么学这个东西,而不是遇见一个东西就埋头苦学。毕竟时间和精力都有限,好了,话不多少,进入正题。

日常开发中你很可能会遇见一个事情是这样子的,提给开发人员的需求并不是一个一个来的,经常会遇见一种情况是两个甚至多个需求同时过来,但是因为这两个需求的上线节点不一致等原因,你不能将功能开发到一块,不然可能上线后有其他问题

这时候你可以选择自己本地人为的做个备份,即在一个项目开发需求 A,在复制一份代码到其他地方开发需求B,两个项目彼此独立,这样是可以满足基本需求,但是很低级而且后面的需求B上线的时候需要在原来的基础上加上已经上线的A的变更,如果都是人为的话很花时间而且容易出错!!!

所有就有了分支的概念,你可以针对不同的需求建立不同的分支,因为两个分支的内容互不影响,这样可以实现需求A先上线且不会带着开发到一半的需求B的内容。另外,到时候需求B需要在需求A的基础上上线的时候,git也提供了很方便的方式可以方便快捷的将需求A的分支代码合并到需求B的分支上面。

2.分支常用命令介绍

  1. git branch
    这个命令后面不带任何参数的时候,显示当前git有多少分支。

  2. git branch 新的分支名字
    这个命令会基于当前分支创建一个新的分支,因为git初始化就有一个master分支,所以第一次创建分支肯定是从master上创建的一个分支
    在这里插入图片描述
    这里可以看到,因为我刚开始初始化了一个新的仓库,里面还没有任何文件,这时候创建分支git会报一个错,提示没有有效的对象,随便将一个文件加到版本库后,重新创建分支branch1,然后通过命令git branch查看,发现当前有两个分支,一个是git初始化就有的默认分支master主分支,还有一个是刚才新建的分支branch1

  3. git checkout 分支名字
    可以看到,刚开始我们在master分支,当前目录有一个之前创建好的test.txt文件,通过命令git checkout branch1切换到之前创建的分支branch1上面,因为分支branch1是基于master创建的,所以目前两个分支内容一致。
    然后我们在分支branch1上面新增一个文件test2.txt,可以看到这个目录有两个文件,并提交到版本库上面,然后再切换回分支master,发现master分支的工作目录下面仍然是一个文件,这印证了我们刚开始说的,通过分支,可以实现分支之间的隔离开发,两个分支的内容可以完全不同,这样我们就可以针对不同的需求创建不同的分支。

    在切换分支时,一定要注意你工作目录里的文件会被改变。 如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。 如果 Git 不能干净利落地完成这个任务,它将禁止切换分支。
    在这里插入图片描述

  4. git branch -d 分支名字
    我们先使用git branch命令查看有哪些分支,然后创建一个新的分支branch2,执行命令git branch -d branch2后,git提示删除成功,然后再执行命令git branch -d branch1,git提示删除失败,并说明原因是因为分支1有没合并的内容。这是因为git默认是你删除的分支内容肯定已经合并到其他分支,如果你删除的分支有部分代码没合并,那么git会有报错提示,并告诉你可以用参数-D确认删除,相当于给开发者一个二次确认的机会,防止因为误操作而删除了部分没有提交的内容。可以看到,使用-D后,提示分支删除。日常开发中,建议我们使用-d,防止误删没合并的文件。
    在这里插入图片描述

  5. git branch -v
    显示当前分支最近一次的提交记录

  6. git branch -b 分支名字
    相当于创建分支和切换分支组合使用命令
    在这里插入图片描述

3. git存储原理

git分支的原理这块比较深奥,一直在犹豫要不要引入这个话题,摊开了说太大,很繁琐,说的少了说不清楚,不说的话下面涉及到分支合并可能读者更是一头雾水,所以权衡再三,还是在可控范围内抛砖引玉。

Git 和其它版本控制系统(包括 Subversion 和近似工具)的主要差别在于 Git 对待数据的方法。 从概念上来说,其它大部分系统以文件变更列表的方式存储信息,这类系统(CVS、Subversion、Perforce、Bazaar 等等) 将它们存储的信息看作是一组基本文件和每个文件随时间逐步累积的差异 (它们通常称作 基于差异(delta-based) 的版本控制)。
在这里插入图片描述
Git 中所有的数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。

Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:24b9da6552252987aa493b52f8696cd6d3b00373

Git 中使用这种哈希值的情况很多,你将经常看到这种哈希值。 实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
在这里插入图片描述
Git 保存的不是文件的变化或者差异,而是一系列不同时刻的 快照 。

在进行提交操作时,Git 会保存一个提交对象(commit object)。 知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。 首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象,
为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。 暂存操作会为每一个文件计算校验和(使用我们在 起步 中提到的 SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中 (Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:
$ git add README test.rb LICENSE
$ git commit -m ‘The initial commit of my project’

当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。

现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个 树 对象 (记录着目录结构和 blob 对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)。
在这里插入图片描述
做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。
在这里插入图片描述
由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字符(40 个字符和 1 个换行符),如此的简单能不快吗?

这与过去大多数版本控制系统形成了鲜明的对比,它们在创建分支时,将所有的项目文件都复制一遍,并保存到一个特定的目录。 完成这样繁琐的过程通常需要好几秒钟,有时甚至需要好几分钟。所需时间的长短,完全取决于项目的规模。 而在 Git 中,任何规模的项目都能在瞬间创建新分支。 同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(译注:即共同祖先)也是同样的简单和高效。 这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支。

上面内容是我从官网拉下来的一些内容,初看会有点繁琐,而且有点难以理解,我这边根据我的理解对一些内容做个说明:
git存储我们提交到暂存区的每个文件都会有加密算法sha1根据文件内容生成一个id,这个id类似一个索引,我们可以根据id找到文件,然后每次提交的时候,git又会将每个子目录的暂存区文件id和目录结构等保存成一个树对象,最后生成一个commit对象保存树对象的指针。这个commit对象的sha1就是我们之前说过的commitid。

这个commit对象还会保存他的上一次commit对象的指针为parent,第一次parent保存的parent是空的,这样我们就能我才能够当前对象回溯到前面的版本。

4.分支合并以及分支冲突解决

1. 分支快进合并

我们从master分支新拉一个分支branch3,这样branch3实际上和master分支内容一样,然后我们切换到分支branch3,修改一个文件,并提交。使用命令git merge 分支名字会将对应分支合并到当前分支,如果需要将branch3的内容合并到master上面,切换到master之后,执行命令git merge branch3,可以看到,git提示我们有个更新,特别留意这边有个fast-forward。然后在master分支 查看test.txt 文件内容发现文件内容确实包含了分支branch3提交的内容。
在这里插入图片描述
在合并的时候,你应该注意到了“快进(fast-forward)”这个词。 由于你想要合并的分支 branch3 所指向的提交 C4 是你所在的提交 C2 的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。
在这里插入图片描述
合并完之后master和branch3指向同一个提交
在这里插入图片描述
这里需要留意,因为快进提交只是一个指针的换位,默认是不会生成一个新的提交id。 可以和下面的情况多对比

2. 分支三路合并以及递归三路合并

上面说的情况是我们在从master创建分支branch3后,没有在master上面进行过修改提交,如果master和branch3都修改了会怎么样呢?
我们在两个分支修改了test.txt 文件的同一行,此时同样执行合并命令
git merge 分支名字,git提示我们自动合并失败,需要修改冲突,手动合并,这种情况也是我们会经常遇到的。在这里插入图片描述
通过git status看到git提示有一个为合并的路径,提示我们解决冲突并提交文件,解决完冲突后用git add 命令标记这个文件冲突解决完毕。
在这里插入图片描述
使用linux的命令vi test.txt编辑冲突文件test.txt可以看到:
在这里插入图片描述
可以看到 <<<<<<< 和=======之间的内容就是我们在branch3也就是当前分支做的修改 , =======和 >>>>>>>之间的内容是我们在被合并的分支master上面做的修改,这种冲突是git无法解决的,因为git不知道用户真实是想要那个版本的修改,所以需要用户自行解决。

如果我们两个版本的修改都想要的话,删除三个标识行的特殊字符,然后esc健退出编辑模式,shift + :+ wq! 键保存退出解决冲突,使用git add添加到暂存区,这里需要留意, 对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。
在这里插入图片描述
这和之前合并 branch3 分支的时候看起来有一点不一样。 在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 branch3 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C3)以及这两个分支的工作祖先(C2),做一个简单的三方合并。
在这里插入图片描述
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交,然后当前分支指向最新的提交。
在这里插入图片描述
细心的话,会发现我在开始复现三方合并,修改文件的时候强调的是同一个文件,同一行,那么其他情况呢?

这里需要留意如果两个分支修改的是同一个文件不同行,或者不同文件的话,是不是冲突的,git会默认帮我们做合并动作,但是三方合并还是会生成一个新的commit id,这里就不罗列种种情况了,感兴趣可以自己尝试复现。

那么递归三方合并又是什么场景呢?
我们合并 ⑥ 和 ⑦ 的时候,我们将其 2 个公共祖先③ 和 ④ 进行 merge 为 X ,在合并 ③ 和 ④时候 我们又需要找到 他们的公共祖先,此时可能又有多个公共祖先,我们又需要将他们先进行合并,如此就是递归了 也就是 recursive merge,
在这里插入图片描述

5.总结回顾

刚开始我们留了一个问题,git分支解决了什么问题,通过后面对git数据存储结构和分支存储形式的了解,可以看到git分支的强大之处,后面学习了git分支的常见操作命令,最后通过举例和画图的方式简述了git分支的快进合并和三路合并,希望读者还是能自己手动逐个实践上面的命令,因为只有自己敲了,才会遇见实际的问题,通过解决一个个问题来逐渐熟悉git的使用,顺便加深对命令的理解。

因为笔者能力和精力有限,实际很难考虑完全使用过程中遇见的各种问题,也不可能针对遇见的每种情况都展开详述,所以有什么问题欢迎评论区讨论。

最后,感谢阅读,如有错误,请不吝指正
———————————————————————————————————
参考资料:

  1. git官网

  2. https://segmentfault.com/a/1190000021712743

  3. https://blog.csdn.net/longintchar/article/details/83049840

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章