1. 这篇文章解决什么问题?
Git 冲突不是异常情况,而是多人协作和分支开发里的正常现象。
常见问题包括:
1. 为什么会产生冲突? 2. 冲突文件里的 <<<<<<<、=======、>>>>>>> 是什么? 3. merge 冲突怎么解决? 4. rebase 冲突怎么解决? 5. 解决冲突后为什么还要 git add? 6. 如果不想继续 merge 或 rebase,怎么取消?这篇只讲冲突处理流程。
merge、rebase、pull的概念放在05-merge-rebase-pull.md里理解。
2. 冲突为什么会发生?
Git 自动合并的前提是:
两边修改的位置不冲突 或者 Git 能判断应该保留哪些内容如果两个分支都修改了同一个文件的同一块内容,Git 就不敢替你决定。
例如main上把一行改成:
inttimeout=30;功能分支上把同一行改成:
inttimeout=60;这时 Git 不知道最终应该是30还是60,就会产生冲突。
3. 冲突发生时 Git 在做什么?
假设当前历史是:
A -- B -- C | main \ D -- E | feature/cache在main上执行:
gitmerge feature/cache如果两边改了同一块内容,Git 会停下来:
Auto-merging config.cpp CONFLICT (content): Merge conflict in config.cpp Automatic merge failed; fix conflicts and then commit the result.这时仓库处于一个“合并进行中”的状态。
需要做三件事:
1. 打开冲突文件 2. 手动决定最终内容 3. git add 标记冲突已解决4. 冲突标记怎么看?
冲突文件里通常会出现:
marker: <<<<<<< HEAD int timeout = 30; marker: ======= int timeout = 60; marker: >>>>>>> feature/cache真实文件里不会有marker:前缀,这里只是为了避免 Git 把笔记示例误判成未解决冲突。
含义是:
上半段:<<<<<<< HEAD 到 ======= 之间,是当前分支的内容 下半段:======= 到 >>>>>>> feature/cache 之间,是被合并进来的分支内容如果你当前在main上执行:
gitmerge feature/cache那么:
HEAD 通常表示 main 当前内容 feature/cache 表示正在合并进来的内容5. 冲突文件应该怎么改?
解决冲突不是删除某一边那么简单,而是决定最终代码应该长什么样。
例如上面的冲突,可以改成:
inttimeout=60;也可以改成:
inttimeout=loadTimeoutFromConfig();关键是:
最终文件里不能留下 <<<<<<<、=======、>>>>>>> 最终代码必须能表达正确业务逻辑解决后执行:
gitstatusgitaddconfig.cppgit add在这里的含义不是“准备提交新功能”,而是:
告诉 Git:这个文件的冲突我已经处理完了6. merge 冲突的完整流程
典型流程:
gitcheckout maingitmerge feature/cache如果出现冲突:
gitstatus打开冲突文件,修改成最终内容。
然后:
gitaddconfig.cppgitstatusgitcommit如果这是普通merge,最后的git commit会生成一个 merge commit。
有时 Git 会自动准备好 commit message,直接保存即可。
7. rebase 冲突的完整流程
典型流程:
gitcheckout feature/cachegitrebase main如果某个提交应用时发生冲突,Git 会停下来。
解决文件后执行:
gitaddconfig.cppgitrebase--continue如果后面的提交继续冲突,就继续重复:
改冲突文件 git add git rebase --continue这就是05里说的:
merge 冲突像一次性结算 rebase 冲突像按提交分期结算8. ours 和 theirs 怎么理解?
冲突时经常会看到:
gitcheckout--oursconfig.cppgitcheckout--theirsconfig.cpp在merge中通常可以这样理解:
ours 当前分支的版本 theirs 被合并进来的分支版本例如当前在main上:
gitmerge feature/cache那么:
ours main theirs feature/cache如果你确定保留当前分支版本:
gitcheckout--oursconfig.cppgitaddconfig.cpp如果你确定保留对方分支版本:
gitcheckout--theirsconfig.cppgitaddconfig.cpp注意:
不要只因为命令方便就整文件保留一边 很多冲突需要把两边逻辑合在一起9. rebase 里的 ours 和 theirs 容易反直觉
在rebase过程中,ours和theirs容易让人迷糊。
因为 rebase 的过程是:
先切到目标基底 再把你的提交一个一个应用上去所以在 rebase 冲突里,ours往往表示当前基底那边,theirs往往表示正在被重新应用的那个提交。
新手不建议在 rebase 冲突里盲目使用:
gitcheckout--oursgitcheckout--theirs更稳的方式是:
打开文件 读冲突上下文 手动改成最终想要的代码10. 不想继续 merge 怎么办?
如果 merge 到一半发现不想继续了:
gitmerge--abort它会尝试回到 merge 之前的状态。
常见场景:
冲突太多,想换一种合并方式 发现合错分支了 还没准备好处理这些冲突执行后再看:
gitstatus确认仓库已经回到正常状态。
11. 不想继续 rebase 怎么办?
如果 rebase 到一半不想继续:
gitrebase--abort它会回到 rebase 开始之前。
如果已经解决了当前冲突,想继续:
gitrebase--continue如果想跳过当前这个提交:
gitrebase--skip--skip要谨慎,因为它表示:
当前这个提交不要了12. 冲突解决后要做什么检查?
解决冲突后,不要只看 Git 是否通过,还要确认代码真的正确。
推荐顺序:
gitstatusgitdiff--cached然后根据项目情况运行:
cmake--buildbuild ctest --test-dir build或者至少运行当前模块的编译和测试。
冲突解决最容易出问题的地方不是语法,而是:
两边逻辑都保留了,但组合后语义不对 删掉冲突标记了,但误删了一段必要逻辑 代码能编译,但配置、路径、条件判断错了13. 减少冲突的习惯
冲突不能完全避免,但可以减少。
比较实用的习惯:
1. 功能分支不要拖太久 2. 经常 fetch,然后让自己的分支跟上 main 3. 不要在一个提交里混合太多无关修改 4. 格式化代码和业务修改尽量分开提交 5. 修改公共头文件、配置文件时更谨慎 6. 合并前先看 git status,保证工作区干净尤其是 C++ 项目里,下面这些文件容易引发多人冲突:
CMakeLists.txt 公共头文件 配置文件 README.md 接口定义文件这些文件改动前最好先同步最新主分支。
14. 总结
冲突的本质是 Git 无法自动判断最终内容。
解决冲突时,先用git status找到冲突文件,再手动改成正确代码,最后用git add标记已解决;如果方向错了,用merge --abort或rebase --abort退回去。