Git 批量修改历史提交的作者与邮箱全指南:从 filter-branch 到 filter-repo

在日常团队协作或个人开源项目维护中,我们几乎都遇到过提交身份信息配置错误的尴尬情况:

  • 在新搭建的开发机或服务器上克隆仓库后,忘记执行 git config --global user.email,导致大量提交直接带上了机器名 root@localhost 或系统默认用户名;
  • 在个人电脑上开发公司业务代码时,不慎混用了个人的私有 GitHub 邮箱;
  • 准备将私有项目脱敏后开源发布时,发现早期历史记录中残留了公司内网专属邮箱或前同事的敏感身份标识。

由于 Git 内部采用哈希链表结构,提交者的姓名与邮箱直接参与了 Commit Hash(校验和)的哈希计算。修改历史提交信息意味着重写从该提交之后的所有 Commit 节点。

本文将由浅入深,分别讲解使用经典脚本 git filter-branch 与新一代官方推荐工具 git-filter-repo 批量重写 Git 历史作者信息的实操方案,并探讨团队协作中的推送注意事项。


一、Git 提交身份的双重属性:Author 与 Committer

在开始修改前,我们需要先明确 Git 内部记录的两个关键身份字段:

  1. Author(作者):实际编写这段代码的人。即便后续代码被他人 cherry-pick、rebase 或以补丁形式应用,Author 仍保持不变。
  2. Committer(提交者):最终将这段代码合并或应用到当前分支仓库的人。

在常规单人提交时,Author 与 Committer 通常是同一人;但在执行 rebase、merge、cherry-pick 等操作后,二者可能会分离。因此在批量替换时,我们需要同时判定并修改这两个字段,确保历史记录彻底整洁一致。

graph TD
    subgraph 原始 Git 提交历史 (包含旧邮箱)
        C1["Commit A (sha: 7a1b2c)"] --> C2["Commit B (旧邮箱: 3f8e9d)"] --> C3["Commit C (sha: 5c4b1a)"] --> C4["HEAD main (sha: 9e2d7f)"]
    end

    subgraph 重写后的 Git 历史 (全新 Commit Hash 树)
        R1["Commit A (sha: 7a1b2c)"] --> R2["Commit B' (修正邮箱: b1e84a)"] --> R3["Commit C' (重新计算父Hash: a7c390)"] --> R4["HEAD' main (sha: f4d211)"]
    end

    C2 -. 遍历重构 .-> R2

二、方案一:经典原生脚本 git filter-branch

这是 Git 原生内置的最经典方案,无需安装任何额外第三方依赖,在任意已经安装了 Git 的终端(Linux、macOS、Git Bash)中均可直接执行。

1. 批量重写脚本

在需要修改的项目根目录下,执行以下 Bash 脚本。请务必将脚本中的 OLD_EMAIL、CORRECT_NAME 和 CORRECT_EMAIL 替换为你自己的实际信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

git filter-branch --env-filter '
# 需要被替换的旧邮箱
OLD_EMAIL="your-old-email@example.com"
# 修正后的正确姓名
CORRECT_NAME="Your Name"
# 修正后的正确邮箱
CORRECT_EMAIL="your-correct-email@example.com"

# 检查并修正提交者(Committer)信息
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi

# 检查并修正作者(Author)信息
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

2. 参数关键点剖析

  • --env-filter:针对每一个提交遍历执行的环境变量过滤器。我们通过重置 GIT_AUTHOR_NAME、GIT_AUTHOR_EMAIL、GIT_COMMITTER_NAME、GIT_COMMITTER_EMAIL 四个环境变量来覆写提交元数据。
  • --tag-name-filter cat:在重写提交对象时,自动重新将对应的 Tag 标签指向新的提交 Hash,保持标签引用有效。
  • -- --branches --tags:双横线表示后续参数为引用范围,指定对本地所有的分支(branches)和标签(tags)同时生效,防止遗漏某些未 checkout 的分支。

3. 清理本地备份与垃圾回收

执行 git filter-branch 后,Git 会在 .git/refs/original/ 目录下保留一份重写前的原始分支快照,以备后悔恢复。如果我们检查 git log 确认提交信息均已修改无误,可以彻底清理备份以释放空间:

1
2
3
4
5
6
7
8
# 1. 彻底删除 filter-branch 生成的原始引用备份
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

# 2. 清除 reflog 记录
git reflog expire --expire=now --all

# 3. 运行 Git 垃圾回收,修剪悬空对象
git gc --prune=now

三、方案二:Git 官方推荐的新一代工具 git-filter-repo

虽然 git filter-branch 是内置命令,但自 Git 2.22 起,官方文档中明确指出其存在执行性能极差、容易破坏某些 GPG 签名或带空格分支引用的问题,官方明确推荐使用 Python 驱动的现代化工具 git-filter-repo。

1. 安装 git-filter-repo

在已安装 Python 3 和 pip 的环境下安装极简:

1
pip install git-filter-repo

或者通过包管理器安装:

  • macOS (Homebrew): brew install git-filter-repo
  • Ubuntu/Debian: sudo apt install git-filter-repo

2. 使用 mailmap 映射文件优雅重写

git-filter-repo 支持直接遵循标准的 .mailmap 格式。我们可以在仓库根目录临时创建一个映射配置文件 my-mailmap:

1
2
# 正确姓名 <正确邮箱> <旧邮箱>
Your Name <your-correct-email@example.com> <your-old-email@example.com>

然后执行单行命令进行重写:

1
git filter-repo --mailmap my-mailmap

提示:git-filter-repo 为了保证安全性,默认要求仓库必须是全新克隆的干净状态(没有任何未提交的修改或未跟踪的文件)。如果提示非全新仓库,可以按需添加 --force 参数强制执行。

git-filter-repo 的执行速度是 git filter-branch 的数十倍乃至百倍以上,能极大缩短包含数千、数万次提交的大型仓库的处理耗时。


四、仅修改最近几次提交?更轻量的备选方案

如果你只是刚刚提交了 1~2 次代码就发现了邮箱错误,完全不需要动用全库重写的大杀器:

1. 仅修改最后一次未推送的提交

1
2
3
4
5
6
# 1. 确保当前全局或本地已配置正确信息
git config user.name "Your Name"
git config user.email "your-correct-email@example.com"

# 2. 使用 --reset-author 参数刷新最后一次提交的作者信息
git commit --amend --reset-author --no-edit

2. 修改最近 N 次提交(交互式 Rebase)

1
2
# 启动最近 5 次提交的交互式 rebase
git rebase -i HEAD~5

在打开的编辑器中,将需要修改的那一行前面的 pick 改为 edit(或简写 e),保存退出。Git 随后会暂停在每个待修改的提交点,此时依次运行:

1
2
git commit --amend --author="Your Name <your-correct-email@example.com>" --no-edit
git rebase --continue

五、推送到远端与团队协同避坑指南

无论是 git filter-branch 还是 git-filter-repo,本质上都改变了历史 Commit 的 Hash 散列值。因此,将修改后的结果同步到远端仓库时,需要特别谨慎:

1. 强制推送到远端

由于远程仓库包含旧的 Hash 链,普通的 git push 会被远程拒收(non-fast-forward),此时需要使用带安全检查的强制推送:

1
2
3
# 强烈推荐使用 --force-with-lease 替代直接的 --force
git push origin --force-with-lease --all
git push origin --force-with-lease --tags

--force-with-lease 会在上游被他人推送了新代码时自动阻断推送,有效避免意外覆盖他人的工作成果。

2. 协作者如何同步(关键避坑)

如果团队里其他成员直接执行 git pull,Git 默认会将本地的旧提交与远端的新提交进行合并(Merge),这会导致被修正的旧提交再次复活并被合入主干,导致整个修改前功尽弃!

正确的做法是通知所有协作者:在推送完成后,清空或重置本地分支:

1
2
3
4
5
6
# 1. 获取远端最新的提交对象
git fetch origin

# 2. 将本地主分支直接硬重置为远端最新版本
git checkout main
git reset --hard origin/main

六、总结

工具 / 命令适用场景优势注意事项
git commit --amend仅改动最近 1 次提交简单快捷,无副作用仅限单一叶子节点
git rebase -i改动最近数次提交精细可控,无需外部依赖节点较多时操作繁琐
git filter-branch批量历史整改原生内置,环境开箱即用较慢,官方已逐步弃用
git-filter-repo批量大型仓库重写极速、安全、支持 mailmap需独立安装 Python 依赖

在执行任何批量重写操作之前,请务必对当前仓库进行完整本地备份(如直接复制一份 .git 目录),确保操作具备可撤销的回退保障。


Git 批量修改历史提交的作者与邮箱全指南:从 filter-branch 到 filter-repo
https://bubao.github.io/posts/c41a7e2b.html
作者
一念
发布于
2024年6月18日
许可协议