ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

<<<<<<< HEAD是什么?Git冲突解决全指南

<<<<<<< HEAD是什么?Git冲突解决全指南 看到一屏的 HEAD我隔着屏幕都能感受到那种绝望。刚把功能分支合并回主分支Git 啪地弹出一堆冲突标记满屏幕的 HEAD、、 feature脑子里只有一个念头代码怎么变成乱码了更崩溃的是根本不敢删这些符号生怕删掉什么关键内容。这个场景我见过太多次几乎每个新人都要经历这么一遭。所以我想把这件事彻底讲清楚HEAD到底是什么为什么冲突标记里写的是HEAD遇到冲突该怎么处理日常要怎么做才能少遇到这种“满屏符号”的鬼故事。这篇文章不是一本完整的 git 使用教程默认你已经装好了 Git如果还没装先找对应系统的安装教程把环境配好就行。我们要聊的是装好之后最让人崩溃的那个瞬间。适合刚接触 Git、正在被 merge 和 rebase 折磨的新手也适合要带新人的老手直接拿去当培训素材。1. 先搞懂 HEAD 这个指针冲突标记就不吓人了1.1 HEAD 是什么一个可以移动的指针很多新人看到HEAD就会以为它是一个特殊分支或者是什么错误提示。其实HEAD在 Git 里只是一个指针指向当前所在的分支而分支本身又是一个指针指向某一次提交。通常的链条是HEAD - refs/heads/master - 最新一次提交。打个比方一个 Git 仓库就像一本贴了很多书签的笔记本每个分支书签都夹在某个时间点的记录上而HEAD就是你现在正在看的那一页。你切换到哪个分支HEAD就跟着夹到哪一页。所以你写代码时所有提交都发生在HEAD指向的位置。想知道自己在哪里两个命令足够git symbolic-ref HEAD会输出类似refs/heads/master的路径告诉你 HEAD 正指向哪个分支git log --oneline -1会显示 HEAD 当前指向的最新提交。搞懂这一点后面冲突标记里的HEAD就很好解释了它代表冲突发生时你所在分支那一边的代码。1.2 为什么冲突标记里的“HEAD”代表当前分支Git 合并两个分支时如果同一个文件、同一行的修改在两边不一样Git 不知道应该保留谁就会停下来把问题抛给你。发生冲突时你正处在目标分支比如要合并 feature 进 master你会在 master 上执行git merge feature所以HEAD此时就代表 master。冲突块里 HEAD下面那一段就是 master 当前的版本 feature下面那一段则是 feature 分支的版本。有人看到HEAD以为 Git 崩溃了其实它只是在喊这里有两个人都改了同一行一个是我的现状HEAD一个是你带来的feature到底留谁你来决定。如果合并的是其他分支后面会显示那个分支的名字所以标记并不会永远只叫 feature。理解到这一层你已经比大多数新人强了。1.3 detached HEAD 又是怎么回事和 HEAD 相关的另一个新人崩溃点是执行git checkout 某个commit的哈希后终端提示You are in detached HEAD state。翻译成人话就是你的 HEAD 不再指向任何分支而是直接指向了一个具体提交。这种状态下如果直接提交新提交会挂在一个无名分支上之后切走就很难找到。解决方法很简单。如果只是临时查看代码看完切回原分支即可如果想基于这个提交继续开发先执行git checkout -b fix-xxx给它一个分支名让 HEAD 重新回到分支上。很多教程会警告你在 detached HEAD 里别乱提交真实原因就是怕提交变成了“孤儿”。你现在知道了原理就不会再被这句话吓住。2. 三分钟亲手复现一次 git 冲突看懂 HEAD2.1 用一条命令链复现冲突现场不要凭空想象冲突长什么样自己动手造一个出来是最快的。打开终端随便找个空目录操作mkdir git-demo cd git-demo git init echo print(hello) app.py git add app.py git commit -m init git checkout -b feature然后修改app.py把内容改成print(hello feature)提交git commit -am feature change切回 master再把同一行改成print(hello master)提交git checkout master # 编辑 app.py git commit -am master change最后执行git merge feature你会看到类似这样的输出Auto-merging app.py CONFLICT (content): Merge conflict in app.py Automatic merge failed; fix conflicts and then commit the result.此时打开app.py就是最经典的崩溃现场。整个过程不到三分钟但你亲手触发了一次真正的冲突后面再看到标记就不会觉得陌生了。2.2 冲突标记三件套、、冲突文件里通常长这样 HEAD print(hello master) print(hello feature) feature三行标记的含义非常重要 HEAD到之间是当前分支也就是 HEAD 侧的内容到 feature之间是要合并进来的分支的内容后面跟着的是传入分支的名字。解决冲突就是三件事决定要保留哪一侧、把不要的代码删掉、最后把三行标记也删干净。如果两边内容都有用就手动拼接不一定要二选一。这里有个容易踩的坑冲突可能出现在多个文件、多个位置千万不要解决完一个文件就以为大功告成建议在编辑器里全局搜索逐个确认。2.3 两种快速解法和一个必须避开的坑如果明确只想保留某一侧可以用命令直接选边。在 merge 冲突的状态下保留当前分支版本git checkout --ours app.py保留传入分支版本git checkout --theirs app.py这两个命令执行后文件已经变成对应版本但不要忘了git add app.py然后git commit提交合并结果。工具栏里的“接受当前更改/接受传入更改”按钮在背后执行的也是类似操作只是更直观。必须避开的坑--ours和--theirs在git rebase时的含义是反的。因为在 rebase 过程中你正在把当前分支的提交一个个“搬到”目标分支上此时 HEAD 侧的身份发生了互换。我见过不止一个新人用反参数直接把同事代码覆盖成空。所以如果对语义没把握老老实实用编辑器手动处理最安全。处理完所有冲突文件后执行git status状态里如果已经不再显示Unmerged paths就可以提交了。标准流程是git add .然后git commitGit 会打开一个预填好合并信息的提交界面直接保存退出即可。注意如果界面上提示有未合并文件说明还有东西没 add 干净宁可在这一步多等十秒也别急着强推提交。这一步做完 HEAD才算真正被你从仓库里赶走。3. 解决冲突后的收尾amend、worktree 与提交习惯3.1 git commit --amend改掉刚刚提交的“后悔药”很多人搜 git commit --amend怎么使用其实这个命令并不神秘。它的作用是修改最近一次提交而不是额外增加一个提交。比如你刚提交完就发现漏了一个文件或者提交信息写错了都可以用它补救。两个高频用法# 修改最近一次提交的信息 git commit --amend -m feat: 修复登录按钮样式 # 补充漏掉的文件且不修改提交信息 git add . git commit --amend --no-edit注意一个关键点--amend不是原地修改而是创建一个新的提交对象来替换旧的提交所以 commit 哈希会变。如果旧提交已经推送到了远端再去 amend 会导致本地和远端历史分叉同事下一次 pull 就会看到莫名其妙的多余提交。经验法则很简单只在提交没有推送出去之前用推送过的公共提交不要去改宁可追加一个新提交把要修补的内容写清楚。3.2 git worktree同时开多个分支减少人为冲突还有相当一部分冲突不是代码写得对错而是切换分支时把工作区搞得一团乱。比如你在 A 分支改了一半被叫去 B 分支修 buggit checkout时报错说本地有修改未提交于是你慌慌张张 stash 或强制丢弃最后两边代码混在一起合并时就爆炸了。git worktree就是为解决这种问题而生的。它允许同一个仓库里挂多个工作目录每个目录对应一个分支互不干扰。常用命令# 把 feature 分支单独开一份到仓库外的目录 git worktree add ../git-demo-feature feature # 查看当前所有 worktree git worktree list # 不需要时移除 git worktree remove ../git-demo-feature使用体验上相当于同一份 Git 历史同时打开了几个“独立窗口”。在 A 窗口开发在 B 窗口修 bug不用反复 stash 和 checkout自然减少了把修改带错分支的问题。注意该功能需要 Git 2.15 以上版本老版本可以先升级。添加 worktree 时目标目录必须不存在或为空如果已经建了同名文件夹先删掉或换个路径。3.3 小步提交 频繁同步让冲突无处可藏冲突本质上不是怪某个人写了错代码而是两边的改动在同一处“撞车”了。撞车概率和改动范围、时间跨度直接相关。两个分支各改同一行拖了两周才发现几乎必冲突如果每天同步很多冲突根本不会发生因为 Git 能自动合并掉不相干的修改。我的习惯是不在一棵大树上做两天以上的开发每次功能点完成就提交每天至少执行一次git pull或从主分支合并最新代码如果 feature 分支周期长每周把主分支 rebase 过来一次。提交粒度小了就算真的冲突定位也很快因为冲突区域通常很小。这个方法不复杂但确实帮我省掉了大量紧急救火的场面。4. 新人高频报错排查not a git repository、bad tree object、login failed4.1 fatal: not a git repository 的原因与自救在终端里执行git status如果看到fatal: not a git repository (or any of the parent directories): .git第一反应不是仓库坏了而是当前目录不在 Git 仓库里。最常见的情况是shell 当前目录是普通文件夹或者你从外层进入了一个还没有git init的子目录又或者上一手把.git目录删了。排查顺序先pwd看当前路径再用git rev-parse --show-toplevel看仓库根目录被 Git 识别成哪里。如果根目录不是你以为的那个说明你在子目录里如果命令报同样的错说明这个目录根本不是仓库。此时要么cd到仓库根目录要么用git init重新初始化但不要随便对已有项目 init 两次否则可能把完整仓库搞成孤儿历史。4.2 bad tree object.git 目录受损后的处理与安全提醒Git 的所有版本信息都保存在.git目录里。如果它内部的对象数据库损坏比如磁盘问题、复制仓库时漏掉了文件、或者被某进程半途写坏你会看到fatal: bad tree object之类的报错。通常的抢救流程是先用git fsck --full检查对象完整性再用git reflog查看最近操作如果远端还有一份完整 clone直接重新 clone 往往比修复更快。顺手说一个安全习惯不要在生产服务器、静态托管目录或公开可访问的目录里暴露.git。因为完整仓库里包含每次提交的全部代码和历史一旦.git目录泄漏别人可以直接把它下载下来还原源码。部署时应该明确禁止访问该目录或者构建产物里干脆不携带它。这不是什么高深的安全知识但很多团队吃过亏。4.3 login failed 与 SSH 免密配置用 HTTPS 方式 push 时经常会遇到Login failed或者 GitLab 类平台提示check api token or GitLab version。原因基本是凭证过期、token 失效或当前凭据管理器和远端平台版本不匹配。与其每次去更新密码不如一次性配置好 SSH 免密。如果还没有密钥生成一个ssh-keygen -t ed25519 -C youexample.com把~/.ssh/id_ed25519.pub的内容添加到代码托管平台的 SSH Keys 设置里然后测试ssh -T gitgitee.com或ssh -T gitgithub.com。看到欢迎信息就说明通了。之后把远端地址换成 SSH 格式比如gitgitee.com:user/repo.git重新 clone 或设置 remote就再也不用输入密码。顺带一提Windows 上配置过 SSH 后如果还说 Permission denied先ssh-add ~/.ssh/id_ed25519把私钥加进 ssh-agent很多坑都是因为 agent 没加载密钥。4.4 新人高频报错速查表报错/现象常见原因最快的处理方式 HEAD冲突标记两个分支改了同一位置手动编辑保留正确内容删除所有标记git add 后提交detached HEAD直接 checkout 了某个 commit想看就切回分支想改就git checkout -b new-branchfatal: not a git repository当前目录不在仓库 / .git 被删cd 到仓库根目录再执行 git 命令fatal: bad tree object.git 对象数据库损坏git fsck --full检查或重新 cloneLogin failed/ token 报错HTTP 凭证过期、token 无效改用 SSH 密钥或更新 token 配置5. 三个在 Git 里平稳活下来的实用习惯5.1 动手之前先看一眼 git status很多冲突和误操作都是因为没看清当前状态就开干。git status只有几行字但会告诉你三件事现在在哪个分支、暂存区有什么、有没有未合并的路径。它就像开车前的仪表盘花不了两秒钟却能避免你把临时文件、冲突标记一起提交上去。我见过最荒唐的一次事故同事在编辑冲突文件时把和留在代码里然后提交推到了远端。只要提交前看一眼 git status 和 git diff这种低级问题完全不会发生。养成这个习惯以后你的 Git 操作会稳一大截。5.2 把提交拆小把信息写清楚提交越小出问题后的定位越容易。“修复登录按钮样式”这样的提交能让后来的人一眼看懂改动意图“更新一堆文件”这种提交一旦出现冲突别人根本不知道你想干嘛只能瞎猜。另外小提交还有额外好处当某个改动引发 bug 时可以用git log -S 关键字或git bisect快速定位到具体提交。如果你的提交粒度太大二分查找会非常痛苦。这条经验不花成本长期受益。5.3 解决冲突前先给文件留个后路我第一次手动解冲突时删标记删得太爽直接把同事那段代码全删了还提交了。等到代码评审才被发现心里只剩崩溃。后来我学乖了在动冲突文件之前先复制一份到工作区外例如cp app.py /tmp/app.py.conflict.bak或者让 IDE 的本地历史自动留档。万一操作失误还能翻回去。还有一个补救手段是git reflog它记录了 HEAD 在仓库里的移动历史。即使你把文件改到面目全非也可以从 reflog 找到解决冲突之前的那个 commit把文件捞回来。掌握 reflog 之后你会发现自己敢大胆操作了因为大多数错误都能撤销。最后分享一个我自己的经验看到 HEAD的那一刻真不用慌。它只是 Git 在请你在两个版本之间做个选择你才是最终拍板的人。第一次解决冲突可能会花二十分钟第三次也许只要两分钟。喝口水平复一下从第一个冲突块开始逐块处理能用编辑器按钮就用按钮能用 status 确认就多确认一次。等你真正理解了 HEAD 的含义再看到满屏标记时你心里想的只会是“哦又来了行吧这次留哪边”
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进