Git 保存的不是文件差异或者变化量,而只是一系列文件快照。
在 Git中提交时,会保存一个提交(commit)对象,它包含一个指向暂存内容快照的指针,作者和相关附属信息,以及一定数量(也可能没有)指向该提交对象直接祖先的指针。当使用 git commit 新建一个提交对象前,Git 会先计算每一个子目录(本例中就是项目根目录)的校验和,然后在 Git 仓库中将这些目录保存为树(tree)对象。之后 Git 创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。提交多次后的的Git对象数据可能如下:
Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动:
分支基本操作
创建分支
git branch testing
这会在当前 commit 对象上新建一个分支指针:
那么,Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针,指向当前所在分支:
切换分支
git checkout testing
运行 git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去。要切换到其他分支,可以执行 git checkout 命令:
git checkout -b branch 可以创建分支并切换到该分支上。
合并分支
git merge [branch]
如下命令先切换到master分支,然后将hotfix分支合并到master分支:
$ git checkout master
$ git merge hotfix
合并时出现了 "Fast forward"(快进)提示。由于当前 master 分支所在的 commit 是要并入的 hotfix 分支的直接上游,Git 只需把指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支,那么 Git 在合并两者时,只会简单地把指针前移,因为没有什么分歧需要解决,所以这个过程叫做快进(Fast forward)。
删除分支
git brach -d [branch]
现在hotfix分支已经没用,可以用git brach -d hotfix删除。
三方合并
如下图所示当把iss53合并到master时,进行了一次三方合并:
这次合并的实现,并不同于之前 hotfix 的并入方式。这一次,你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的 commit (C4)并非想要并入分支(iss53)的直接祖先,Git 不得不进行一些处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)和它们的共同祖先(C2)进行一次简单的三方合并计算。Git 没有简单地把分支指针右移,而是对三方合并的结果作一新的快照,并自动创建一个指向它的 commit。
如果你修改了两个待合并分支里同一个文件的同一部分,Git 就无法干净地把两者合到一起,需要人工介入。
查看分支
git branch
列出所有分支
分支式工作流程
长期分支
由于 Git 使用简单的三方合并,所以就算在较长一段时间内,反复多次把某个分支合并到另一分支,也不是什么难事。也就是说,你可以同时拥有多个开放的分支,每个分支用于完成特定的任务,随着开发的推进,你可以随时把某个特性分支的成果并到其他分支中。比如仅在 master 分支中保留完全稳定的代码,即已经发布或即将发布的代码。与此同时,他们还有一个名为 develop 或 next 的平行分支,专门用于后续的开发,或仅用于稳定性测试 —— 当然并不是说一定要绝对稳定,不过一旦进入某种稳定状态,便可以把它合并到 master 里。这么做的目的是拥有不同层次的稳定性:当这些分支进入到更稳定的水平时,再把它们合并到更高层分支中去。
特性分支
一个特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。该技术允许你迅速且完全的进行语境切换 —— 因为你的工作分散在不同的流水线里,每个分支里的改变都和它的目标特性相关,浏览代码之类的事情因而变得更简单了。你可以把作出的改变保持在特性分支中几分钟,几天甚至几个月,等它们成熟以后再合并,而不用在乎它们建立的顺序或者进度。
现在我们来看一个实际的例子。请看图 3-20,起先我们在 master 工作到 C1,然后开始一个新分支 iss91 尝试修复 91 号缺陷,提交到 C6 的时候,又冒出一个新的解决问题的想法,于是从之前 C4 的地方又分出一个分支 iss91v2,干到 C8 的时候,又回到主干中提交了 C9 和 C10,再回到 iss91v2 继续工作,提交 C11,接着,又冒出个不太确定的想法,从 master 的最新提交 C10 处开了个新的分支 dumbidea 做些试验。
现在,假定两件事情:我们最终决定使用第二个解决方案,即 iss91v2 中的办法;另外,我们把 dumbidea 分支拿给同事们看了以后,发现它竟然是个天才之作。所以接下来,我们抛弃原来的 iss91 分支(即丢弃 C5 和 C6),直接在主干中并入另外两个分支。最终的提交历史将变成图 3-21 这样:
远程分支
远程分支(remote branch)是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行 Git 的网络活动时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。
假设你们团队有个地址为 git.ourcompany.com 的 Git 服务器。如果你从这里克隆,Git 会自动为你将此远程仓库命名为 origin,并下载其中所有的数据,建立一个指向它的 master 分支的指针,在本地命名为 origin/master,但你无法在本地更改其数据。接着,Git 建立一个属于你自己的本地 master 分支,始于 origin 上 master 分支相同的位置,你可以就此开始工作:
要是你在本地 master 分支做了会儿事情,与此同时,其他人向 git.ourcompany.com 推送了内容,更新了上面的 master 分支,那么你的提交历史会开始朝不同的方向发展。不过只要你不和服务器通讯,你的 origin/master 指针不会移动:
可以运行 git fetch origin 来进行同步。该命令首先找到 origin 是哪个服务器(本例为 git.ourcompany.com),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到它最新的位置:
推送
git push (远程仓库名) (分支名)
要想和其他人分享某个分支,你需要把它推送到一个你拥有写权限的远程仓库。你的本地分支不会被自动同步到你引入的远程分支中,除非你明确执行推送操作。换句话说,对于无意分享的,你尽可以保留为私人分支,而只推送那些协同工作的特性分支。
如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行:
git push origin serverfix
意为“取出我的 serverfix 本地分支,推送它来更新远程仓库的 serverfix 分支”。接下来,当你的协作者再次从服务器上获取数据时 - "git fetch origin",他们将得到一个新的远程分支 origin/serverfix。值得注意的是,在 fetch 操作抓来新的远程分支之后,你仍然无法在本地编辑该远程仓库。换句话说,在本例中,你不会有一个新的 serverfix 分支,有的只是一个你无法移动的 origin/serverfix 指针。如果要把该内容合并到当前分支,可以运行 git merge origin/serverfix。如果想要一份自己的 serverfix 来开发,可以在远程分支的基础上分化出一个新的分支来:git checkout -b serverfix origin/serverfix。这会切换到新建的 serverfix 本地分支,其内容同远程分支 origin/serverfix 一致,你可以在里面继续开发了。
从远程分支检出的本地分支,称为跟踪分支(tracking branch)。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。反过来,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。在克隆仓库时,Git 通常会自动创建一个 master 分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。
删除远程分支
git push (远程仓库名) :(分支名)
如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的 master 分支(或任何其他存放稳定代码的地方),可以用这个语法来删除它。如果想在服务器上删除 serverfix 分支,运行下面的命令:
$ git push origin :serverfix
衍合
把一个分支整合到另一个分支的办法有两种:merge(合并) 和 rebase(衍合)。
当开发进程分叉到两个不同分支,又各自提交了更新:
之前介绍过,最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并:
其实,还有另外一个选择:你可以把在 C3 里产生的变化补丁重新在 C4 的基础上打一遍。在 Git 里,这种操作叫做衍合(rebase)。有了 rebase 命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。在这个例子里,可以运行下面的命令:
$ git checkout experiment
$ git rebase master
它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序施用每一个差异补丁文件:
现在,你可以回到 master 分支然后进行一次快进合并。
现在,合并后的 C3(即现在的 C3')所指的快照,同三方合并例子中的 C5 所指的快照内容一模一样了。最后整合得到的结果没有任何区别,但衍合能产生一个更为整洁的提交历史。
你可以经常使用衍合,确保在远程分支里的提交历史更清晰。比方说,某些项目自己不是维护者,但想帮点忙,就应该尽可能使用衍合:先在一个分支里进行开发,当准备向主项目提交补丁的时候,再把它衍合到 origin/master 里面。这样,维护者就不需要做任何整合工作,只需根据你提供的仓库地址作一次快进,或者采纳你提交的补丁。请注意,合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。
永远不要衍合那些已经推送到公共仓库的更新。
在衍合的时候,实际上抛弃了一些现存的 commit 而创造了一些类似但不同的新 commit。如果你把commit 推送到某处然后其他人下载并在其基础上工作,然后你用 git rebase 重写了这些commit 再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。