ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Git能力摸底测验:覆盖撤销恢复与协作的实战题库

Git能力摸底测验:覆盖撤销恢复与协作的实战题库

最近我在团队内部做了一次 Git 能力摸底测验,起因其实很简单:新来的同学简历上都写着“熟练使用 Git”,可一聊到具体场景——比如改错了提交怎么补救、跟远程仓库冲突怎么处理、历史提交乱成一团怎么整理——答案就含糊了。不是他们基础差,是日常开发里 Git 的很多操作已经变成肌肉记忆,大家只会用那几条固定命令,一旦遇到“异常情况”就抓瞎。于是我花了几个晚上设计了一套 Git 测验,覆盖从入门到进阶、从日常提交到灾难恢复的各类场景,今天把整套思路、题目拆解和实战结果分享出来。这套内容适合三类人:想系统自查 Git 水平的开发者、需要给团队做技术摸底的技术负责人,以及正在准备面试想查漏补缺的同学。我尽量不讲废话,直接给你能复用的题库思路和解题原理。

1. 为什么一套 Git 测验值得认真设计

先说动机。我在做技术面试时发现一个规律:问“git rebase 和 git merge 有什么区别”,十个人有八个能答上来“rebase 是变基,merge 是合并,rebase 提交历史更干净”。但紧接着问“那在什么场景下你一定会选 rebase,什么场景下绝对别用”,能答利索的人立刻少一半。再追问“rebase 过程中出现冲突,如何恢复到 rebase 之前的状态”,能马上给出准确命令的人就更少了。这说明很多人对 Git 的理解停留在“知道概念、用过基本命令”的层面,没有形成“场景 -> 命令 -> 底层原理 -> 风险控制”的完整认知链条。

我给团队设计这套测验,目标不是考倒谁,而是做一次“认知扫描”。通过一组覆盖不同难度和场景的题目,把每个人的知识盲区暴露出来。盲区暴露了,后续的培训和结对编程才有方向。这套测验放在新人入职两周后、或者老员工半年一次的技术复盘里,效果都很不错。它不需要花一整天的严肃考试时间,做成 30 分钟左右的限时答题即可,重点看解题思路而非死记硬背。

2. 测验的整体结构与设计思路

2.1 设计原则:不考背诵,考决策

这点我想放在最前面。Git 命令本身是有限的,Google 一下全都能查到。真正体现水平的是面对一个具体问题时的决策能力:选哪个命令、为什么选它、操作之后会产生什么影响、万一出错了怎么回滚。所以我出题时始终坚持三个原则:

第一,每道题都设定一个具体的场景,不给“请解释 git merge 的作用”这种开放式概念题。场景化的题目才能逼出真实水平。第二,命令类题目必须附带“操作后果”的追问,检验答题人是不是真的理解这条命令干了什么。第三,所有题目都配有“翻车后的恢复方案”,检验的是对 Git 安全网的理解程度。

2.2 测验模块划分

整套测验我分成五个模块,从基础到进阶层层递进:

模块考察方向题目数建议耗时
基础操作状态查看、文件管理、提交规范86分钟
分支与合并分支模型、合并策略、冲突解决88分钟
撤销与恢复reset、revert、checkout、restore 辨析88分钟
协作与远程远程交互、推送拉取、PR 工作流88分钟
疑难杂症reflog、cherry-pick、detached HEAD 等810分钟

总分 100 分,每模块 20 分,题型全部是“场景 + 多选/填空/简答”。实际操作中我发现,多选和填空的区分度最好,简答题适合用来观察答题人的表述逻辑。如果团队人数多,建议全部用选择和填空,方便统计得分分布。

2.3 评分与结果解读

测验结束后,我会把每个人的得分拆成五维雷达图:基础操作、分支合并、撤销恢复、协作远程、疑难杂症。这样能直观看到这个人属于“基础扎实但协作经验少”,还是“命令熟练但底层原理薄弱”。比如有一个挺有意思的案例:某位同学基础操作和分支合并拿了满分,但疑难杂症模块几乎零分,这说明他日常工作流比较固定,很少处理异常情况,一旦遇到“提交历史写错”或者“误删分支”就会慌了手脚。针对这种情况,提升重点就是让他专门练习撤销和恢复类操作。

3. 核心题目拆解与原理说明

3.1 基础操作模块:看似简单,处处有坑

基础模块的题目我并没有出“git commit 的作用是什么”这种题,而是聚焦在状态理解和文件流转上。举个例子,我出了一道这样的题:

场景:你在分支 feature/login 上工作,修改了 login.js 和 README.md,然后执行了git add login.js。这时候你执行git commit -m "update login",请选出正确的描述。

选项里有几个陷阱:有人说“两个文件都会被提交”,有人说“只有 login.js 被提交”,还有人说“README.md 的修改会被保留在工作区”。正确答案是:只有 login.js 被提交,README.md 的修改仍然保留在工作区。这题考察的是暂存区(Staging Area)的概念是否清晰。

出错的人往往把git add和git commit理解成一个不可拆分的动作。实际上 Git 的操作链条是:工作区 -> 暂存区 -> 本地仓库 -> 远程仓库。git add只负责把工作区的改动放入暂存区,git commit只提交暂存区里的内容。这个理解不到位,后面所有关于git restore --staged、git commit --amend的操作都会跟着混乱。

另一道高频题是git log的查看技巧。场景是:提交历史有几十条,你想只看最近 3 条的完整 diff。很多人条件反射回答git log -p -3,也有很多人写git log -3 -p,两个写法其实都行。但我要问的其实是git log --oneline -3和git log -3 --oneline的区别——实际上没有区别,--oneline只是改变输出格式。这题的真正考点是:当提交记录特别长时,如何快速筛选出自己关心的范围。实操里最实用的命令组合其实是git log --oneline --graph --all -10,一条命令同时看到分支拓扑和最近提交。建议团队统一养成这个习惯。

3.2 撤销与恢复模块:最容易看出水平差异

撤销恢复是我个人最喜欢的测验模块,因为这个区域命令最多、最容易混淆、也最能体现一个人对 Git 底层数据模型的理解。核心不外乎四个命令:git reset、git revert、git checkout、git restore。

我出了一道辨识度非常高的选择题:你刚刚提交了一个有 bug 的代码到本地仓库,还没有推送到远程,现在想把这次提交彻底撤销,要求保留工作区的修改。可选操作包括:

  • A.git reset --hard HEAD~1(错误,工作区修改也没了)
  • B.git reset --soft HEAD~1(正确,撤回提交但保留改动在暂存区)
  • C.git revert HEAD(不推荐,会新增一条反向提交)
  • D.git checkout HEAD~1 -- .(错误用法,会把旧版本覆盖到工作区)

这道题的正确率比我预想的低。很多同学能脱口而出git reset能撤销提交,但说不清--soft、--mixed、--hard三个参数的区别。这里我建议用一条线来记忆:reset的实质是“移动 HEAD 指针”,三个参数决定移动指针后,暂存区和工作区的状态怎么处理。--hard是三区全部回到目标提交的状态,工作区、暂存区、本地仓库全面重置,最危险也最彻底;--mixed是默认值,把暂存区回退到目标提交状态,但工作区文件不动;--soft最温柔,连暂存区都不动,只挪动 HEAD 指针,修改全部保留在暂存区里。

尤其要强调:reset --hard是真正的“高危操作”,执行完工作区里所有未提交的修改都会消失。不是不能用它,而是用之前必须确认没有需要保留的东西,或者已经用git stash做过备份。我在测验里专门设置了一道判断题:“执行git reset --hard HEAD~1之后,被丢弃的提交是否还能找回?”正确答案是能,至少在短时间内能,线索就是后面要讲的git reflog。很多同学在这里答错,因为他们以为 hard 就是“物理删除”。实际上 Git 的提交对象只要还被 refs 或者其他对象引用,就不会被立即清除。这个认知在灾难恢复时能救命。

再看git revert,它的设计逻辑和reset完全不同。revert不是把历史抹掉,而是“新增一条提交,用来反向应用目标提交的改动”。也就是说历史完整保留,只是追加了一个“撤销提交”。这个特性决定了它的使用场景:当你已经推送了提交到远程共享分支,别人可能已经基于这份代码继续开发了,此时绝不能reset,因为改写历史会引发一堆冲突。正确做法就是revert,安全、可追溯、不影响他人。所以我的口诀是:本地没推,随便 reset;远程已推,只能 revert。

3.3 分支与合并模块:冲突解决能力是分水岭

分支操作是 Git 日常使用频率最高的功能,但多数人的水平停留在“创建分支、切换分支、合并分支”三板斧。测验里我设置了一个典型的冲突场景:

你在 dev 分支修改了 config.js 的第三行,同事在 feature/payment 分支也修改了同一文件的同一行,两边都提交了。现在你在 dev 分支执行git merge feature/payment,系统提示 CONFLICT。请写出完整的处理流程。

这题看起来不难,但很多人的答案只有“打开文件,手动修改,然后 commit”。我会追问三个细节:第一,冲突标记长什么样?第二,解决完冲突后需不需要执行git add?第三,提交信息是手写还是 Git 自动生成?

先说冲突标记。文件冲突后,Git 会在冲突区域插入<<<<<<< HEAD、=======、>>>>>>> feature/payment三组标记,分别代表当前分支的内容、另一分支的内容。这个知识点属于必须掌握的常识,但确实有工作两三年的同学说“每次都是照着教程删标记,从没注意过标记名字”。我建议实战中不要直接删整个冲突块,而是逐段判断保留哪边、还是两边都留,必要时还可以参考冲突文件旁边的.orig备份文件。处理完所有冲突文件之后,git add把它们标记为已解决,然后执行git commit完成合并提交。如果是标准 merge 操作且全部冲突都解决了,Git 会生成一条默认提交信息,直接git commit不带-m参数即可,编辑器里会看到“Merge branch 'feature/payment' into dev”这条现成信息。

再往下深挖,merge 和 rebase 的选择问题才是这个模块的题眼。我在测验中给出的建议性回答是:如果分支是公用的、或者你希望完整保留真实的开发时间线,选 merge;如果分支是你自己维护的功能分支、希望 Dev 主干保持线性整洁,选 rebase。但要注意,rebase 的代价是“重写提交哈希”,一旦 rebase 了推送到远程的分支,别人的本地副本就全部失联了。这条规则是铁律:绝不对已推送的公共分支做 rebase。实际项目中,很多优秀团队会明令禁止在主干分支上 rebase,只允许在个人功能分支内部使用。

3.4 协作与远程模块:覆盖日常协作的真实痛点

远程协作是团队开发的主战场,这个模块我主要考察 fetch 与 pull 的区别、push 被拒绝的处理、以及 PR 流程中的常见问题。

先看 fetch 和 pull。严格来说,git pull是git fetch加git merge的组合操作。fetch 只做一件事:把远程仓库的最新提交下载到本地对应的远程追踪分支上,比如 origin/dev,但不改动你的工作区、当前分支和本地提交。pull 则更进一步,自动帮你把远程追踪分支合并到当前分支。很多人遇到的“pull 之后代码被莫名改了”或者“pull 冲突没法撤销”等问题,根源就是没有理解这个组合关系。

基于这个理解,我再出一道实操题:远程 dev 分支更新了很多提交,你本地也有一个未推送的提交,但你不想让本地的提交和远程的更新自动合并,想先看看远程改了什么。正确做法是什么?答案是先git fetch,然后用git log HEAD..origin/dev查看远程新增提交,再决定是git merge还是git rebase。直接pull当然也能达到目的,但如果自动合并产生了冲突,处理起来就会被动很多。这里我还想分享一个习惯:用git pull --rebase替代默认的git pull。为什么?因为 pull 默认走 merge,会在你的本地提交和远程提交之间制造分叉,时间久了本地分支的图形会变得异常曲折。--rebase则把你的本地提交逐个重放到远程最新提交之上,历史保持线性。注意,前面说过 rebase 对公共分支有风险,但对自己的本地分支做pull --rebase是完全安全且推荐的。

push 被拒绝是另一个高频场景。当你的本地提交基于的远程版本已经过时,push 会被拒绝,提示“non-fast-forward”。很多初级开发者这时候第一反应是git pull,然后发现又冲突了,最后靠git push --force强行覆盖。这条路径其实踩了两个坑:第一,pull 时 merge 产生的分叉让历史变得奇怪;第二,--force如果是针对共享分支,可能会直接覆盖掉同事刚推送的提交,非常危险。正确的安全操作是:先git fetch,查看 origin/dev 和本地 dev 的差异,然后执行git pull --rebase,把本地提交重放到最新远端之上,最后再git push。如果确认不需要保留本地那个过时提交,也可以先git reset --hard origin/dev再重新开始。如果实在需要强制推送,优先用--force-with-lease代替--force,前者会检查远程分支在最近一次 fetch 之后是否被改动过,相当于给强制推送加了一把安全锁,能防止误伤同事的新提交。

3.5 疑难杂症模块:区分进阶玩家的试金石

疑难杂症模块比较有意思,题目不多,但每题都能拉开差距。我选了三个最具代表性的场景:误删分支恢复、detached HEAD 状态理解、cherry-pick 精确移植提交。

误删分支是很多开发者的噩梦。场景描述:你有一个功能分支 feature/homepage,里面积累了 20 多个提交,某次清理时不小心执行了git branch -D feature/homepage。提示信息显示分支已删除,提交记录还能找到吗?答案是:能,只要知道被删分支指向的提交哈希。怎么找?用git reflog。reflog 是 Git 的“本地操作日志”,记录了 HEAD 在本地仓库中每一次移动的历史,包括提交、切换分支、拉取、复位等操作。误删分支后,执行git reflog,找到HEAD@{n}中对应“branch -D”之前的那一条记录,记录里会显示一个哈希值,那就是分支曾经指向的提交。然后执行git branch feature/homepage <哈希>,分支就原地复活了。实操中,只要被删的分支提交没有被 git gc 清理(保留周期默认是 90 天左右),这套找回流程基本都能成功。这是我认为每一位开发者都应该掌握的保命技能。

再说 detached HEAD。很多人一看到“You are in 'detached HEAD' state”就懵了。其实这个状态的意思是:你当前没有处于任何分支上,HEAD 直接指向了一个具体的提交。切换分支时,如果git checkout <commit-hash>,Git 并不要求你必须在一个分支上,它允许你“悬空”检出某个历史提交。在这个状态下做的任何新提交,都不属于任何分支,除非你马上把它保存下来,否则切走之后这些提交会变成“孤儿提交”,很容易丢失。处理办法很明确:如果你只是想看看旧代码的样子,看完了切回原分支就行;如果你想在旧的提交基础上开新分支,执行git switch -c new-branch-name或者git checkout -b new-branch-name,就把当前这个 detached 位置变成了一个新分支的起点。测验里这题正确率不算高,因为大多数人的日常工作根本不会主动 checkout 历史提交。

最后一个题目是 cherry-pick。场景是:你在 dev 分支工作,但同事在 feature/payment 分支提交了一个针对你正在开发模块的修复。你不能把整个 feature/payment 分支合并过来,因为里面有大量不相关的东西,你只想把这个修复挪到自己分支上。怎么做?答案就是git cherry-pick <commit-hash>。这个命令的作用是“把指定的提交应用到当前分支”,相当于给这条提交生成一份内容相同的复制品,放到你所在分支的顶端。这在我实际项目中用得非常多——错误修复需要紧急同步到多个维护分支,但每个分支又不能让别的分支的“半成品代码”混入,cherry-pick 就是最干净的手段。

4. 常见失分点与排查技巧实录

整套测验做完,我统计了团队的实际得分情况,有一些失分点非常集中,这里整理给大家参考。

4.1 团队实测中的高频错误

失分点错误认知正确认知
reset 三档参数混淆以为--hard和删除脑子里的代码一样不可恢复--hard会清掉工作区和暂存区改动,但提交对象短期内可通过 reflog 找回
pull 和 fetch 等同以为 pull 就是 fetch 的加强版pull 是 fetch + merge 的组合,不加参数时自动执行 merge
对已推送分支使用 rebase觉得 rebaase 看起来更整洁,哪里都能用已推送公共分支 rebase 会导致所有协作者本地历史错乱
冲突标记只会删遇到冲突就整个冲突块删掉,不知道<<<<<<<和>>>>>>>各代表哪边要逐行判断保留内容,理解 HEAD 和另一分支的两侧含义
push 被拒绝就 force默认用--force强行推送优先pull --rebase解决分叉;必须强制推送时用--force-with-lease
detached HEAD 慌乱切走在游离状态做了提交后直接切走,导致新提交丢失立刻用git switch -c 新分支名把当前状态固化为一个分支

这张表是基于 12 人团队实测结果整理出来的高频失分点,我把它贴在组内 Wiki 上之后,后续几场测验里同类错误的重复率下降了大概四成。说明一旦把“模糊地带”明确成“正确认知”,大家再遇到同类问题时就有了明确判定依据。

4.2 我自己踩过的几个坑

这里分享两个我认为最有代表性的实操教训。

第一个坑是关于git push --force-with-lease的。我之前在一个共享分支上需要改掉一个包含敏感信息的提交,确认了远程分支几小时内没人动过,就执行了git push --force强行覆盖。结果当天下午同事说他在我推送之前已经在本地基于旧的提交做了一个新功能,一 push 就冲突了。虽然最后通过 reflog 找回了,但这次经历让我长记性:凡是涉及共享分支的强制推送,务必先 fetch,然后确认自己和团队成员都没有未同步的工作,再考虑是否真的需要强推。如果必须强推,一定用--force-with-lease,它会在推送前核对远程分支的最新状态和你本地记录的 origin 引用是否一致。简而言之,--force-with-lease体现的是“我先确认别人没动过,再决定推不推”的协作意识。

第二个坑是git commit --amend的误用。有一次我把某个模块的五个相关改动分成了多次提交,最后想整理成一条完整的提交信息,直接对最后一条执行了git commit --amend。乍看没问题,但其实前面的四次提交还是孤立的,并没有合并到一起。要真正合并多个提交,正确工具是git rebase -i,它会进入交互式界面,让你把提交合并、修改信息、调换顺序。我当时没有用这个工具,导致提交历史还是不整洁,最终还得再走一次 rebase 流程,浪费时间。现在我对团队的建议是:凡是“修改多条历史提交”的需求,一律先想到rebase -i;凡是只想改最近一条的提交信息或补充漏掉的文件,才用amend。

4.3 测验后的快速复盘流程

每次测验结束,我会花 20 分钟做一轮快速复盘,流程固定:先看整体平均分,找到最弱的模块;然后把错误率最高的 5 道题挑出来逐题讲解,每道题都结合真实代码仓库演示一遍流程;最后给每个人发一份个性化的“薄弱点清单”,上面标注了需要重点复习的命令和使用场景。比如说,某成员在“疑难杂症”模块出错较多,那他的清单里就会列出 reflog、cherry-pick、detached HEAD 三个主题,附带我写的命令速查。这套复盘流程比单纯的分数更有价值,因为分数的意义不在于排名,而在于指明下一步该练什么。

5. 从测验结果到能力提升路径

5.1 按分数段制定训练方案

测验结束后,我会把得分划成三档,分别匹配不同的提升路径。

40 分以下的同学属于“命令可用但不稳定”的阶段,常见表现是:能完成 commit、push、pull 这些日常动作,但一旦出现冲突、撤销、分支合并就不知所措。我的建议是不要再通过零散搜索学命令了,先做一件系统的事:在本地建一个练习仓库,固化为每天 15 分钟的练习任务。任务内容不复杂——新建分支、修改文件、制造冲突、解决冲突、再合并分支,把这些动作重复做十次。每次做的时候,刻意用口述的方式解释每一条命令的含义,直到能不假思索地说出“这是把暂存区的东西提交到本地仓库”这类描述。

40 到 70 分的同学属于“常用场景熟练,异常场景薄弱”,他们对日常协作命令已经很顺了,但 reflog、cherry-pick、reset 和 revert 的辨析、强制推送的安全边界等进阶知识还是模糊的。对这部分同学,我会推荐一个“场景模拟清单”——故意制造一些小型灾难,比如提交到错误分支、误删分支、本地提交与远程脱节,然后给自己限定 10 分钟恢复。这个练习的关键是每次都把恢复步骤写在笔记里,把零散的经验沉淀成一套自己的“应急手册”。

70 分以上的同学已经具备较强的实战能力,提升重点转向了“协作约定”和“性能优化”。我会建议他们关注几个项目级话题:如何设计团队的 Git 分支模型、如何用 GitHub Actions 自动化 Git 检查、如何处理超大仓库的浅克隆和稀疏检出、如何用 Git LFS 管理大文件。这些内容不仅关乎个人效率,更是从“会用 Git”迈向“管好 Git 仓库”的分水岭。

5.2 推荐的自测方式

如果不想大动干戈组织全团队测验,自己单练也可以。我给个人开发者推荐一个简单的自测方法:打开你手上体量最大的那个工程项目,在终端里执行三组命令,看你能不能立刻解释输出结果。第一组是git log --graph --oneline --all -20,检查你能不能在 10 秒内从图形中识别出主干和分支的关系。第二组是git reflog -20,看看你能不能说出每一条记录对应的操作和含义。第三组是git status -sb,用带分支信息的简短格式,判断当前工作区是否干净。这三组命令覆盖了历史查看、操作追踪、工作区状态三大基本面,能完成并解释清楚,说明基础已经比较扎实了。

更进阶的自测是“带着问题去搜索”:给自己设定一个之前不敢轻易做的任务,比如“把最近三条历史提交合并成一条”或者“把误删的远程分支找回来”,规定自己只能靠 man 文档和官方文档完成任务,不看搜索引擎里现成的复制粘贴答案。这种练法比刷题有效得多,因为它复刻了真实生产环境中的问题场景,强迫你去理解命令背后的行为逻辑。

6. 一点个人体会

这套 Git 测验设计完、跑完两轮之后,我最大的感受是:多数人缺的并不是命令数量,而是对 Git 数据模型和安全机制的整体认知。Git 的命令不算多,但每条命令都有它的适用边界和风险等级,理解力到了,很多操作自然就记住了。我也越来越觉得,比“牢记命令”更重要的是“敢于实操那些看起来危险的操作”,所谓“危险操作”,几乎都有对应的恢复手段,reflog 就是最典型的安全网。如果你也想提升团队的整体 Git 水平,不妨从这套测验模板出发,按照你们团队的实际情况替换场景和题目,跑一轮之后看看得分分布,你会对自己团队的真实水平有一个非常清晰的判断。学习 Git 不像学习一门语言那样需要背诵大量词汇,它更像是在练一套可以不断复盘的操作习惯——练得多了,翻车时自然就不慌了。

RELATED READING

延伸阅读

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