git push、rebase和merge

Pushing

当想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 - 必须显式地推送到想要分享的分支。 这样,就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。

如果希望和别人一起在名为serverfix的分支上工作,可以像推送第一个分支那样推送它。 运行 git push (remote) (branch):

$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
 * [new branch]      serverfix -> serverfix

Git 自动将serverfix分支名字展开为refs/heads/serverfix:refs/heads/serverfix,那意味着,“推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。” 我们将在Git 内部原理部分详细学习 refs/heads/部分。

也可以运行git push origin serverfix:serverfix,它会做同样的事 - 相当于它说,“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支” 可以通过这种格式来推送本地分支到一个命名不相同的远程分支。

如果并不想让远程仓库上的分支叫做serverfix,可以运行git push origin serverfix:awesomebranch来将本地的serverfix分支推送到远程仓库上的awesomebranch分支。

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支origin/serverfix ,指向服务器的serverfix分支的引用:

$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。 换一句话说,这种情况下,不会有一个新的serverfix分支 - 只有一个不可以修改的origin/serverfix指针。

可以运行git merge origin/serverfix将这些工作合并到当前所在的分支。 如果想要在自己的serverfix分支上工作,可以将其建立在远程跟踪分支之上:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

这会给你一个用于工作的本地分支,并且起点位于origin/serverfix。

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”(有时候也叫做 “上游分支”)。 跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

当克隆一个仓库时,它通常会自动地创建一个跟踪origin/master的master分支。 然而,如果你愿意的话可以设置其他的跟踪分支 - 其他远程仓库上的跟踪分支,或者不跟踪master分支。 最简单的就是之前看到的例子,运行git checkout -b [branch] [remote]/[branch]。 这是一个十分常用的操作所以 Git 提供了--track快捷方式:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

如果想要将本地分支与远程分支设置为不同名字,可以增加一个不同名字的本地分支的上一个命令:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

现在,本地分支sf会自动从origin/serverfix拉取。

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用-u或--set-upstream-to选项运行git branch来显式地设置。

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.

当设置好跟踪分支后,可以通过@{upstream}或@{u}快捷方式来引用它。 所以在master分支时并且它正在跟踪origin/master时,如果愿意的话可以使用git merge @{u}来取代git merge origin/master。

如果想要查看设置的所有跟踪分支,可以使用git branch的-vv选项。 这会将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
  master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  testing   5ea463a trying something new

这里可以看到iss53分支正在跟踪origin/iss53并且 “ahead” 是 2,意味着本地有两个提交还没有推送到服务器上。 也能看到master分支正在跟踪origin/master分支并且是最新的。 接下来可以看到serverfix分支正在跟踪teamone服务器上的server-fix-good分支并且领先3 落后 1,意味着服务器上有一次提交还没有合并入同时本地有三次提交还没有推送。 最后看到testing分支并没有跟踪任何远程分支。

这些数字的值来自于你从每个服务器上最后一次抓取的数据。 这个命令并没有连接服务器,它只会告诉你关于本地缓存的服务器数据。 如果想要统计最新的领先与落后数字,需要在运行此命令前抓取所有的远程仓库。 可以像这样做:

$ git fetch --all; git branch -vv

Pull

当git fetch命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作git pull,在大多数情况下它的含义是一个git fetch紧接着一个git merge命令。

如果有一个像之前章节中演示的设置好的跟踪分支,不管它是显式地设置还是通过clone或checkout命令创建的,git pull都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。

由于git pull经常令人困惑所以通常单独显式地使用fetch与merge命令会更好一些。

删除远程分支

假设你已经通过远程分支做完所有的工作了 - 也就是说你和你的协作者已经完成了一个特性并且将其合并到了远程仓库的master分支(或任何其他稳定代码分支)。 可以运行带有--delete选项的git push命令来删除一个远程分支。 如果想要从服务器上删除serverfix分支,运行下面的命令:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]         serverfix

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

变基

在 Git 中整合来自不同分支的修改主要有两种方法:merge以及rebase。 下面主要讲解“变基”,怎样使用“变基”。

变基的基本操作

回顾之前在 分支的合并 中的一个例子,你会看到开发任务分叉到两个不同分支,又各自提交了更新。

分叉的提交历史。
分叉的提交历史

之前介绍过,整合分支最容易的方法是merge命令。 它会把两个分支的最新快照( C3和C4 )以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。

通过合并操作来整合分叉了的历史。
通过合并操作来整合分叉了的历史

其实,还有一种方法:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次。 在 Git 中,这种操作就叫做 变基。 使用rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。在这个例子中,运行:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 、变基操作的目标基底分支 )的最近共同祖先 ,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 , 最后以此将之前另存为临时文件的修改依序应用。(译注:写明了 commit id,以便理解,下同)

将 `C4` 中的修改变基到 `C3` 上。
将C4中的修改变基到C3上

现在回到master分支,进行一次快进合并。

$ git checkout master
$ git merge experiment
master 分支的快进合并。
 master 分支的快进合并

此时,C4'指向的快照就和上面使用merge命令的例子中C5指向的快照一模一样了。

merge和rebase整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。

一般这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,首先在自己的分支里进行开发,当开发完成时需要先将开发的代码变基到origin/master上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

更有趣的变基例子

在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,也可以指定另外的一个分支进行应用。 就像从一个特性分支里再分出一个特性分支的提交历史中的例子那样。 你创建了一个特性分支(server) ,为服务端添加了一些功能,提交了C3和C4。 然后从C3上创建了特性分支client,为客户端添加了一些功能,提交了C8和C9。 最后,你回到server分支,又提交了C10 。

从一个特性分支里再分出一个特性分支的提交历史。
从一个特性分支里再分出一个特性分支的提交历史

假设你希望将client中的修改合并到主分支并发布,但暂时并不想合并server中的修改,因为它们还需要经过更全面的测试。 这时,你就可以使用git rebase命令的--onto选项,选中在client分支里但不在server分支里的修改(即C8和C9),将它们在master分支上重放:

$ git rebase --onto master server client

以上命令的意思是:“取出client分支,找出处于client分支和server分支的共同祖先之后的修改,然后把它们在master分支上重放一遍”。 这理解起来有一点复杂,不过效果非常酷。

截取特性分支上的另一个特性分支,然后变基到其他分支。
截取特性分支上的另一个特性分支,然后变基到其他分支

现在可以快进合并master分支了。(如图快进合并 master 分支,使之包含来自 client 分支的修改):

$ git checkout master
$ git merge client
快进合并 master 分支,使之包含来自 client 分支的修改。
快进合并 master 分支,使之包含来自 client 分支的修改

接下来你决定将server分支中的修改也整合进来。 使用git rebase [basebranch] [topicbranch]命令可以直接将特性分支(即本例中的 server)变基到目标分支(即master)上。这样做能省去你先切换到master分支,再对其执行变基命令的多个步骤。

$ git rebase master server

如图 将 server 中的修改变基到 master 上 所示,server中的代码被“续”到了master后面。

将 server 中的修改变基到 master 上。
将 server 中的修改变基到 master 上

然后就可以快进合并主分支 master 了:

$ git checkout master
$ git merge server

至此,client和server分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成图 最终的提交历史 中的样子:

$ git branch -d client
$ git branch -d server
最终的提交历史。
最终的提交历史

变基的风险

变基也并非完美无缺,要用它得遵守一条准则:不要对在你的仓库外有副本的分支执行变基。如果遵循这条金科玉律,就不会出差错。 

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容相似但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用git rebase命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

让我们来看一个在公开的仓库上执行变基操作所带来的问题。 假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。 你的提交历史如图所示:

克隆一个仓库,然后在它的基础上进行了一些开发。
克隆一个仓库,然后在它的基础上进行了一些开发

然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:

抓取别人的提交,合并到自己的开发分支。
抓取别人的提交,合并到自己的开发分支

接下来,这个人又决定把合并操作回滚,改用变基;继而又用git push --force命令覆盖了服务器上的提交历史。 之后你从服务器抓取更新,会发现多出来一些新的提交。

有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交。
有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交

结果就是你们两人的处境都十分尴尬。 如果你执行git pull命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:

你将相同的内容又合并了一次,生成了一个新的提交。
你将相同的内容又合并了一次,生成了一个新的提交

此时如果你执行git log命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。 此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 很明显对方并不想在提交历史中看到C4和C6,因为之前就是他把这两个提交通过变基丢弃的。

用变基解决变基

如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。

实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和—— 即 “patch-id”。

如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。

举个例子,如果遇到前面提到的 有人推送了经过变基的提交,并丢弃了你的本地开发所基于的一些提交 那种情境,如果我们不是执行合并,而是执行git rebase teamone/master, Git 将会:

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)

  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)

  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4')

  • 把查到的这些提交应用在teamone/master上面

从而我们将得到与 你将相同的内容又合并了一次,生成了一个新的提交 中不同的结果,如图 在一个被变基然后强制推送的分支上再次执行变基 所示。

在一个被变基然后强制推送的分支上再次执行变基。
 在一个被变基然后强制推送的分支上再次执行变基

要想上述方案有效,还需要对方在变基时确保 C4' 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。

在本例中另一种简单的方法是使用git pull --rebase命令而不是直接git pull。 又或者你可以自己手动完成这个过程,先git rebase teamone/master,再git fetch。

如果你习惯使用git pull,同时又希望默认使用选项--rebase,你可以执行这条语句git config --global pull.rebase true来更改pull.rebase的默认配置。 

只要你把变基命令当作是在推送前清理提交使之整洁的工具,并且只在从未推送至共用仓库的提交上执行变基命令,就不会有事。 假如在那些已经被推送至共用仓库的提交上执行变基命令,并因此丢弃了一些别人的开发所基于的提交,那就有大麻烦了。

如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行git pull --rebase命令,这样尽管不能避免伤痛,但能有所缓解。

变基 vs. 合并

至此,你已在实战中学习了变基和合并的用法。下面讨论一下提交历史到底意味着什么。

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用_谎言_掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。

另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。

 Git 允许对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。通过分别学习两者的用法可以根据实际情况作出明智的选择是用变基还是合并。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

总结

已经讲完了 Git 分支与合并的基础知识。 你现在应该能自如地创建并切换至新分支、在不同分支之间切换以及合并本地分支。 你现在应该也能通过推送你的分支至共享服务以分享它们、使用共享分支与他人协作以及在共享之前使用变基操作合并你的分支。 

posted @ 2023-08-24 21:19  luckylan  阅读(865)  评论(0)    收藏  举报