分布式版本控制系统 Git | 七
Git 分支
分支简介
提及 Git 分支的内容,我们要先理解 Git 保存的并不是文件的变化或者差异,而是一系列的不同时刻的文件快照。
Git 在进行提交操作的时候,会保存一个提交对象(commit object)。这个提交对象包含了一个指向暂存内容快照的指针,还包括提交者的姓名,邮箱,提交的信息以及指向它的父对象的指针。当然,首次提交产生的提交对象没有父对象。其他普通提交操作产生的对象都有一个父对象,当多个分支合并产生的提交对象则有多个父对象。
分支创建与合并
上面这篇文章中,我们提到每次提交,Git 会将它们串成一条时间线,这时间线就是一个分支。未新建分支之前,这个就是主分支,即是 master
分支。
Git 如何创建新分支?使用 git branch <branch_name>
,这里它会创建一个可以移动的新指针。比如创建一个 dev 分支:
$ git branch dev
这时,便会在提交对象上创建一个指针,如下图:
现在创建了一个分支,但是 Git 如何分辨两个分支,如何知道目前在哪个分支?这里要提及一个特殊指针 HEAD
。
HEAD
严格来说不是指向提交,默认情况下,而是指向 master
,而 master
或者新创建的 dev
才是指向提交,HEAD
指向的就是当前分支。刚才 git branch dev
仅仅是创建了一个新分支,并不会自动切换到新分支上去。
HEAD 指向的是当前所在的分支,可以使用命令 git log
查看各个分支当前所指的对象。配合参数 --decorate
:
$ git log --oneline --decorate
861b17e (HEAD -> master, dev) Initial commit of project
此时,master
和 dev
分支均指向校验和为 861b17e
开头的提交对象。
切换分支
切换已经存在的分支,可以使用 git checkout <branch>
或者 git switch <branch>
,这里使用的是 git switch
更容易理解:
$ git switch dev
Switched to branch 'dev'
这样 HEAD 就指向了 dev
分支。
上面是将创建跟切换操作分开。Git 还提供一个命令能够创建的同时切换到分支:
$ git switch -c iss007
或者
$ git checkout -b iss007
使用带 -c
参数的 git switch
命令,或者使用带 -b
参数的 checkout
命令。
其实上面的命令可以分解为:
$ git branch iss007
$ git switch iss007
$ git branch iss007
$ git checkout iss007
假设现在在 iss007
分支上工作,并做了提交。在这个过程中,iss007
分支会不断向前推进。
$ vim ISSUE
$ git commit -a -m "Fix issue 007 [issue 007]"
假设,这个时候有另外一个紧急的问题需要解决。使用 Git 这个时候不需要将这个问题跟 issue 007 混合在一起。也不用去还原关于 iss007 的修改,再添加关于这个紧急问题的修改,只需要需要回到 master
分支,创建新分支解决紧急问题。
这里需要注意,切换到 master
分支之前,需要留意工作目录和暂存区有没有未提交的文件,它会检测出分支冲突阻止切换分支(待会介绍这种情况)。这里假设工作目录是干净的:
$ git switch master
Switched to branch 'master'
$ git switch -c hotfix
Switched to a new branch 'hotfix'
$ vi ISSUE
$ git commit -a -m "fixed the hot issue"
现在紧急问题已经修复,这个时候就可以合并回 master 分支部署到线上,如下:
$ git switch master
$ git merge hotfix
Updating 861b17e..48a6cfa
Fast-forward
ISSUE | 1 +
1 file changed, 1 insertion(+)
create mode 100644 ISSUE
注意 Fast-forward
,这个表示的是“快速模式”,也就是直接将 master 指向 hotfix 的当前提交,合并速度快。
注意:,并不是所有的合并都能 Fast-forward
。
同时,我们可以删除 hotfix
分支,因为已经完成任务了:
$ git branch -d hotfix
Deleted branch hotfix (was 48a6cfa).
冲突解决
现在紧急的问题已经修复部署上线,可以回到 iss007
分支继续修改文件。假设 iss007
分支也经过修改解决了问题。
这个时候也考虑合并会 master 部署到线上:
$ git switch master
$ git merge iss007
CONFLICT (add/add): Merge conflict in ISSUE
Auto-merging ISSUE
Automatic merge failed; fix conflicts and then commit the result.
这个时候会出现合并冲突,因为 iss007 分支与 hotfix 分支修改的是同一个文件。Git 不会自动创建新的合并提交。需要手动解决冲突之后再提交。
使用 git status
查看处于未合并状态的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both added: ISSUE
no changes added to commit (use "git add" and/or "git commit -a")
现在查看冲突的文件:
<<<<<<< HEAD
Fix hot issue.
=======
Fix issue 007.
Fix issue continue and done.
>>>>>>> iss007
Git 用 <<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,修改内容如下:
Fix hot issue.
Fix issue 007.
Fix issue continue and done.
将标记的部分删除,根据需求解决冲突,然后用 git add
将其标记为冲突已解决。
$ git add ISSUE
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: ISSUE
这个时候就可以提交部署了:
git commit -a -m "Merge branch 'iss007'"
[master 7059dd0] Merge branch 'iss007'
分支管理策略
git branch
除了创建和删除分支。不加参数时,命令能够得到所有分支的列表:
$ git branch
dev
iss007
* master
*
号表示当前的分支(即使 HEAD
指针指向的分支)。
加上 -v
参数也可以查看每个分支最后的提交信息:
$ git branch -v
dev 861b17e Initial commit of project
iss007 5267039 Fix issue 007 again
* master 7059dd0 Merge branch 'iss007'
git branch
命令还有两个选项 --merged
和 --no-merged
。这两个选项分别过滤合并或尚未合并到当前的分支。
例如:查看已经合并到当前的分支:
$ git branch --merged
iss007
* master
因为已经合并过 ·iss007`,所以这个时候也可以删除该分支,这里并不会失去任何东西:
$ git branch -d iss007
Deleted branch iss007 (was 5267039).
查看所有包含未合并工作的分支,可以使用 git branch --no-merged
:
$ git branch --no-merged
dev
这里显示了 dev 分支,它包含了还未合并的工作,现在尝试使用删除命令会运行失败:
$ git branch -d dev
error: The branch 'dev' is not fully merged.
If you are sure you want to delete it, run 'git branch -D dev'.
如果真的想要删除这些未合并的工作,可以根据下面的提示,使用 -D
选项强制删除。
抓取分支
多人协作的情况下,大家都会往 master
和 dev
分支推送各自的修改。
但是当协作者修改的是同个文件,且先一部提交到远程仓库时,这个时候,当你修改完问题提交的时候,就会提示冲突。
解决办法:用 git pull
抓取文件,在本地合并解决冲突再推送即可。
这里参考上面冲突解决的步骤就可以。
变基(Rebase)
每次合并再 push 后,分支会变得混乱。
Git 有一种成为 rebase 的操作,能够让 Git 的提交历史变成直线。
这里我用之前学习的例子来加以说明。
和远程分支同步后,对文件做了两次提交,用 git log
查看:
* 24be579 (HEAD -> master) add author
* 94448fe add comment
* 6a7291a store Git Learn
* e36f4e9 (origin/master) add content about co-operative and update catalog
* 6cd4087 add knowledge about feature branch
注意到 Git 用 (Head -> master)
和 (origin/master)
标识当前分支的 HEAD 和远程 origin 的位置分别是 24be579 add author
和 e36f4e9 add content..
,本地比远程分支快 3 个提交
现在尝试推送本地分支:
$ git push origin master
To github.com:username/git_learn.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'git@github.com:username/git_learn.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
失败了,说明有人先往远程库推送了分支。先 pull 一下
$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:damengsanqianqiu/git_learn
e36f4e9..538bd7c master -> origin/master
CONFLICT (add/add): Merge conflict in hello.py
Auto-merging hello.py
Automatic merge failed; fix conflicts and then commit the result.
这里自动合并失败,手动解决冲突(详情略)。
解决冲突,再提交。再用 git status
查看状态
$ git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
加上刚才合并的提交,现在我们分支比远程分支超前 4 个提交。
用 git log
看看
$ git log --graph --pretty=oneline --abbrev-commit
* 2027993 (HEAD -> master) fix conflict of hello.py
|\
| * 538bd7c (origin/master) set exit = 1
* | 24be579 add author
* | 94448fe add comment
* | 6a7291a store Git Learn
|/
* e36f4e9 add content about co-operative and update catalog
现在分支比较乱,这个时候,rebase 就派上用场了,用 git rebase
试试:
$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
再用 git log
看看,
$ git log --graph --pretty=oneline --abbrev-commit
* 2027993 (HEAD -> master) fix conflict of hello.py
* 24be579 add author
* 94448fe add comment
* 6a7291a store Git Learn
* 538bd7c (origin/master) set exit = 1
原来分叉的提交现在变成一条直线。我们注意到 Git 把我们本地的提交“挪动”了位置,放到了 538bd7c (origin/master) set exit = 1
之后,这样整个提交历史就成了一条直线。rebase 操作前后,最终的提交内容是一直的,但是,本地的 commit 修改内容已经变化了,他们的修改不再基于 e36f4e9 add content...
,而是基于 538bd7c (origin/master) set exit = 1
,但最后的提交 2027993
内容是一致的。
这就是 rebase 操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点就是本地的分叉提交已经被修改过了。
关于变基与合并,这里会有不同的观点,有的觉得提交历史就是记录发生过什么,使用变基会将痕迹抹除,另外的观点会觉得提交历史就是项目过程发生的事情,没人在意第一版的手稿,大部分手册也是多次修订后才能方便使用。
这里并没有必要追究一个完整简单的答案。需要根据实际情况去判断作出选择。
不过,总体的原则是,尚未推送或分享的本地修改,可以根据情况执行变基清理历史,已推送到别处的提交,不要执行变基操作。
以上就是本篇的主要内容。
目前,关于 Git 的介绍就已经算是更新完成。但是,这些内容还远远没有将 Git 讲透彻。但是这些内容能够让你简单了解 Git,也希望这些内容能够让你上手学习,慢慢了解。
这里若是希望更深入了解 Git 的使用可以查看官方文档:
https://git-scm.com/book/en/v2
欢迎关注微信公众号《书所集录》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。