1. Tagging

像大多数VCS软件一样,Git可以标记Tag。通常,人们利用这个功能来标注软件的不同发布版本(v1.0,v2.0等等)

第六章 Git_git

Git支持两种创建Tag的方式:

1. 轻量级的(lightweight)

2. 带标注的(annotated)

我们重点学习第二种方式。

1.1 创建Tag

git tag -a v1.4 -m "my version 1.4"

1.2 查看所有Tag

git tag

1.3 查看某一个Tag

git show v1.4

1.4 推送Tag

默认情况下,"git push"命令并不会将本地的Tag推送到远程仓库。你需要使用以下命令明显地把本地所有的Tag推送到远程仓库:

git push origin --tags

也可以只推送某一个Tag到远程仓库

git push origin v1.4

这样的话,当其他人clone或者pull的时候,也会获得所有的Tag。

1.5 删除Tag

删除本地Tag

git tag -d v1.4

注意以上的这个命令并不会删除远程仓库的Tag,为了删除远程仓库中的Tag,需要使用以下命令

git push origin --delete <tagname>

1.6 检出Tag

检出Tag,使用以下的命令:

git checkout v2.0

然而这个命令会让你的仓库陷入“detached HEAD”状态,该状态会带来不好的影响。

第六章 Git_推送_02

在"detached HEAD"状态下,如果你做了新的修改,并且进行了提交,之前已经标记好的Tag和它所标记的文件是不会发生任何改变的,而你所作的新的提交将存在一个临时的分支上,且一旦切换到其他分支,这个临时分支将很难找回,除非你记得住该临时分支的精确Hash,如下:

第六章 Git_推送_03

如果从打算创建tag的一开始,就直接使用以下的命令,以上的问题就不会出现:

git checkout -b version2 v1.0

这个命令也会检出一个Tag,并且当你做了修改并且提交时,新的提交会被保存到version2这个分支上。你甚至可以沿着这个分支继续开发,在另一个方向上不断创新。

1.8 Tag的意义

对我们,我们可以在开发项目期间,对软件进行迭代升级,每当到了一个可发布的节点上时,就可以打一个Tag。这样我们在将来可以随时检出Tag,来打包生成软件的不同版本。

2. Rebase

在Git中,有两种合并分支的方式:git merge 和 git rebase。本节我们将详细学习 git rebase 命令的使用,以及在什么情况下适合使用 git rebase,在什么情况下不适合使用 git rebase。

2.1 The Basic Rebase

考虑以下场景:

第六章 Git_推送_04

正如我们已经知道的,我们可以使用

第六章 Git_远程仓库_05

除了用git merge来合并分支外,我们还有另一个选择:git rebase 。 我们可以将experiment分支中的C4提交,通过git rebase命令,放在master分支中的C3提交的右边这就是所谓的rebasing。命令如下:

$ git checkout experiment

$ git rebase master

First, rewinding head to replay your work on top of it...

Applying: added staged command

效果如下图所示:

第六章 Git_git_06

git rebase 的工作流程是这样的:先找到两个分支(一个是当前分支experiment,另一个是master分支)的最近的共同祖先(C2)。然后把当前所在分支(从共同祖先之后)的所有修改存入一个临时文件中,最后把这些临时文件中的修改,按顺序一个一个地合并到目标分支最近提交的右边。如果有冲突,则在解决冲突之后,键入 git rebase --continue 继续进行 rebase 操作。

在git rebase操作完成以后,我们可以切回master分支,进行一个快速合并:

$ git checkout master

$ git merge experiment

第六章 Git_远程仓库_07

此时,上图中的C4’与“git merge图”中的C5是完全一样的。也就是 git merge 和 git rebase 合并支的最终效果别无二致。但是用git rebase来合并分支会让提交历史看起来很“干净”。如果你在一个被rebase的分支上查看提交历史,会发现提交历史的图是线性的:就好像所有提交都是按顺序一个一个进行的,尽管这些提交曾经可能存在于两个并行的提交。

再次强调:无论是使用git rebase和git merge来合并分支,最终的效果是一样的,它们的区别仅仅只是提交历史不同而已。

2.2 More Interesting Rebases(选修)

考虑以下场景:

第六章 Git_远程仓库_08

你从master分支(的C2提交上)创建了一个server分支,你打算在这个server分支上开发server-side的新功能,并在server分支上进行了一次提交(C3),随后你又从server分支(的C3提交上)创建了另一个client分支,并在client分支上进行了两次提交(C8、C9)。最后,你切换回master分支,并做了两次提交(C5、C6)。

假设此时你想合并client分支上的提交到master主分支,以发布一个软件的发行版,但此时你又不想将server分支上的提交合并到master中。说得直白一点:范围就是client上的所有提交(C3、C8、C9),去掉该范围中同时也属于server的提交(C3),把剩下的提交(C8、C9)合并到master。

以下命令可以满足这个需求

git rebase --onto master server client

该命令的含义是:先找到client分支,然后找出client分支是从server分支的哪次提交创建出来的(就是C3提交),把从那次提交(C3提交)之后的所有提交合并到master分支最新提交的右边。听起来有点复杂,但是该命令确实很强大,效果如下:

第六章 Git_推送_09

现在你可以让master合并client分支了,这是一种“快速合并”

$ git checkout master

$ git merge client

第六章 Git_远程仓库_10

假设在server分支经过测试后,你决定把server分支也合并到master分支上。此时你还在master分支上,原本你需要先从master切换到server分支,然后在执行 git rebase master 来把server分支的提交合并到master分支上的,但通过以下命令,你就不需要先切换到server分支上:

git rebase master server

该命令会直接把server分支上的提交,放到master分支最新提交的右边:

第六章 Git_git_11

然后快速合并master和server:

$ git checkout master

$ git merge server

然后删除server和client分支。此时client和server的所有提交都在master分支上,server和client分支也就没有存在的价值了:

$ git branch -d client

$ git branch -d server

第六章 Git_推送_12

2.3 The Perils of Rebasing

git rebase也是有缺点的,不能滥用。总结一句话就是:

Do not rebase commits that exist outside your repository and that people may have based work on.

翻译版本一:git rebase命令最好在你的本地仓库中使用,不要让git rebase命令应用于远程仓库。

翻译版本二:你可以在本地仓库随意地对你本地的提交进行rebase,但是最好不要将远程仓库中以及存在的分支进行rebase操作。

翻译版本三:rebase只用于你的本地仓库。一旦提交推送到了远程仓库,就不要试图对已经push到远程仓库的提交进行rebase。

无论怎么翻译,都是那一个意思。如果你遵守这条准则,那么你就不会惹来什么麻烦。但如果你违背了这条准则,你的同事就会讨厌并鄙视你了。

让我们来看一个例子,看看违反这条准备回带来什么麻烦。

第一步:你克隆了远程仓库到本地,并且在本地仓库中做了一些提交

第六章 Git_推送_13

第二步:其他小伙伴也把该远程仓库克隆下来,也做了一些提交,这些提交中包含了合并分支的操作,然后将这些提交push到了远程仓库中。随后你把远程仓库的最新修改pull了下来,并且与你本地自己的分支进行了合并,此时模型看起来是这个样子:

第六章 Git_git_14

第三步:刚刚的那个小伙伴,决定使用rebase来替换merge,然后他又使用了 git push --force 来覆盖了远程仓库,随后你又从远程仓库pull了最新修改下来:

第六章 Git_git_15

这个rebase操作,实际上是丢弃了C4和C6提交,而C6提交恰恰是你C7提交所依赖的一个提交。此时问题就显现了出来:如果你此时进行pull操作,且为了把最新修改合并到你自己的分支中,你将会再做一次合并的提交(下图中的C8):

第六章 Git_推送_16

如果你运行git log命令,你将会看到上面这样的提交历史,你会发现有两个提交(C6和C4')有着相同的作者、日期和提交信息(因为C4'是由C4和C6 rebase 而生成的,C4'和C6的提交效果就是一样的),这会让人感到困扰。更糟糕的是,如果此时你进行了push操作,包含C4和C6的修改被推送到了远程仓库中,之前的那个小伙伴随后把包含C4和C6的修改pull了下来,就会发现他之前通过rebase丢弃掉了C4和C6,又死灰复燃了! 避免这样混乱的问题的关键在于:遵守上面那条准则!

当然,如果你发现你确实已经陷入这样的问题当中,也不是不能解决这个问题。参考 https://git-scm.com/book/en/v2/Git-Branching-Rebasing中的Rebase When You Rebase”。

2.4 Rebase vs. Merge

现在你已经学会了git merge和git rebase。这两个命令都是用来合并分支的,那么应该如何抉择呢?

如果“提交历史”是为了记录开发的过程实际发生了什么,就不应该使用git rebase。因为git rebase会掩盖开发过程中的事实。尽管git merge会产生一系列的看起来较为混乱的提交,但它却如实地记录了开发过程中实际发生了什么。

如果“提交历史”是为了描述你的项目是按什么逻辑一步一步进展的,那就不应该使用git merge,毕竟git merge会让提交历史显得混乱。在你开发的过程中,你可以会需要记录一些失误,bug修复之类的修改。但当你打算把你的成果向全世界展示的时候,你更应该展示出一个更加连贯的线性故事,故事中描述了如何从A步骤进展到B步骤,再从B步骤到C步骤的过程。此时就应该使用git rebase。这样项目的读者就会有一个清晰的脉络来理解项目的进展。

但是具体问题需要具体分析,在什么情况使用什么命令,merge 还会 rebae,最终取决于你和你的团队。

最佳实践:在push之前,通过rebase来让你的提交看起来清爽一些。同时永远不要把已经push到远程仓库的提交,再次进行rebase。

3. SSH协议