git学习笔记之二 -- git分支

时间:2021-12-17 12:18:35

  前面对git基础作了简单的总结,这次对git的杀手锏--分支做一总结。

Git分支简介

  几乎每个版本控制系统都以某种形式支持分支,可以使你的工作从开发主线上分离开来,以免影响开发主线。很多版本控制系统常常要创建一个源代码目录的副本,略微低效。而Git保存的不是文件的差异和变化,而是一些列不同时刻的文件快照。当进行commit操作的时候,Git会保存一个提交对象(commit object),该提交对象包含作者的姓名和邮箱,提交时输入的信息,指向父对象的指针和一个指向暂存内容快照的指针,首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

  形象的说,工作目录下有三个文件,暂存操作会为每个文件计算校验和,并把当前版本的文件快照使用blob对象保存到Git仓库中,最终将校验和添加到暂存区中等待提交,当进行commit提交时,Git先会计算每一个子目录的校验和,然后在Git仓库中将这些校验和保存为树对象,随后Git就会创建一个提交对象,除了上面提到的还会保存这个树对象的指针,就可以在需要时重现此次保存的快照。此时Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。

  做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。

  Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。

创建分支 git branch

git branch testing                         ##创建testing分支,但不会切换到testing分支

这会在当前提交对象上创建一个指针。Git有一个名为HEAD的特殊指针,指向当前所在的分支。

你可以用git log 命令查看各个分支当前所指的对象,--decorate 参数提供此命令

$ git log --oneline --decorate
472aa16 (HEAD -> testing, master) all      ## testing,master分支均指向校验和为472aa16的提交对象
24a207f add
de99f0e all

切换分支 git checkout

git checkout testing                 ## 切换到testing分支,这是HEAD就指向testing

这时,做些修改提交一次,看提交历史

$ git log --oneline --decorate
c2183f6 (HEAD -> testing) new branch       ## 这里可以看到HEAD指向testing分支,testing分支向前移动,master分支却没变化
472aa16 (master) all
24a207f add
de99f0e all

这是切换回master分支,

git checkout master                    ##切换回master

此时其实是两步操作,一是使HEAD指向master分支,二是将工作目录恢复至master分支所指向的快照内容,也就是说忽略了刚才testing分支下的修改。

再做一次提交后,运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况

$ git log --oneline --decorate --graph --all
* 2b6f868 (HEAD -> master) new tt.txt       ## HEAD指向master,一次添加文件的提交
| * c2183f6 (testing) new branch          ## testing分支上次提交的校验和为c2183f6的提交对象
|/
* 472aa16 all
* 24a207f add

 分支的新建与合并

实际工作中,你可能会遇到类似的工作流。开发一个项目,为实现某个功能,创建一个分支iss,在分支iss上开展工作。此时有一个bug急需处理,你将按如下方式处理:

  1. 切换到你的线上分支(production branch)。

  2. 为这个紧急任务新建一个分支,并在其中修复它。

  3. 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支。

  4. 切换回你最初工作的分支iss上,继续工作。

首先,你运行带参数-b的git checkout命令创建了iss分支并切换到iss分支,是git branch iss和git checkout iss两个命令的简写

git checkout -b iss                        ## 新建分支iss,参数-b表示并切换到iss分支

你已经做了一些提交,HEAD已经指向iss,此时修复bug,就需要切换到master分支,新建fix分支来处理bug,在切换分支前要要留意你的工作目录和暂存区里那些还没有被提交的修改。当切换分支的时候,Git会重置工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。

git checkout -b fix                     ## 新建并切换到fix分支,修改bug

合并分支 git merge

bug修改完成提交后,你要与master分支合并,就需要先切换到master分支,然后再合并

$git checkout master                    ## 切换回master分支
$ git merge fix                ## 合并fix分支
Updating 558d34c..0f49f4e
Fast-forward                      ## 快进
index.html | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 index.html

你会注意到Fast-forward这个词,是由于master分支是需要合并的fix分支直接上游,git只是简单的将指针移动。也就是说当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧,就叫做Fast-forward.

删除分支 git branch -d

$ git branch -d fix                     ## 删除不需要的fix分支
Deleted branch fix (was 0f49f4e).

现在切换回iss分支,由于在fix上的修改没有包含到iss中,你可以用git merge master命令将master合并到iss分支,也可以等iss完成修改后合并回master分支

$ git merge iss
Merge made by the 'recursive' strategy.    ## 合并提交
tt.txt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)

由于master分支所在的提交不是iss分支所在提交的直接祖先,Git会把两个分支的末端所指的快照(0f49f4e和88502d9),以及这两个分支的工作祖先(558d34c),进行一个简单的三方合并,合并结果不是简单的快进(Fast-forward),而是生成了个新的快照(ce91eb7)并创建一个新的提交指向它,这个过程是一次合并提交,合并完成后就可以删除iss分支了。需要说明,Git 会自行决定选取哪一个提交作为最优的共同祖先,并以此作为合并的基础。

$ git log --oneline --decorate --graph --all
* ce91eb7 (HEAD -> master) Merge branch 'iss' ## 合并提交后生成一个新的快照,并指向新的提交
|\
| * 88502d9 (iss) iss                ## iss分支的最后一次提交
| * 8de2c03 second
| * be76065 new iss
* | 0f49f4e fix               ## master分支的最后一次提交
|/
* 558d34c all                  ## iss分支和master分支的工作祖先
* 863dcdc test

遇到冲突时的分支合并

如果在两个不同的分支中,对同一文件的同一部分进行了不同的修改,Git就无法自动合并。使用git status命令就会看到发生冲突的文件,打开冲突的文件,(=======)分割的上段为当前分支master的修改,下段为iss的修改,删除冲突标识符,手动修改保留一个分支的修改,对每个文件使用git add命令表示冲突已解决,git commit来完成合并提交

<<<<<<< HEAD
master edit.
=======
iss edit.
>>>>>>> iss

分支管理

$ git branch                         ## 列出当前所有分支的一个列表,*表示HEAD指向的分支
iss
* master
$ git branch -v                   ## 参数-v查看每个分支的最后一次提交
iss 5735ce0 iss edit
* master 3ab0a47 merge
$ git branch --merged                 ## 查看已合并到当前分支的分支
iss
* master
$ git branch --no-merged                ## 查看未合并到当前分支的分支
testing
$ git branch -d testing                 ## 未合并的分支删除时,如果你确认需要删除,参数-D强制删除
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

分支开发工作流

长期分支:

只做简单的快进合并,反复的把一个分支合并到稳定的分支。常常会只在master分支保留完全稳定的代码(有可能仅仅是已经发布或即将发布的代码),还有一些develop或next的平行分支,被用来做后续开发或测试稳定性,这些分支不必保持稳定状态,一旦达到稳定状态就可以合并到稳定的master分支。可以用这种方法维护更多层次的分支。

特性分支:

特性分支对任何规模的项目都适合。性分支是一种短期分支,它被用来实现单一特性或其相关工作。之前的iss分支和fix分支就是这种用法。在iss和fix分支中提交一些更新,合并到主干后又删除它们,这样能使你快速并完整的切换,更容易的看出做了哪些改动。你可以在改动的特性分支中保留很久,等成熟后再合并。

请牢记:当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中 —— 没有与服务器发生交互。

远程分支

远程引用是对远程仓库的引用(指针),包括分支和标签等等。可以用git remote show获取远程分支的更多信息,但常用的做法是远程分支跟踪。远程跟踪分支是远程分支状态的引用。它们以(remote)/(branch)的形式存在。远程仓库的名字origin跟分支master,没有什么特殊的含义,master是git init时默认的分支名字,origin是git clone时远程仓库默认的名字。

git fetch origin 从远程抓取仓库origin中本地没有的数据。

推送

当需要公开分享一个分支时,将其推送到一个有写入权限的远程仓库,本地的分支不会自动的与远程的仓库同步,你必须显示的推送你想要分享的分支。这样你就可以把不愿分享的内容放到私人分支上,需要和别人协作的内容放到公开的分支上。

git push origin serverfix                ## 推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支
git push origin serverfix:serverfix     ## 推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支
git push origin serverfix:awesomebranch      ## 推送本地的 serverfix 分支,将其作为远程仓库的 awesomebranch 分支,给远程分支改名

其他协作者从服务器上抓取数据时,就会在本地生成一个远程分支origin/serverfix,指向服务器serverfix分支的引用。但当抓取新的远程跟踪分支时,本地不会自动生成一个可编辑的副本,即不会有一个新的分支serverfix,只是一个不可修改的origin/serverfix的指针。可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。

如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:git checkout -b serverfix origin/serverfix

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”。 跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入git pullGit 能自动地识别去哪个服务器上抓取、合并到哪个分支。当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/mastermaster 分支。如果想跟踪其他分支,就运行git checkout -b [branch] [remotename]/[branch]。因为比较常用Git提供了--track快捷方式。

git checkout --track origin/serverfix          ## 跟踪远程分支serverfix
git checkout -b sf origin/serverfix         ## 本地分支与远程分支设置不同的名字
git branch -u origin/serverfix           ## 设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支
git branch -vv                     ## 查看设置的所有跟踪分支

git branch -vv 查看每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后,这些都是与最后一次抓取的数据作比较,要想统计最新的需要在git branch -vv前先执行git fetch --all命令抓取所有的远程仓库。

拉取

当用git fetch命令从服务器抓取本地没有的数据时,不会修改工作目录中的内容,只会让你自己合并。git pull命令大多数情况下是执行git fetch后接着执行git merge。如果设置好跟踪分支,git pull都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。所以通常单独显式地使用 fetchmerge 命令会更好一些。

删除远程分支

如果远程分支的工作已经完成并合并到远程仓库的master分支,就可以用带有--delete选项的git push命令删除远程分支了。

$ git push origin --delete serverfix            ##从服务器上删除 serverfix 分支

基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

变基

变基一般是为了确保在向远程分支推送时能保持提交历史的整洁,修改了提交历史。但请记住不要对在你的仓库外有副本的分支执行变基。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。变基就不做说明了。

Git笔记先写到这。下次用到更高级的功能继续补充。