likes
comments
collection
share

修改 git 的历史 commit,你能想到几种方案?

作者站长头像
站长
· 阅读数 55

最近遇到一个 git 的问题:

我在某个文件里写了一段不应该提交上去的内容,没注意,提交上去了。

后来又提交了很多个 commit。

之后我发现了这个,又把它去掉了,提交了一个新的 commit。

修改 git 的历史 commit,你能想到几种方案?

这样虽然新的 commit 没有这段内容了,但老的 commit 里依然有这个内容。

可我不想保留这段内容的记录,也就是想修改历史 commit。

这种问题大家会怎么解决呢?

我能想到的有三种方案,分别来试一下。

修改 git 的历史 commit,你能想到几种方案?

我们先创建了个 git 项目。

修改 git 的历史 commit,你能想到几种方案?

写了个 index.md,每行内容提交一个 commit。

git log 可以看到,一共 5 个 commit:

修改 git 的历史 commit,你能想到几种方案?

git show 看下 222 和 333 那个 commit:

git show f5482b

修改 git 的历史 commit,你能想到几种方案?

修改 git 的历史 commit,你能想到几种方案?

可以看到,这个 333 的 commit 就是我们想改掉的。

但是现在后面提交了 444、555 这俩 commit 了,怎么改掉它呢?

很容易想到的是 reset 到 333 那个 commit,重新提交,然后把后面的 commit 再一个个 cherry-pick 回去。

我们试一下:

首先把 444、555 这俩 commit 记下来,待会还要用

修改 git 的历史 commit,你能想到几种方案?

然后 git reset 到 333 那个 commit:

git reset --hard 65dfee

修改 git 的历史 commit,你能想到几种方案?

把私密信息去掉,重新提交:

git add .
git commit --amend

修改 git 的历史 commit,你能想到几种方案?

这样,这个 commit 就干净了。

然后把后面的 444 和 555 再 cherry-pick 回来。

cherry-pick 就是单独取一个 commit 过来。

git cherry-pick 0b700f

修改 git 的历史 commit,你能想到几种方案?

会有冲突,解决之后 continue 就好:

git add .
git cherry-pick --continue

修改 git 的历史 commit,你能想到几种方案?

再 cherry-pick 555 的 commit 的时候依然有冲突,因为历史 commit 改了:

修改 git 的历史 commit,你能想到几种方案?

同样是解决之后重新 add 和 cherry-pick --continue

修改 git 的历史 commit,你能想到几种方案?

这样再看下 333 那个 commit,就干净了:

git show 9aded3

修改 git 的历史 commit,你能想到几种方案?

不过这样还是挺麻烦的,git reset 到那个 commit,修改之后重新提交。

之后 cherry-pick 每个 commit 的时候都需要解决一次冲突,因为历史 commit 变了。

当 commit 多的时候就不合适了。

这时候可以用第二种方案: git rebase。

很多同学只会 git merge 不会 git rebase,其实这个很简单。

merge 就是只合并最新 commit,所以只要解决一次冲突,然后生成一个新的 commit 节点。

修改 git 的历史 commit,你能想到几种方案?

而 rebase 则是把所有 commit 按顺序一个个的合并,所以可能要解决多次冲突,但不用生成新 commit 节点。

修改 git 的历史 commit,你能想到几种方案?

merge 是合并最新的,所以只要处理一次就行。

rebase 是要一个个 commit 合并,所以要处理多次。

rebase 除了用来合并两个分支外,还可以在某个分支回到某个 commit,把后面 commit 重新一个个合并回去。

很适合用来解决我们这个问题。

首先回到初始状态:

修改 git 的历史 commit,你能想到几种方案?

然后找到 222 的 commit:

git rebase -i f5482ba

这样就是重新处理从 333 到 HEAD 的 commit,一个个合并回去。

-i 是交互式的合并。

修改 git 的历史 commit,你能想到几种方案?

可以看到,三个 commit 都列了出来,前面的 pick 就是指定怎么处理这个 commit。

下面有很多命令:

pick 是原封不动使用这个 commit

reword 是使用这个 commit,但是修改 commit message

edit 是使用这个 commit,但是修改这个 commit 的内容,然后重新 amend。

squash 是合并这个 commit 到之前的 commit

后面的命令就不看了,很明显,这里我们要用的是 edit 命令。

修改 git 的历史 commit,你能想到几种方案?

改成 edit,然后输入 :wq 退出

提示现在停在了 333 这个 commit,你可以修改之后重新 commit --amend:

修改 git 的历史 commit,你能想到几种方案?

之后再 rebase --continue 继续处理下个 commit。

修改 git 的历史 commit,你能想到几种方案?

这时候会提示冲突,因为历史 commit 变了。

解决之后,重新 add、commit。

修改 git 的历史 commit,你能想到几种方案?

然后 git rebase --continue 继续处理下个 commit:

修改 git 的历史 commit,你能想到几种方案?

历史 commit 变了,依然会冲突。

合并之后重新 add、commit.

修改 git 的历史 commit,你能想到几种方案?

然后再次 git rebase --continue

修改 git 的历史 commit,你能想到几种方案?

因为所有 commit 都处理完了,这时候会提示 rebase 成功。

这时候 git show 看下 333 那个 commit,就已经修改了:

修改 git 的历史 commit,你能想到几种方案?

大家有没有发现,其实 git rebase 和我们第一种方案 git reset 回去再一个个 cherry-pick 是一样的?

确实,其实 git rebase 就是对这个过程的封装,提供了一些命令。

你完全可以用 cherry-pick 处理一个个 commit 来代替 git rebase。

这两种方案都要解决冲突,还是挺麻烦的。

又没有什么不用解决冲突的方案呢?

有,就是 filter-branch。

它可以在一系列 commit 上自动执行脚本。

比如 --tree-filter 指定的脚本就是用来修改 commit 里的文件的。

我们再回到初始状态:

创建了一个 script.js

const fs = require('fs');

const content = fs.readFileSync('./index.md', {
    encoding: 'utf-8'
});

console.log(content);

就是读取 index.md 的内容并打印。

然后执行 filter-branch 命令:

git filter-branch --tree-filter 'node 绝对路径/script.js' 9aded3..HEAD

这里指定用 --tree-filter,也就是处理每个 commit 的文件,执行 script 脚本。

也就是从 222 那个 commit 到当前 HEAD 的 commit,每个 commit 执行一次 script 脚本。

大家觉得执行结果一样么?

修改 git 的历史 commit,你能想到几种方案?

答案是不一样,因为每个 commit 这个文件的内容不同。

那我们在这个 script 里改变了文件的内容不就行了?

const fs = require('fs');

const content = fs.readFileSync('./index.md', {
    encoding: 'utf-8'
});

const newContent = content.replace('私密信息', '');

fs.writeFileSync('./index.md', newContent);

再跑下试试:

修改 git 的历史 commit,你能想到几种方案?

执行成功,提示 main 分支已经被重写了。

然后我们再 git show 看下 333 那个 commit

修改 git 的历史 commit,你能想到几种方案?

修改 git 的历史 commit,你能想到几种方案?

确实去掉了私密信息。

再看看 444 的 commit:

修改 git 的历史 commit,你能想到几种方案?

这就是 filter-branch 的方案。

相比 reset + cherry-pick 或者 rebase 的方案,这种不需要一个个合并 commit,解决冲突。

只需要写个修改内容的脚本,然后自动执行脚本来改 commit 就行,便捷很多。

但是,要注意的是,改历史 commit 肯定是需要 git push -f 才能推到远程仓库的。

而改了历史 commit 的结果我们也都看到了,需要把后面的 commit 一个个重新合并,解决冲突。

这对于多人合作的项目来说,是很不好的。

可以让所有组员先把代码 push,在修改完历史 commit 之后,再重新 clone 代码就好了。

总结

当你不小心把私密信息提交到了某个历史 commit,就需要修改这个 commit 去掉私密信息。

我们尝试了 3 种方案:

第一种是 git reset --hard 到那个分支,然后改完之后 git commit --amend,之后再把后面的 commit 一个个 cherry-pick 回来。

第二种是 git rebase -i 这些 commit,它提供了一些命令,比如 pick 是使用这个 commit,edit 是重新修改这个 commit。我们在要改的那个 commit 使用 edit 命令,之后 git rebase --continue,依次处理后面的 commit。

其实 reabse 就是对 cherry-pick 的封装,也就是自动处理一个个 commit。

但不管是 cherry-pick 还是 rebase ,合并后面的 commit 的时候都需要解决冲突,因为改了历史 commit 肯定会导致冲突。

第三种方案是用 filter-branch 的 --tree-filter,他可以在多个 commit 上自动执行脚本,你可以在脚本里修改文件内容,这样就不用手动解决冲突了,可以批量修改 commit。

但改了历史 commit 需要 git push -f,如果大项目需要这么做,要提前和组员共同好,先把代码都 push,然后集中修改,之后再重新 clone。

这就是修改历史 commit 的 3 种方案,你还有别的方案么?

转载自:https://juejin.cn/post/7287041017291374627
评论
请登录