Git
本地
什么是HEAD

- 真正在时间线上的是commit
- branch名只是一个指针,指向某个commit
HEAD也只是一个特殊的指针,其身份其实跟main这种分支名没什么两样- 它通常指向某个分支的最新commit(等同于指向该分支名)
cat .git/HEAD查出来的就是“分支名”
- 但你可以将它指向历史上任一commit detached head
cat .git/HEAD查出来的就是commit的名字
- 很少真的需要将
HEAD指向一个commit,而是指向branch的末端(即表现为reference)- 指向了(
git checkout commitid)那就detached了
- 指向了(
git log --pretty=format:"%h: %d" -1可以打印head和分支的关系git show HEAD可以查看HEAD
HEAD is YOU
- HEADis a symbolic reference pointing to wherever you are in your commit history.
- It follows you wherever you go, whatever you do, like a shadow.
- If you make a commit, HEAD will move.
- If you checkout something, HEAD will move.
- If you have moved somewhere new in your commit history, HEAD has moved along with you.
- To address one common misconception: you cannot detach yourself from HEAD.
- HEAD can point to a commit, yes, but typically it does not.
- when you do certain things (e.g., commit or reset), the attached branch will move along with HEAD.
即分支是跟着HEAD移动,
Detached HEAD
我也不知道分离HEAD有什么用,果然,操作一下后,官方提示是给测试的:

我在分离的HEAD上做一次提交后:

- 从C3开始(就是checkout到的位置)产生了新的分支
- 新的提交也是标记为是在
HEAD上做的 此时你再切换分支,这个commit是会被丢弃的(看警告文字),测试结果也是切走再切回原分支,这个hello的commit确实不在了
HEAD、Index、Working Directory
- HEAD 指向最近一次commit里的所有snapshot
- Index 缓存区域,只有Index区域里的东西才可以被commit
- Working Directory 用户操作区域
也就是说
add是把当前内容stage到index区域commit则是把index里的东西复制(?)到一个指定区域,随后HEAD就指向它 而checkout则相反- 首先就是更改
HEAD指向目前分支的最后一次commit - 然后用这个commit指向的snapshot替换掉index区域里的内容
- 把index区域里的内容填充到工作区域(即编辑器能操作的代码)
Reset
reset实际上有3个步骤,根据不同的参数可以决定执行到哪个步骤(--soft, --mixed, --hard)。
- 改变HEAD所指向的commit(
--soft) - 执行第1步,将Index区域更新为HEAD所指向的commit里包含的内容(
--mixed) - 执行第1、2步,将Working Directory区域更新为HEAD所指向的commit里包含的内容(
--hard)
实测
- 做一个
_commit, 一直到commit状态,HEAD正常指向它 - 做一个
_indexed,只add,显示staged,即到了indexed区域 - 添加一个
_workidr, 什么也不做,但是表示了当前可见的工作区域
现在三个区域分别有三个文件,文件名表示了它们的位置,我们做一次soft的reset:git reset HEAD~1 --soft:
- HEAD往上溯了一个commit,
- index区域却出现了
_commit文件,说明soft是会改index的。 - 这个commit从历史里消失了
所以上文的描述是不准确的,HEAD从_commit那个提交往上溯后,这个文件如果不是直接丢弃,必然是要么到index,要么到work dir的。【实测并没有丢弃】。
恢复提交结构,mixed(默认,不带参数一样)一次
- 发现三个文件都到了working dir
如果我没对working directiory的理解有误的话,从上述两个试验可以得知
- soft就是把目标是时间线后的提交都放到index里去
- mixed则把目标时间线后的提交都放回working dir
- 并且把index区域的也放回了working dir
猜猜hard参数会怎么处理?
_commit和_indexed都消失了!_workdir仍然不影响
所以,
- reset不会影响用户编辑过却没有过任何操作的文件(unstaged)
soft, mixed, hard决定了把reset目标时间线后文件的归宿- soft把commit掉的放回indexe区域(staged)
- mixed把commit掉的和staged了的全部改为unstaged状态,不丢弃
- hard直接丢弃commit和staged了的
上文或下文,indexed和staged会混用,一个是位置,一个是行为,就不追求严谨和统一了,都表示add后文件的位置和状态
reset 指定文件
我对文件c1.txt做了三次提交,每次添加一行文字(得到"c1, c2, c3"), 然后再执行一次git reset HEAD~2 c1.txt
先直观看下结果:
可见还是比较复杂的
- 把当前文件跟目标文件进行比对,把差异进行了改动并且存到了index区域,
- 也就是说这次reset后,可以直接commit,就能提交目标版本的文件
- 但是在当前工作区域,又做了恢复成当前编辑状态的改动记录,
- 也就是说,如果对改动后悔了,把这些改动也
staged, 你会神奇地发现,两个相反的改动就抵消了,即一少枨科要提示没有任何变动 - 同样,你把staged的进行unstage,也会在unstage区域互相抵消
- 也就是说,如果对改动后悔了,把这些改动也
- 只是从别的commit取文件,并不会改变HEAD的指向
V.S. Checkout
我们会用checkout切换分支,那么同样的命令改用reset呢?看示意图:

上面用reset来恢复文件到某个提交,这个我以前用checkout更多:git checkout HEAD~2 c1.txt,直接看结果:
checkout把需要做的改动提交到indexed区,但不会像reset一样把恢复改动的变更放到unstaged区里去。
强制修改分支位置
checkout等都是修改HEAD,分支里记录的内容是不动的,那么怎么直接让分支指向某个提交呢?
git branch -f main HEAD~3
这个直接让main指向了之前的三级父提交,而HEAD却没变。
checkout是移HEAD(叫分离HEAD), 而branch -f是强制修改分支的的指向(即内容)reset是带着HEAD一起移动
Revert
reset直接把历史改写了,而且不影响远程分支- 向上一个提交回滚:
git reset HEAD^,注意要^一下 - 而用
revert:git revert HEAD,并不需要向上翻一层 - 而是把前一次提交复制出来创建了一个新提交(也可以理解为撤销本次提交所需要进行的变动)
- 这样就保留了完整的A改为B又改回A的两次操作的历史(而reset里相当于你没做过任何事)
Merge V.S. Rebase
- 都用于合并不同分支的修改
- A分支
mergeB分支,合并后A分支拥有了B分支所有的改动- 并且把这些改动作为A分支的一个新的提交
- B分支仍然存在
- A分支
rebaseB分支,合并后只剩下一条线了- 从共同父节点出来所有的变动,会以commit为单位挂到B分支后面
- A和B成了一条线上的两个不同的时间节点,不再有分岔

思考题,那A rebase B和B rebase A有什么区别?
Demo

- 首先,把C3挂到C2后面,根据上面的图,我用了
git rebase bugFix main,结果变成了把C2挂到C3后面,所以前面的图可能有误?- 没有误,上面说的是单参数的情况
- 单参数是把当前分支的提交拼到目标分支后
- 双参数是把参数2分支的提交拼到参数1分支后,也就是说上面的命令实为把main挂到bugFix后,我们修改下。
没有了这个困惑后,要实现图中的效果,就是这三句话了:
git rebase main bugFix
git rebase bugFix side
git rebase side another
# 最后,再把main指向当前
git branch -f main HEAD
对比:
- Rebase 使你的提交树变得很干净, 所有的提交都在一条线上
- Rebase 修改了提交树的历史
- 比如, 提交 C1 可以被 rebase 到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。
- 一些开发人员喜欢保留提交历史,因此更偏爱 merge。
- 而另一些人可能更喜欢干净的提交树,于是偏爱 rebase。
交互式rebase
,要回滚到b
- 我们可能会用
git reset HEAD~4
但如果同时还要保留c和e(甚至还要排下序)呢?
- 用rebase(加上需要交互的参数
-i,即interactive)git rebase -i HEAD~4,然后根据弹出的对话框操作(通常用vim打开个文档) - git会按照你的选择依次复制出对应的commit(成新的commit)
- 并将HEAD指向最新的一个commit
- 所以如果你在交互窗口里选了所有的commit,那么就可以起到"纯排序commit"的作用
用
cherry-pick来实现: 回到A的前一次提交,再pickA, ammend, 再把B pick出来
Demo
如果你在main上想修复一个bug,先做了一个commit,写了debug的代码,然后又做了一个commit,里面对感兴趣的地方加了日志输出,最后修复了bug,而之前的debug和print显然是不需要也不希望提交上去的(比如你最终定位到问题后只改动了一行代码)
我们要把C4应用到C1上,当然可以选择checkout到main,再cherry-pickC4,或者:git rabase -i HEAD~3 时只选择C4,得到下图

这时只需要git branch -f main HEAD把main强制指向bugFix即可。
注意, rabase -i后就生成了一条新的分支,把C4的变动挂到了main后面,这一点与rebase还是有区别的,rebase会把自己的commit挂到目标分支上去
demo2 假设做了一个提交A,然后做了个B,发现需要修正一下A。一般我们再做一个提交C就是了,里面就是修复A的内容,但我们还可以这么做:
git rebase -i将A,B换个顺序git commit --amend,追加修改git rebase -i恢复排序- 把main移到修改后的最前端
这样做的唯一不同就是没有增加一次commit,而是“修改”了它。
git describ
看图,v1_2_gC2表示:
- 距离
main最近的标签是v1 v1到当前有2次提交- 当前的hash是
C2
这样看git describe side也就好懂了,就是距离v2有一个提交
^和~(相对引用修饰符)
HEAD~2表示往上溯两个父提交HEAD^2表示当前提交是由几个提交合并而来的,这里面的第2个提交- 时间顺序,从1开始

还可以链式使用:
git checkout HEAD~^2~2

可见第一个~后应该是省略了一个1

这个要求是在C2处创建一个bugWork分支:
git branch bugWork HEAD^^2^
git branch bugWork HEAD~^2~
git branch bugWork HEAD~1^2~1
以上三句都是等价的,知道是在做相对引用就行
远程
origin/main表示的是远程分支- 你平时可能意识不到远程分支存在本地
- 一个例子,
git fetch其实就是把代码从服务端下载到本地的远程分支 - 所以本地代码并没有发生改变,分支也没有移动
- 所以你才需要人为去
merge一下(这下你知道merge的是什么了吧) - 所以才能把两个命令合并的
pull
- 一个例子,
- 当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 o/main)。
- 然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 main。
除了merge,任何本地分为的操作都是可以的
git cherry-pick o/main
git rebase o/main
git merge o/main
...
你在push之前,你的同事已经多次push了代码,并改动了你使用的API(或者说代码冲突了),你直接push是会失败的,以前我是先pull,修复了冲突后再push。
这里介绍fetch后rebase
git fetch
git rebase o/main
git push
其中第二句话我们知道,main上的新增commit会挂到o/main上去(此时main和o/main在同一条线),至于修复冲突,应该与merge没区别

使用merge和rebase再push,对远端的影响是不一样的。
最后
git pull是fetch + mergegit pull --rebase则是相应的rebase版
要实现这个目的,自然是rebase2到1,再把side3 rebase进来,再把remote上的rebase进来:
git rebase side1 side2 # 记住,是把后面的挂到前面分支
git rebase side2 side3
git branch -f HEAD # 把main移到最顶端
git checkout main # 远端只要main,我们切换到main
git pull --rebase
git push
为什么有人偏爱rebase?看看如果使用merge,会产生什么样的图:
确实能看到每一个commit怎么来的,哪一个又是哪些合并的,但是这个追溯有什么用?
追踪远程分支
前面说过,main和o/main会默认建立track,我们可以手动设置别的关联
git checkout -b totallyNotMain o/main
也就是说从一个分支建出来的分支,会自动track这个分支,即执行push的时候,会自动往这个分支提交
方法二:
git branch -u o/main foo # 如果就在foo上,可以省略foo
输出:
local branch "main" set to track remote branch "o/main"
git push origin main表示不管你的HEAD在哪,从本地找到main分支,提交到origin的main分支git push origin foo:main则表示不管你的HEAD在哪,把本地的foo提交到远端的main- 还可以加相对引用
git push origin foo^:main - 还可以上传无端不存在的分支
git push origin main:newBranch
- 还可以加相对引用
empty:branch
git push origin :foo会删除远程分支git fetch origin :bar却是新建本地分支的意思...
以前的一些笔记: Notes
Children