
最近给团队做技术摸底时我临时出了一套 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 playerexample.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从暂存区撤出但保留它在工作区的文件内容。参考答案新版Gitgit 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-hashrevert不是回退历史而是新增一个反向提交把之前那个提交的改动抵消掉。这样历史一直是向前走的协作者拉取后不会出现历史错乱。很多人习惯reset --hard后直接强推这在单一分支单人开发时问题不大但多人协作时会让所有人的本地仓库失联属于事故级操作。我把 reset 三个参数对三个区域的影响整理成一张表考试时可以直接对照参数HEAD指针暂存区工作区典型用途--soft移动不变不变重新提交--mixed默认移动重置不变撤销暂存--hard移动重置重置彻底丢弃改动4.3 reflog重置操作之后唯一的后悔药题目场景刚才执行git reset --hard HEAD~2后发现丢了一个重要提交要求找回。参考答案git reflog git reset --hard 目标commit-hashgit 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 --rebasestash后新建文件不见了未跟踪文件被留在工作区别的分支也能看到用stash push -u连未跟踪文件一起搁置冲突解决后不删标记就提交代码里残留编译出错编辑完必删标记再git addcommit在公共分支上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日常操作会顺畅很多。考试是一时的但这些操作手感是每天都要用的练扎实了才算真正过关。