Git 进阶使用
一、git merge
1、git merge 原理
- 找到当前分支的 HEAD 提交
- 找到要合并的 feature 分支的 HEAD 提交
- 找到这两个分支的最近公共祖先
- 三方合并
- 如果这三份内容在某个文件的某处不一致 → Git 自动尝试合并
如果某一行在两个分支中都被不同地修改过 → 冲突,你必须手动解决
2、merge 遇到的坑——手动改动
假如你有两个分支,分别是 main 和 feature.
main:hello = "world"feature:hello = "earth"
这两个分支都改了 hello 的值,然后这两个分支的最近公共祖先提交的快照都是没有 hello 的。如果 main 和 feature 合并,git 以最近公共祖先为基础,分别比较这两个分支的 HEAD 节点有什么地方跟公共祖先是不一样的。然而 git 会发现这两个 HEAD 都对 hello 改过,所以就不知道该应用哪个更改,于是就会这样:
<<<<<<< HEAD
hello = "world"
=======
hello = "earth"
>>>>>>> feature
然后你就要手动更新这个文件来告诉 git 应该应用哪个分支的更改。
二、git fetch
1、git fetch 原理
- git 先看当前分支和要 fetch 的分支有没有最近公共祖先节点
- 如果有最近公共祖先节点,那就对比远程和本地有哪些节点是不同的,然后在本地的最近公共祖先节点的基础上新建一个分支,然后把远程和本地比不同的节点都加到这个分支里
- 如果没有最近公共祖先节点,就新建一个分支来存远程分支的所有节点,但这个分支跟本地的分支是平行的,不相交的。
三、git rebase
1、git rebase 原理
- 找到这两个分支的最近公共祖先
- 把 (公共祖先 , 当前分支 HEAD 节点 ] 区间的所有节点一个个地通过复制更改的方式追加到 rebase 那个分支的末尾。
注意:新复制的节点的 commit id 已经和原来的不一样了,所以旧的节点看起来就消失了。但其实旧的节点并没有消失,它会临时被 git reflog 引用,此时节点的引用计数不为 0;所以你依然可以通过 git reflog 来恢复旧的节点。但如果过了一定时间后,git reflog 就会收回对这个节点的引用,此时节点的引用计数为 0,节点就会被销毁了。
2、git rebase VS git merge
综合来讲,git rebase 这个指令比较鸡肋。就是虽然可以把一个分支的所有节点追加到另一个分支上,但是追加的部份是这两个分支共有的,浪费当前分支。
四、git cherry-pick
1、git cherry-pick 有什么用
如果你想把另一个分支的某个特定节点相对于前一个节点的更改应用到当前分支的 HEAD 节点上,那么 git cherry-pick 是最适合的。
2、git cherry-pick 原理
cherry-pick 之前:A--B--C <- main\\ D--E--F <- feature在 main 分支 cherry-pick E 后:A--B--C--E' <- main\\ D--E--F <- feature
这里我们以在 main 分支 cherry-pick E 为例:
- git 先算出来 diff(E, D) 的部份
- git 在最新提交(C节点)的上下文定位到要加 diff(E, D) 的地方
- 如果定位到地方,就直接新建一个节点,应用这个 diff。
- 如果定位不到,就人工编辑,然后再执行 cherry-pick --continue
3、git cherry-pick 定位不到的情况
同一段代码被改动但方式不同
在 feature E 中
- return fetchDataFromDB();
+ return fetchDataFromAPI(); // E 相对 D 比改为 fetch API在 main C 中
- return fetchDataFromDB();
+ return fetchDataFromCache(); // C 相对 B 比改为 fetch cache此时会定位失败,因为 git 在 C 中找不到 return fetchDataFromDB() 了
- 当前分支的最新节点找不到上下文
feature E 中,跟 D 相比,在 func4() 下面加了 func5():func4() {}
func5() {}main C 中,没有 func4() 那堆代码此时因为 git 找不到 func4(),就不知道 func5() 应该加到哪个位置,定位失败,所以会造成冲突
4、在 merge commit 处如何 cherry-pick
A -- B -- C ------- G(HEAD) <- main\ /D -- E -- F <- feature
假如我们要 cherry-pick G,那么 git 因为不知道取 diff(G, F) 还是 diff(G, C),所以会报错。不过我们可以通过 git cherry-pick -m 1 G 来让 git 取 diff(G, F),git cherry-pick -m 2 G 来让 git 取 diff(G, C).
五、git reset
1、git reset --soft
HEAD 指针回退,工作区的内容不会变化。但暂存区存的东西变成了新 HEAD 后面的当前分支的所有节点内容相对于 HEAD 的 diff。然后新 HEAD 当前分支后面的节点都被移除。
所以在 soft & mixed & hard 当中,有且只有 soft 才能做到把多个节点合成一个节点。mixed 和 hard 都不行。
2、git reset --mixed
HEAD 指针回退,工作区的内容不会变化。但暂存区并没有存后面当前分支所有节点相对于新 HEAD 的 diff。然后新 HEAD 当前分支后面的节点都被移除。
3、git reset --hard
HEAD 指针回退,工作区的内容会变成当前新 HEAD 的内容;暂存区没有存后面当前分支所有点相对于新 HEAD 的 diff,然后新 HEAD 当前分支后面的节点都被移除。
4、从分支开始处向前 reset
HEAD 回退,但 git 并不会删除任何分支,所以分支开始处不会被删;取而代之的是 git 会把这个交点直接改为不属于当前分支的节点,但不会删除这个节点。然后新 HEAD 节点成为新的分支开始处节点。(这样可以确保其他分支的历史内容不会变化)。
reset 前(当前在 main 分支):A -- B(HEAD) <- main\ D -- E -- F <- feature在 main 分支 reset 后:A(HEAD) <- main\ B -- D -- E -- F <- feature此时 B 节点已不属于 main 分支的了,但 B 节点不会被删
5、从 merge commit 向前 reset
HEAD 回退,并且虽然这个节点是个 merge commit,但 git 会删掉这个节点。可能是因为删了这个节点并不会引起分支的删除,所以就删掉了。
reset 前(当前在 main 分支):A -- B -- C ------- G(HEAD) <- main\ /D -- E -- F <- feature在 main 分支 reset 后:A -- B -- C(HEAD) <- main\ D -- E -- F <- feature
六、git revert
1、git revert 基本原理
git revert 某节点的原理是:git 会算这个节点跟前一个节点的 diff,然后新建一个节点,内容是 revert 节点的内容 - diff。
原来的提交历史:A -- B -- C -- D (HEAD)| git revert C|现在的提交历史:A -- B -- C -- D -- C' (HEAD)其中 C' 是 C - diff(C, B) 后的内容
2、对一个 merge-commit revert 怎么解决
A -- B -- C ------- G (main)\ /D -- E -- F (feature)
就像这个例子,我要 git revert G 的话,git 就会感到很疑惑:G 相对 F 和 C 都有 diff,那我新建节点时应该减哪个分支的 diff 呢,于是就拿不准就报错了。但幸运的是,我们可以用 git revert -m <数字> G 来告诉 git 要保留哪个分支的 diff,其中数字就是分支的编号,然后 1 就是 main 分支。
所以如果执行 git revert -m 1 G 的话,意思就是保留 diff(G, C);然后新建的节点内容就是 G - diff(G, F) 了。
七、git stash
1、git stash 原理
git 会保留当前工作区的所有代码和暂存区的所有更改,然后工作区会变成 HEAD 的内容,暂存区没有更改。
2、git stash pop & git stash apply
git stash pop 会应用最近一次 stash,然后删除最近的 stash;git stash apply 应用最近一次 stash,但不删除最近的 stash.
八、git flow 规范
1、feature
用来开发某个功能的分支。每个 feature 实现完,都要合并到 develop 里。
2、develop
所有功能的集成分支。
3、release
当 develop 的某个提交足够成熟了,才分出 release 分支进行 debug,如果 release 没问题,就同时向 develop 跟 master 合并分支。
4、hotfix
紧急处理 bug 的分支,一旦处理完同时向 master 和 develop 合并。
5、master
给用户用的版本。