Git 避坑指南:常见错误和修复方法

Git 避坑指南:常见错误和修复方法

Git 很强大,但强大也意味着操作不当后果严重。一个命令敲错,可能半天的代码就没了,或者整个团队的分支历史都会出问题。这篇文章总结了我踩过的和你可能踩到的坑,以及修复方法。每个坑都写清楚"怎么避免"和"万一中了怎么修复",建议收藏备用。

坑一:git push --force 删了别人的代码

这是最容易犯也是最严重的错误,没有之一。

你在自己的分支上 rebase 了,发现 push 被拒绝(因为远程分支和你本地的历史不一样)。你一着急,加了 --force 推送上去了。结果:远程分支上别人的提交全没了,被你的提交覆盖了。

怎么避免:永远不要对公共分支(main、develop)用 --force。自己的功能分支可以用 --force-with-lease 替代 --force,它在覆盖前会检查远程分支是否有更新的提交,如果有就拒绝推送,更安全。

在日常开发中,能用 git pull --rebase 解决的就不要用 force push。团队成员之间如果有共享分支,应该达成共识:共享分支禁止 force push。

怎么修复:如果你刚 force push 不久,时间和运气都在你这边:

第一步:立刻通知团队其他人暂停 pull。如果他们在你 force push 之后 pull 了,需要用 git reset --hard origin/branch-name 回退到被覆盖的状态,但这要求他们没有基于新历史做新的提交。

第二步:在本地用 git reflog 找到被覆盖之前的提交 hash:

git reflog
# 找到之前的提交,比如 abc1234

第三步:回到那个状态:

git reset --hard abc1234
git push --force

git reflog 记录的是 HEAD 的移动历史,默认保留 90 天。只要在这个窗口期内,理论上都能找回来。但如果同事已经拉取了被覆盖的代码并且做了新的提交,情况会更复杂,需要所有人协调处理。我见过有团队因为 force push 覆盖了别人的代码,花了一整个下午来恢复的案例。

坑二:提交了敏感信息

你写配置文件的时候忘了删掉数据库密码、API Key 或 Token,提交推送上去了。等你想起来,密码已经在 GitHub 的历史里了,而且很可能已经被缓存、被爬取。

怎么避免

  • 把敏感信息放在环境变量文件里(.env),然后在 .gitignore 里排除。
  • 提交前做一次 "git diff --cached 全文搜索" 检查是否有敏感关键字。
  • 安装 git-secretstruffleHog,能在提交前自动扫描敏感信息。

如果还没推送,赶紧把密码改掉,然后 amend 提交:

git add .
git commit --amend -m "原来的说明"

amend 会修改最近一次提交,覆盖掉包含密码的版本。关键是"还没推送"——一旦 push 了就要用下面的方法。

如果已经推送了:amend 之后还需要 force push 才能覆盖远程。但更安全的做法是用 git filter-repo 工具彻底从历史中删除这个文件:

git filter-repo --path 文件名 --invert-paths

注意:filter-repo 会改写所有提交的历史。执行后需要 force push,所有人需要重新克隆仓库。这不是一个轻松的操作,所以最好在推送之前就避免。

GitHub 也提供了 Secret Scanning(秘密扫描)功能,如果你的仓库是公开的,它会主动检测 AWS 密钥、GitHub Token、数据库密码等常见密钥格式,并通知你和服务商撤销。私有仓库也有相同的功能但需要在 Settings 里开启。

额外提醒:如果你的密钥已经泄露到公开仓库,正确的做法是立即轮换密钥(重新生成),而不是仅仅从 Git 历史中删除。因为 GitHub 的缓存、Fork、以及 Web Archive 都可能已经被保存了,光删提交是不够的。

坑三:大文件提交了

你顺手把一个 500MB 的视频文件提交了。push 的时候慢得要死,而且这个文件会永远留在仓库历史里,每次克隆都要下载这个文件。如果有同事 clone 到一半断了,下次还要重头再下 500MB。

怎么避免:大文件不要用 Git 管理。用 Git LFS(Large File Storage)来处理。安装 LFS 后:

git lfs install
git lfs track "*.mp4"
git lfs track "*.avi"
git lfs track "*.zip"
git add .gitattributes
git commit -m "配置 LFS"

LFS 会把大文件存在单独的服务器上(通常是 GitHub 的 LFS 存储或自建),Git 仓库里只保留一个几十字节的指针文件。克隆时不会自动下载大文件的实际内容,除非你运行 git lfs pull

怎么修复:如果大文件已经提交进历史了,用 filter-repo 删掉:

git filter-repo --path 大文件.avi --invert-paths

这个操作会从整个仓库历史中移除该文件。但如果仓库已经公开并且有人 clone 了,他们clone 的仓库里仍然有大文件,需要他们重新 clone。

经验之谈:对于新项目,建议在仓库初始化之初就配置好 LFS 和 .gitignore,比事后再补救省事一百倍。

坑四:git reset --hard 丢了代码

你在工作区改了很多代码,觉得太乱了想重新开始,执行了 git reset --hard。结果工作区的修改全没了。

怎么避免:reset --hard 之前先确认暂存区或工作区没有未提交的修改。用 git status 看看,确认没有重要修改。如果不确定,先 git stash 藏起来:

git stash save "实验性修改"

需要恢复的时候 git stash pop 就能回来。stash 支持多个藏身处,git stash list 查看所有。

还有一个更安全的替代品:用 git reset --soft 代替 --hard,它只移动 HEAD 指针,不会清除暂存区和工作区的内容。或者用 git reset --mixed(默认),只清除暂存区,工作区保留。

怎么修复

  • 如果修改曾经被 add 到暂存区,用 git fsck 找回:
git fsck --lost-found

这会在 .git/lost-found 目录里放回能找到的丢失对象。但这些文件原来的目录结构和文件名都没了,你需要一个文件一个文件地打开看,自己判断是哪里的代码。

  • 如果修改从来没有被 add 过,Git 没有记录过这些内容,那就真的没了。这也是为什么我建议频繁 add 和 commit——多存档总比不存档好。每次完成一个小功能或修复一个小 bug 就提交一次,颗粒度越小越安全。

一个心理建设:在使用任何"破坏性命令"之前(reset、clean、checkout --、force),养成一个习惯——先在脑子里问自己"万一这错了怎么办"。如果答案是你承受不起的,那就先备份。

坑五:多人同时改了同一个文件

你改了文件 A 的第一行,同事改了文件 A 的第十行。你先 push,他后 push 的时候被拒绝了。他 pull 之后发现冲突,但他不太会解决冲突,直接删了你的修改保留了你的。

怎么避免

  • 分工时尽量按文件划分,避免多人同时改同一个文件。
  • 如果必须改同一个文件,提前沟通,约定谁负责哪部分。
  • 把单文件尽量拆小,减少冲突概率。一个或者几千行的 God 文件是冲突的重灾区。

怎么修复:如果代码已经被错误地覆盖了,不要慌,用 git reflog 找到你的提交,cherry-pick 回来:

git cherry-pick 你的提交ID

cherry-pick 会把指定提交应用到当前分支,产生一个新的提交(内容一样但 hash 不同)。

如何解决冲突:如果 pull 时提示冲突,正确的做法:

git mergetool  # 或者手动编辑冲突文件
# 冲突标记格式:
# <<<<<<< HEAD
# 你的修改
# =======
# 同事的修改
# >>>>>>> branch-name
# 删除标记,保留你想要的代码
git add 冲突文件
git commit

Git 内置的合并工具不太好使的时候,可以配置 Beyond Compare、VS Code 或 IntelliJ IDEA 作为外部合并工具,可视化界面下解决冲突会直观很多。

坑六:在错误的分支上提交了

你本来想在 feature 分支上提交,结果忘了切分支,直接在 main 上改了代码并提交了。这种场景非常常见,特别是开了多个终端窗口或者多个 IDE 项目的时候。

怎么修复

方法一(推荐):用 git cherry-pick + reset 迁移提交:

git checkout feature
git cherry-pick <在master上的提交ID>   # 把那次提交复制到 feature
git checkout main
git reset --hard HEAD~1                # 把 main 上多出来的那次提交撤销

这个方法安全且可追溯,不需要强制推送。

方法二:用 git stash(更通用):

git stash                        # 先把 main 上未提交的修改藏起来
git checkout feature
git stash pop                    # 在 feature 上释放

方法三:如果已经 push 到了远程的 main 分支,那就需要用 git revert(创建一个反向提交来撤销),而不是 git reset(因为 reset 需要 force push)。

一个核心原则

Git 里的大部分操作都是可逆的,前提是你知道怎么逆。所以遇到不确定的操作时,先查一下或者先备份。

一个简单的备份方法就是复制整个项目文件夹:

cp -r my-project my-project-backup

Windows 用户可以直接把项目文件夹压缩一份zip。有了备份,你就可以放心折腾了。折腾完了发现不对,删掉重来就行。

还有一个不怕出错的办法:用 GitHub 或 GitLab 远程仓库做备份。每次完成一小阶段就 push 一下。哪怕你在本地搞砸了,远程仓库的副本还在。而且 Git 本来就鼓励频繁提交和推送,push 得越频繁越安全。

最后

Git 的坑大多出在 force、reset、filter 这些"破坏性操作"上。记住一个原则:对公共分支永远不要做破坏性操作,对自己的分支可以大胆试错。

还有一个好习惯:重要操作之前先在测试仓库里试一遍,确认没问题再在正式仓库里操作。先在 tmp/test-branch 上练习一遍 force push 是怎么操作、reflog 怎么看、cherry-pick 怎么做,等熟练了再去正式环境操作。

Git 是你的一把功能强大的瑞士军刀——锋利、灵活,但也容易割到手。但只要掌握了这些避坑方法,它会成为你开发生涯中最可靠的伙伴。