这一讲开始讲分支,也是 git 相当重要的部分,是开发必须掌握的技能
在多人开发过程中,想要增加新功能或者修 bug 的时候,都会另开开一个分支出来,在确认没有问题之后再和主分支合并,这样就不会影响在运行中的产品,当然在单人开发过程中也可以采取这种开发模式
开始使用分支
在 git 中使用分支很简单,只要使用 git branch
命令即可
$ git branch
* master
如果 branch
后面没有参数,它只会输出当前这个项目中有哪些分支,git 默认会设置一个 master 的分支,前面的星号代表当前处于这个分支上
新建分支
如果需要增加一个分支,就可以在 git branch
后面跟上想要的分支的名称
$ git branch cat
$ git branch
cat
* master
可以看出,确实多了一个 cat
分支,但是我们当前还是处在 master
分支上
更改分支名称
如果你觉得一个分支名不对你胃口,可以随时更改,是不会影响文件和目录的,更改分支的方法是 git branch -m old_name new_name
$ git branch -m cat cat2.0
$ git branch
cat2.0
* master
即使是 master
分支,也可以改名
$ git branch -m master main
$ git branch
cat2.0
* main
删除分支
删除对应的英语是 delete,所以参数是 -d
$ git branch
cat1.0
cat2.0
* main
$ git branch -d cat1.0
Deleted branch cat1.0 (was e47b10f).
$ git branch
cat2.0
* main
但 git 不允许用 -d
删除未合并的分支
$ git branch -d cat2.0
error: The branch 'cat2.0' is not fully merged.
If you are sure you want to delete it, run 'git branch -D cat2.0'.
但是你可以使用 -D
将其删除
$ git branch -D cat2.0
Deleted branch cat2.0 (was 3f18f81)
在 git 中,没有什么分支是不能删的,但是你不能删除你当前所在的分支
$ git branch
b2
* main
$ git branch -d main
error: Cannot delete branch 'main' checked out at '/home/paradox/Code/learngit/learnbranch'
不过你可以移动到其他分支之后再删除它
你以为这样的规则下 git 必须要有一个分支?
不不不,你可以把分支全删了
通过
git checkout
到一个没有贴分支的 commit,让它成为一个detached HEAD,这时候我们就处于在所有其他分支以外的状态,然后用git branch -d/D
大开杀戒吧!
切换分支
切换分支使用的指令之前在还原被reset消失的文件时出现过,git checkout
$ git branch
b2
* main
$ git checkout b2
Switched to branch 'b2'
$ git branch
* b2
main
使用 git checkout
就能轻松地切换了分支
一般来说,要想切换到那个分支就必须要先存在那个分支,如果你输入的分支参数还不存在项目中,就会出错,但你可以用 -b
参数,这样如果你要切换的分支不存在时 git 就会帮你新建这个分支并切换过去
$ git checkout b3
error: pathspec 'b3' did not match any file(s) known to git
$ git checkout -b b3
Switched to a new branch 'b3'
$ git branch
b2
* b3
main
分支的切换非常的方便,在切换分支的时候工作区的文件可能会发生变化,但是别担心,那些文件都在,只要你切换回去就会回来
$ git branch
* cat
master
$ ls
cat1 x.c
$ git checkout master
Switched to branch 'master'
$ ls
x.c
$ git checkout cat
Switched to branch 'cat'
$ ls
cat1 x.c
如果有文件改动了一半就切换分支会发生什么
假设我们现在处在 cat 分支上,现在我们来新建两个文件,一个 git add
,一个不 git add
$ git log --oneline
d5164b9 (HEAD -> cat, master) cmt1
$ ls
a b file
$ git status
On branch cat
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: a
Untracked files:
(use "git add <file>..." to include in what will be committed)
b
现在我们切换回 master 分支来看看会发生什么
$ git checkout master
A a
Switched to branch 'master'
$ ls
a b file
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: a
Untracked files:
(use "git add <file>..." to include in what will be committed)
b
原本没有 a
和 b
文件的 master 分支居然出现了这两个文件,而且 a
还是在暂存区的状态,也就是说,切换分支不会影响已经在工作目录中的那些改动
但有时候有文件没有提交的情况下 git 会不让你切换过去
$ git checkout master
Switched to branch 'master'
$ cat file
wt
$ git checkout cat
Switched to branch 'cat'
$ echo hello > file
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
file
Please commit your changes or stash them before you switch branches.
Aborting
原因大概是同样的文件,但是文件更改并没有相对的先后顺序,然后 git 无法做出这次修改对目标分支的合并,就不会让你切换过去,当然,checkout
也有强制执行的参数 -f
$ git checkout master -f
Switched to branch 'master'
$ cat file
wt
这时候的 file
就是 master 分支的状态,cat 分支的文件因为被覆盖就消失了,因为没有被 git 管理,回到 cat 分支也找不回原来的 file
了
当然这种情况是要避免的,通过 commit
和 stash
来保存这些文件,以免造成文件丢失的风险
切换时会发生什么
git 在 git checkout
时主要做了以下两件事
- 更新暂存区和工作目录
- 变更 HEAD 的位置
什么叫更新暂存区和工作目录?git 会根据分支指向 commit 的内容来更新暂存区和工作目录,我们来看下面的例子
$ git log --oneline
74fbcf5 (HEAD -> master) 1
$ ls
x
$ cat x
1
我们的项目现在有一个 x
文件,文件内容是一个数字 1,这时候我们把当前分支换成 cat
并添加一些修改并提交
$ git checkout -b cat
Switched to a new branch 'cat'
$ cat x
1
2
$ git add .
$ git commit -m 2
[cat fdde6ba] 2
1 file changed, 1 insertion(+)
然后我们切换回 master
分支,x
文件回到了 master
分支的状态
$ cat x
1
2
$ git checkout master
Switched to branch 'master'
$ cat x
1
前面讲到过,commit 对象会指向一个 tree 对象,当我们拿出了一个 commit,那就能顺着这个对象把整个目录都找出来,切换分支的时候就是把原先分支对应的 commit 的目录都移除,然后换上目标分支贴着的 commit 对应的目录,有点换驾驶员的感觉
而 HEAD 指向的是当前所在的分支,checkout
到一个分支,就会让 HEAD 指向那个分支,你可以 checkout
一个 commit 的 SHA-1,但那会让你的 HEAD 成为一个 detached HEAD,即便那个 commit 上有贴分支,HEAD 也不会指向那个分支,而是指向 commit,这样看来 HEAD、commit、branch 之间的关系没有看上去那么简单了吧
分支的含义
看到这里,我相信你对分支肯定有了一定的误解,来看看分支的含义吧
如果从字面上来看,分支是从一条线上的一个点向外延伸出的另一条线,类似分岔铁路
从表面上看确实是这么一回事,所以这就会引起误解,其实分支并不是这么一回事
之前提到过,git 的 逻辑是一张 DAG(有向无环图),而分支就是在 DAG 中,贴在 commit 上用来标记 commit 的贴纸,当前处在的贴纸会随着 commit 而前进
上面的图片可以清晰地呈现分支的逻辑,你可能和之前的我一样认为黄色虚线圈起来的部分就是分支,那你可能就看的不是很清晰。其实只有那张褐色的贴纸才是分支,如果觉得分支包含 commit 历史的话,在面对分岔复杂情况下 commit 的 DAG 时,再贴上几张 branch,你可能就要抓狂了(如上图的 cat1.0 和 cat2.0,你能指出他们分别包含了哪些 commit 吗)
为了看清楚 branch,我们再来梳理一下,先抛开 branch 不管,我们只看 commit 的部分
如果没有分支,这样情况的 commit 的 DAG 能存在的吗?
答案是可以,上一篇中学到 commit 对象的本质是一堆经过 SHA-1 计算之后的压缩文件,文件的内容是指向前一个 commit 对象和包含目录信息的 tree 对象,既然我们能做到指向前一个对象,那么把它想象成一张图,我们的目标是建立这张图,假设当前我们只有上图的 a 点作为起始点,现在我们能用的操作有 commit
和 reset
,把之前的知识抽象成图论,那么借助 commit
可以创建一个新的点并且建立一条有向边指向之前所在的点,reset
可以跳转到指定的点上,那么上图我们可以从 a 一路 commit
完成 abcde 的部分,再用 reset
分别跳转到 b 和 c 完成剩下的部分,这么算下来,最少需要 9 次 commit
和 3 次 reset
就能建立上面的 DAG 了
这么看下来好像和分支没什么关系,那么分支到底是什么呢?
贴纸!
如果我们不用分支,那当我们处于 e 点想跳转到 b2 点的情况下,就必须知道 b2 点的 SHA-1 是多少,通过 git reflog
不难知道 b2 的 SHA-1 是多少,但如果提交很多呢,你就需要在一堆 SHA-1 中寻找你要的 b2,是不是有点大海捞针的感觉。实际开发中我们也不愿意浪费时间在找 SHA-1 上,这时候就需要借助贴纸了,我们在 b2 上贴上分支 git branch b2
,告诉 git,在 b2 这个 commit 上贴上 b2
,那么之后我们只需要 git checkout b2
就能跳转到 b2 点,而不用像大海捞针一样的找 b2 的SHA-1 再 reset
,是不是有点传送点的意思
现在再回来看这张图,是不是就清晰很多,分支只是贴在某个 commit 上的贴纸而已!
上面使用 reset 跳转只是为了从本质上认识分支,实际上 reset 操作会影响到 branch,reset 会让当前所在的分支贴到指定的 commit 上,checkout 就不会,checkout 只会移动 HEAD 指针,不会影响到 branch,随带一提,当前所在的分支会随着 commit 操作移动,想象成把当前的贴纸撕下来贴到新的 commit 上就行了
2023/01/13越学越感觉到 git 设计的精妙,感觉理解了一切!但我还是没法理解 git 的前后 commit 差异分析信息是怎么存储的,因为平时用 lazygit 或者在 github 上看历史代码都能看到 commit 修改了什么代码的信息,希望这本书后面能讲讲,但这本书好像都不会讲 stash,后面可能要自己查资料写文章了,但我感觉 stash 好像没有存在的必要啊…
这一篇说了开分支和分支的原理,下一篇文章会讲分支合并,也是非常重要的一部分,敬请期待