分支——你可以把你的工作从开发主线上分离开来,以免影响开发主线。但在很多版本控制系统中,常常需要完全创建一个源代码目录的副本,这样就显得略微低效,尤其对于大的项目,会耗费很多时间。
分支——Git 的‘必杀型特技’,正是这一特性使 Git 从众多版本控制系统中脱颖而出。Git 处理分支的方式难以置信的轻量,创建新分支几乎能在瞬间完成,且在不同分支间的切换也很便捷。
3.1Git 分支简介
如Git起步所说,Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。
假设现在我的工作目录里有三个将要被暂存的文件: README test.rb LICENSE。暂存操作会为每一个文件计算校验和,然后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区等待提交:
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
如图,进行git commit
提交操作后,Git 仓库中有五个对象:
三个 blob 对象——保存文件快照;
一个树对象——记录着目录结构和 blob 对象索引;
一个提交对象——包含着指向前述树对象的指针和所有提交信息;
做些修改该后再提交,这次产生的提交对象会包含一个指向2上次提交对象(父对象)的指针,首次提交无父对象。
Git 分支:其实本质上仅仅是指向提交对象的可变指针。Git 的默认分支名字是 ‘master’。每次提交,分支会自动向前移动,指向最后那个提交对象。
注:Git 的 ‘master’ 分支并不是一个特殊分支,与其它分支没有区别,只是 git init
默认创建它。
3.1.1 分支创建——`git branch [name]
执行 git branch testing
,会在当前所在的提交对象上创建一个指针:
特殊指针:HEAD
Git 有一个名为 HEAD 的特殊指针,它指向当前所在的本地分支(可将 HEAD 想象为当前分支的别名)。
命令行查看各个分支所指对象,参数为
--decorate
:
$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project
如图,‘master’ 和 ‘testing’ 分支均指向校验和以 f30ab 开头的提交对象。
3.1.2 分支切换——git checkout
执行 git checkout testing
:
此时再次修改文件并提交:
$ vim test.rb
$ git commit -a -m 'made a change'
如图:HEAD 分支指向的当前 ‘testing’ 分支随着提交操作自动向前移动,但 ‘master’ 分支没有,它仍然指向运行 git checkout
时所指的对象。现在切回 ‘master’ 分支查看 git checkout master
:
这条命令做了两件事:
- 使 HEAD 指回 ‘master’ 分支;
- 将工作目录恢复成 ‘master’ 分支所指向的快照内容。
即忽略了在 ‘testing’ 分支所做的修改,以便于向另一个方向进行开发。若 Git 不能干净利落地完成这个任务,它将禁止切换分支。
若此时再做修改并提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
现在这个项目的提交历史就产生了分叉。
命令行查看:
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
由于 Git 的分支实质上仅是包含所指对象校验和的文件,所以它的创建和销毁都异常搞笑。这也是 Git 与其它版本控制系统最鲜明的对比,同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础也是同样的简单和高效。
3.1.3 分支合并——git merge
若我在上述切换到 ‘testing’ 分支时,修改了 README 内容为: testing,切换到回 ‘master’ 分之后,修改了 README 内容为:master,此时分支为 ‘master’,若要合并两个分支,执行:git merge testing
:
如上,由于两个文件内内容不同,执行命令后,会提示解决 README 文件内的冲突,然后再提交。
3.1.4 分支删除——`git branch -d [name]
如上,合并了 ‘testing’ 和 ‘master’ 分支内容后,若你不再需要 ‘testing’ 分支,你可在任务追踪系统中关闭此项任务,并删除这个分支。
git branch -d testing
3.3 分支管理
-
git branch
: 不加任何参数运行时,会得到当前分支的一个列表
$ git branch
iss53
* master
testing
*字符后面的分支即为当前所在分支。
-
git branch -v
:查看每一个分支的最后一次提交
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
git branch --no-merged
:查看还未合并的分支git branch -d [name]
:删除分支,当删除的分支还未合并时,会删除失败,如果确定要丢失,可使用-D
选项强制删除。
3.4 远程分支
远程引用是对远程仓库的引用,包括分支、标签等。
git ls-remote
——显式获得远程引用的完整列表。
git remote show
——获得远程分支信息。
更常用的做法:远程跟踪分支
远程跟踪分支是远程分支状态的引用,像是你上次连接到远程仓库时,那些分支所处状态的书签。
它们以(remote)/(branch)形式命名。例如,当你从远程服务器克隆一个仓库时,Git 会自动将其命名为 ‘origin’,拉取它的所有数据,创建一个指向它的 ‘master’ 分支的指针,并且在本地将其命名为 ‘origin/master’,Git 也会给你一个与 ‘origin’ 的 ‘master’ 分支指向同一个地方的本地 ‘master’ 分支,这样你就有工作的基础。
git fetch origin
:查找 ‘orign’ 是哪一个服务器,从中抓取本地没有的数据,并且更新本地数据库,移动 ‘origin/master’ 指针指向新的、更新后的位置。
推送——git push (remote) (branch)
当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。
例如,要将内容推送到 ‘master’ 分支上,执行 git push origin master
如何避免每次输入密码:
如果你正在使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码。 默认情况下它会在终端中提示服务器是否允许你进行推送。Git 拥有一个凭证系统来处理这个事情:执行git config --global credential.helper store
,这种模式会将凭证用明文的形式存放在磁盘中,并且永不过期。这意味着除非你修改了你在 Git 服务器上的密码,否则你永远不需要再次输入你的凭证信息。这种方式的缺点是你的密码是用明文的方式存放在你的 home 目录下。
跟踪分支
- 从远程拉取本地分支:
从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”(有时候也叫做 “上游分支”)。跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入git pull
,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支。 然而,如果你愿意的话可以设置其他的跟踪分支 - 其他远程仓库上的跟踪分支,或者不跟踪 master 分支。 运行git checkout -b [branch] [remotename]/[branch]
。 这是一个十分常用的操作所以 Git 提供了 --track 快捷方式:git checkout [branch] [remotename]/[branch]
,你也可以将本地分支命名为其它名字。
- 从远程拉取本地分支:
- 本地已有分支跟踪:
git branch -u [remotename]/[branch]
或git branch --set-upstream-to [remotename]/[branch]
查看设置的所有跟踪分支:执行git branch -vv
这回将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个分支与本地分支是否是领先、落后或是都有。
如果想要统计最新的领先与落后数字,需要在运行此命令前抓取所有的远程仓库。 可以像这样做:
- 本地已有分支跟踪:
git fetch --all
git branch -vv
拉取
git fetch
: 令从服务器上抓取本地没有的数据,它并不会修改工作目录中的内容。 只会获取数据然后让你自己合并。
git pull
:大多数情况下,它是一个git fetch
紧接着一个git merge
,git pull
会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入哪个远程分支。
删除远程分支
例如想删除 ‘testing’ 分支:
git push origin --delete testing
这个命令做的只是从服务器上移除这个指针,Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。
3.5 变基
Git 中整合来自不同分支的修改主要有两种方法:merge
以及rebase
。
3.5.1 变基的基本操作
-
1.通过合并操作整合分叉的历史:
整合分支最容易的方法是 merge
命令。它会把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。
- 通过变基操作来整合分叉的历史:
过程:提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。
即使用rebase
命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。
- 通过变基操作来整合分叉的历史:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。
现在回到 master 分支,进行一次快进合并。
$ git checkout master
$ git merge experiment
此时,C4' 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一样了。 这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。
请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
变基的风险:不要对在你的仓库外有副本的分支执行变基
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。
如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase
命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
变基的原则:只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。