news 2026/10/19 2:12:01

Git 第二章:分支是一个 41 字节的文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Git 第二章:分支是一个 41 字节的文件

如果多个人修改同一个项目,而且所有人都往同一条线上提交,那么麻烦并不在于「乱」,而在于无法单独撤回某一部分。修复 bug 的提交和添加功能的提交交替排列在同一条历史上,如果想把功能那部分拿掉,就得从一堆相互穿插的提交里把它们挑出来。分支解决的正是这个问题,它让每一条工作线各自独立地前进,回退时也只需要处理自己的那一条。

这一章的命令并不多,只有branch、switch、checkout,再加上-d、-D、-m三个参数。难点也不在命令本身,而在于「分支」这个词听起来像是一份代码的副本。如果真的按照副本来理解,后面几乎每一条规则都会记反,比如为什么创建一百个分支也不占空间、为什么切换分支是瞬间完成的、为什么有时候修改了文件还能切换过去,有时候却不能。

所以这一篇先把分支拆到文件的层面来看。看过之后,上面那些规则就不需要死记了,因为它们都能推导出来。

一、分支到底是什么

常见的说法是,分支是「从主干上引出来的一条线」。这个比喻画在白板上很好用,但是它会让人以为分支是一个很重的东西。实际打开.git目录看一下:

gitinit demo&&cddemoecho"a">f.txt&&gitadd.&&gitcommit-m"one"ls.git/refs/heads/# mastercat.git/refs/heads/master# 9a2f86f27b271d52c04ac45b9a54040c5f8e1543wc-c<.git/refs/heads/master# 41

代码 1:一个分支在磁盘上的全部内容。

一个分支就是.git/refs/heads/下的一个文件,文件里写着一个 40 位的提交号,再加上一个换行符,一共 41 个字节。不多不少,里面没有任何别的东西。

这一点可以解释后面的一连串现象:

  • 创建分支是瞬间完成的,而且几乎不占空间,因为写一个 41 字节的文件根本花不了多少时间;
  • 创建一百个分支,也不会让仓库变大一百倍,因为提交对象是共用的,分支只是记录了从哪一个提交开始看;
  • 删除分支并不会删除提交,因为删掉的只是那个文件,提交本身仍然保存在.git/objects里。

这里还有一个容易被忽略的细节,在空仓库里执行git branch,什么都不会输出。

gitinit demo2&&cddemo2gitbranch# (没有任何输出,退出码 0)

代码 2:在还没有任何提交的时候,一个分支都列不出来。

这并不是命令出了问题。分支是「记录着某个提交的文件」,既然还没有任何提交,自然也就没有东西可以记录。master要等到第一次commit之后才会真正出现,在那之前,它只是一个将要被创建的名字。

二、HEAD:记着分支名的那一行

有了分支之后,还缺少一样东西,那就是 Git 需要知道当前是在哪一条分支上工作,这就是HEAD的作用。

HEAD同样是一个文件,而且比分支文件还要小:

cat.git/HEAD# ref: refs/heads/mastergitswitch featurecat.git/HEAD# ref: refs/heads/feature

代码 3:切换分支时,.git/HEAD里的这一行被改写了。

所以整个模型分为两层:

  • 分支记录着一个提交号;
  • HEAD记录着一个分支名。

图 1:这三样东西都是文件。master和feature各自记录着一个提交号,而HEAD记录的是分支名,并不是提交号。所以在feature上再提交一次时,只需要改写refs/heads/feature里的那一行,HEAD一个字都不用改动。

「切换分支」在文件层面的含义,就是改写.git/HEAD里的那一行,再把工作区里的文件更新成新分支所指向的那个提交的样子。前半步只是修改一个字符串,后半步才是真正需要花时间的部分。

补个坑:HEAD 里存的是 ref 时切换才只改字符串,分离头指针下切走前得先建分支。

这个结构还留了一个口子,那就是 HEAD 里保存的内容不一定是ref: ...这种形式,也可以直接是一个提交号。

gitswitch--detachHEADcat.git/HEAD# 21d461219b84aec02bce153c03d3146c208b4f98gitsymbolic-ref--shortHEAD# fatal: ref HEAD is not a symbolic ref

代码 4:分离头指针,也就是 HEAD 跳过分支,直接指向了一个提交。

这种状态叫作分离头指针(detached HEAD)。在这种状态下,照样可以修改文件,也可以提交,但是新的提交并没有任何分支记录着它。一旦切换走,这个提交的编号就只剩下git reflog里还保留着了。查看历史上的某个版本时,就会遇到这种状态,看完之后切换回来就可以了。如果想在这个版本上继续工作,就要先执行git switch -c <名字>,给它创建一个分支。

三、git branch:看和建

gitbranch# feature# * master

代码 5:*标出的是 HEAD 当前指向的分支。

在命令后面加上一个名字,就可以创建分支:

gitbranch tmpgitrev-parse--shortmaster# 9a2f86fgitrev-parse--shorttmp# 9a2f86fgitsymbolic-ref--shortHEAD# master

代码 6:新分支创建在当前的提交上,但 HEAD 并没有跟着移动过去。

这里有两件事需要分开记住。

第一,新分支创建在 HEAD 当前所在的提交上。它并不是创建在master上,而是创建在当前所在的位置。如果当前位于feature上,执行git branch tmp之后,tmp和feature就会指向同一个提交。

第二,git branch <名字>创建完分支之后,并不会切换过去。这是一个单纯的创建操作。如果想在创建之后立即切换过去,可以使用git switch -c <名字>,其中-c是 create 的意思,这样一步就能完成两件事。

四、切换:为什么 switch 和 checkout 是两条命令

在切换分支这件事上,git switch <分支>和git checkout <分支>的效果是一样的。既然效果一样,为什么还要有两条命令呢?

答案在 Git 2.23 的发布说明里写得很直白:

Two new commands "git switch" and "git restore" are introduced to split "checking out a branch to work on advancing its history" and "checking out paths out of the index and/or a tree-ish to work on advancing the current history" out of the single "git checkout" command.

也就是说,checkout一个人承担了两项工作。如果给它一个分支名,它就切换分支;如果给它一个文件路径,它就会用暂存区里的内容覆盖工作区里的那个文件。两件事共用一条命令,就留下了一个真实存在的坑:

echo"重要的未提交改动">>f.txtgitcheckout f.txt# Updated 1 path from the indextail-1f.txt# a

代码 7:本来想切换分支,结果却把改动覆盖掉了,而提示只有一句Updated 1 path。

由于f.txt并不是一个分支名,所以checkout执行了第二种操作,用暂存区里的版本覆盖了工作区里的文件。刚才添加的那一行改动就这样消失了,整个过程没有确认,没有警告,退出码也是 0。

这坑的隐蔽处在于退出码为 0,脚本里 `checkout f.txt` 写错也不会报错,静默丢改动。

如果换成switch,结果是这样的:

echo"又改一次">>f.txtgitswitch f.txt# fatal: invalid reference: f.txttail-1f.txt# 又改一次

代码 8:switch只认识分支,遇到认不出来的参数就会停下来。

这就是把两项工作拆开的价值。switch只负责切换分支,如果参数不是分支名,它就会报错退出,而覆盖文件的那一半功能则交给了git restore。所以日常切换分支时使用switch,可以避免一整类误操作。

需要说明的是,git switch的 man 页到现在仍然标注着THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.。不过这条命令已经用了很多年,在日常切换分支时是稳定可靠的。如果看到别人的教程或者脚本里写的是checkout,也不能说那是过时的写法,因为checkout并不会消失,这两条命令会长期并存。

五、删除:-d 和 -D 差在哪

gitbranch-dfeature# error: The branch 'feature' is not fully merged.# If you are sure you want to delete it, run 'git branch -D feature'.

代码 9:-d会先检查这条分支上的提交有没有被合并到别处。

-d是带有检查的删除,而-D是强制删除。两者的区别就在于这道检查。如果分支上有些提交还没有合并到别的地方,-d就会阻止删除,因为一旦删掉这条分支,那些提交就没有任何分支记录着它们了。

这道检查涉及到「合并」,而合并要到下一章才会讲。但是并不能因此就一律使用-D,日常应该默认使用-d,让它帮忙做这道检查。如果确认真的要删除,报错信息里已经写好了使用-D的命令,照着输入就可以了。反过来说,如果习惯性地使用-D,就相当于永久关掉了这道保险。

另外还有一条限制:

gitswitch featuregitbranch-Dfeature# error: Cannot delete branch 'feature' checked out at '/tmp/demo'

代码 10:不能删除自己当前所在的分支。

从模型上来看,这是必然的结果。因为 HEAD 记录着分支名,如果把那个分支文件删掉了,HEAD 就会指向一个不存在的东西。所以在删除分支之前,要先切换到别的分支上。

另外,即使真的删错了分支,提交一般也还在。git reflog记录着 HEAD 的每一次移动,可以从中找回那个提交的编号,然后执行git branch <名字> <提交号>,把分支重新创建出来。因为删掉的只是一个 41 字节的指针,而不是提交的内容。

六、改名:-m

-m有两种用法,它们的区别需要记清楚:

# 一个参数:改当前分支的名字gitbranch-mnew-name# 两个参数:改任意一条分支的名字,不用切过去gitsymbolic-ref--shortHEAD# mastergitbranch-mfeature feat-newgitbranch--format='%(refname:short)'# feat-new# master

代码 11:使用两个参数的形式时,即使当前在别的分支上,照样可以改名。

当只有一个参数时,-m修改的是当前分支的名字,所以这种写法确实要求 HEAD 位于那条分支上。而使用两个参数的形式时,已经写明了要修改哪一条分支,所以无论当前在哪里都可以执行。

七、切换被拒绝的时候,Git 在护什么

这一节是整章里最容易记反的地方。

一种常见的说法是「修改过的文件如果没有提交,就不能切换分支,而新建的文件则可以」。这种说法是根据改动的种类来划分界线的,但 Git 划分的并不是这条线。实际运行一遍看看:

# f.txt 在 master 和 feature 上内容相同echo"改动未提交">>f.txtgitswitch feature# Switched to branch 'feature'# M f.txttail-1f.txt# 改动未提交

代码 12:修改过的文件虽然没有提交,但切换照样成功了,而且改动也跟着带了过来。

输出里的M f.txt这一行,是 Git 在提示这个修改被带到了新的分支上。下面换一种情况:

# 这次 f.txt 在两个分支上内容不同echo"master 上的未提交改动">>f.txtgitswitch feature# error: Your local changes to the following files would be overwritten by checkout:# f.txt# Please commit your changes or stash them before you switch branches.# Aborting

代码 13:同样是修改过但没有提交,这一次却被拦住了。

新建的文件也同样会有两种结果:

# draft.txt 在 feature 上不存在echo"草稿">draft.txtgitswitch feature# Switched to branch 'feature' ← 文件跟着过来了# other.txt 在 feature 上是被追踪的文件echo"本地新建的 other.txt">other.txtgitswitch feature# error: The following untracked working tree files would be overwritten by checkout:# other.txt# Please move or remove them before you switch branches.# Aborting

代码 14:新建的文件同样有可能阻止切换。

把这四种情况放在一起,规则就清楚了。Git 是否阻止切换,看的并不是改动的种类,而是切换过去之后,会不会覆盖掉还没有提交的内容。

情况目标分支上那个文件结果
改了已追踪文件内容相同放行,改动跟着走
改了已追踪文件内容不同拦下(会覆盖改动)
新建未追踪文件不存在放行,文件跟着走
新建未追踪文件已被追踪拦下(会覆盖新文件)

其实两条报错信息已经把意思说得很明白了,它们的关键词都是would be overwritten,也就是「会被覆盖」。Git 并不介意带着没有提交的改动切换分支,它只是拒绝在切换的过程中弄丢这些改动。

被放行的那两种情况还有一个副作用值得了解,那就是没有提交的改动并不属于任何一个分支。它们留在工作区里,切换到哪个分支,就会跟到哪个分支。如果在master上改了一半,然后切换到feature,并在那里提交了,这个改动就进入了feature的历史。当切换被阻止时,可以用stash暂存,也可以先提交一次,但更值得先想一想的是,这一半的改动原本打算放在哪一条分支上。

值得点明:未提交改动跟着你走,所以切分支前先想清楚它属于哪条线,别顺手在错的分支提交。

小结

把这一章的命令,按照「它改动了哪个文件」重新排列一遍:

命令它做的事
git branch列出.git/refs/heads/下的分支,*标出 HEAD 在哪
git branch <名>在当前提交上写一个 41 字节的新文件,不切过去
git switch <名>改写.git/HEAD那一行,再把工作区刷成对应提交
git switch -c <名>上面两件事一步做完
git checkout <名>同样能切;但参数是文件名时会改成覆盖文件
git branch -d <名>删分支文件,先检查提交有没有被合并走
git branch -D <名>跳过检查直接删;不能删 HEAD 正站着的那条
git branch -m <新>改当前分支的名字(两个参数则改指定的那条)

真正需要记住的,只有开头那两句话,分支是记录着提交号的文件,而 HEAD 是记录着分支名的文件。这一章里其余的规则都可以由此推导出来,包括创建分支为什么快、删除分支为什么不会丢失提交、为什么不能删除自己当前所在的分支,以及切换分支时 Git 到底在保护什么。

不过这里还留着一个明显的缺口,那就是分支分出去之后,要怎样再合并回来。如果两条分支各自修改了同一个文件的同一行,Git 应该以哪一边为准呢?下一章讲合并和冲突。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:11:58

嵌入式三阶RC滤波器的移位优化与定点实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 2:11:56

Flow-SDE02:Flow-SDE在RLinf中的代码实现【从确定性ODE到可计算LogProb的随机SDE】【PPO训练时,随机选择一个去噪步骤执行SDE,其他步骤仍然采用ODE(混合去噪策略】

我会把“论文公式 → RLinf 实际代码 → 改造前 ODE 分支 → 改造后 SDE 分支 → LogProb/PPO”的整条链路重新对齐,重点修正前面容易混淆的地方:GR00T 代码采用的是 x0_pred/x1_pred 参数化,不能把某一行权重修正机械地当成论文公式逐字翻译。接下来我会以 RLinf 当前源码为…

作者头像 李华
网站建设 2026/10/19 2:11:26

scanf库函数的常用介绍

scanf1.1基础用法scanf 是 C 语言标准库函数&#xff0c;使用要写头文件&#xff1a; #include <stdio.h>scanf() 函数⽤于读取⽤⼾的键盘输⼊。程序运⾏到这个语句时&#xff0c;会停下来&#xff0c;等待⽤⼾从键盘输⼊。 ⽤⼾输⼊数据、按下回⻋键后&#xff0c; scan…

作者头像 李华
网站建设 2026/10/19 2:10:47

JS 1-25

JavaScript书写位置 内部JavaScript 直接写在html文件里&#xff0c;用script标签包住 规范&#xff1a;script标签写在上面 拓展&#xff1a; alert(‘你好&#xff0c;js’) 页面弹出警告对话框 e.g. <body><!-- 内部js --><script>// 页面弹出警示框alert…

作者头像 李华
网站建设 2026/10/19 2:10:47

图片里能不能找到指定目标?用华为云码道开发 OpenCV 模板匹配工具

前面已经做过 ORB 特征匹配&#xff0c;这次换一种完全不同的思路&#xff1a;Template Matching&#xff0c;模板匹配。这次我用华为云码道辅助开发了一个 OpenCV 模板匹配工具&#xff0c;通过把一张小模板在大图中不断滑动&#xff0c;寻找局部结构最相似的位置。1. 这次工具…

作者头像 李华