Rebase
rebase 与 merge 其实都是完成类似的工作的, 但是背后的工作方式却有很大的不同, 让我们用以下两张图来说明一下两者的区别
第一张图是 merge 的工作原理, 当我们在 mywork 分支中调用 git merge origin,git 就会根据 c2,c4,c6 这三个点来计算合并出一个新的 commitc 点 c7.
第二张图则是 rebase 的工作原理, 当我们在 mywork 分支中调用 git rebase origin 时, c5 变成了 c5',c6 变成了 c6', 并且父 commit 指向了 c4, 而原来的 c5 和 c6 就没有用了.
从上面就可以看出, 在执行 git rebase 后会将两条分支有变成在同一条 commit 链上, 而 merge 则是依然可以看到分叉的 commit 链, 背后呢其实就是等于 rebase 修改了 git 的提交历史, 把之前分支上的 commit 一个一个接到另一条分支上
当然 rebase 的过程中也一样有可能会产生冲突, 这个时候只要我们碰到一个冲突, 就需要解决它, 然后执行 git add 添加, 接着执行
git rebase --continue
去转接下一个 commit, 直到所有 commit 都转接到了另一条分支上.
在 rebase 进行中的任何时刻, 我们都可以通过执行 git rebase --abort 将分支恢复到 rebase 之前的状态.
rebase 的好处就是会让我们的 commit 链变得整洁好看, 但是对于使用 rebase 需要注意的点就是不要在 master 分支上执行, 因为通常我们在 master 分支上的是相对稳定的代码, 我们不会想要去修改到它的历史, 另外, 不要对已经推送到远程的分支进行 rebase, 通常只对本地还未推出的分支做, 因为远程的分支可能其他的用户已经将它们拉取到各自的本地版本库, 我们不希望他们因为我们执行了 rebase 而要更改自己原本的提交历史, 这即便对 git 来说也很难处理, 会出现一些意想不到的情况. 如果是上述这些场景还是应该使用 merge 来完成.
指令
rebase 时直接使用根基分支的修改
当我们执行 rebase 时, 当遇到冲突时, 我们除了可以手动的去解决冲突外, 还可以通过执行 git rebase --skip 直接的使用根基分支的修改
场景
如果我已经 rebase 完了, 怎么回到原状
我们可以先通过 git reflog 找到之前 rebase 的那条记录 id
然后执行
git reset commit-id --hard
就可以回到 rebase 前的状态了.
另外在每次做 rebase 前, git 都会为我们创建一个 ORIG_HEAD 记录 rebase 前的状态让我们不需要 reflog 就可以快速切回去, 所以就可以直接执行
git reset ORIG_HEAD --hard
如何我想去修改历史信息
假设这里我们想要从 init commit 开始重新整理 git 的历史信息, 我们可以先定位到 init commit 那条 commit id, 接着通过
git rebase -i commit-id
进入互动模式
这里可以看到 commit 以一个倒序的顺序排列出来了, 这里 pick 就代表的是不对 commit 进行改变, 如果想改变的话可以将 pick 替换成 r 或者 reword, 然后保存退出
接着 git 就会循环跳出刚才我们改成 r 的每个 commit 让我们去修改
全部修改完后我们再看回 git 历史, 就发现我们刚才的改动全部应用上了
这里可以看到除了 git 历史信息被修改了, 连对应的 commit-id 都被修改了, 而且它们之后的所有 commit 也因此跟着被修改了
如果是非文字的文件产生冲突了怎么办
当非文字的文件产生冲突时, 其实我们是没有办法决定具体哪一行出了问题的, 所以这时候我们可以执行
git checkout 文件名 --theirs
来使用别人的文件或者执行
git checkout 文件名 --ours
来使用我们的文件
如果我想把过去的多个 commit 合并成一个 commit
跟上面一样第 2 个一样我们先进入互动模式, 将 pick 改为 s 或者 squash, 这样连在一起的 s 就会与他们上一个 pick 合并成一个 commit 了, 接着我们保存退出, git 依旧是循环让我们去修改我们改动的 commit, 完成之后我们就做到了合并的效果了
如果我想把过去的一个 commit 拆成多个 commit
一样是进入互动模式, 接着将 pick 改为 e 或者 edit, 接着保存退出, 这里我们就会看到 git 提示我们可以开始去修改 commit, 并且在结束后执行 rebase continue 的动作, 其实这里 git 就暂时将我们转至我们要修改的那个 commit
这时候我们就可以执行 git reset HEAD^, 看过之前的文章就知道这里默认其实就是 mixed 模式, 所以当前 commit 前后的修改就被转至工作区, 接着我们就可以按我们想要的 commit 形式重新把工作区的文件添加到暂存区并且提交, 最后执行 rebase continue, 我们就成功完成了拆分 commit 的动作
如果我想在 commit 之间加入新的 commit
其实这个跟上面的场景几乎差不多, 唯一的区别是我们不需要再使用 git reset, 而是直接添加提交我们要的修改就可以了
如果我想调整 commit 的顺序或者删除掉几个 commit
调整顺序的话我们只需要在互动模式中直接把 pick 的每条 commit 调成我们要的顺序即可, 而对于删除 commit, 我们也只需要把想删掉的 pick 直接删除就可以了. 当然在做这些操作之前都要思考清楚, 毕竟像调整顺序的话如果你把对一个文件的修改 commit 放到了这个文件的创建 commit 之前, 那就会出事了
我不小心把账号密码放到了 git 里面, 怎么删除呢
git 提供了一个 filter-branch 指令帮我们批量的对 commit 进行改动, 假设我们的账号信息放在了 config/database.yml 中, 我们可以执行
git filter-branch --tree-filter "rm -f config/database.yml"
, 这样 git 会对每一条 commit 都执行删除的操作, 那如果我后悔了想要恢复回来怎么办呢, 我们可以执行
git reset refs/original/refs/heads/master --hard
就可已恢复刚才的删除操作了, 这个 refs/original/refs/heads/master 其实就是我们之前说的 ORIG_HEAD, 是 git 在我们做一些危险操作时备份出来的一个指针以便我们撤回之前的操作. 所以这里如果我们真的要彻底删除还需要把 refs/original/refs/heads/master 也一并删除掉. 接着还有一个找的回来的地方就是 reflog, 所以我们也要清理掉 reflog, 它默认是 30 天清除一次, 我们可以通过执行
git reflog expire --all --expire=now
让 git 马上清除掉. 最后我们在这一系列操作之后就会有一堆闲置没有用的对象了, 我们可以通过
git fsck --unreachable
列出来这些对象, 当然因为他们已经没有用了, 我们就可以直接执行 git gc --prune=now 把它们即刻清理掉, 这样我们就彻底的把账号信息从 git 中删除了. 从这里也看得出来其实对于 git 来说要真正意义上删除记录是非常困难的, 所以我们都经常说 git 永远有后悔药吃, 因为总是有办法可以找回原来的历史记录.
Q&A
reset,revert 和 rebase 有什么区别
前面我们介绍过了 reset 和 rebase, 那我们先来说一下 revert, 我们可以通过执行
git revert commit-id
来还原某一个 commit, 但是这个还原不是删除那个 commit, 其实 revert 是加了一个新的 commit 然后把内容修改为我们想要 revert 的那个 commit 之前的状态 (这里如果在 revert 时想用默认的 commit 信息不去修改的话可以加上 --no-edit 选项).
所以可以看出 reset 和 rebase 是会改变历史记录的, 而 revert 则不会
reset 多用于还没有推送到远程的 commit, 我们可以将它还原至某个我们指定的 commit
rebase 也多用于没有推送到远程的 commit, 当然他的能力更强大一些, 不管你想做新增, 修改或是删除等都可以做到, 很适合用来整理本地的 commit 历史
revert 的话就比较适合已经推出去 commit, 或者一些团队开发规范下不准使用 reset 和 rebase 的情况
来源: https://juejin.im/post/5ada9ca2518825671a6357e4