Git分支管理
分支创建与切换
-
创建一个分支可以使用
git branch
命令,比如可以使用这个语句创建一个testing分支,这会在当前所在的提交对象上创建一个指针:1
git branch testing
-
创建完testing分支后,情况如下:
-
使用
git branch
创建好分支后并不会自动切换到新的分支,切换到一个分支,可以使用git checkout
命令,比如这条命令将会切换HEAD指针到testing分支:1
git checkout testing
-
HEAD指针指向了testing分支
-
当然,Git也可以在创建分支的同时切换到当前分支:
1
git checkout -b <branch-name>
分支提交
-
HEAD当前指向testing分支,那么现在更新一些内容然后进行提交:
1
2vim test.rb
git commit -a -m 'update testing' -
提交完成后,分支情况就变成现在这样:
-
如果现在再将HEAD移动回master分支:
1
git checkout master
这个时候,你的工作目录和你在开始修改tetsing分支之前一模一样。当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。 Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。
-
现在在master分支下再进行一次修改并提交:
1
2vim test.rb
git commit -a -m 'update master'
项目分叉历史
-
查看当前项目的分叉历史,可以使用
git log
命令进行查看,它会输出提交历史、各个分支的指向以及项目的分支分叉情况:1
2
3
4
5
6
7git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) update master
| * 87ab2 (testing) update testing
|/
* 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的分支实质上仅是包含所指对象校验和(长度为40的SHA-1值字符串)的文件,所以创建和销毁分支都可以做到非常的高效。
删除分支
-
使用带有
-d
的git branch
命令可以删除分支:1
git branch -d testing
合并分支
-
假设当前已有master分支如下:
-
现在,需要解决追踪系统的#53问题,新建了一个iss53分支:
1
2git branch iss53
git checkout iss53 -
现在在iss53分支进行了一些更改:
1
2vim index.html
git commit -a -m 'added a new footer [issue 53]'
快进式(fast-forward)合并
-
现在需要对主分支进行一次紧急修复,切换到master分支并进行一次热修复:
1
2
3
4git checkout master
git checkout -b hotfix
vim index.html
git commit -a -m 'fixed the broken email address' -
经过检验,热修复分支解决了问题,那么现在需要将master分支与hotfix分支进行合并:
1
2
3
4
5
6git checkout master
git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)由于你想要合并的分支
hotfix
所指向的提交C4
是你所在的提交C2
的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。
二路合并
- 二路合并是最简单的一种合并方式,将两个文件进行逐行比对,如果行内容有差异就报冲突,这时候就需要人工合并差异了。
三路合并(递归三路合并)
-
对于较少差异的分支,使用二路合并尚且可以接受,但对于较大差异的分支,使用二路合并就非常麻烦了。因此出现了三路合并方法:首先找到两个分支的共同父节点(Base),如果A分支对某内容进行了修改,B分支对这个内容未进行修改,那么会应用A分支的修改,反之应用B分支的修改。
-
回到之前的情景。解决完紧急问题后,需要恢复到之前的工作中,进行后续工作的提交。同时删除hotfix分支,因为已经完成了此次修复,不再需要:
1
2
3
4
5
6
7
8git branch -d hotfix
Deleted branch hotfix (3a0874c).
git checkout iss53
Switched to branch "iss53"
vim index.html
git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+) -
现在iss53问题已经被修复,需要与master分支进行合并:
1
2
3
4
5
6git checkout master
Switched to branch 'master'
git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+) -
因为master分支并非是iss53分支的父节点,Git会对这两个分支进行一次简单的三路合并。虽然此次合并写作’recursive’策略,但并非递归三路合并,在Git的输出中,三路合并与递归三路合并都是’recursive’策略,递归三路合并策略通常用于更加复杂的合并场景。
处理合并冲突
-
如果两个分支都对同一段内容做了更改,Git就无法完成合并操作,在合并时就会报合并冲突:
1
2
3
4git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.可以使用
git status
查看当前产生冲突而未合并状态的文件1
2
3
4
5
6
7
8
9
10
11git 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 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
1
2
3
4
5
6
7<<<<<<< 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
分支所指示的版本在=======
的下半部分。 为了解决冲突,你必须选择使用由=======
分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:1
2
3<div id="footer">
please contact us at email.support@github.com
</div> -
手动合并完成后,对冲突文件使用
git add
将其标记为冲突已解决。 -
如果想使用图形化界面来处理冲突操作,可以使用
git mergetool
,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突。退出工具后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决: 你可以再次运行git status
来确认所有的合并冲突都已被解决。
分支管理
-
使用不带参数的
git branch
命令可以查看当前分支的列表:1
2
3
4git branch
iss53
* master
testing其中,带有
*
的master分支代表当前HEAD指针所指向的分支。 -
如果需要查看每一个分支的最后一次提交,可以使用
git branch -v
命令:1
2
3
4git 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
筛选当前已合并或未合并到当前分支的分支:1
2
3git branch --merged
iss53
* master -
可以使用
git branch -d
删除分支,如果删除分支包含了还未合并的工作,则会删除失败:1
2
3git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.可以使用
-D
完成强制删除。
远程分支
-
假设有一个在
git.ourcompany.com
的 Git 服务器。 如果从这里克隆,Git 的clone
命令会为你自动将其命名为origin
,拉取它的所有数据, 创建一个指向它的master
分支的指针,并且在本地将其命名为origin/master
。 Git 也会给你一个与 origin 的master
分支在指向同一个地方的本地master
分支。 -
如果在本地的
master
分支做了一些工作,在同一段时间内有其他人推送提交到git.ourcompany.com
并且更新了它的master
分支,这就是说你们的提交历史已走向不同的方向。 即便这样,只要你保持不与origin
服务器连接(并拉取数据),你的origin/master
指针就不会移动。 -
如果要与远程仓库同步数据,运行
git fetch <remote>
命令(本例中为git fetch origin
)。 这个命令查找 ``origin’’ 是哪一个服务器(在本例中,它是git.ourcompany.com
), 从中抓取本地没有的数据,并且更新本地数据库,移动origin/master
指针到更新之后的位置。
变基
变基的使用
-
在 Git 中整合来自不同分支的修改主要有两种方法:
merge
以及rebase
。假设现在有两个分支分别提交了更新 -
如果使用
merge
合并两个分支,那么会产生一次新的快照。 -
使用另一种方法:提取C4中引入的补丁和修改,然后再C3的基础上应用一次,这种操作称为变基(rebase)。比如,检出experiment分支,将其变基到master分支上:
1
2
3
4git 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分支上,进行一次快进合并:
1
2git checkout master
git merge experiment此时的C4’快照就和使用merge合并产生的C5快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。例如向某个其他人维护的项目贡献代码时,首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到
origin/master
上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。 -
对于一个主题分支中再分出一个主题分支的提交,如下图结构
如果希望合并client到master分支,但不希望将server分支合并,此时就可以使用
--onto
选项,选中在client
分支里但不在server
分支里的修改(即C8
和C9
),将它们在master
分支上重放:1
git rebase --onto master server client
这条语句的意思是:取出
client
分支,找出它从server
分支分歧之后的补丁, 然后把这些补丁在master
分支上重放一遍,让client
看起来像直接基于master
修改一样。 -
那么现在可以使用快进合并合并master分支:
1
2git checkout master
git merge client -
如果现在需要将server分支也合并进来,使用变基操作,完成合并,然后删除多余的分支:
1
2
3
4git checkout master
git merge server
git branch -d client
git branch -d server
变基的问题
-
如果当别人正在基于分支进行开发,那么不应该将分支使用变基合并到主分支上。变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用
git rebase
命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。 -
比如,现在有一个公开仓库,你从当前仓库克隆到本地并在此基础上进行开发。
-
然后,某人向该仓库提交了一些修改,其中还包括一次合并。你抓取了远程分支上的修改,然后合并到你的本地分支上。
-
接下来,这个开发者又决定将合并操作回滚,更换为使用变基合并,并使用
git push --force
命令覆盖了服务器上的提交历史。然后你向服务器抓取更新,会发现多出来一些新的提交。 -
此时,如果你执行
git pull
命令提交,就会将已经抛弃的C4和C6快照恢复,最终仓库会如下图所示,变得混乱。
解决因变基产生的合并问题
-
假如真的遇到了类似的问题,可以使用一些操作解决。实际上,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 fetch
,再git rebase teamone/master
。
Learn Git Branching
- 在Learn Git Branching上,可以进行一部分git命令的教学实验,或者使用沙盒进行测试,通过具象化的图形可以帮助对Git操作的理解。