我们在迭代项目(特别是做脚手架或重构)时,经常会干一件“偷懒”的事:
不想处理繁琐的分支切换,直接把老项目的代码文件夹复制一份,改个名字(比如 v0.2.1),然后在这个新目录里大刀阔斧地修改。改完之后,心满意足地 git init,准备推送到原来的 GitHub 仓库。
今天在开发我的 go-scaffold v0.2.1 版本时,我就这么干了。结果在推送时,Git 给了我当头一棒。
事故现场
我在本地初始化了仓库并提交了所有代码,但在 git push 时遇到了报错:
$ git push -u origin main
To github.com:rei0721/go-scaffold.git ! [rejected] main -> main (fetch first)error: failed to push some refs to 'github.com:rei0721/go-scaffold.git'hint: Updates were rejected because the remote contains work that you do nothint: have locally.发生了什么?
这个报错的原因很简单:“平行宇宙”。
- 远程仓库:有着 v0.1.0 以前的提交历史。
- 本地仓库:虽然代码是基于 v0.1.0 修改的,但因为我是新建的文件夹并执行了
git init,在 Git 眼里,这是一个全新诞生的宇宙,和远程仓库没有任何血缘关系(Common Ancestor)。
并不是最好的选择
通常大家会搜到两种解决方案,但在这个场景下都不完美:
git push -f(强推):这会直接覆盖掉远程仓库的所有历史。但我想保留之前的提交记录,毕竟那是项目的演进史。git pull --allow-unrelated-histories:强制合并两个不相关的历史。这通常会导致成百上千个文件的 Merge Conflict(因为 Git 认为你在两边分别创建了同名文件),处理起来极其痛苦。
最佳解决方案:Soft Reset “嫁接”法
我们需要做的,是把本地这“一坨”已经改好的文件,假装是基于远程最新代码修改的。
核心思路:把本地 Git 指针强制移到远程的最新节点上,但保留工作区的所有文件内容不变。
1. 获取远程历史
首先,把远程仓库的“地图”拿下来(这一步不会修改你的代码文件):
git fetch origin main2. 软重置(Magic Step)
这是最关键的一步。我们使用 --soft 参数:
git reset --soft origin/main这条命令的意思是:“Git,请把我的当前版本回退到 origin/main 的状态,但是,把我和 origin/main 之间所有的文件差异,都保留在‘暂存区’(Staged)里。”
执行完这步,原本那个“断头”的本地 Commit 消失了,取而代之的是所有文件变成了待提交状态。
3. 重新提交
现在,Git 认为我是在远程历史的基础上,一次性修改了所有文件。我们可以愉快地提交了:
git add .git commit -m "feat(core): add user and rbac modules with async execution framework"4. 处理 Tag (如果之前打过)
因为我之前在“平行宇宙”里打过 v0.2.1 的标签,那个标签现在指向一个不存在的 Commit,需要重打:
# 删除旧标签git tag -d v0.2.1# 在新的历史树上重新打标签git tag -a v0.2.1 -m "feat(core): v0.2.1 release"5. 推送
现在,本地历史和远程历史完美连成了一条线:
git push -u origin maingit push origin v0.2.1总结
以后如果再因为手动复制项目目录导致 Git 历史断裂,千万不要慌张地强推或合并。记住 git reset --soft origin/main,它可以帮你把断掉的历史优雅地接回去。
既保留了以前的提交记录,又把最新的 v0.2.1 完美发布了,舒服。