Git正解 脱水版 【3. Git分支】

3.1 基础知识

重申一遍,Git不会直接保存,包含变更的文件,而是存储文件的变更差异,这将极大优化版本控制系统,对存储空间的需求,当用户提交时,Git将保存这个提交对象,该提交对象包含了一个指针,指向用户暂存区的所有数据,同时提交对象中,还包含了作者名,邮件地址,提交描述,以及另一个指针,指向当前提交的上一次提交(即父提交),如果当前提交没有父提交,则为初始提交,有一个父提交,则为正常提交,如果存在多个父提交,当前提交则为多个分支的合并提交。

假定开发目录中有三个文件,将其暂存并提交,进入暂存区时,将会计算每个文件的校验码,校验码将作为Git仓库中,每个文件的标识,

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'

当用户提交时,Git将计算目录的校验码,并保存到仓库,这类校验码将视为一个目录树对象,之后Git将创建一个提交对象,如上所述,因此Git仓库将保存5个对象,3个blob对象(3个文件的变更数据),1个tree对象(目录信息,blob对应的文件名),1个指针,指向根目录树,以及提交对象,
在这里插入图片描述
持续提交之后,将得到一个提交链表,
在这里插入图片描述
Git分支可视为一个可变指针,并指向一个提交,Git的默认分支为master,通常情况下,在首次提交时,用户都能见到master分支,而用户的后续提交,都将导致master分支自动前移,指向最新提交,注意,master分支并不具有特殊的意义,只是git init会使用默认名master,初始化分支,而大多数用户懒得修改而已。

创建分支

以下创建一个新分支testing,

$ git branch testing

在这里插入图片描述
Git必须依靠一个特殊指针HEAD,才能知道用户处于哪个分支,注意,HEAD在其他VCS系统中,含义不同,在Git中,HEAD指针可标识,用户当前所处的本地分支,如果用户已进入master分支,使用git branch,只能创建一个新分支,无法切换到新分支,
在这里插入图片描述
使用git log附带–decorate选项,可查看HEAD指针指向的当前分支,

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project

可见master和testing指针,均指向f30ab提交。

切换分支

使用git checkout,切换到testing分支,

$ git checkout testing

在这里插入图片描述
已进入testing分支,再进行一次提交,

$ vim test.rb
$ git commit -a -m 'made a change'

在这里插入图片描述
如图所示,testing分支已前移,而master分支保持不变,再使用git checkout master,切换到master分支,该命令实现了两个操作,第一,将HEAD指针移动到master指针位,其二恢复工作区,与快照(即master所指向的提交记录)关联的工作区,这等同于返回到,testing分支之前的分离点,注意,切换分支,会改变工作区的内容,如果切换到旧分支,工作区将恢复成,旧分支最后一次提交的工作区环境,如果Git无法恢复工作,分支切换也将失败。

进入master分支后,再进行一次提交,

$ vim test.rb
$ git commit -a -m 'made other changes'

在这里插入图片描述
此时项目提交历史已出现分叉,当用户创建并切换到新分支,完成相应的开发后,可返回主分支,继续之前暂停的开发,不同分支的变更相互独立,除了在不同分支之间切换,用户还可以合并不同的分支,而所有的操作只需三个命令,branch,checkout,commint。

使用git log --oneline --decorate --graph --all,可显示所有提交,同时可查看分支指针的位置,提交历史的分离点。

$ 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中,得到了完美的演绎,使用git checkout -b <分支名>,可创建一个分支,并切换到分支。

3.2 分支合并

假定用户在主分支master中,已完成一组提交,
在这里插入图片描述
这时用户准备修补53号问题,可创建一个分支,名称即为问题号,

$ git checkout -b iss53
Switched to a new branch "iss53"

在这里插入图片描述
尝试修复问题,将初始修改,提交到iss53分支,

$ git commit -a -m 'added a new footer [issue 53]'

在这里插入图片描述
此时又出现一个需要立即修复的问题,同时用户无法在iss53分支中,修复这个新问题,在Git中,用户无需手动进行一大堆恢复操作,以返回iss53之前的工作环境,只需切换到master分支。

注意,如果工作区或暂存区存在未提交的变更,那么分支切换将失败,所以在切换分支之前,用户应当整理出一个干净的工作场景,后续将介绍,Git提供的几种整理方法,假定所有变更都已提交,顺利切换到master分支。

此时已经回到修复53号问题之前的工作环境,注意,当完成分支切换后,Git会恢复之前工作区,对应于当前分支的最后一次提交,同时Git会使用添加,删除,修改等方法,尽可能恢复之前的工作区,之后,用户需创建一个hotfix分支,用于修复新问题,

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
1 file changed, 2 insertions(+)

在这里插入图片描述
运行测试,保证补丁能正常工作,再将hotfix分支,合并到master分支,

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
    index.html | 2 ++
    1 file changed, 2 insertions(+)

在这里插入图片描述
合并的结果,master和hotfix指向同一个提交,这时hotfix分支已完成使命,可以删除,

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

之后返回iss53分支,继续53号问题的修复,

$ 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(+)

在这里插入图片描述
由于hotfix分支并未包含iss53分支的变更,因此可选择将master合并到iss53分支,也可等待时机,将iss53合并到master。

分支合并

假定53号问题已被修复,可将iss53分支,合并到master,

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

此时与hotfix合并稍有不同,因为iss53和master的分离点,在master指针之后,所以不存在直接的继承关系,这种情况下,Git将进行三方合并,即两个分支的最新提交,与两分支的公共祖先,实现三方合并,
在这里插入图片描述
基于三方合并,Git将自动生成一个提交快照,即一个合并提交,所以它拥有多个父提交,
在这里插入图片描述
完成合并后,iss53分支也不再需要,可以删除,

$ git branch -d iss53

合并冲突

有时合并并不顺利,如果需合并的两条分支,修改了同一文件的相同位置,Git将无法合并,假设iss53和hotfix出现冲突,

$ 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 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 protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div>
>>>>>>> iss53:index.html

=======上部是HEAD指向的当前分支,所包含的文本,下部是iss53包含的文本,用户只能选择其一,<<<<<<<, =======, >>>>>>>都是冲突标记,可手动删除,当用户完成所有冲突文件的修改后,可使用git add,重新暂存冲突文件,等待Git解析。另外Git还提供一个图形工具,用于冲突修改,可运行git mergetool,修改步骤与命令行操作类似。

再次运行git status,确认所有冲突文件,已被Git解析,

$ git status
On branch master
All conflicts fixed but you are still merging.
    (use "git commit" to conclude merge)
Changes to be committed:
    modified: index.html

当所有冲突文件已被暂存,可运行git commit,完成合并操作,默认的提交描述,如下,

Merge branch 'iss53'
Conflicts: index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#

如果方便其他人查阅本次合并,用户可在提交描述中,加入更多的细节,比如合并的处理过程,处理冲突的理由。

3.3 分支管理

git branch可创建和删除分支,如果不附带任何选项,可列出当前的所有分支,

$ git branch
  iss53
* master
  testing

*表示当前分支,即HEAD指向的分支,如果附带-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

附带–merged和–no-merged选项,可列出已合并和未合并到当前分支的分支,

$ git branch --merged
  iss53
* master
$ git branch --no-merged
  testing

已合并分支通常可以直接删除,如果删除未合并分支,将会失败,并输出错误信息,

$ git 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选项,可强制删除未合并分支,如果–merged和–no-merged之后,未附加提交名或分支名,均默认为当前分支,而附加的提交名或分支名,便于查询其他分支的合并信息,比如查看master分支的未合并分支,

$ git checkout testing
$ git branch --no-merged master
topicA
featureB

3.4 分支工作流

长期分支

由于Git采用了简单的三方合并机制,使得分支之间的合并异常简单,这意味着,在开发周期的不同阶段,用户都可拥有多个分支,并能定期进行合并,多数Git用户都有一个相似的工作流,比如master分支只包含稳定代码,也可能只包含发行版本,同时会包含一个平行分支develop或next,用于开发或稳定性测试,其中的代码不要求稳定,如果一旦稳定,将合并到master分支,当局部(特性)分支或短期分支已通过测试,且确认无错时,也可合并到develop分支,事实上,稳定分支总处于提交历史的后端,而前沿分支比较靠前,
在这里插入图片描述
同时还能维护不同等级的稳定性,比如有些大型项目还会提供一个proposed或pu分支,用于合并一些无法放入next或master的分支,因此多个分支则具有不同等级的稳定性,分支一旦超过稳定级别,将被合并到更高级的分支中,实际上无需提供多个长期分支,但它们有助于大型项目或复杂项目的管理。

特性分支

特性分支适用于任何项目,同时特性分支都是短期分支,用于实现单一功能,比如之前的iss53和hotfix分支,注意,此处提及的合并,只涉及本地仓库,与远程仓库无关。

3.5 远程分支

远程引用是远程仓库所包含的指针,它可指向分支,标签等,使用git ls-remote [远程仓库名]或git remote show [远程仓库名],可列出所有远程分支,所包含的所有引用,更常见的用法,则是使用远程分支的本地副本,查看这些信息。

远程分支的本地副本,即为远程分支的镜像,用户无法移动本地副本,Git需要使用这些本地副本,实现与远程仓库的同步,因此它们更像是书签,可提醒用户,远程仓库所包含的远程分支,以及访问远程分支的最新时间。

远程分支的本地副本,可命名为<远程仓库名>/<远程分支名>,如果查看远程仓库origin中master分支的最新访问时间,可使用名称origin/master,如果用户需协作,解决一个开发问题,同时协作者已推送了iss53分支,而用户在本地仓库也创建了iss53分支,那么远程仓库的同名分支,则描述为origin/iss53。举例说明,假定存在一个远程仓库git.ourcompany.com,用户已克隆该仓库,Git会自动将仓库副本,放入本地目录origin,同时生成master指针,指向本地的origin/master分支,之后Git会将用户的本地master指针,也指向origin/master的master指针位,即为Git自动合并,这时用户可进行后续开发。

注意,origin与master一样,不具有任何特殊的意义,只是大多数用户已经习惯了这类默认命名,运行git clone时,将生成一个默认的origin本地目录,如果运行git clone -o booyah,origin将被替换为booyah。
在这里插入图片描述
当用户在本地master分支上进行开发时,其他协作者向远程仓库git.ourcompany.com的master分支,推送了数据,这时用户的仓库副本与远程仓库之间,存在不同步,
在这里插入图片描述
这时用户首先需要与远程仓库保持同步,运行git fetch origin,从远程仓库获取最新数据,
在这里插入图片描述
假定用户需要添加另一个远程仓库git.team1.ourcompany.com,运行git remote add,添加给远程仓库,并命名为teamone,运行git fetch teamone,以获取teamone对应远程仓库的所有数据,同时本地多出一条跟踪分支teamone/master,
在这里插入图片描述

推送分支

如果用户需要共享一条本地分享,则必须推送到远程仓库,同时用户应当具有远程仓库的写入权限,本地分支不会自动同步到远程仓库,因为用户未必希望共享所有的本地分支,如果用户需要共享一条本地分支serverfix,可使用git push <远程仓库名> <本地分支名>,

$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix

Git可自动将serverfix扩展为refs/heads/serverfix:refs/heads/serverfix名称,其意为,将本地分支serverfix推送到远程仓库的serverfix分支,refs/heads/的含义,后续将介绍,运行git push origin serverfix:serverfix,也能得到相同的结果,但是有了这个命令根式,用户能在推送本地分支时,重新定义相应远程分支的名称,运行git push origin serverfix:awesomebranch,其意为,将本地分支serverfix推送到远程仓库的awesomebranch分支。

注意,如果用户使用Http协议,进行数据推送,Git服务器将询问用户名和登录密码,当用户输入信息后,服务器将告知,是否运行推送,如果用户不想每次推送,都输入用户名和登录密码,可设置credential cache,也就是将多个推送,都保存在内存中,使用git config --global credential.helper cache,一次性完成多个推送。

之后项目协作者可从远程仓库,获取serverfix分支,并得到本地分支origin/serverfix,

$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
* [new branch] serverfix -> origin/serverfix

注意,用户获取的远程分支,是一个不可编辑的副本,虽然用户得到了一个serverfix分支,但只是一个无法移动的origin/serverfix指针,为了将远程分支,合并到用户的当前分支,可运行git merge origin/serverfix,如果用户希望基于远程分支的本地副本origin/serverfix,创建一个属于自己的本地分支serverfix,可运行,

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

这时用户已得到一个本地分支,起点为origin/serverfix的指针位。

跟踪分支

基于远程分支的本地副本,创建一个本地分支(见上例),可称之为跟踪分支,间接关联到远程分支,如果在跟踪分支中,运行git pull,Git会自动获取对应的远程分支,再合并到跟踪分支。

当用户完成远程仓库的克隆,Git将自动创建master分支,用于跟踪,远程分支的本地副本origin/master,如果用户希望配置其他的跟踪分支,或者修改跟踪分支的跟踪对象,可使用git checkout -b <本地分支名> <远程仓库名>/<远程分支名>,同时Git提供了一个等价选项–track,

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

还有更简单的方法,如果用户切换的分支名,并不存在,同时用户输入的分支名,正好匹配远程仓库的分支名,Git将自动创建一个跟踪分支,如下,

$ git checkout serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

如果用户需要自定义跟踪分支的名称,可使用

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

如果用户需要修改跟踪分支,可使用以下原型,假设<本地跟踪分支名>未输入,则使用当前分支,
git branch (–set-upstream-to=<远程分支名> | -u <远程分支名>) [<本地跟踪分支名>]

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.

假设存在一个跟踪分支,该跟踪分支对应的远程分支的本地副本,可使用标识@{upstream}或简写为@{u},如果跟踪分支为master,远程分支的本地副本为origin/master,那么git merge @{u}等价于git merge origin/master。

如果想查看所有的本地分支,可使用git branch -vv,输出将显示所有分支的相关信息,比如对应的远程分支,与远程分支的同步状态,

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] forgot the brackets
  master    1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  testing   5ea463a trying something new

*表示当前所处分支,ahead 2是指,本地有两个提交,未推送的远程分支,behind 1是指,有一个来自远程分支的提交,还未合并,同时testing分支未跟踪远程分支。

应当注意,以上记录的基准点,远程分支的上一次获取,因此上述命令只会比较,远程分支的本地副本,而无法直接比较远程分支,所以首先进行远程分支的同步,再查看本地分支的状态,结果将会更加准确,比如

$ git fetch --all; git branch -vv

pull命令

git fetch可获取远程仓库的同步数据,且不会对工作区产生影响,同时该命令所获的同步数据,必须由用户手动合并,另外还有一个类似命令git pull,等同于git fetch加git merge,即git pull将比较远程分支与本地副本之间的差异,并同步这些差异,再进行本地合并。

删除远程分支

假定一个远程分支,已经合并到远程master分支,则可使用git push --delete,删除远程分支,如下,

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]  serverfix

Git服务器中,删除分支等同于移除分支指针,分支数据还将继续保存,直到垃圾收集操作启动,如果分支被误删除,这类机制更易于恢复。

3.6 衍合

Git中分支间的变更集成,有两种方法,合并和衍合。

基本操作

使用之前合并的例子,两个不同分支上,都有提交,
在这里插入图片描述
最简单的方法,使用三方合并,基于两个最新递交C3和C4,以及一个最近的共同祖先C2,生成一个新提交C5,
在这里插入图片描述
另一种方法,从C4中,导出变更补丁,添加到C3中,这被称为衍合,即获取一条分支中,提交包含的所有变更,并将其,应用到其他分支,比如在示例中,先切换到experiment分支,将其衍合到master分支,

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

上述操作将从,拥有共同祖先的两个分支开始,一条分支为衍合本体,另一条分支为衍合宿主,首先获取衍合本体分支中,所有提交的所有变更,将这些变更保存成临时文件,切换到衍合宿主分支,将临时文件的所有数据,逐个合并到宿主分支。
在这里插入图片描述
衍合本体分支experiment基于所有的提交变更C4,生成临时文件,同时删除C4,再指向宿主分支的最新提交C3,临时文件将与C3进行比较,保持不存在冲突,再由临时文件生成一个新提交C4‘,切换到master,合并experiment分支,使master指针指向C4’提交,

$ git checkout master
$ git merge experiment

在这里插入图片描述
衍合的结果C4’,等同于三方合并的结果C5,但是衍合能获得一个更简明的提交历史,虽然看上去是一个线性历史,但实际上存在并行开发的过程,通常情况下,衍合是为了得到一个简明的远程分支,同时用户只是开发协作者,并非远程仓库的维护者,所以用户在本地分支上完成开发后,可衍合到远程仓库的本地副本origin/master,之后可将衍合结果(即C4‘),发送给维护者,而维护者无需进行再次合并,只需推送C4’,应当注意,无论是合并还是衍合,最终结果都一样。

有趣的衍合

首先用户可以实现多个分支的重复衍合,而不仅仅针对一个目标分支,比如下例,基于之前的提交历史,用户增加一个server分支,用于开发服务端的功能,并完成了几次提交,之后用户又增加了一个client分支,用于开发客户端的功能,也完成了几次提交,
在这里插入图片描述
假定用户需要将client分支,合并到主分支master,同时保留server分支,便于今后的测试,由于server和client的变更并无重叠,可将C8,C9衍合到master分支,使用git rebase --onto,

$ git rebase --onto master server client

将C8和C9的变更,生成两个临时文件,删除C8和C9,由于衍合到master,两个临时文件需逐个比对,共同祖先C2之后的所有提交,以保证不存在冲突,基于临时文件,生成两个新提交C8‘和C9’,切换到master,合并C8‘和C9’,

$ git checkout master
$ git merge client

在这里插入图片描述
此时server分支通过测试,也需要衍合到master,操作步骤与client分支相似,

$ git rebase master server
$ git checkout master
$ git merge server

由于server和client分支已衍合到master,这两个特性分支可以删除,

$ git branch -d client
$ git branch -d server

在这里插入图片描述

衍合的风险

衍合的便利也存在弊端,记住一条准则,
不要衍合已推送到远程仓库的提交,因为协作者可能正在使用这些提交。

衍合会丢弃之前的提交,而创建一个新提交,这将导致协作者的开发基础被删除,从而出现不可收拾的混乱,假定远程仓库和用户本地的提交历史如下,
在这里插入图片描述
之后协作者的推送中,包含了一次提交合并,用户需获取远程仓库的最新数据,如下,
在这里插入图片描述
假定协作者进行一次衍合,并使用git push --force推送到远程仓库,覆盖了原有的提交历史,这时用户又必须获取远程仓库的最新数据,
在这里插入图片描述
如果运行git long,将看到两次提交,具有相同的作者名,提交日期,提交描述,这显然是一个冲突,如果项目的所有协作者,都随意衍合,Git版本控制很快会崩溃。

衍合的修复

如果协作者的强制提交,导致远程仓库中原有记录被覆盖,用户可查出强制提交的协作者,以及所覆盖的原有记录,一切的基础,即SHA-1校验码,引入提交的补丁,Git也会为其,生成一个校验码,这被称为补丁id,如果用户获取了覆盖提交,即协作者衍合生成的新提交,Git通常能够确定,提交的唯一性,

比如之前的示例中,如果用户运行git rebase teamone/master,Git将完成以下操作,

  • 确认分支中提交的唯一性,即C2, C3, C4, C6, C7
  • 确认未合并过的提交,即C2, C3, C4
  • 确认本地分支中,并非基于覆盖提交的提交,即C2, C3
  • 远程仓库的本地副本teamone/master所包含的提交

Git自动修复衍合的结果如下,
在这里插入图片描述
如果C4和C4‘能够生成相似的补丁文件,那么衍合的恢复才能成功,否则Git不会将其视为一个拷贝,并能自动添加一个类C4的补丁。

为了实现衍合的恢复,用户可运行git pull --rebase,而不是git pull,或者手动输入git fetch与git rebase teamone/master,如果衍合提交只在本地仓库,则无需担心,如果衍合提交已推送到私有远程仓库,协作者未进行同步,这也无需担心,如果衍合提交已推送到公共远程仓库,协作者已获取了衍合提交,混乱则不可避免,如果有必要,应当保证所有协作者都了解,运行git pull --rebase,可避免衍合带来的一些副作用。

在这里插入图片描述

发布了80 篇原创文章 · 获赞 10 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/osoon/article/details/103831259