Git(2)之分支模型

Git基础之分支介绍

Author:onceday date:2023年3月4日

满满长路有人对你微笑过嘛…

windows安装可参考文章:git简易配置_onceday_CSDN博客

參考文档:

  • 《progit2.pdf》,Progit2 Github。
  • 《git-book.pdf》

1. 概述

在提交时,git会保存一个提交对象(commit object)。该提交对象包含以下内容:

  • 包含一个指向暂存内容快照的指针。
  • 作者的姓名和邮箱。
  • 提交时输入的信息。
  • 指向它的父对象指针。

根据提交对象的不同时刻,其父对象信息也不一样:

  • 首次提交产生的提交对象没有父对象。
  • 普通提交产生的提交对象有一个父对象。
  • 有多个分支合并产生的提交对象有多个父对象。

如果存在一个工作目录,里面有三个暂存和要被提交的文件。暂存操作会为每一个文件计算校验和,然后会把当前版本的文件快照保存到Git仓库(使用blob对象)中,最终将检验和加入到暂存区域中等待提交。

当使用git commit提交时,Git会先计算每一个子目录的校验和,然后在Git仓库中这些校验额保存为树对象。随后,Git便会创建一个提交对象,除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。

现在,Git仓库中有三个层次的对象:

  • 多个blob对象(保存着文件快照),对应提交的文件修改。
  • 一个树对象,记录着目录结构和blob对象索引。
  • 一个提交对象,包含指向前述树对象的指针和所有提交信息。

如下图所示:

Git(2)之分支模型_第1张图片

对于git而言,其保存的是一系列不同的快照,如Snapshot1,而不是具体的内容(tree+blob)。

这里需要注意,Git并不是不保存数据,而是将数据保存和版本控制分开,保存的数据可以用于多个版本,从而降低版本分支操作的开销。

1.1 分支简介

每个提交对象commit除了指向提交内容的指针之外,还包含作者信息,提交信息以及它的父对象指针。但首次提交是没有父对象的,而多个分支合并产生的提交对象,则有多个父对象

Git(2)之分支模型_第2张图片

如图所示,如果再在首次提交的基础上做出修改,然后再提交,就会得到second commit,第二次提交就会包含指向父对象的指针。

对于Git的分支,如release/master/test等,其本质是指向提交对象的可变指针。Git的默认分支名字是master,对于当前的显示分支(HEAD指向),每次提交都会自动向前移动。master分支只是默认创建的分支,没有任何特殊之处,现在的git安装时可以选择默认分支名了,可以替换master

所以对于Git,创建新分支,就是创建一个可以移动的新的指针。如使用git branch test创建一个新的指针。在使用git branch命令之后,并不会将HEAD移动到新分支处,HEAD也是一个指针,表示当前所在的分支对象。

使用git log --decorate命令可以查看各个分支当前所指的对象

ubuntu->py-code:$ git log --decorate --oneline
0acf60b (HEAD -> test, tag: v1.1, master) third commit.
5c08dbf (tag: v1.0, release) second commit
220cb01 first commit

切换分支使用git checkout命令,然后再提交一次:

ubuntu->py-code:$ git checkout test 
ubuntu->py-code:$ git commit -a -m "made a change"
[test e01c849] made a change
 1 file changed, 1 insertion(+), 1 deletion(-)
ubuntu->py-code:$ git log --oneline 
e01c849 (HEAD -> test) made a change
0acf60b (tag: v1.1, master) third commit.
5c08dbf (tag: v1.0, release) second commit
220cb01 first commit

这个时候HEAD指针指向test分支,然后test分支又自动移到最新commit上。如下:

Git(2)之分支模型_第3张图片

这个时候,master分支还指向原来的第三个提交,release依然指向第二个分支。可以使用checkout再切回master分支,这时候会发生两件事情:

  • 第一个是将HEAD指针指回master分支。
  • 第二个是将工作目录恢复成master分支所指向的快照内容。

因此,分支切换会改变工作目录中的文件,如果git无法直接完成文件切换,比如工作区有未提交的文件,那么就会报错。此时可以暂存(stash)、提交(commit)、忽略(ignore)以及删除(delete)等操作。

如果接下来在master分支做出修改,再提交,那么项目的提交历史将会产生分叉。

Git(2)之分支模型_第4张图片

使用命令查看如下:

ubuntu->py-code:$ git log --oneline --all --graph 
* 0339c00 (HEAD -> master) made anther change
| * e01c849 (test) made a change
|/  
* 0acf60b (tag: v1.1) third commit.
* 5c08dbf (tag: v1.0, release) second commit
* 220cb01 first commit

Git分支创建只是创建了一个指针(长度为40的SHA-1值字符串)的文件,所以创建和删除都很高效。而其他的版本控制系统往往还需要复制文件。

2. 分支工作流

2.1 分支合并

Git(2)之分支模型_第5张图片

如上图所示,当前正开发到commit 46时,因为问题(issue 1)开出了一个新分支,然后又因为问题(issue 2)开出第二个分支。在解决这些问题之后,我们是希望master分支移动到commit 50的。如下:

  1. 首先,切换分支到master,git checkout master

  2. 合并issue 2mastergit merge issue2,这样就把issue2所在的分支合入到master分支中。

  3. 因为master分支可以直达issue2分支,因此属于fast-forward合并,直接移动master指针到达commit 48即可

  4. 然后再把issue 1合入到master,这时就存在冲突,因为从commit 46达到commit 50存在两条路径,commit 50存在两个父提交。

  5. 这个时候,Git会使用commit 49commit 48这个两个分支末端,以及这两个分支的公共祖先commit 46,来做一个简单的三方合并。

  6. 三方合并存在冲突时,可以使用git status去查看未合并文件(unmerged状态),Git会在冲突区域加入下面的额外字符:

    <<<<<<< HEAD:index.html
    xxxx(当前HEAD指针指向的内容,即master分支)xxxx
    =======
    xxxx(要合并分支指向的内容,即issue1分支)xxxxx
    >>>>>>> issue1:index.html
    

    =======是分界线,但这个并非完全正确,实际合并时要慎重考虑。

  7. 再解决merge冲突之后,对冲突文件使用git add,将它们暂存起来,这样可以将其标记为冲突已解决状态。

  8. 最后再使用git commit完成最终的提交,即commit 50master指针也会指向这个提交。

可以使用git branch --merged看看那些分支还未合并到当前分支。使用git branch --no-merged查看哪些分支未合并到当前分支。

ubuntu->py-code:$ git log --oneline --all --graph 
* 0339c00 (HEAD -> master) made anther change
| * e01c849 (test) made a change
|/  
* 0acf60b (tag: v1.1) third commit.
* 5c08dbf (tag: v1.0, release) second commit
* 220cb01 first commit
ubuntu->py-code:$ git branch --merged 
* master
  release
ubuntu->py-code:$ git branch --no-merged 
  test

git branch --merged/--no-merged name,可以用来查看指定名字的分支合并状态,这样无需切换分支。

2.2 远程分支

远程分支是对远程仓库的引用,包括分支、标签等等,可以通过git ls-remote 来显示地获取远程引用的完整列表。

ubuntu->requests:$ git ls-remote 
From [email protected]:onceday/requests.git
1e62a3ec18e19f85ddae03b4cbbdf0b4c62834c0        HEAD
cd4762d5a3b56d8933d1d9c1dff365fc5db4c768        refs/heads/3.0
6ab16db7bd55bc63dca2b6ef8ad04d37117927af        refs/heads/bug/5671
1e62a3ec18e19f85ddae03b4cbbdf0b4c62834c0        refs/heads/main
9c6bd54b44c0b05c6907522e8d9998a87b69c1cd        refs/heads/proposed/3.0.0
190a68550ac2172d9651470558f90cb6b754d065        refs/heads/update-3.0
fa1b0a367abc8488542f7ce7c02a3614ad8aa09d        refs/heads/v2.27.x
22701d149ad9585cc01b3f9bda4e78cd77ffb996        refs/tags/2.0
b0555b68e7da5a64c46ce24567dbfa178eca1259        refs/tags/v0.10.0
8493d8329e9b606503923323af6417844508a744        refs/tags/v0.10.1
......

可以看到,不光有远程分支,还有各类标签也都显示出来了

使用git remote show 可获取远程分支的更多信息

ubuntu->requests:$ git remote show origin
* remote origin
  Fetch URL: [email protected]:onceday/requests.git
  Push  URL: [email protected]:onceday/requests.git
  HEAD branch: main
  Remote branches:
    3.0            tracked
    bug/5671       tracked
    main           tracked
    proposed/3.0.0 tracked
    update-3.0     tracked
    v2.27.x        tracked
  Local branch configured for 'git pull':
    main merges with remote main
  Local ref configured for 'git push':
    main pushes to main (up to date)

这里有一个关键的要素是远程tracked分支,这些分支无法进行移动,它们只会反应远程仓库的这些分支的真实情况,并且在fetch之后,更新到远程仓库最新的状态。

这些远程追踪分支以/的形式命名,一般clone时,默认远程分支名字是origin

本地仓库的远程分支,只会在同远程服务器更新数据之后,才会主动更新数据并且移动分支,否则就会保持本地的状态不会改变。

同步远程分支的命令为:git fetch ,该命令只会同步数据,而不会对本地分支做出修改

推送远程分支的命令为git push ,如果和远程分支之间存在冲突,那么就会推送失败,并且提示原因,极端情况下(如不再需要远程分支,可以使用git push -f 来覆盖远程分支,注意,这可能导致数据丢失

一般都使用远程ssh连接来访问数据库,这样安全,而且不需要频繁输入密码。如果使用https方式连接远程仓库,那么每次都需要输入密码,当然,也可通过某种方式避免这个过程。

如果远程分支和本地分支存在冲突,可使用git merge /将远程分支合并到本地之后,再推送本地分支到远程仓库。

追踪分支的一个好处就是输入git push/pull的时候可以自动识别远程分支。使用如下命令即创建一个本地追踪分支:git checkout -b /

对于上面的main分支,其追踪的远程分支就是remote/main,因此其pushpull便默认绑定该远程分支。可以使用git branch -u /来为当前分支绑定远程分支。

下面是一些常见的操作命令

  • git push --delete 删除远程分支,只是删除指针,数据会在后续等待垃圾回收。

3. 变基(rebase)

一般来说,新手最常用的合并方法是merge,但这种方法存在一个缺陷,那就是会将一段长时间的交叉开发过程混合在一起,在提交代码时,就会遇到commit杂乱分布的场景。

Git(2)之分支模型_第6张图片

如上所示,merge是在两个分支之上,合并出一个新的commit(C5),C5提交有两个父对象,因此merge过程保留了所有的信息。

如果不需要保留C4 -> C5的提交信息,那么就需要用到rebase,如下:

Git(2)之分支模型_第7张图片

rebase基于master分支,将自C2提交之后的所有提交(C4以及更多的提交)先reset出来暂存,然后在C3提交的基础之上,一个个再apply上去。这里就是在C3基础之上应用C4,如果存在冲突,那么需要先修改冲突。然后暂存冲突文件并提交。这一次就生成了C4'提交,如果C4后面还有更多的提交,那么一直循环处理,直到experiment分支自C2之后的所有分支都已合并成功。

rebase和merge本质上没有太大差别,但rebase的不足之处是会抹去原先的提交记录,因此在多人开发的场景下使用,反而造成合并冲突更多

因此,rebase更推荐在个人本地仓库使用,用来对最终的提交进行commt整理,merge在多人合并时使用,特别是已经提交出去的分支,最好不要用rebase去抹除

3.1 跨分支变基

Git(2)之分支模型_第8张图片

对于这种情况,希望将C8和C9合并到master分支中,但是不需要C3,此时可使用下面的命令:

git rebase --onto master server client

该命令会把C8和C9提出来,然后放回master分支进行提交,如下:

Git(2)之分支模型_第9张图片

然后可以快速合并master分支,使之包含来自client分支的修改。

git checkout master
git merge client

rebase操作命令一般如下

git rebase  

你可能感兴趣的:(Iinux小白之路,计算机基础知识,git)