Skip to content

重写历史

概述

Git 允许修改提交历史,这在整理提交记录、修复错误、清理敏感信息时非常有用。但重写历史需要谨慎使用,特别是在已推送的提交上。

修改最近提交

修改提交信息

bash
# 修改最近一次提交的信息
git commit --amend

# 直接指定新的提交信息
git commit --amend -m "新的提交信息"

# 修改提交信息但不改变提交内容
git commit --amend --no-edit

修改提交内容

bash
# 添加遗漏的文件到最后一次提交
git add forgotten-file.txt
git commit --amend --no-edit

# 修改最后一次提交的文件
# 1. 修改文件
vim src/file.js

# 2. 暂存修改
git add src/file.js

# 3. 修改提交
git commit --amend --no-edit

修改作者信息

bash
# 修改最后一次提交的作者
git commit --amend --author="新作者 <new@email.com>"

# 修改作者和提交者
git commit --amend --author="新作者 <new@email.com>" --committer="提交者 <committer@email.com>"

# 使用配置的作者信息
git commit --amend --reset-author --no-edit

修改提交时间

bash
# 修改提交时间
git commit --amend --date="2024-01-15 10:00:00"

# 使用相对时间
git commit --amend --date="now"

# 修改作者日期和提交日期
GIT_AUTHOR_DATE="2024-01-15 10:00:00" GIT_COMMITTER_DATE="2024-01-15 10:00:00" git commit --amend --no-edit

修改历史提交

使用 git rebase -i

bash
# 交互式变基最近 3 次提交
git rebase -i HEAD~3

# 从某个提交开始变基
git rebase -i <commit-hash>

# 编辑特定范围的提交
git rebase -i <start-commit>^ <end-commit>

交互式变基命令

执行 git rebase -i 后会打开编辑器:

bash
pick abc1234 第一次提交
pick def5678 第二次提交
pick ghi9012 第三次提交

# 命令说明:
# p, pick = 使用提交
# r, reword = 使用提交,但修改提交信息
# e, edit = 使用提交,但停下来修改
# s, squash = 使用提交,但合并到前一个提交
# f, fixup = 使用提交,但丢弃提交信息
# d, drop = 删除提交

修改历史提交信息

bash
# 1. 开始交互式变基
git rebase -i HEAD~3

# 2. 将需要修改的提交从 pick 改为 reword
reword abc1234 第一次提交
pick def5678 第二次提交
pick ghi9012 第三次提交

# 3. 保存并退出,Git 会让你编辑每个 reword 提交的信息

编辑历史提交内容

bash
# 1. 开始交互式变基
git rebase -i HEAD~3

# 2. 将需要编辑的提交改为 edit
edit abc1234 第一次提交
pick def5678 第二次提交
pick ghi9012 第三次提交

# 3. 保存退出后,Git 会停在该提交
# 4. 修改文件
vim src/file.js

# 5. 暂存修改
git add src/file.js

# 6. 修改提交
git commit --amend

# 7. 继续变基
git rebase --continue

合并历史提交

bash
# 1. 开始交互式变基
git rebase -i HEAD~3

# 2. 使用 squash 合并提交
pick abc1234 第一次提交
squash def5678 第二次提交
squash ghi9012 第三次提交

# 3. 保存后会让你编辑合并后的提交信息

删除历史提交

bash
# 方法一:使用 drop
git rebase -i HEAD~3
# 将要删除的提交改为 drop
drop abc1234 要删除的提交
pick def5678 保留的提交

# 方法二:直接删除
git rebase --onto <commit>^ <commit> HEAD

# 方法三:使用 revert(保留历史)
git revert <commit-hash>

重新排序提交

bash
# 交互式变基时直接调整顺序
git rebase -i HEAD~3

# 调整顺序
pick def5678 第二次提交
pick abc1234 第一次提交
pick ghi9012 第三次提交

git filter-branch

警告

git filter-branch 已被弃用,推荐使用 git filter-repo。但了解它仍然有用。

从所有提交中删除文件

bash
# 从所有提交中删除敏感文件
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch path/to/sensitive-file' \
  --prune-empty --tag-name-filter cat -- --all

# 删除整个目录
git filter-branch --force --index-filter \
  'git rm -rf --cached --ignore-unmatch path/to/directory' \
  --prune-empty -- --all

修改作者信息

bash
# 修改所有提交的作者
git filter-branch --env-filter '
OLD_EMAIL="old@email.com"
CORRECT_NAME="新名字"
CORRECT_EMAIL="new@email.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
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

清理仓库

bash
# 清理 filter-branch 的备份
rm -rf .git/refs/original/

# 清理 reflog
git reflog expire --expire=now --all

# 运行垃圾回收
git gc --prune=now --aggressive

git filter-repo

git filter-repo 是推荐的仓库重写工具,比 filter-branch 更快更安全。

安装

bash
# macOS
brew install git-filter-repo

# Linux
pip install git-filter-repo

# 或从源码安装
pip install git-filter-repo

删除敏感文件

bash
# 从历史中删除文件
git filter-repo --path path/to/sensitive-file --invert-paths

# 删除整个目录
git filter-repo --path path/to/directory --invert-paths

# 删除匹配模式的文件
git filter-repo --path-glob '*.env' --invert-paths

保留特定文件

bash
# 只保留特定路径
git filter-repo --path src/ --path-rename src/:project/

# 保留多个路径
git filter-repo --path src/ --path lib/ --path docs/

修改邮箱和用户名

bash
# 创建映射文件 mailmap.txt
# old@email.com new@email.com
# another@old.com correct@email.com

git filter-repo --mailmap mailmap.txt

使用回调函数

bash
# 修改提交信息
git filter-repo --message-callback '
return message.replace(b"bug", b"fix")
'

# 修改作者信息
git filter-repo --name-callback '
return name.replace(b"Old Name", b"New Name")
'

# 修改邮箱
git filter-repo --email-callback '
return email.replace(b"old.com", b"new.com")
'

高级用法

bash
# 删除空提交
git filter-repo --prune-empty always

# 保留特定分支
git filter-repo --refs refs/heads/main refs/heads/develop

# 从特定提交开始
git filter-repo --refs abc1234..HEAD

# 强制推送到远程
git filter-repo --force

重写历史后的处理

强制推送

bash
# 重写历史后需要强制推送
git push --force origin main

# 更安全的方式(推荐)
git push --force-with-lease origin main

通知团队成员

重写已推送的历史后,团队成员需要:

bash
# 团队成员需要重新获取仓库
git fetch origin

# 重置本地分支
git reset --hard origin/main

# 或者变基
git rebase origin/main

清理远程引用

bash
# 清理远程跟踪分支
git remote prune origin

# 清理所有过时的引用
git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin

常见场景

场景一:删除意外提交的密码

bash
# 1. 找到包含密码的文件
git log --all --full-history -- config/secrets.yml

# 2. 使用 filter-repo 删除
git filter-repo --path config/secrets.yml --invert-paths

# 3. 清理并强制推送
git gc --prune=now
git push --force-with-lease origin main

# 4. 更改密码!(最重要)

场景二:整理混乱的提交历史

bash
# 1. 交互式变基
git rebase -i HEAD~10

# 2. 合并相关提交
pick abc1234 功能 A 第一部分
squash def5678 功能 A 第二部分
pick ghi9012 功能 B
squash jkl3456 功能 B 修复

# 3. 重写提交信息
# 4. 强制推送
git push --force-with-lease origin feature-branch

场景三:分割大仓库

bash
# 使用 filter-repo 提取子目录为新仓库
git filter-repo --path src/module-a/ --path-rename src/module-a/:

安全警告

何时可以重写历史

可以重写

  • 本地未推送的提交
  • 个人分支上的提交
  • 团队约定可以重写的分支

不要重写

  • 主分支(main/master)
  • 已共享的分支
  • 有其他人在使用的分支

重写历史的风险

bash
# 重写历史会导致:
# 1. 提交哈希改变
# 2. 其他人的本地分支出现冲突
# 3. 可能丢失提交
# 4. CI/CD 历史可能混乱

最佳实践

1. 重写前备份

bash
# 创建备份分支
git branch backup-branch

# 或创建整个仓库的备份
cp -r .git .git-backup

2. 使用 --force-with-lease

bash
# 不推荐
git push --force origin main

# 推荐
git push --force-with-lease origin main

3. 小范围修改

bash
# 只修改必要的提交
git rebase -i HEAD~3  # 而不是 HEAD~50

4. 及时沟通

重写共享分支历史前,通知所有团队成员。

总结

操作命令说明
修改最近提交git commit --amend修改最后一次提交
交互式变基git rebase -i修改历史提交
删除文件git filter-repo --path ... --invert-paths从历史删除文件
修改作者git filter-repo --mailmap批量修改作者信息
强制推送git push --force-with-lease安全地强制推送

使用建议

  • 重写前先备份
  • 只在必要时重写历史
  • 优先使用 filter-repo 而非 filter-branch
  • 重写后及时通知团队成员