Git高级技巧:rebase、cherry-pick、bisect实战

18 阅读5分钟

用了好几年Git,大部分人的操作可能就是add、commit、push、pull、merge。够用是够用,但遇到一些复杂场景就抓瞎了。

这篇聊几个进阶操作,都是我实际工作中用得上的。

rebase:让提交历史干净点

合并多个commit

开发一个功能,写着写着提交了七八次,有些commit message还写得很随意,比如"fix"、"xxx"、"临时提交"。

合到主分支之前,最好把这些合成一个有意义的提交。

# 合并最近4个commit
git rebase -i HEAD~4

会打开一个编辑器:

pick abc1234 添加用户模块
pick def5678 fix
pick ghi9012 临时提交
pick jkl3456 完善用户模块

把后面几个的pick改成squash(或者s):

pick abc1234 添加用户模块
s def5678 fix
s ghi9012 临时提交
s jkl3456 完善用户模块

保存退出,会让你重新编辑commit message,这时候写一个完整的描述就行了。

最后这个功能只有一个干净的commit。

修改某个历史commit

发现之前某个commit有问题,想改一下,但不是最新的那个。

# 找到要改的commit的前一个
git rebase -i <commit-hash>^

# 把要改的那行pick改成edit
# 保存退出后,git会停在那个commit
# 做你的修改
git add .
git commit --amend
git rebase --continue

有风险,改完历史commit后需要force push,别在公共分支上干这事。

rebase代替merge

有些团队要求用rebase而不是merge来同步主分支,保持线性历史。

# 在feature分支上
git fetch origin
git rebase origin/main

# 有冲突就解决,然后
git rebase --continue

我个人习惯是自己的分支用rebase,合到主分支用merge。各有利弊,看团队规范。

cherry-pick:摘樱桃

把某个commit单独拿过来,不带整个分支的其他东西。

场景:hotfix需要同步到多个分支

线上有个bug,在main分支修了。但release/1.0分支也需要这个修复。

# 先找到修复的commit hash
git log --oneline main
# 假设是 abc1234

# 切到需要同步的分支
git checkout release/1.0
git cherry-pick abc1234

如果有冲突,解决后:

git add .
git cherry-pick --continue

摘多个commit

# 连续的几个
git cherry-pick abc1234^..def5678

# 不连续的
git cherry-pick abc1234 def5678 ghi9012

只摘代码不提交

有时候只想把改动拿过来,但不想直接提交,想再改改。

git cherry-pick -n abc1234
# 改动会放到暂存区,不会自动commit

bisect:二分法找bug

这个真的救过我命。

有一天线上报了个bug,但不知道是哪个版本引入的。几百个commit一个个看太慢了。

git bisect用二分法快速定位。

# 开始bisect
git bisect start

# 告诉git当前版本有bug
git bisect bad

# 告诉git某个老版本没bug(比如上周的release)
git bisect good v1.2.0

然后git会checkout到中间的某个commit,你测试一下有没有bug:

# 如果这个版本有bug
git bisect bad

# 如果这个版本没bug
git bisect good

git会继续二分,几次之后就能定位到具体是哪个commit引入的bug。

# 找到后,git会告诉你
# abc1234 is the first bad commit

# 结束bisect
git bisect reset

如果测试可以自动化,还可以:

git bisect run ./test.sh
# test.sh返回0表示good,非0表示bad
# git会全自动找到问题commit

stash:临时存一下

写到一半,突然要切分支处理别的事。

# 存起来
git stash

# 切分支干活...

# 回来后恢复
git stash pop

stash可以存多个:

git stash list
# stash@{0}: WIP on feature: abc1234 xxx
# stash@{1}: WIP on main: def5678 yyy

# 恢复指定的
git stash apply stash@{1}

# 删除
git stash drop stash@{0}

给stash加个描述,不然多了分不清:

git stash push -m "用户模块写了一半"

reflog:后悔药

误操作把commit搞丢了?别慌,git其实都记着。

git reflog

会显示所有操作历史,包括那些"丢失"的commit:

abc1234 HEAD@{0}: reset: moving to HEAD~1
def5678 HEAD@{1}: commit: 重要的提交

找到要恢复的commit hash,checkout或reset回去就行:

git checkout def5678
# 或
git reset --hard def5678

reflog默认保留90天,只存在本地,是最后的救命稻草。

几个实用alias

配到~/.gitconfig里:

[alias]
    co = checkout
    br = branch
    ci = commit
    st = status
    
    # 好看的log
    lg = log --oneline --graph --decorate
    
    # 上次commit改了啥
    last = log -1 --stat
    
    # 撤销上次commit但保留改动
    undo = reset --soft HEAD~1
    
    # 暂存所有并commit
    ac = !git add -A && git commit -m

用起来:

git lg
git last
git undo
git ac "fix: 修复登录问题"

几个坑

公共分支别rebase

rebase会改变commit历史。你rebase了,别人pull的时候会很惨,各种冲突。

自己的分支随便rebase,公共分支别动。

force push要小心

# 这个会覆盖远程,别人的提交可能丢失
git push -f

# 稍微安全一点,只有远程没新提交才会成功
git push --force-with-lease

merge还是rebase

这个争论没意义。merge保留完整历史,rebase保持线性。看团队规范,统一就行。

我的习惯:

  • 自己feature分支同步main:rebase
  • feature合到main:merge(保留分支历史)
  • hotfix同步到多个分支:cherry-pick

Git的命令很多,但真正常用的就这些。把这几个场景搞明白,基本够用了。

遇到不确定的操作,记得先备份分支:

git branch backup-xxx

有了后悔药,心里踏实。