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

最近遇到一个 git 的问题:

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

后来又提交了很多个 commit。

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

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

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

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

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

我们先创建了个 git 项目。

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

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

git show 看下 222 和 333 那个 commit:

sql 复制代码
git show f5482b

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

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

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

我们试一下:

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

然后 git reset 到 333 那个 commit:

css 复制代码
git reset --hard 65dfee

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

sql 复制代码
git add .
git commit --amend

这样,这个 commit 就干净了。

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

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

git cherry-pick 0b700f

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

csharp 复制代码
git add .
git cherry-pick --continue

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

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

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

sql 复制代码
git show 9aded3

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

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

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

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

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

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

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

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

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

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

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

首先回到初始状态:

然后找到 222 的 commit:

css 复制代码
git rebase -i f5482ba

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

-i 是交互式的合并。

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

下面有很多命令:

pick 是原封不动使用这个 commit

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

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

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

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

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

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

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

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

解决之后,重新 add、commit。

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

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

合并之后重新 add、commit.

然后再次 git rebase --continue

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

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

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

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

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

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

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

有,就是 filter-branch。

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

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

我们再回到初始状态:

创建了一个 script.js

javascript 复制代码
const fs = require('fs');

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

console.log(content);

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

然后执行 filter-branch 命令:

python 复制代码
git filter-branch --tree-filter 'node 绝对路径/script.js' 9aded3..HEAD

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

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

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

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

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

javascript 复制代码
const fs = require('fs');

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

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

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

再跑下试试:

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

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

确实去掉了私密信息。

再看看 444 的 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 种方案,你还有别的方案么?

相关推荐
花花鱼3 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09336 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang135828 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning28 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人38 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱00139 分钟前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼9211 小时前
【Ajax】跨域
javascript·ajax·cors·jsonp
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css