Git原理与高级使用(4)

1,421 阅读8分钟

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来完成。

指令

  1. rebase时直接使用根基分支的修改

    当我们执行rebase时,当遇到冲突时,我们除了可以手动的去解决冲突外,还可以通过执行git rebase --skip直接的使用根基分支的修改

场景

  1. 如果我已经rebase完了,怎么回到原状

    我们可以先通过git reflog找到之前rebase的那条记录id

    然后执行git reset commit-id --hard就可以回到rebase前的状态了。

    另外在每次做rebase前,git都会为我们创建一个ORIG_HEAD记录rebase前的状态让我们不需要reflog就可以快速切回去,所以就可以直接执行git reset ORIG_HEAD --hard

  2. 如何我想去修改历史信息

    假设这里我们想要从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也因此跟着被修改了

  3. 如果是非文字的文件产生冲突了怎么办

    当非文字的文件产生冲突时,其实我们是没有办法决定具体哪一行出了问题的,所以这时候我们可以执行git checkout 文件名 --theirs来使用别人的文件或者执行git checkout 文件名 --ours来使用我们的文件

  4. 如果我想把过去的多个commit合并成一个commit

    跟上面一样第2个一样我们先进入互动模式,将pick改为s或者squash,这样连在一起的s就会与他们上一个pick合并成一个commit了,接着我们保存退出,git依旧是循环让我们去修改我们改动的commit,完成之后我们就做到了合并的效果了

  5. 如果我想把过去的一个commit拆成多个commit

    一样是进入互动模式,接着将pick改为e或者edit,接着保存退出,这里我们就会看到git提示我们可以开始去修改commit,并且在结束后执行rebase continue的动作,其实这里git就暂时将我们转至我们要修改的那个commit

    这时候我们就可以执行git reset HEAD^,看过之前的文章就知道这里默认其实就是mixed模式,所以当前commit前后的修改就被转至工作区,接着我们就可以按我们想要的commit形式重新把工作区的文件添加到暂存区并且提交,最后执行rebase continue,我们就成功完成了拆分commit的动作

  6. 如果我想在commit之间加入新的commit

    其实这个跟上面的场景几乎差不多,唯一的区别是我们不需要再使用git reset,而是直接添加提交我们要的修改就可以了

  7. 如果我想调整commit的顺序或者删除掉几个commit

    调整顺序的话我们只需要在互动模式中直接把pick的每条commit调成我们要的顺序即可,而对于删除commit,我们也只需要把想删掉的pick直接删除就可以了。当然在做这些操作之前都要思考清楚,毕竟像调整顺序的话如果你把对一个文件的修改commit放到了这个文件的创建commit之前,那就会出事了

  8. 我不小心把账号密码放到了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

  1. 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的情况