Git 工作流踩坑:如何正确处理 force push 与历史提交

Git 操作中,最危险的命令之一就是 git push --force。一旦用错,可能覆盖他人代码、丢失提交历史。本文总结 force push 的正确用法,以及误操作后的恢复方法。

force push 的风险

普通 push vs force push

1
2
3
4
5
6
7
# 普通 push:只推送本地有而远程没有的提交
git push origin feature

# Force push:用本地分支完全覆盖远程分支
git push --force origin feature
# 或
git push -f origin feature

风险场景

1
2
3
4
5
6
7
8
9
10
11
12
13
远程分支状态:
A - B - C (origin/feature)

你的本地分支(rebase 后):
A - B - D - E (feature)

普通 push:
! [rejected] non-fast-forward
原因:C 和 D/E 没有共同的祖先

Force push:
+ A - B - D - E (origin/feature)
- C 被删除

危险

  1. 如果其他人在 C 基础上开发,他们的提交会「丢失」
  2. 如果你 force push 到 main/develop 分支,影响更大
  3. 误操作后可能永久丢失代码

protected branch 设置

GitHub 设置

1
2
3
4
5
6
7
8
Settings -> Branches -> Branch protection rules

添加规则:
- Branch name pattern: main
- ✓ Require pull request reviews before merging
- ✓ Require status checks to pass before merging
- ✓ Do not allow bypassing the above rules
- ✓ Do not allow force pushes <-- 关键

GitLab 设置

1
2
3
4
5
6
Settings -> Repository -> Protected branches

保护 main 和 develop:
- Allowed to merge: Maintainers
- Allowed to push: No one (或 Maintainers)
- ✓ Prevent force push <-- 关键

命令行检查

1
2
3
4
# 查看分支保护状态(如果使用 git config)
git config branch.main.protected # true 表示保护

# 或使用阿里云等平台

reflog 恢复历史

reflog 是什么

reflog 记录了本地仓库所有 HEAD 的移动历史。即使提交被删除、rebase、reset,只要 reflog 还在,就能恢复。

1
2
3
4
5
6
7
8
9
10
# 查看所有操作历史
git reflog
# 或
git reflog show

# 输出示例:
550e8400 HEAD@{0}: reset: moving to HEAD~2
a1b2c3d4 HEAD@{1}: commit: 添加新功能
e5f6g7h8 HEAD@{2}: rebase: 应用补丁
1234567 HEAD@{3}: checkout: moving to main

恢复误删的提交

1
2
3
4
5
6
7
8
9
10
11
12
# 场景:rebase 失败,丢失了提交

# 1. 查看 reflog
git reflog

# 输出:
# a1b2c3d4 HEAD@{0}: rebase: abort
# 550e8400 HEAD@{1}: reset: moving to HEAD~3
# ...

# 2. 恢复到某个状态
git checkout a1b2c3d4 # 找到丢失提交的 hash

恢复误 force push 的分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 场景:force push 后发现有问题,需要恢复

# 1. 查看其他分支的 reflog
git reflog origin/main

# 输出:
# 1234567 origin/main@{0}: pull: Fast-forward
# abcdefg origin/main@{1}: push: force push
# ...

# 2. 恢复到 force push 之前的状态
git reset --hard abcdefg

# 3. 重新推送(注意:这仍然是 force push)
git push --force origin main

历史提交修改

修改最后一次提交

1
2
3
4
5
6
# 修改提交信息
git commit --amend -m "新的提交信息"

# 添加漏提交的文件
git add forgotten_file.txt
git commit --amend --no-edit

修改历史提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# rebase 到需要修改的提交之前
git rebase -i HEAD~5

# 编辑:
pick abc1234 提交1
pick def5678 提交2
pick ghi9012 提交3
pick jkl3456 提交4 <-- 要修改
pick mno7890 提交5

# 把要修改的行改为 edit 或 e
pick jkl3456 提交4
-> edit jkl3456 提交4

# 保存退出后
# 修改文件
git add .
git commit --amend
git rebase --continue

撤销历史提交(不修改历史)

如果需要撤销某个历史提交,但不改变历史,使用 git revert

1
2
3
4
5
# 创建一个新提交,撤销指定提交的更改
git revert <commit-hash>

# 例如:撤销 HEAD~3 的提交
git revert HEAD~3

安全规范制定

1. 分支命名规范

1
2
3
4
5
6
7
# 分支命名
main # 主分支
develop # 开发分支
feature/<ticket>-xxx # 特性分支
bugfix/<ticket>-xxx # 修复分支
hotfix/<ticket>-xxx # 紧急修复
release/<version> # 发布分支

2. 提交规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 提交信息格式
<type>(<scope>): <subject>

# 示例
feat(user): 添加用户注册功能
fix(order): 修复订单支付bug
docs(readme): 更新文档
style(ui): 调整样式
refactor(api): 重构API接口
test: 添加单元测试

# 详细规范
# type: feat, fix, docs, style, refactor, test, chore
# scope: 影响范围
# subject: 简短描述

3. Pre-push Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
# .git/hooks/pre-push

branch=$(git symbolic-ref --short HEAD)

# 禁止 force push 到 main 和 develop
if [ "$branch" = "main" ] || [ "$branch" = "develop" ]; then
echo "不允许直接推送 $branch 分支,请使用 PR/MR"
exit 1
fi

# 强制使用 --force-with-lease
# 检查远程分支是否有更新
echo "建议使用 'git push --force-with-lease' 代替 'git push --force'"

4. Alias 配置

1
2
3
4
5
6
7
8
9
10
11
# ~/.gitconfig

[alias]
# 安全强制推送
force-with-lease = push --force-with-lease

# 查看分支历史
lg = log --oneline --graph --decorate --all

# 清理已合并的分支
clean-branches = !git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -r git branch -d

5. Git Config 保护

1
2
3
4
5
6
# 保护重要分支
git config branch.main.protected true
git config branch.develop.protected true

# 启用 force with lease
git config --global push.useForceWithLease true

实战:误删分支恢复

场景1:删除了本地分支

1
2
3
4
5
6
7
8
9
10
# 删除了 feature 分支
git branch -D feature

# 恢复方法1:从 reflog 恢复
git reflog
# 找到 feature 分支最后的 commit hash
git checkout -b feature <commit-hash>

# 恢复方法2:如果知道远程有
git checkout feature # 远程有就会创建本地分支

场景2:force push 后发现有问题

1
2
3
4
5
6
7
# 1. 找到 force push 之前的 commit
git reflog origin/feature
# abc1234 origin/feature@{0}: push: force push
# def5678 origin/feature@{1}: push: force push <-- 这个是正常的

# 2. 强制恢复到正常版本
git push --force origin def5678:feature

场景3:rebase 后提交丢失

1
2
3
4
5
6
7
8
9
10
11
# rebase 过程中中断了
git rebase --abort

# 或者 rebase 后发现结果不对
git reflog
# 看到 rebase 之前的状态
git reset --hard HEAD@{1}

# 如果已经提交,用 reflog 找回
git reflog
git checkout <之前的commit>

场景4:reset –hard 后恢复

1
2
3
4
5
6
7
8
9
10
# 错误地 reset 了
git reset --hard HEAD~5

# 查看 reflog 找到 reset 前的状态
git reflog
# HEAD@{0}: reset: moving to HEAD~5
# HEAD@{1}: commit: 重要提交在这里!

# 恢复到 reset 之前
git reset --hard HEAD@{1}

最佳实践总结

应该做的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 使用 --force-with-lease 代替 --force
git push --force-with-lease

# 2. 提交前检查
git status
git log --oneline -5

# 3. 创建备份分支
git branch backup-before-rebase

# 4. 小心操作公共分支
git push origin main # 不用 force

# 5. 定期 git fetch,保持远程同步

不应该做的

1
2
3
4
5
6
7
8
# 1. 不要 force push 到 main/develop
git push --force origin main # 危险!

# 2. 不要在公共分支上 rebase
git rebase origin/main # 在 feature 分支可以,main 不行

# 3. 不要 force push 自己不熟悉的分支
# 4. 不要在没有备份的情况下 force push

恢复能力检查清单

1
2
3
4
5
6
7
8
9
10
11
# 1. 确认 reflog 是开启的(默认开启)
git config core.logAllRefUpdates # 应该为 true

# 2. 定期检查 reflog
git reflog | head -20

# 3. 重要操作前创建备份
git branch backup-$(date +%Y%m%d%H%M%S)

# 4. 团队沟通
# force push 前必须通知团队成员

总结

Git 安全操作要点:

操作 风险 安全做法
git push --force 覆盖他人代码 使用 --force-with-lease
git rebase 丢失提交 不要 rebase 公共分支
git reset --hard 丢失工作 git branch backup
git push 到 main 破坏主分支 使用 PR/MR

安全规范:

  1. 保护分支:main/develop 禁止 force push
  2. reflog:定期检查,确保历史完整
  3. 沟通:force push 前必须通知团队
  4. 备份:重要操作前创建备份分支
  5. 强制:使用 --force-with-lease 代替 --force