分支简介
Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。
当使用git commit
进行提交操作时,Git会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象,它除了包含上面提到的那些信息外, 还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
现在,Git 仓库中有五个对象:三个blob 对象
保存着文件快照)、一个树对象
(记录着目录结构和 blob 对象 索引)以及一个提交对象
(包含着指向前述树对象的指针和所有提交信息)。
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是master
。 在多次提交操作之
后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。
分支创建
Git 是怎么创建新分支的呢?
Git只是为你创建了一个可以移动的新的指针。 比如,创建一个 testing
分 支,使用git branch
命令:
# 这会在当前所在的提交对象上创建一个指针
$ git branch testing
那么,Git 又是怎么知道当前在哪一个分支上呢?
Git有一个名为 HEAD
的特殊指针,指向当前所在的本地分支(将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在master
分支 上。 因为git branch
命令仅仅创建一个新分支,并不会自动切换到新分支中去。
可以使用git log
命令查看各个分支当前所指的对象。提供这一功能的参数是--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
分支切换
要切换到一个已存在的分支,你需要使用 git checkout
命令。 我们现在切换到新创建的testing
分支去:
# 这样 HEAD 就指向 testing 分支了
$ git checkout testing
那么,这样的实现方式会给我们带来什么好处呢? 现在不妨再提交一次:
$ vim test.rb
$ git commit -a -m 'made a change'
如图所示,testing
分支向前移动了,但是master
分支却没有,它仍然指向运行git checkou
t时所指的对象。 现在我们切换回 master
分支看看:
$ git checkout master
这条命令做了两件事。 一是使 HEAD 指回 master
分支,二是将工作目录恢复成 master
分支所指向的快照内容。 也就是说,你现在做修改的话,项目将始于一个较旧的版本。 本质上来讲,这就是忽略testing
分支所做的修改,以便于向另一个方向进行开发。
我们不妨再稍微做些修改并提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
现在,这个项目的提交历史已经产生了分叉(如图)。 因为刚才你创建了一个新分支,并切换过去进行了一些工作,随后又切换回 master
分支进行了另外一些工作。 上述两次改动针对的是不同分支:你可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。 而所有这些工作,你需要的命令只有branch
、checkout
和 commit
。
你可以简单地使用git log
命令查看分叉历史。运行git log --oneline --decorate --graph --all
,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。
$ git log --oneline --decorate --graph --all
创建并切换分支
$ git checkout -b iss53
# 它是下面两条命令的简写:
$ git branch iss53
$ git checkout iss53
合并分支
你可以运行你的测试,确保你的代码修改是正确的,然后将其合并回你的 master
分支来部署到线上:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在合并的时候,“fast-forward”
是指,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推 进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “fast-forward”
。
因为,master
分支所在提交并不是原本 iss53
分支所在提交的直接祖先,而是合并 hotfix
分支,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4
和 C5
)以及这两个分支的工作祖先(C2
),做一个简单的三方合并。
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。
删除分支
$ git branch -d hotfix
遇到冲突时的分支合并
有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们,在合并它们的时候就会产生合并冲突:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用git status
命令来查看那些因包含合并冲突而处于未合并状态的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
这表示HEAD
所指示的版本(也就是你的master
分支所在的位置,因为你在运行merge
命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。
在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这 些原本有冲突的文件,Git 就会将它们标记为冲突已解决。
如果你想使用图形化工具来解决冲突,你可以运行git mergetool
,该命令会为你启动一个合适的可视化合并工具(在这里 Git 使用opendiff
做为默认的合并工具),并带领你一步一步解决这些冲突:
$ git mergetool
# This message is displayed because 'merge.tool' is not configured.
等你退出合并工具之后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决。你可以再次运行git status来确认所有的合并冲突都已被解决。如果确定之前有冲突的的文件都已经暂存了,这时你可以输入git commit
来完成合并提交。
分支管理
查看当前所有分支的一个列表:
$ git branch
iss53
* master
testing
注意 master
分支前的*
字符:它代表现在检出的那一个分支(也就是当前 HEAD
指针所指向的分支)。 这意味着如果在这时候提交,master
分支将会随着新的工作向前移动。
要查看每一个分支的最后一次提交:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
--merged
与 --no-merged
这两个选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。
$ git branch --merged
iss53
* master
# 在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d删除掉;
# 因为你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。
$ git branch --no-merged
testing
# 这里显示的分支,因为它包含了还未合并的工作,使用 git branch -d 命令删除它时会失败;
# 如果真的想要删除分支并丢掉那些工作,可以使用 -D 选项强制删除它。
$ git branch -D
分支开发工作流
长期分支
Git 使用简单的三方合并,在整个项目开发周期的不同阶段,你可以同时拥有多个开放的分支,你可以定期地把某些特性分支合并入其他分支中。
比如只在 master
分支上保留完全稳定的代码,还有一些名为 develop
或者next
的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master
分支了。 这样,在确保这些已完成的特性分支(短期分支,比如之前的 iss53
分支)能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。
可以用这种方法维护不同层次的稳定性。 一些大型项目还有一个 proposed
(建议) 或 pu: proposed updates
(建议更新)分支,它可能因包含一些不成熟的内容而不能进入 next
或者 master
分支。
这么做的目的是使你的分支具有不同级别的稳定性。当它们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。
使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。
特性分支
特性分支是一种短期分支,它被用来实现单一特性或其相关工作。
当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中 —— 没有与服务器发生交互。
远程分支
远程引用是对远程仓库的引用(指针),包括分支、标签等等。
可以通过git ls-remote (remote)
来显式地获得远程引用的完整列表,或者通过git remote show (remote)
获得远程分支的更多信息。 然而,一个更常见的做法是利用远程跟踪分支。
远程跟踪分支是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。
它们以(remote)/(branch)
形式命名。 例如,如果你想要看你最后一次与远程仓库origin
通信时 maste
分支的状态,你可以查看 origin/master
分支。 你与同事合作解决一个问题并且他们推送了一个 iss53
分支,你可能有自己的本地 iss53
分支,但是在服务器上的分支会指向 origin/iss53
的提交。
假设你的网络里有一个在 git.ourcompany.com
的 Git 服务器。 如果你从这里克隆,Git 的 clone
命令会为你自动将其命名为 origin
,拉取它的所有数据,创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master
。 Git 也会给你一个与 origin
的 master
分支在指向同一个地方的本地 master
分支,这样你就有工作的基础。
(“origin”
是 git clone 时默认的远程仓库名字。 如果你运行 git clone -o booyah
,那么你默认的远程分支名字将会是 booyah/master
。)
为什么创建分支之后,别人更新了代码而本地不更新?
如果你在本地的 master
分支做了一些工作,然而在同一时间,其他人推送提交到 git.ourcompany.com
并 更新了它的 master 分支,那么你的提交历史将向不同的方向前进。 也许,只要你不与 origin
服务器连接,你 的 origin/master
指针就不会移动。
如果要同步你的工作,运行git fetch origin
命令。 这个命令查找 “origin” 是哪一个服务器(在本例中是 git.ourcompany.com
),从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master
指针指向新的、更新后的位置。
推送
当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 - 你必须显式地推送想要分享的分支。 这样,你就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。
如果希望和别人一起在名为serverfix的分支上工作,你可以像推送第一个分支那样推送它。运行git push (remote) (branch)
:
$ git push origin serverfix
这里有些工作被简化了。 Git 自动将 serverfix
分支名字展开为refs/heads/serverfix:refs/heads/serverfix
,那意味着,“推送本地的 serverfix
分支来更新远程仓库上的 serverfix
分支。”
你也可以运行git push origin serverfix:serverfix
,它会做同样的事-相当于它说,“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支” 可以通过这种格式来推送本地分支到一个命名不相同的远程分支。 如果并不想让远程仓库上的分支叫做 serverfix
,可以运行 git push origin serverfix:awesomebranch
来将本地的serverfix
分支推送到远程仓库上的awesomebranch
分支。
下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix
,指向服务器的 serverfix
分支的引用:
$ git fetch origin
要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。不会有一个新的 serverfix
分支 - 只有一个不可以修改的 origin/serverfix
指针。
可以运行 git merge origin/serverfix
将这些工作合并到当前所在的分支。 如果想要在自己的 serverfix
分支上工作,可以将其建立在远程跟踪分支之上:
$ git checkout -b serverfix origin/serverfix
这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix
。
跟踪分支
从一个远程跟踪分支检出一个本地分支会自动创建一个 跟踪分支(上游分支)。
跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入git pull
,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master
的 master
分支。 然而,你也可以设置其他的跟踪分支 - 其他远程仓库上的跟踪分支,或者不跟踪 master 分支。 最简单的就是之前看到的例子,运行 git checkout -b [branch] [remotename]/[branch]
。 这是一个十分常用的操作所以 Git 提供了 --track 快捷方式:
$ git checkout --track origin/serverfix
如果想要将本地分支与远程分支设置为不同名字,你可以轻松地增加一个不同名字的本地分支的上一个命令:
$ git checkout -b sf origin/serverfix
# 现在,本地分支 sf 会自动从 origin/serverfix 拉取
设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用-u
或--set-upstream-to
选项运行git branch
来显式地设置。
$ git branch -u origin/serverfix
- 上游快捷方式
当设置好跟踪分支后,可以通过@{upstream}
或@{u}
快捷方式来引用它。 所以在 master 分支时并且它正在跟踪origin/master
时,如果愿意的话可以使用git merge @{u}
来取 代git merge origin/master
。
查看设置的所有跟踪分支:
$ git branch -vv
- 注意:命令返回的值来自于你从每个服务器上最后一次抓取的数据。
这个命令并没有连接服务器,它只会告诉你关于本地缓存的服务器数据。 如果想要统计最新的领先与落后数字,需要在运行此命令前抓 取所有的远程仓库。可以像这样做:
$ git fetch --all; git branch -vv
拉取
当 git fetch
命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull
在大多数情况下它的含义是一个git fetch
紧接着一个 git merge
命令。
如果有一个设置好的跟踪分支,不管它是显式地设置还是通过 clone
或checkout
命令为你创建的,git pull
都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。
删除远程分支
$ git push origin --delete serverfix
变基
在 Git 中整合来自不同分支的修改主要有两种方法merge
以及 rebase
。
- 整合分支最容易的方法是
merge
命令。 它会把两个分支的最新快照(C3
和C4
)以及二者最近的共同祖先(C2
)进行三方合并,合并的结果是生成一个新的快照(并提交)。
- 还有一种方法:
rebase
。你可以提取在C4
中引入的补丁和修改,然后在C3
的基础上应用一次。 在 Git 中,这种操作就叫做变基
。
$ git checkout experiment
$ git rebase master
假设你希望将 client
中的修改合并到主分支并发布,但暂时并不想合并 server
中的修改,可以使用 git rebase
命令的 --onto
选项,选中在client
分支里但不在server
分支里的修改(即 C8
和C9
),将它们在 master
分支上重放:
$ git rebase --onto master server client
# 取出client分支,找出处于client分支和server分支的共同祖先之后的修改,
# 然后把它们在master分支上重放一遍
现在可以快进合并master分支了:
$ git checkout master
$ git merge client
接下来你决定将 server
分支中的修改也整合进来。 使用git rebase [basebranch] [topicbranch]
命令可以直接将特性分支(即本例中的 server
)变基到目标分支(即 master
)上。这样做能省去你先切换到 server
分支。
$ git rebase master server
$ git checkout master
$ git merge server
参考文献:https://www.git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere