Git提交相关内容
在Git提交时,会保存一个提交对象,该对象包含一个指向暂存区内容快照的指针,包含本次提交作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交时是没有祖先,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。在使用git commit新建一个提交对象前,Git会先计算每个子目录的校验和,然后在Git仓库中将这些目录保存为树(tree)对象。之后Git创建的提交对象,除了包含相关提交信息,还包含指向这个树对象(项目根目录)的指针,如此,它就可以在将来需要的时候,重现此次快照的内容。作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针,即本次提交的对象会包含一个指向上次提交的对象的指针。
Git分支简介
Git分支本质仅仅是个指向commit对象的可变指针,所以创建一个分支,实际上便是创建了一个新的分支指针。Git会使用master作为分支的默认名字。在若干次提交后,就有一个指向最后一次提交对象的master分支,该分支在每次提交时都会自动向前移动。同时,Git仓库中会保存一个名为HEAD的文件,该文件实质上是一个特别的指针,该特别的指针指向当前正在工作中的本地分支的指针,而Git也正是通过该HEAD指针来知道当前处于哪个分支上工作。而由于Git中的分支实际上仅是一个包含所指对象校验(40个字符长度SHA-1字串)和的文件,所以创建和销毁一个分支非常廉价,因为新建一个分支就是向一个文件中写入41个字符(外加一个换行符)。
Git切换分支时注意事项
切换分支的时候应该保持一个清洁的工作区域,即暂存区域和工作目录里的修改都被提交到git仓库中了。如果这两个区域还有没有提交的修改,它会和即将检出的分支产生冲突从而阻止Git切换分支。
Git命令选项—merged和—no-merged
在当前分支中使用命令git branch --merged能够刷选出与当前分支已经合并的分支,使用命令git branch –no-merged能够刷选出没有与当前分支合并的分支。
好的Git开发方式
好的Git开发方式应该仅在master分支中保留完全稳定的代码(即已经发布或即将发布的代码)。与此同时,创建一个名为develop/next的平行分支专门用于后续的开发,或仅用于稳定性测试,当代码进入某种稳定状态,便可以把它合并到master分支。这样,在确保这些已完成的特性分支能够通过所有测试,并且不会引入更多的错误后,就可以并到主干分支中,等待下一次的发布。
特性分支
特性分支是指短期的,用来实现某单一特性或与其相关共组的分支。任何规模的项目中都可以使用特性分支,而且能够很好地帮助开发。
远程分支
远程分支(remote branch)是对远程仓库中的分支的索引。它们是一些无法移动的本地分支;只有在 Git 进行网络交互时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。远程分支的表示方法: (远程仓库名)/(分支名)。当我们从远程仓库中克隆一个git仓库时,Git会自动为你将此远程仓库命名为origin,并下载其中所有的数据,建立一个指向它的master分支的指针,在本地对应的名称为origin/master,但你无法在本地更改数据。接着,Git建立一个属于你自己的本地master分支,始于origin上master分支相同的位置,如此,便能开始工作。
跟踪远程分支
从远程分支checkout
出来的本地分支,称为_跟踪分支(tracking branch)_。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入git push
,Git 会自行推断应该向哪个服务器的哪个分支推送数据。反过来,在这些分支里运行git pull
会获取所有远程索引,并把它们的数据都合并到本地分支中来。克隆仓库时,Git会自动创建一个名为master的分支来跟踪origin/master,这也正是git
push和git pull开始就能正常工作的原因。当然,也可以设定为其他跟踪分支,命令格式为:git checkout –b <分支名> <远程名>/<分支名>。例如,$gitcheckout –b sf origin/serverfix。在1.6.2版本以上,可以用—track选项简化。例如,$git checkout --trackorigin/serverfix,即在本地创建一个名为serverfix的分支跟踪远程分支origin/serverfix。
删除远程分支
命令格式: git push <远程仓库名> :<本地分支名>。例如,$git pushorigin :serverfix,该命令即是删除本地的serverfix分支。
分支的衍合
把一个分支整合到另一个分支的办法有两种:merge 和 rebase(译注:rebase 的翻译暂定为“衍合”,大家知道就可以了。)。在本章我们会学习什么是衍合,如何使用衍合,为什么衍合操作如此富有魅力,以及我们应该在什么情况下使用衍合。
基本的衍合操作
请回顾之前有关合并的一节(见图 3-27),你会看到开发进程分叉到两个不同分支,又各自提交了更新。
图 3-27. 最初分叉的提交历史。
之前介绍过,最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)。如图 3-28 所示:
图 3-28. 通过合并一个分支来整合分叉了的历史。
其实,还有另外一个选择:你可以把在 C3 里产生的变化补丁在 C4 的基础上重新打一遍。在 Git 里,这种操作叫做_衍合(rebase)_。有了 rebase 命令,就可以把在一个分支里提交的改变移到另一个分支里重放一遍。
在上面这个例子中,运行:
$ git checkoutexperiment
$ git rebasemaster
First,rewinding head to replay your work on top of it...
Applying:added staged command
它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,然后以基底分支(也就是主干分支master)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3’),从而改写experiment 的提交历史,使它成为 master 分支的直接下游,如图 3-29 所示:
图 3-29. 把 C3里产生的改变到 C4 上重演一遍。
现在回到 master 分支,进行一次快进合并(见图 3-30):
图 3-30. master 分支的快进。
现在的 C3’ 对应的快照,其实和普通的三方合并,即上个例子中的 C5 对应的快照内容一模一样了。虽然最后整合得到的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来会更 清楚:仿佛所有修改都是在一根线上先后进行的,尽管实际上它们原本是同时并行发生的。
一般我们使用衍合的目的,是想要得到一个能在远程分支上干净应用的补丁 — 比如某些项目你不是维护者,但想帮点忙的话,最好用衍合:先在自己的一个分支里进行开发,当准备向主项目提交补丁的时候,根据最新的origin/master 进行一次衍合操作然后再提交,这样维护者就不需要做任何整合工作(译注:实际上是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址作一次快进合并,或者直接采纳你提交的补丁。
请注意,合并结果中最后一次提交所指向的快照,无论是通过衍合,还是三方合并,都会得到相同的快照内容,只不过提交历史不同罢了。衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一起。
有趣的衍合
衍合也可以放到其他分支进行,并不一定非得根据分化之前的分支。以图 3-31 的历史为例,我们为了给服务器端代码添加一些功能而创建了特性分支 server,然后提交 C3 和 C4。然后又从 C3 的地方再增加一个client 分支来对客户端代码进行一些相应修改,所以提交了 C8 和 C9。最后,又回到server 分支提交了 C10。
图 3-31. 从一个特性分支里再分出一个特性分支的历史。
假设在接下来的一次软件发布中,我们决定先把客户端的修改并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。这个时候,我们就可以把基于 server 分支而非 master 分支的改变(即 C8 和 C9),跳过 server 直接放到master 分支中重演一遍,但这需要用git rebase 的 --onto 选项指定新的基底分支master:
$ git rebase--onto master server client
这好比在说:“取出 client 分支,找出 client 分支和 server 分支的共同祖先之后的变化,然后把它们在master上重演一遍”。是不是有点复杂?不过它的结果如图 3-32 所示,非常酷(译注:虽然client 里的 C8, C9 在 C3之后,但这仅表明时间上的先后,而非在 C3 修改的基础上进一步改动,因为server 和client 这两个分支对应的代码应该是两套文件,虽然这么说不是很严格,但应理解为在 C3 时间点之后,对另外的文件所做的 C8,C9 修改,放到主干重演。):
图 3-32. 将特性分支上的另一个特性分支衍合到其他分支。
现在可以快进 master 分支了(见图 3-33):
$ git checkoutmaster
$ git mergeclient
图 3-33. 快进master 分支,使之包含 client 分支的变化。
现在我们决定把 server 分支的变化也包含进来。我们可以直接把 server 分支衍合到 master,而不用手工切换到 server 分支后再执行衍合操作 — gitrebase [主分支] [特性分支]命令会先取出特性分支server,然后在主分支master 上重演:
$ git rebase master server
于是,server 的进度应用到 master 的基础上,如图 3-34 所示:
图 3-34. 在master 分支上衍合 server 分支。
然后就可以快进主干分支 master 了:
$ git checkoutmaster
$ git mergeserver
现在 client 和 server 分支的变化都已经集成到主干分支来了,可以删掉它们了。最终我们的提交历史会变成图 3-35 的样子:
$ git branch-d client
$ git branch-d server
图 3-35. 最终的提交历史
衍合的风险
呃,奇妙的衍合也并非完美无缺,要用它得遵守一条准则:
一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。
如果你遵循这条金科玉律,就不会出差错。否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
在进行衍合的时候,实际上抛弃了一些现存的提交对象而创造了一些类似但不同的新的提交对象。如果你把原来分支中的提交对象发布出去,并且其他人更新下载后在其基础上开展工作,而稍后你又用git rebase 抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。
下面我们用一个实际例子来说明为什么公开的衍合会带来问题。假设你从一个中央服务器克隆然后在它的基础上搞了一些开发,提交历史类似图 3-36 所示:
图 3-36. 克隆一个仓库,在其基础上工作一番。
现在,某人在 C1 的基础上做了些改变,并合并他自己的分支得到结果 C6,推送到中央服务器。当你抓取并合并这些数据到你本地的开发分支中后,会得到合并结果 C7,历史提交会变成图 3-37 这样:
图 3-37. 抓取他人提交,并入自己主干。
接下来,那个推送 C6 上来的人决定用衍合取代之前的合并操作;继而又用 git push --force 覆盖了服务器上的历史,得到 C4’。而之后当你再从服务器上下载最新提交后,会得到:
图 3-38. 有人推送了衍合后得到的 C4’,丢弃了你作为开发基础的 C4 和 C6。
下载更新后需要合并,但此时衍合产生的提交对象 C4’ 的SHA-1 校验值和之前 C4 完全不同,所以 Git 会把它们当作新的提交对象处理,而实际上此刻你的提交历史 C7 中早已经包含了 C4 的修改内容,于是合并操作会把 C7 和C4’ 合并为 C8(见图3-39):
图 3-39. 你把相同的内容又合并了一遍,生成一个新的提交 C8。
C8 这一步的合并是迟早会发生的,因为只有这样你才能和其他协作者提交的内容保持同步。而在 C8 之后,你的提交历史里就会同时包含 C4 和 C4’,两者有着不同的 SHA-1 校验值,如果用git log 查看历史,会看到两个提交拥有相同的作者日期与说明,令人费解。而更糟的是,当你把这样的历史推送到服务器后,会再次把这些衍合后的提交引入到中央服务 器,进一步困扰其他人(译注:这个例子中,出问题的责任方是那个发布了 C6 后又用衍合发布 C4’ 的人,其他人会因此反馈双重历史到共享主干,从而混淆大家的视听。)。
如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些尚未公开的提交对象,就没问题。如果衍合那些已经公开的提交对象,并且已经有人基于这些提交对象开展了后续开发工作的话,就会出现叫人沮丧的麻烦。