git branch看不到分支_Git 分支整合与工作流

现代软件开发离不开版本控制系统。没有版本控制,在快速更迭的代码变更当中很容易迷失自己的方向,很容易丢失误删的更改。

Git 版本控制系统由 Linux 的领袖 Linus 创造,并首先使用在 Linux 的项目开发中,在当下已经几乎相当于版本控制系统的同义词。

Git 的发展和普及非常迅猛。然而,每天跟 Git 打交道的开发者们对 Git 又了解多少呢?

如果要详细介绍 Git 的每一个细节,恐怕一个月都讲不完。本文从 Git 版本控制在协作上最关键的合并操作入手,首先介绍为了分析 Git 问题所需要了解的 Git 对象模型基础,然后介绍了合并操作的意义和实践方案。随后,给出实际开发过程中指导合并的高层思想,即 Git 工作流的最佳实践。最后,分享若干个 Git 开发过程中的实用技巧。

Git 对象模型

本节将以最简略的形式讲解 Git 对象模型。话不多说,直接上图。

e8abfcbbf84a358bf99b98b5f5fab022.png

上图出现了若干个概念,其中属于 Git 对象模型的包括提交(commit)对象、树(tree)对象、块(blob)对象和标签(tag)对象。对象被存储在 .git/objects 下面,由对象内容的 SHA1 值作为索引。其中第一个字节为子目录文件名,其余字节为文件名中文件的名字。这主要是为了规避某些文件系统的性能局限性。

回到正题,我们分别介绍 Git 对象模型中的四种基础对象。

块对象即二进制大对象(Binary Large Object),用来指代某些可以包含任意数据的变量或文件。在 Git 中,一个块对象保存一个文件的数据。注意,并不包含元数据,即使是文件名也不包括。从而,内容一致的文件将对应同一个块对象。

一个树对象代表一层目录信息。它记录块对象标识符、路径名和一个目录里所有文件的元数据,包括子目录。它可以递归地引用其它目录树或子树对象,从而建立一个包含文件和子目录的完整层次结构。

一个提交对象保存版本库中每一个变化的元数据,包括作者、提交者、提交日期和日志消息。每一个提交对象都指向一个目录树对象,这个目录树对象在一张完整的快照中捕获提交时版本库的状态,

一个标签对象分配一个可读的名字给一个特定对象,通常是一个提交对象。

图中的分支名属于引用的一部分,将会体现在 .git/refs.git/logs/refs 里面。分支通过 HEAD 信息引用一个提交对象,通过追溯提交对象的父提交,能够追溯出整个提交历史。

在后面的讨论中,我们会采用形如下图的提交关系图来展示提交历史。

ef447cf7eac0276da2b4271388c84f87.png

其中,同一水平线上的提交代表同一个分支的提交,当然,提交历史是从 HEAD 提交开始追溯父提交得到的。分支的名称将会标出,而圆圈则代表不同的提交对象,其中字母表示提交对象的命名。

Git 分支合并

在大多数的版本管理系统中,每个提交都只有一个唯一的父提交。在这样的系统里面合并 their_branch 到 our_branch 的时候,会在 our_branch 上创建一个新的提交,包含 their_branch 和 our_branch 差异;反过来,合并 our_branch 到 their_branch 的时候,会在 their_branch 上创建一个新的提交,也包含 their_branch 和 our_branch 差异。这也就是说交换参与合并分支的主宾关系会创建出不同的提交。

但是,合并操作在客观上是符合交换律的。同一时刻的两个分支,合并一个到另一个并没有顺序上的区别。换句话说,合并操作可以表示为,合并 our_branch 和 their_branch 的所有修改到单个分支中。

在这样的观察的背景下,Git 会为每次合并操作创建一个提交对象,该提交对象引用一个树对象,树对象最终引用合并后的工作空间的所有文件信息。这个提交对象的父提交是一个列表,包括参与合并的每一个分支合并时的 HEAD 提交。

ece9ca8569501f2c1f4d2b8aec479951.png

为了强调这个提交对象对所有分支是同等的,如果在合并后切换到参与合并的另一个分支,将刚才的当前分支合并到现在的当前分支,将会应用 fast-forward 策略直接将合并提交添加到现在的当前分支并赋值给 HEAD 变量。查看两个分支中合并提交的 SHA1 值,将会发现它们对应了同一个提交对象。

Git 还提供了模仿上面提到的不对称的合并操作的实现通道,以及其他类似合并的操作。在看到这些其他可能性之前,我们先讨论 Git 常规的合并流程里两个需要考虑的问题,冲突的发现和冲突的解决。

Git 的合并策略同时处理这两个问题,它既判断分支合并时是否存在冲突,又决定在不同判断结果下实际的合并动作。

首先看到的是两种退化的场景,已经最新(already up-to-date)和快进(fast-forward)。在这两种场景下合并都会以一种平凡的形式完成,并不会引入一个合并提交。

已经最新的情况很好理解,在这种情况下合并操作将会是一个空操作,即什么也不做。问题在于 Git 是如何判断当前请求的合并操作是这种情况的。

退化情况的判断也应该是简单的,实际上,Git 会判断来自其他分支的所有提交是否已经在目标分支上。如果结果为真,显然目标分支已经包含了本次合并请求中它所要合并的所有提交。因此,没有新的提交添加到目标分支上。一个平凡的情况是合并某个分支到它自己上,你将会得到已经最新的提示。

$ git merge master # 在 master 分支上
Already up to date.
$

另一种退化情况是快进。如果 Git 发现目标分支的 HEAD 已经在其他分支中完全表示时,也就是说,目标分支的所有提交在其他分支中,Git 不会创建一个合并提交来完成合并,而是直接把其他分支相对目标分支 HEAD 的新提交直接加在目标分支上。当然,索引和工作目录也会相应调整,以反映最终的提交状态。这种情况实际上是已经最新的情况的对称情景。

$ git init
$ git commit --allow-empty -m "A"
$ git commit --allow-empty -m "B"
$ git checkout -b dev
$ git commit --allow-empty -m "C"
$ git checkout master  
$ git merge dev
Updating d60b3a7..a0962fa
Fast-forward
$ git --no-pager log --format=oneline 
a0962fad9588d72b2455b40b469c583e132cef5c (HEAD -> master, dev) C
d60b3a774d5657604121fb1fbce67f9195521075 B
39fcfaedf34bc4788a1811390f028725eb35c6f4 A

这两种情况中,已是最新的情况是强制执行的。然而,你可以在合并的时候指定 --no-ff 选项禁用快进策略,以在快进适用的情况下仍然创建出一个合并提交。

接下来我们讨论可以使用 --strategy=<strategy> 选项指定的合并策略。

第一个策略是 resolve 策略,该策略只能用于合并两个分支。合并时采用三路合并算法,即两个分支的最近共同祖先(也称为合并基础)、当前分支的 HEAD 提交以及另一个分支的 HEAD 提交的合并,表现为对当前分支施加从合并基础到另一个分支 HEAD 的所有修改。

第二个策略是 recursive 策略,该策略可以认为是 resolve 策略的改进版。唯一的不同是它可以处理两个分支之间有多个合并基础的情况,例如历史上发生过相互合并。在这种情况下,该策略将生成一个临时合并来包含所有的合并基础,然后以此为合并基础执行一个三路合并。

recursive 策略是 Git 默认的合并策略,在 Linux 2.6 的内核开发中被证明能够减少合并时的噪声冲突。这个策略能够接受一系列的参数来定制冲突解决策略,具体参考 git-merge 手册页的 MERGE STRATEGIES 章节。

第三个策略是 octopus 策略,该策略用于多个分支合并。它的实现非常直观,即在内部多次调用 recursive 策略,对要合并进来的分支每个调用一次。这个策略和手动两两使用 recursive 策略的不同之处是只产生唯一的一个合并提交。

3f705515f53b18602c7fdbf711b1535f.png

第四个策略是 ours 策略,该策略可以合并任意数量的分支。它使用当前分支的 HEAD 提交作为合并结果,也就是说,该策略仅仅创建一个新的合并提交,其结果和当前分支的 HEAD 提交相同,但把合并列表的所有 HEAD 提交标记为父提交。这个策略可以简单地看成添加父提交的操作。

第五个策略是 subtree 策略,这个策略主要用来在项目中记录子项目或者第三方项目。由于它只是使用了合并的接口但并不是常规的合并,这里不做展开。

通常来说,你在合并两个分支时使用 recursive 策略,合并多个分支时使用 octopus 策略,解决合并时出现的冲突,并提交一个合并提交。不需要了解更复杂的策略。

三路合并时,如果出现文件冲突时,Git 为冲突文件保留三个版本,即合并基础的的版本(编号为 1),当前分支的版本(即 ours 版本,编号为 2)以及另一个分支的版本(即 theirs 版本,编号为 3)。这些版本不会直接出现在工作目录中,而是需要使用 git ls-files -s 来查看。对于无冲突的版本,将会做编号为 0 的展示。

在工作目录的冲突文件中,Git 将使用三路合并标记线来标记冲突行,如下所示。

hello
<<<<<<< HEAD
worlds
=======
world
>>>>>>> 9e014aa63ae16f18e8567caddf309949b130881b

其中包含在 <<<<<<<======= 之间的是当前分支的修改,而包含在 =======>>>>>>> 之间的是另一个分支的修改。这种展示有助于你在解决冲突的时候区分两侧的修改分别是什么。

当解决完冲突后,只需要使用 git add 命令记录文件的更改,Git 就会将该文件从冲突列表中移除。注意就算你什么也不动,直接执行 git add 命令,Git 也会将该文件从冲突列表中移除。这是因为 Git 并不理解三路合并标记线的含义,以及在特殊情况下(例如本文写作时)三路合并标记线是预期的文件内容。

我们在本节开头的地方提到,Git 提供了不对称的合并操作的实现通道,以及其他类似合并的操作。

对于前者,只需要再合并的时候加入 --squash 选项,Git 将进行类似合并提交的操作。具体来说,产生相同的文件结果,但是不产生合并提交,而是需要你手动完成提交。这里需要强调的是,默认提交只会将 HEAD 提交作为父提交,而不会像合并一样有多个父提交。

对于后者,我们将在下一节中讨论。

Git 提交变基

Git 的合并操作将两个分支通过一个合并提交关联起来,在查看 Git 历史时,我们从 HEAD 提交往回追溯,当看到一个合并提交时,会分别罗列出两个父提交的祖先路径。总的来说,git log 查看 Git 历史得到的提交序列是一个提交图的拓扑排序。

这样的好处是我们总是能追溯真实的版本历史,我们知道版本发展的所有中间状态。但是,中间状态并不总是有意义的。例如,开发特性过程中修复一个开发过程引入的缺陷是否有意义就值得讨论。

有些人会认为这真实地反应了项目开发的历史,展示了代码迂回前进的迭代历程,而不是直接出现一个最终结果,仿佛编程是有一个全能的上帝直接将最终结果告知程序员。然而,也有一些人认为中间状态会产生噪声。尤其是在发布分支上,我们只需要有具体意义的变更历史。

在这一节中,我们将会介绍提交变基(rebase)操作,它将产生和合并类似的效果。

变基操作,顾名思义,是用来改变一系列提交的基础的。变基需要指定新的基础,这通常是一个目标分支名。默认情况下,不在目标分支的当前分支提交会变基。

下图为 topic 变基之前。

cfefced936fc704d0b678e3aefa4f279.png

下图为 topic 变基为 master 之后。

dc1bb4b646d20616cac46de4242374ee.png

变基操作进行的时候,每次只迁移一个提交,从各自原始的提交位置逐个迁移到新的提交基础。因此,每个移动的提交都可能有冲突需要解决。

如果发现冲突,Git 会报告冲突的文件,并挂起变基操作,这就像合并时遇到冲突一样。

当解决完所有冲突后,可以执行 git rebase --continue 恢复执行变基操作。该命令会提交解决的冲突然后处理要变基的下一个提交。

如果产生冲突的提交可以从新的历史中抛弃,可以执行 git rebase --skip 来跳过这个提交。通常不会希望这么做,尤其是当后续提交依赖这个提交时,实际上还是需要最终解决冲突。

最后,如果变基的过程已经让你迷失,你开始怀疑变基的意义或者无法确定变基的结果是安全的,可以执行 git rebase --abort 来放弃变基操作。Git 会将提交历史恢复到变基之前的状态。

同样的,变基有已经最新的和快进的平凡情况。

$ git init
$ git commit --allow-empty -m "A"  
$ git commit --allow-empty -m "B"
$ git checkout -b dev
$ git commit --allow-empty -m "C" 
$ git checkout master  
$ git rebase dev  
Successfully rebased and updated refs/heads/master.
$ git rebase dev
Current branch master is up to date.
$ git --no-pager log --format=oneline 
a6015bfb8d0587afc944e6356a4e6e646d6a7181 (HEAD -> master, dev) C
c663acee6432589d3538d39117b281ce064d0203 B
838bbff062d3f39c8bd599edfd5b7e6580be2211 A
$

我们可以看到,这类似于分支合并。实际上,变基然后合并是 GitHub 上一个可选的合并策略。

但是通过变基来达成类似合并的效果,如果不是快进的情况,必须注意它的实际操作是每次迁移一个提交,从各自原始的提交位置逐个迁移到新的提交基础。因此,变基之后的提交跟原来的提交是不同的提交对象!

这一点很重要。在分布式开发的场景下,使用合并操作产生合并提交并推送到远端代码库,并不会修改历史提交的提交对象信息。但是,如果你对提交历史进行变基,历史提交的提交对象就会改变。默认情况下,远端代码库将拒绝你的推送,因为这个版本跟远端版本是不匹配的(diverged)。

如果你执意使用 --force 选项推送,那么远端的历史提交将会被改变。这样,你的开发伙伴将会发现它的提交历史和远端也是不同的。如果是在开源项目这种开发伙伴成千上万的场景,整个团队解决这个冲突的过程将会是一场噩梦!

变基之后的提交对象不同于原来的提交对象还将带来一个问题。如果某人基于另一个人的 branch_a 分支开发了 branch_b 分支,branch_a 和 master 有差异,如果 branch_a 变基到 master 上,并由 master 快进合并,branch_b 再变基到 master 上,并由 master 快速合并时,branch_a 和 master 的差异将会重复两次。如下所示。

$ git init
$ git:(master) git commit --allow-empty -m "A"
$ git:(master) git commit --allow-empty -m "B"
$ git:(master) git checkout -b bob            
$ git:(bob) git commit --allow-empty -m "D"
$ git:(bob) git checkout -b anna
$ git:(anna) git commit --allow-empty -m "E"
$ git:(anna) git checkout master  
$ git:(master) git commit --allow-empty -m "C"
$ git:(master) git checkout bob  
$ git:(bob) git rebase master  
Successfully rebased and updated refs/heads/bob.
$ git:(bob) git checkout anna  
$ git:(anna) git rebase bob  
Successfully rebased and updated refs/heads/anna.
$ git:(anna) git --no-pager log --format=oneline 
2040b16584d4a793c0d3459aa5f60c896e69ec3b (HEAD -> anna) E
5ec6fd1d17b3f0b6ce749a007683c37badbfe924 D
dd643faef11ed65130d66fd251350709dbc19539 (bob) D
8dd57e9ae5e899d1fb4a3779b68ffb22130ecf00 (master) C
4bed7acb2cac75e9d6bea05d3e27e9366d73b0af B
a40e740c8fb13b3cb6ec059bbd9482ff360cf5b7 A

可以看到,相同的提交 D 出现了两次。虽然差异提交的内容是相同的,但是由于变基产生新的提交对象,Git 会将他们视作不同的提交,同时出现在提交历史中。这显然是噪声。

因此,在《Pro Git》以及其他 Git 资料中对于变基都有一条黄金准则。

不要改变已经发布的分支的提交历史。

当然,万一你要面对变基后的历史,可以参考 Git 工作流一节中对 cherry-pick 的介绍。

前面我们提到,提交变基本质上是用来改变一系列提交的基础的。默认情况下,不在目标分支的当前分支提交会变基。既然有默认情况,当然也就有非默认的自定义情况。

变基操作的所有配置本文不会一一罗列,这里仅介绍一站式的交互式变基操作界面。

通过执行 git rebase -i <branch> 你将开启一个变基操作页面。

pick b53e1337ab ...
...
pick c66eac7de4 ...

# Rebase 4d200ed87d..c66eac7de4 onto 371f3de537 (10 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

上面信息中需要进行变基操作的有十个提交,这里省略了具体提交的内容和中间的提交项。实际情况下将会有十行提交信息供你操作。

交互式页面将会显示变基操作逐个应用提交的过程,具体地说,是将命令自上而下执行。默认命令是使用该提交(pick),即在新的分支基础上使用该提交。其他的命令如上所示,常用的有修改提交信息(reword)、修改提交内容(edit)以及将提交合并到前一个提交中(squash/fixup)。

回到本节的开头,我们说过变基的结果和合并非常类似。那么自然会有个问题,我们在合并两个分支的文件差异的时候,应该使用合并命令还是变基命令呢?

这个问题的答案因团队而异。总的来说,你需要在团队中找到一个团队能够认可的方案。

如前所述,合并操作真实地反应了项目开发的历史,展示了代码迂回前进的迭代历程,而不是直接出现一个最终结果;正确使用变基操作使公共分支的提交历史线性前进,结合上 squash/fixup 等压缩提交历史的操作,能够展现出一个干净的、富有表达力的提交历史。

变基操作在实践上应该通过调整提交的应用过程,使得最终合并的效果显得和快进一样。在这种情况下,采用合并或是变基来合并文件差异,从文件视角上是没有区别的。

我个人更喜欢使用变基操作来合并差异。这使得项目的 master 分支线性前进,而不是呈现出复杂的合并流程。同时,基于变基进行合并能够避免产生合并提交,一个合并提交仅记录了合并的来源以及解决冲突差异,而这往往不是最令人感兴趣的。

下一节将介绍一种规范的 Git 工作流,这使得变基操作不再充满风险,而变成一个能有效整合不同开发者开发进度的工具。

Git 工作流

Git 工作流并不是 Git 工具规范的一部分,而是开发者基于 Git 的特点所总结和推出的一系列最佳实践。本文限于篇幅不会逐一罗列,仅对实际工作中采用的工作流进行介绍。同时,不仅包括工作流本身,还包括为了实现该工作流开发者所需要知道的操作过程和最佳实践。

对于工具链或框架项目,即所谓的基础设施,这类项目的特点是发布周期较长,发布后需要维护已发布版本,因为用户未必能够及时升级,而是使用一个旧版本。这种项目通常会区分成开发者个人仓库的开发分支、远端仓库的不稳定开发分支和发布分支,如下图所示。

5f1ae6649e4b514711655ec0f0ba26c1.png

上图是 Flink 的工作流。

  • devn 分支没有一个固定的名字,代表开发者个人仓库的开发分支,通常对应一个特性或者缺陷修复
  • master 分支是所有开发者都知晓的、公共的、不稳定的开发分支,对应 X.Y-SNAPSHOT 的快照版本(版本号的问题是另一个话题)
  • release-X.Y 是 X.Y 系列版本的基础分支,在语义化版本的语境下,该分支将只接受缺陷修复型提交
  • release-X.Y.Z-RCn 是一个标签版本,是一个静态的版本,对应 X.Y.Z 的第 n 次预发版本
  • release-X.Y.Z 是一个标签版本,是一个静态的版本,对应 X.Y.Z 的发布版本

对于普通的开发者来说,为了适应这个工作流,需要熟悉以下操作。

  1. 从远端仓库克隆代码仓库,记得将远端仓库而不是你的个人仓库的远端镜像设置为 origin 远端,这将简化后续的操作
  2. 添加你的个人仓库的远端镜像为 dev 远端
  3. 当需要开发一个新的特性或者修复缺陷时,首先执行在 master 分支上执行 git pull 拉取远端仓库的最新提交。由于远端仓库的提交都是线性的,这个拉取应当永远以快进的形式成功合并
  4. 从 master 分支上创建出个人分支 devn 进行开发。开发过程记得随时暂存更改,只有暂存的更改才会被 Git 对象库记录并可以恢复。此外,可以通过本节后续将提到的技巧调整开发过程的提交,使其更简练和富有表达力
  5. 开发完成后,执行 git push dev 命令,将你的分支推送到 dev 远端上。由于这是你的个人仓库的远端镜像,你必然有权限推送,并且这不需要显式的写出 -u <upstream> <your-branch> 参数,Git 会自动推断
  6. 在远端交互界面上发起合并请求

如果合并请求被接受并合并到 master 上,那么恭喜你完成了一次代码贡献!

在合并的时候如果出现冲突,项目的维护者一般会要求开发者自行解决冲突。对于 Flink 社区,它还将要求开发者不使用合并提交来解决冲突,而是采用变基或者重做等手段解决冲突,以展现一个干净的提交历史。开发者解决冲突的一般流程如下。

  1. 切换到 master 分支,通过 git pull 命令拉取最新的 master 分支提交。同样,我们从来没有用除此之外的办法修改 master 分支,因此它应该可以成功的拉取
  2. 切换到 devn 分支,通过 git rebase master 命令将开发分支变基到 master 分支上,并解决冲突
  3. 如果评审意见有要求的修改,执行相应的修改并提交
  4. 推送新的 devn 分支到个人仓库的远端镜像。这一步,由于此前的变基操作,通常需要强制推送。但这没有关系,因为你将覆盖远端一个自己的分支,这个分支从未被发布,因此有不应该有其他人依赖它

接下来,回到合并请求的环境里,等待下一轮评审意见。

这里我们提到被变基的分支从未被发布。是的,你也从来不应该依赖别人未发布的分支,尽管在互联网时代别人推到 GitHub 等环境中时你可能已经可以获得了。注意别人的变基操作将会彻底打乱你的计划。然而,这也并非没有例外。

例外之一就是你可以依赖自己未发布的分支。这是当然的,我们希望你至少知道自己对自己的分支做过什么。实际情况中,如果一个特性包含若干个子特性,子特性之间相互依赖,那么你可以创建一系列的分支,每个分支以完成度步进来分出新的分支。只要你知道自己在做什么,变基就没有什么可怕的。毕竟,最糟糕的情况也是你的提交完全作废,不会有另一个开发伙伴被你坑惨了。

如果需要多个人协同开发子特性,这一系列的分支并不只有你可见的时候,情况也没有那么糟糕。只要你保证每次发布分支之后不再改变已经发布的历史,对于更新的本地历史,你仍然可以愉快的变基、压缩和修改。但记得千万不要修改已经发布的历史,这就是黄金法则!当其他人想要加入你新的更改的时候,只需要 cherry-pick 你的新更改就可以。

例外之二并不是一个特殊的情况,而是当你不得不解决一个变基的基础分支时,我们不希望鲁莽地将当前分支变基到变基的基础分支上。这不仅会带来不可预知的冲突,还有可能出现前面提过的相同文件更改集合但不同的提交对象的问题。你绝对不会希望看到这样的提交历史!

解决方案跟上一段类似,你可以利用 cherry-pick 命令。首先,基于变基后的基础分支创建出一个新的分支。然后,将你刚才开发的分支和基础分支的差异逐个 cherry-pick 到新的分支上。这样你就完成了一次实质的文件合并,并且,提交历史是干净的。但是请注意这一系列的提交都是新的提交对象,只不过它们变更是一模一样的。

如果需要覆盖远端镜像对应刚才开发分支的分支,只需将这个新创建的分支的远端分支设置为所要的分支,一个强制推送便可解决问题。通常来说,cherry-pick 之后的流程如下。

$ git checkout devn-cherry-picked
$ git branch -M devn # 强制重命名
$ git push dev -f

上面我们介绍了工具链和框架的工作流,接下来我们看到业务项目的工作流。开发者的具体操作流程是类似的,但是从分支图来看,项目的分支将如下图所示。

0a4440cfdafb29f255163c0bc989d922.png

业务项目的特点是,只要灰度完成,线上就总是最新版本的代码,因此工作流中不存在为某个提交缺陷修改用的分支。虽然现实生活中一些被广泛使用的服务将会运行不同的版本,但我们希望版本有缺陷时直接升级到最新版本,而不是在旧版本上岔开应用一系列的补丁。这将会导致每个运行实例到底包含哪些提交时完全不可知的。

另外,对于频繁发布的业务项目来说,采取一个明确的公共测试分支或者成为预发分支,将能整合不同开发者的开发结果,使得测试环境的版本包含所有开发者的阶段性成果。这能有效避免不同开发者并发测试时互相覆盖测试环境的实例版本,也能尽早发现不同开发者提交之间可能的冲突。注意到这些描述都指向一个动态更新的分支,这是 release-X.Y.Z-RCn 这样静态的预发版本标签所不具有的能力。

Git 实用技巧

在最后,介绍几个实际操作 Git 时常用的命令,能够用于调整提交。

第一个是 git rebase -i 命令,这个命令的具体作用前文已经提过,这里再次提及,以强调它的强大之处。

第二个是 git commit --amend 命令,这个命令能够修改 HEAD 提交。通常,这用于在进行提交之后发现有错误的代码或者需要改进的地方,同时这些更改不是独立的,因此需要被合并到 HEAD 提交中的情况。结合 -a 选项能够减少命令执行前的 git add . 操作,结合 --reset-author 能够调整提交作者信息,这在不同工作环境下需要以不同的身份以及邮件地址提交时会非常有用。

第三个是 git fetch -p 以及 git fetch dev -p 命令,这里的 dev 是遵循工作流一节对应的个人仓库的远端镜像。通过 -p 选项,能够删除本地记录的远端曾经存在但已经删除的分支。这能够减少 git checkout 时补全候选项的干扰,相当于人工对分支触发一次垃圾回收。相信我,你不会希望本地或者远端有大量废弃或者过期的分支的。

最后一个是 git reset --hard HEAD 命令。这个命令的常见意图可以用 git stash 来代替,即放弃当前工作区未提交的所有更改,包括索引和文件更改。采用 git stash 能够将你要放弃的更改转而存储在 stash 目录下,即使以后仍然要丢弃,可以执行相应的命令,而 git reset --hard HEAD 对于没有对应 Git 对象的文件内容来说,就是真的丢失了。虽然在某些情况下能够通过检索文件系统来找回,或者 IDE 会缓存本地更改,但是其本身确实是极端危险的操作。但是当你确定你的所有更改不过是实验性的更改的时候,也不怕使用这个命令。这个命令能够以最便利的形式重置你的工作空间。

扩展阅读

关于 Git 的材料有很多,其中最重要的是 Git 的手册页。对于 git subcommand 来说,可以通过 man git-subcommand 来查看相关的手册页。例如,关于合并的一切可以查看 man git-merge 手册页,手册页有最详尽的细节。

书籍方面,Git 有一本优秀的开源书籍《Pro Git》,值得一提的是,本书有完整的中文版。

另一本优秀的书籍是《Git 版本控制管理(第二版)》。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值