最近给团队做技术摸底时,我临时出了一套 Git 实操题,结果挺出乎意料:能熟练背出git init、git clone的人不少,但真到"提交信息写错了怎么改""推送被拒了怎么处理""冲突文件怎么收场"这些高频场景,很多人直接卡住了。这让我意识到,Git 的核心技能从来不是背命令,而是把工作区、暂存区、仓库、远程这几个概念内化成手感。所以我把那套题重新压缩整理成一个精编版,也就是这次分享的"Git 核心技能实操强化考试(精编版)"。它不考概念填空,全部是命令行实操,你会在本地搭一个模拟远程环境,然后把开发中最常见的十几类操作完整走一遍,每题都有参考答案、原理讲解和判卷要点。适合准备技术考核的开发者,也适合带新人的导师直接拿来当题本。
1. 先把考场搭好:本地裸仓库与模拟远程环境的初始化
1.1 为什么要用本地目录模拟"远程"
实操考试最怕依赖外部网络和某代码托管平台,网速一波动,题目还没读完,人先慌了。所以我选择把"远程仓库"直接建在本地目录里,用一个--bare裸仓库来扮演服务器。这种方式在真实开发里也有用:内网环境、离线项目、单机自动化脚本,经常就是靠本地裸仓库做中心存储,不牵扯任何外部平台。
所谓裸仓库,就是没有工作区的Git仓库,里面只有.git目录那一套内部结构,不能直接在里面改文件,只能接收push。你可以把它理解成一台"只管存档、不让人靠近办公桌"的文件服务器。实际开发中,某代码托管平台上的远程仓库本质上也是这种裸仓库。先在本地把这一层模拟出来,后面所有远程操作都能真实跑通,这才是考试的第一步。
1.2 环境初始化:三条命令跑出一个完整考场
首先建立两个目录,一个放考生工作区,一个放远程裸仓库。在终端里执行:
mkdir -p git-exam/player git-exam/remote.git cd git-exam/remote.git git init --bare执行完git init --bare后,remote.git目录里会出现HEAD、branches、config、objects、refs等文件,但没有工作区。接下来进入player目录,初始化本地仓库,并配置一个提交身份。Git 要求提交前必须知道用户名和邮箱,否则会拒绝提交,这一步不配置好,后面所有题都会翻车。
cd ../player git init -b main git config user.name "Player" git config user.email "player@example.local"如果你的 Git 版本比较老,不支持git init -b main,就先执行git init,再执行git branch -m main把默认分支名改成 main。接着把本地仓库和模拟远程关联起来:
git remote add origin ../remote.git git remote -vgit remote add origin这里的origin是给远程仓库起的默认别名,相当于通讯录里的名字。后面push、pull都靠它定位远程地址。
1.3 环境自检:先做一次最小提交验证链路
考场搭好之后,不能直接开考,要先做一次最小链路验证,确保本地提交能推到模拟远程。我建议先创建一个README.md提交并推送:
echo "# demo" > README.md git add README.md git commit -m "init: 初始化演示仓库" git push -u origin main看到main -> main之类的推送提示就说明链路通了。-u参数的含义是"设置上游分支",告诉Git 当前本地分支默认跟踪远程的哪个分支,之后直接敲git push就能推送,不用每次写完整地址。这一步做完,考场才算真正就绪。
2. 基础抢分题:暂存区、提交历史与文件生命周期里的细节
2.1 第1题:完成一次标准提交,并说清三个区的流转
题目场景:在模拟项目X里新建app.py和config/settings.ini,完成一次"工作区 -> 暂存区 -> 仓库"的完整提交,然后用一条命令查看最近一条提交记录。
参考答案:
touch app.py mkdir -p config touch config/settings.ini git status git add app.py config/settings.ini git status git commit -m "feat: 新增应用入口与配置文件" git log --oneline -1很多人做这道题时,会直接跳过中间的git status,这是最大的失分点。git status不只是"看看有什么变化",它是在告诉你当前Git处于什么状态:哪些文件被改了还没暂存,哪些暂存了还没提交,哪些文件还没被跟踪。如果你连自己的仓库状态都说不清楚,后面所有撤销、回滚操作都会变成盲猜。
原理上,工作区是"草稿桌",你在这张桌上改文件;暂存区是"待发货清单",git add相当于把文件放上清单;仓库是"按下快门的底片",git commit才真正生成一个永久快照。为什么 Git 要设计暂存区这一层?因为它允许你把一个阶段的改动拆成多个逻辑提交。比如你同时改了登录模块和支付模块,可以分两次git add分别提交,历史就清晰得多。判卷时我会重点看:能不能说清git add .和git add 指定文件的区别,以及为什么不该把所有文件一股脑加进来。
2.2 第2题:提交信息写错了,怎么不新增一坨历史
题目场景:刚才那次提交信息里,feat少写了个字母,要求修改成正确信息,但不允许新增一条提交记录。
参考答案:
git commit --amend git commit --amend -m "feat: 新增应用入口与配置文件"先用第一条命令打开默认编辑器修改,如果你只想快速替换,就用第二条直接指定新信息。很多人刚接触--amend会以为它只是"改个名",实际上它的工作机制是:用一个新的提交对象,替换掉原来那个提交。所以修改之后,这个提交的 SHA-1 哈希值一定会变。
这里藏着第一个重要考点:如果这个提交已经推送到了远程,就不要轻易amend,因为你会改写公共历史,下次push大概率被拒绝,协作者的本地历史也会跟着错乱。判卷时我会问一句:未推送的提交可以用amend,已推送的提交应该用什么?答案是后面第4章要讲的revert,这里先埋个伏笔。
2.3 第3题:文件暂存错了,如何优雅地撤出
题目场景:你在git add .时不小心把.env这类本地配置文件也放进暂存区了,要求只把.env从暂存区撤出,但保留它在工作区的文件内容。
参考答案(新版Git):
git restore --staged .env老版本Git可以这样写:
git reset HEAD .env这道题是基础题里最容易混淆的。git restore --staged做的事是"把暂存区里的内容恢复成 HEAD 版本",翻译过来就是:把 .env 从待发货清单里拿掉,但草稿桌上的文件原封不动。很多人分不清restore --staged和git rm --cached,这两者的本质区别在于:
git restore --staged <file>:只撤销本次暂存,文件仍然被Git跟踪,适合"加错文件"。git rm --cached <file>:把文件从Git的跟踪列表里彻底移除,之后它会变成未跟踪文件,适合"以后都不希望这个文件被提交"。
判卷时我会让考生解释这两种命令的使用场景,能说清楚的人,说明真的理解了暂存区和跟踪状态,而不是死记命令。
2.4 第4题:删除与重命名文件,别让Git猜你的意图
题目场景:删除docs/old.md,并把README.md重命名为README.zh-CN.md,要求Git能够正确识别这次删除和重命名。
参考答案:
git rm docs/old.md git mv README.md README.zh-CN.md git statusgit rm不只是帮你删工作区文件,它同时会把"删除这个文件"这件事记录到暂存区,一次操作完成两个动作。git mv同理,重命名后工作区和暂存区同时更新,后续提交能直接看到renamed状态。
很多初学者习惯手动rm和mv,认为最后git add .也能被Git识别。这确实也行,Git在提交时可以根据内容相似度自动判断出重命名,但有个坑:如果文件改动大,Git可能识别成"删了一个文件、加了一个新文件",历史看起来就很乱。用git mv和git rm是在给Git明确的信号,让它少猜一点。判卷时我会看终端里的git status是否显示了renamed,以及考生是否能解释Git重命名检测的原理。
3. 分支与合并:快进、三方合并和冲突解决的完整链路
3.1 快进合并:为什么有时合并后看不到"合并提交"
题目场景:基于main创建一个feature/login分支,在分支上提交两次代码,然后切回main合并该分支,要求合并后提交历史是一条直线,没有多余的合并提交。
参考答案:
git checkout main git checkout -b feature/login echo "print('login')" > login.py git add login.py git commit -m "feat: 新增登录模块" echo "print('login done')" >> login.py git add login.py git commit -m "feat: 完成登录流程" git checkout main git merge feature/login git log --graph --oneline --all合并后你会发现git log --graph显示的是一条直线,没有出现 "Merge branch 'feature/login'" 这样的提交。原因是:main自feature/login分支创建以来没有产生任何新提交,Git 不需要真正"合并"两份不同的历史,只需要把main的指针直接往前移动到feature/login的顶端。这就是 fast-forward(快进)合并。
判卷时我会重点问:快进合并和普通合并的区别是什么?什么时候会触发快进,什么时候不会?答不出"当当前分支没有分叉时,Git会直接移动指针"这一点,说明对分支本质的理解还停留在表面。分支其实不过是指向某个提交的指针,理解了这个,快进合并就一点都不神奇。
3.2 构造一个真实的冲突现场
题目场景:要求考生故意制造一个合并冲突,并复述冲突产生的原因。最稳定的构造方式是:让两个分支修改同一个文件的同一行。
具体操作:
echo "version 1: hello" > welcome.txt git add welcome.txt git commit -m "docs: 初始化欢迎语" echo "version 2 from main" > welcome.txt git add welcome.txt git commit -m "docs: main分支更新欢迎语" git checkout -b feature/pricing echo "version 2 from feature" > welcome.txt git add welcome.txt git commit -m "docs: feature分支更新欢迎语" git checkout main git merge feature/pricing执行到最后一条git merge时,Git 会提示CONFLICT,这个冲突现场就构造成功了。这里的关键知识点是:冲突不是 Git 在惩罚你,而是因为两个分支都对同一处内容做了修改,Git 无法判断哪边才是你想要的最终结果,它只能请你来做裁判。很多同学第一次见到冲突会慌张,其实冲突是正常流程的一部分,处理多了就会发现它比想象中可控。
3.3 冲突解决全流程:从定位到提交
遇到冲突后,第一步不是急着编辑文件,而是先摸清战况:
git statusGit 会明确列出冲突文件,并提示"both modified"之类的状态。打开冲突文件后,你会看到类似这样的标记:
<<<<<<< HEAD version 2 from main ======= version 2 from feature >>>>>>> feature/pricing<<<<<<< HEAD到=======之间是当前分支(main)的内容,=======到>>>>>>> feature/pricing是被合并分支的内容。你需要做的是:把文件改成你真正想要的最终版本,然后删掉这三行冲突标记,保存文件。
之后按顺序执行:
git add welcome.txt git commit --no-edit注意:这里用的是git commit,不是git merge --continue,因为 Git 在冲突解决并git add之后,已经把本次合并的最终结果准备好了,只需要提交收尾。--no-edit表示直接使用默认的合并提交信息,不用打开编辑器。提交完成后用git status确认工作区干净。
判卷时我会检查:考生是否会把冲突标记也一起提交进去。这种情况我见过太多次了,编辑完文件不删标记,结果是文件里残留一堆<<<<<<<,代码直接编译不过。所以我要强调:删标记不是可选项,是必做步骤。
3.4 判卷要点:从提交图判断你是否真的理解分支
完成上述操作后,我通常会让考生展示提交图:
git log --graph --all --oneline输出结果里会出现一个带分叉和交汇点的图形。merge提交的特点是有两个父提交,一个是主分支的历史,一个是被合并分支的历史。从这个图里可以很直观地看出:哪段历史是直线(对应快进合并),哪段历史出现了分叉和交汇(对应真正的三向合并)。
三向合并这个名词稍微深入一点:Git 合并时不是简单对比两个分支的最新文件,而是找出这两个分支的共同祖先提交,然后对比"共同祖先 -> 当前分支"和"共同祖先 -> 被合并分支"这两组差异,再把两组差异叠加起来。如果两组差异改到了同一处,才产生冲突。理解了这一点,你就能解释为什么"两个分支改不同文件"永远不会冲突,而"改同一行"必冲突。这就是合并的本质。
4. 远程协作与撤销回滚:最容易丢分的两个大项
4.1 推送被拒绝:非快进更新的标准处理姿势
题目场景:本地main已经提交了两个 commit,但远程main上也有别人新推的提交,此时git push被拒绝,要求不丢失本地提交,完成安全推送。
参考答案:
git fetch origin git pull --rebase origin main git push origin main先说为什么会被拒绝。Git 推送的基本原则是:只有当本地提交能够"快进"到远程最新提交时,才允许直接推送。如果远程有本地没有的提交,本地直接推上去会覆盖远程历史,Git 会拒绝这种非快进更新(non-fast-forward)。
这里我特别推荐用git pull --rebase,而不是默认的git pull。默认pull内部是fetch + merge,会产生一个额外的合并提交,多人高频协作时历史会变成一团毛线球。而pull --rebase是fetch + rebase,它会把你本地未推送的提交"拔起来",临时放到一边,把远程的新提交接到底部,再把你的提交重新放上去。放上去的过程中如果发生冲突,逐个解决后执行:
git rebase --continue如果改乱了想放弃这次变基,执行:
git rebase --abort判卷时我会看考生是否清楚:rebase冲突时不要直接git commit,而是要git add后git rebase --continue。这个细节能筛掉一大半人。
4.2 撤销三种境界:reset的soft、mixed、hard与revert的区别
撤销是Git实操考试里最容易被扣分的大项,因为很多人只会一个git reset --hard,完全不管后果。我拆成三个场景来考。
场景一:刚提交完就发现漏了一个文件,想把这个提交拆掉重来,但不希望改动丢失。正确操作是:
git reset --soft HEAD~1--soft只移动 HEAD 指针到上一个提交,暂存区和工作区都不动。换句话说,commit 没了,但改动还在暂存区里,你可以重新git add漏掉的文件,再重新提交。
场景二:本地已经改得乱七八糟,想彻底丢弃所有未提交的改动,恢复到远程最新状态。正确操作是:
git reset --hard origin/main--hard会把 HEAD、暂存区、工作区全部覆盖,这是最危险的撤销方式。执行前一定要确认你不需要保留这些改动了,或者先打个临时分支留个后路。
场景三:一个提交已经推送到了远程,后来发现里面有严重问题,要求在不改写公共历史的前提下修复。正确操作是:
git revert <commit-hash>revert不是回退历史,而是新增一个"反向提交",把之前那个提交的改动抵消掉。这样历史一直是向前走的,协作者拉取后不会出现历史错乱。很多人习惯reset --hard后直接强推,这在单一分支单人开发时问题不大,但多人协作时会让所有人的本地仓库失联,属于事故级操作。
我把 reset 三个参数对三个区域的影响整理成一张表,考试时可以直接对照:
| 参数 | HEAD指针 | 暂存区 | 工作区 | 典型用途 |
|---|---|---|---|---|
--soft | 移动 | 不变 | 不变 | 重新提交 |
--mixed(默认) | 移动 | 重置 | 不变 | 撤销暂存 |
--hard | 移动 | 重置 | 重置 | 彻底丢弃改动 |
4.3 reflog:重置操作之后唯一的后悔药
题目场景:刚才执行git reset --hard HEAD~2后发现丢了一个重要提交,要求找回。
参考答案:
git reflog git reset --hard <目标commit-hash>git reflog查看的是 HEAD 指针的"引用日志",它会记录你每一次 HEAD 移动的痕迹。注意,git log看的是提交历史,git reflog看的是"你曾经站在哪里"。即使reset --hard把分支指针移走了,被移走的提交对象依然躺在Git对象库里,只要没过期、没被垃圾回收,就能通过 reflog 找到并恢复。
执行git reflog后,你会看到类似这样的输出:
a1b2c3d HEAD@{0}: reset: moving to HEAD~2 e4f5g6h HEAD@{1}: commit: feat: 重要功能 7h8i9j0 HEAD@{2}: commit: docs: 修复文档找到对应提交的哈希值后git reset --hard e4f5g6h,就回到了丢失之前的状态。判卷时我会问:reflog 默认会保留多久?答案是默认约90天,之后旧记录会被清理。所以遇到reset --hard翻车,第一时间别慌,先reflog,能救回来的概率极高。这里我还会补充一个实用习惯:比较大范围的重置操作之前,先执行git branch backup打个临时分支,成本几乎为零,但能让你在 10 秒内回滚,比事后翻 reflog 还稳。
5. 进阶工作流:stash、cherry-pick与rebase的高分姿势
5.1 场景题:紧急修bug时如何安放未提交的改动
题目场景:你正在feature/payment分支上开发支付模块,代码写了一半还没有提交,突然需要紧急切到main分支修复一个线上 bug。要求不提交半成品,不丢失当前进度,完成分支切换和后续恢复。
参考答案:
git stash push -m "wip: 支付模块半成品" git stash list git checkout main # 修复bug并提交后切换回来 git checkout feature/payment git stash popgit stash的作用是把当前未提交的改动(包括暂存区的)打包保存到一个"搁置栈"里,让工作区瞬间变干净,这样你才能放心切换分支。pop会把最近一次 stash 的改动重新应用回来,并从栈里移除该记录;如果想保留记录,用git apply。
这道题最容易踩的坑是:新建的文件(从未被Git跟踪过)默认不会被 stash 保存。也就是说,如果支付模块里有一个新建的payment.py还没git add过,执行git stash后这个文件仍然会安静地躺在工作区里。解决方案是记住这个参数:
git stash push -u -m "wip: 包含未跟踪文件"-u也就是--include-untracked,把未跟踪文件也一并搁置。这个细节我不止一次在实际验收里考过,能答出来的人,说明真的被 stash 坑过。
5.2 场景题:只挑某一次提交过来用
题目场景:main分支上有一个 bugfix 提交,哈希值是a1b2c3,但你不能把整个main分支合并过来。现在需要把这个修复精确带到release/v2分支上。
参考答案:
git checkout release/v2 git cherry-pick a1b2c3cherry-pick的全称是"挑选",它会把指定提交的改动生成一个补丁,应用到当前分支上,并生成一个新的提交。这里有个必须理解的原理:因为新提交的父提交不同、应用时的上下文不同,所以即使改动内容完全一样,生成的新提交哈希值也一定和源提交不同。
如果应用时发生冲突,解决后执行:
git add <冲突文件> git cherry-pick --continue如果想中断这次挑选,执行git cherry-pick --abort。判卷时我会问:为什么cherry-pick之后哈希值变了?能答出"提交的哈希由内容、父提交和提交时间共同决定"的人,说明对Git对象模型有真实理解,而不是只记住了命令。
5.3 场景题:整理本地分支历史,和远程保持清爽
题目场景:本地feature/report基于一个较老的main创建,期间main前进了很多提交。要求在不产生多余合并提交的前提下,把本地分支的基底更新到最新main。
参考答案:
git checkout feature/report git rebase mainrebase的原理是:把当前分支从分叉点开始的每一个提交"重新播放"到目标分支的最新提交之上,所以最终历史是一条线,看起来就像你是从最新的 main 开始开发的。这和生产环境的git pull --rebase origin main同理。
我把merge和rebase的取舍总结成一句话:merge 保留真实历史,rebase 加工出清晰历史。如果你在乎"这件事当时真实发生了什么",用 merge;如果你在乎"这段历史读起来是否顺畅",用 rebase。
但这里有一条绝对不能碰的黄金法则:不要 rebase 已经推送到公共分支的提交。因为 rebase 会改写提交的哈希,等于把已经公布出去的历史换掉了,协作者的本地仓库会以为出现了两条分叉,最终结果是一堆重复提交和惨烈的强制推送。判卷时我会问考生能不能说出这条红线,说不出的,高分局基本无望。如果你的历史已经乱成一团,想压缩成清晰的提交块,还可以用:
git rebase -i HEAD~3交互式 rebase 会把最近三条提交列出来,你可以把其中几条标记为squash,合并成一条,发出前注意这同样只能用于尚未推送的本地提交。
6. 判卷自测:常见扣分点红黑榜与自查清单
6.1 常见扣分点红黑榜
把这套题反复用了三轮之后,我整理出一张"红黑榜",基本覆盖了考生最容易丢分的操作,每一条都是从真实踩坑里提炼出来的:
| 常见错误 | 后果 | 正确姿势 |
|---|---|---|
提交已推送后仍然用--amend修改并强推 | 历史被改写,协作者仓库错乱 | 已推送用revert,未推送才用amend |
误执行git checkout .丢弃全部工作区改动 | 工作区所有未提交内容无法找回 | 先用stash再决定去留 |
reset --hard后找不到提交就以为永远丢了 | 其实提交还在对象库里 | 马上git reflog找回,或重置前打备份分支 |
想撤销暂存却用了git rm --cached | 文件被取消跟踪,状态变 untracked | 只是想撤暂存用restore --staged |
直接用默认git pull,历史大量分叉 | 合并提交满天飞,影响协作者阅读 | 想线性历史用pull --rebase |
stash后新建文件不见了 | 未跟踪文件被留在工作区,别的分支也能看到 | 用stash push -u连未跟踪文件一起搁置 |
| 冲突解决后不删标记就提交 | 代码里残留<<<<<<<,编译出错 | 编辑完必删标记,再git add+commit |
在公共分支上rebase并强推 | 所有协作者历史失联,出现重复提交 | 公共分支只向前演进,用merge或revert |
这张表我建议打印出来贴在工位边上,比背十遍命令都好用。考试的时候,每做错一类,就在对应行画个勾,最后看哪类勾最多,就知道你的薄弱区在哪。
6.2 自测结果分析:不同分数段对应的补强方向
如果你在自测中出现了以下几种情况,我对症下药给点建议。
基础题丢分,说明你对工作区、暂存区、仓库的"三层模型"缺乏手感。别急着刷高级操作,先在一个空仓库里把add、commit、restore、reset来回玩一个小时,直到你看到git status的输出,心里能立刻浮现出当前文件处在哪个区域。这个感觉建立不起来,后面所有操作都是空中楼阁。
分支与合并丢分,重点练习构造冲突和解决冲突。你可以故意在两个分支上改同一个文件,然后反复合并,直到不看文档也能条件反射地处理那三行冲突标记。合并本身并不难,难的是心态:把冲突当成一个需要判断的提示,而不是一个需要害怕的错误。
远程与撤销丢分,建议在本地模拟远程环境里反复练习"推送被拒"和"reset 误操作"两个剧本。把git fetch、git pull --rebase、git reset --hard、git reflog串在一起玩,直到你形成肌肉记忆:推送被拒先 fetch 看差距,reset 之前先想 reflog。这套组合拳练熟之后,线上再出什么问题,你至少不会当场慌神。
进阶工作流丢分,说明你对"提交对象"的理解还不够深。stash、cherry-pick、rebase本质上都是在操作提交对象和引用指针。建议你仔细体会一件事:cherry-pick为什么哈希会变?rebase为什么能改变历史形状?理解了 Git 的提交哈希由内容、父提交和时间共同决定,这几道题就不再是背诵题。
我给这套题取名"精编版",是因为它没有把考纲铺得很大,只挑了日常开发里出现频率最高的十几个场景。最后再分享一个我用了很多年的小习惯:把常用的 Git 命令配成短别名,真正减少手脑之间的摩擦:
git config --global alias.st status git config --global alias.co checkout git config --global alias.br branch git config --global alias.cm "commit -m" git config --global alias.lg "log --graph --oneline --all --decorate"配好之后,查看提交图只需要敲git lg,日常操作会顺畅很多。考试是一时的,但这些操作手感是每天都要用的,练扎实了,才算真正过关。