hello 我是逆境
第一次参加实习面试前,小周背过不少 Git 命令。
add、commit、push,他念得很顺。可面试官换了个问法:“你刚提交的代码还没推送,现在想撤销提交,但代码得留下,怎么办?”
小周脑子里一下冒出五六条命令。reset 好像可以,revert 好像也行,后面到底该接 soft、mixed 还是 hard?他不敢赌。
这种卡壳很常见。Git 的命令不少,真正难的却不是记命令,而是弄清楚:现在的代码在哪一层,你希望哪一层发生变化。
这篇文章从一个普通开发者的工作日讲起。小周会切错分支、遇到冲突、误删提交,也会一点点把代码救回来。每道高频题最后都有一段“面试回答”,理解之后再读两遍,基本就能复述。
第一站:早上九点,先看懂 Git 这座“版本仓库”
小周入职第一天,组长没有急着讲命令,而是在白板上画了四个地方:
工作区 暂存区 本地仓库 远程仓库
正在修改的文件 下次要提交的内容 已提交的历史 团队共享的历史
│ git add │ git commit │ git push
└──────────────→ └───────────────→ └──────────────→
后面的绝大多数 Git 问题,都能回到这张图上。

图 1:代码依次经过工作区、暂存区、本地仓库和远程仓库。
1. Git 和 SVN 有什么区别?
可以把 SVN 想成公司的中央档案室。完整档案主要放在服务器上,员工拿到的是一份工作副本。服务器出问题或者网络断开,很多操作会受影响。
Git 更像每位员工都拿到了一整套档案副本。本地仓库里有完整的提交历史,所以断网时仍然可以提交、创建分支、查看记录。等网络恢复,再把结果同步给 GitHub、GitLab 之类的远程仓库。
面试回答:Git 是分布式版本控制系统,每个开发者本地都有完整仓库,可以离线提交和查看历史;SVN 是集中式版本控制系统,版本历史主要由中央服务器管理。Git 的分支更轻量,分布式协作也更灵活。
2. Git 的工作区、暂存区和本地仓库分别是什么?
小周正在编辑的 UserService.java 位于工作区。执行 git add 后,Git 把文件当前内容放进暂存区,表示“下次提交准备带上它”。执行 git commit 后,暂存区里的内容才进入本地仓库,成为一段正式历史。
把它想成寄快递就很好记:
工作区:桌上正在整理的物品
暂存区:已经装进箱子的物品
本地仓库:已经封箱并登记的包裹
箱子装好后,你仍然可以继续修改桌上的文件。此时工作区和暂存区保存的就不是同一个版本。
面试回答:工作区是实际编辑文件的地方;暂存区保存下一次准备提交的内容;本地仓库保存已经提交的版本历史。典型流程是修改文件后执行
git add进入暂存区,再通过git commit写入本地仓库。
3. git add 到底做了什么?
很多初学者把 git add 理解为“开始跟踪文件”。这只说对了一部分。它真正做的事,是把文件此刻的内容写入暂存区。
例如,小周写完第一版代码后执行:
git add UserService.java
随后他又改了两行。如果现在直接提交,提交的是第一次 add 时的版本,后改的两行还留在工作区。想把最新内容也交上去,需要再次执行 git add。
面试回答:
git add会把文件当前版本写入暂存区,作为下一次提交的内容。文件在add之后又被修改,需要再次add,否则新修改不会进入本次提交。
4. git commit 做了什么?
git commit 会根据暂存区创建一个新的提交对象,并让当前分支指向它。
一次提交并不是只有代码。它还记录根目录快照、父提交、作者、提交者、时间和提交说明。正因为这些信息都参与提交对象的计算,哪怕只改一句 Commit Message,提交哈希也会变化。
面试回答:
git commit根据暂存区生成新的提交对象。提交中包含项目快照、父提交、作者和提交者信息、时间以及提交说明。提交成功后,当前分支会指向这个新提交。
5. Git 保存的是文件差异,还是文件快照?
从使用者的角度看,Git 每次提交保存的是整个项目在那个时刻的快照。
这不代表每次都把所有文件复制一遍。没有变化的文件会复用已有对象;发生变化的文件才会形成新的内容对象。Git 在存储和传输时还会做压缩,但它的核心模型仍然是快照。
面试回答:Git 在逻辑上按提交保存项目快照,而不是把每次提交简单理解成一组文件差异。未变化的内容会复用已有对象,因此不会机械地重复保存全部文件。
6. Git 中有哪些重要对象?
小周打开 .git 目录时,看见一堆认不出的文件。组长告诉他,先记住四种对象:
- Blob 保存文件内容,但不保存文件名。
- Tree 保存目录结构、文件名以及它们指向的 Blob 或子 Tree。
- Commit 指向一个根 Tree,同时记录父提交、作者和提交说明。
- Tag 可以指向某个对象,通常用来给发布提交起一个固定名字。
分支不属于这四类对象。它只是一个引用,保存着某个提交的哈希。
面试回答:Git 的核心对象有 Blob、Tree、Commit 和 Tag。Blob 保存文件内容,Tree 组织目录结构,Commit 记录项目快照和父提交等信息,Tag 通常用于固定标记某个提交。分支和 HEAD 属于引用。
7. Git 的提交哈希是怎么来的?
Git 会根据对象类型、长度和内容计算哈希。传统仓库主要使用 SHA-1,新格式仓库也支持 SHA-256。
提交对象里不只有代码快照,还有父提交、作者、时间和说明。因此修改一次历史提交,会得到新的提交哈希;它后面的子提交因为记录了父提交哈希,也会跟着变化。这正是 rebase 会“改写历史”的原因。
面试回答:Git 根据对象类型、长度和内容计算哈希。提交对象包含根 Tree、父提交、作者、提交者、时间和提交说明,这些内容发生变化都会产生新的提交哈希。修改较早的提交还可能导致后续提交哈希连锁变化。

图 2:正常情况下 HEAD 跟着分支走;分离状态下,HEAD 直接指向某次提交。
8. HEAD 是什么?
HEAD 可以理解成“你现在站在哪里”的路标。
正常情况下,它先指向当前分支,分支再指向最新提交:
HEAD → main → C3
小周在 main 上提交一次后,会变成:
HEAD → main → C4
HEAD 和 main 看起来一起向前走,其实是当前分支移动到了新提交,HEAD 仍指着这个分支。
面试回答:HEAD 表示当前检出的提交。正常情况下,HEAD 指向当前分支,分支再指向某个提交。产生新提交后,当前分支向前移动,HEAD 仍跟随这个分支。
9. 什么是 Detached HEAD?
午休时,小周想看看三个月前的代码,于是直接检出了某个提交。此时 HEAD 没有指向分支,而是直接指向提交:
HEAD → C2
main → C8
这叫 Detached HEAD,也就是“分离头指针”。在这里可以修改和提交,但新提交没有正式分支长期指着它。切走之后,它可能变得难找。
如果这次修改需要保留,及时创建分支:
git switch -c repair-from-old-version
面试回答:Detached HEAD 表示 HEAD 直接指向某个提交,而不是指向分支。此时仍能提交,但新提交不属于正式分支。若需要保留,应及时用
git switch -c创建分支。
10. Git 分支的本质是什么?
新建分支并不会复制一份项目。Git 只是新建一个引用,让它指向当前提交:
main → C3
feature → C3
小周切到 feature 提交后,只有 feature 向前移动:
main → C3
feature → C4
这也是 Git 创建和切换分支很快的原因。
面试回答:Git 分支本质上是一个指向提交的可移动指针。创建分支只是创建一个新引用,不需要复制整个项目,因此操作很轻量。
第二站:上午十点,弄清每天都在用的命令
小周已经知道代码存在哪里了。接下来再看命令,感觉会完全不同:每条命令只是让某一层的数据发生变化。

图 3:fetch 把更新取到远程跟踪分支,pull 还会继续合入当前分支。
11. git fetch 和 git pull 有什么区别?
远程同事提交了新代码。git fetch 会把远程数据下载到本地,并更新 origin/main 这类远程跟踪分支,却不直接改动小周当前的 main。
git fetch origin
这像快递先送到小区驿站,你可以看一眼是什么,再决定是否拿回家。
git pull 则更像直接取件并拆包。它通常先 fetch,再把远程变化合入当前分支:
git pull ≈ git fetch + git merge
如果使用 git pull --rebase,第二步会改成 rebase。
面试回答:
git fetch只下载远程更新并刷新远程跟踪分支,不会自动修改当前本地分支;git pull会在 fetch 之后继续执行 merge,或按配置执行 rebase。fetch 更便于先检查再合并,pull 更方便但动作更多。
12. origin 是什么?
origin 只是远程仓库的一个名字。执行 git clone 时,Git 通常把克隆来源默认命名为 origin,但它并不是不可更改的关键字。
git remote -v
git remote rename origin company
一个本地仓库也能配置多个远程,例如把自己 Fork 的仓库叫 origin,把原项目仓库叫 upstream。
面试回答:
origin是克隆仓库时默认创建的远程仓库名称,本质上只是一个可修改的别名。一个本地仓库可以同时配置多个远程。
13. 什么是远程跟踪分支?
origin/main 不是远程服务器上的实时分支,而是本地保存的一份“上次同步时,远程 main 在哪里”的记录。
本地分支:main
远程跟踪分支:origin/main
远程服务器分支:远程仓库中的 main
别人刚推送完代码,小周本地的 origin/main 不会凭空更新。执行 git fetch 后,它才会移动到最新位置。
面试回答:远程跟踪分支是本地对远程分支状态的记录,例如
origin/main。它由 fetch 或 pull 更新,并不等于服务器上的实时状态,也不能像普通本地分支那样直接在上面持续提交。
14. checkout、switch 和 restore 有什么区别?
老教程里到处都是 git checkout。它既能切分支,又能恢复文件,还能检出某次提交,职责太多,很容易把人绕晕。
现代 Git 提供了两个更清楚的命令:
git switch feature # 切换分支
git switch -c feature # 创建并切换分支
git restore app.java # 恢复文件
checkout 仍然可用,维护老项目时也经常见到。面试时说清楚职责拆分即可。
面试回答:
git checkout是历史较久的多用途命令,可以切换分支或恢复文件。较新的 Git 将常用职责拆成git switch和git restore:前者管理分支切换,后者恢复文件,语义更明确。
15. git stash apply 和 git stash pop 有什么区别?
小周的功能写到一半,线上突然有 Bug。他又不想为了半成品创建一次提交,于是先把修改临时收起来:
git stash push -m "unfinished login page"
修完 Bug 回来后,两种恢复方式都能取出修改:
git stash apply stash@{0}
git stash pop
apply 恢复后保留 stash 记录;pop 在成功恢复后通常会删除对应记录。担心冲突或想重复使用时,先用 apply 更稳妥。
面试回答:
stash apply和stash pop都会恢复临时保存的修改。apply 恢复后保留 stash 记录,pop 恢复成功后会删除对应记录。存在冲突风险时,可以先使用 apply。
16. git commit --amend 有什么用?
小周刚提交就发现漏了一个配置文件。他可以把文件加入暂存区,再修补最近一次提交:
git add config.example
git commit --amend
amend 不是打开旧提交原地涂改,而是创建一个新提交来替换它。因此提交哈希会变化。若旧提交已经推送并被同事使用,随意 amend 会给别人制造麻烦。
面试回答:
git commit --amend用于修改最近一次提交,可以调整提交说明或补充遗漏文件。它会创建新提交替换原提交,所以哈希会变化,通常只用于尚未共享的本地提交。
17. git cherry-pick 有什么作用?
测试分支里有十个提交,小周只想把其中一个 Bug 修复带到发布分支。这时不必合并整个分支,可以执行:
git switch release
git cherry-pick <commit-id>
Git 会把指定提交造成的修改重新应用到当前分支。内容相似,但由于父提交和环境不同,通常会产生一个新哈希。
面试回答:
git cherry-pick会把指定提交的修改应用到当前分支。它适合只同步部分提交,例如将修复带到发布分支。cherry-pick 生成的是新提交,提交哈希通常不同。
第三站:午后合代码,merge 和 rebase 终于打起来了
小周开发功能时,main 也在继续前进:
F1---F2 feature
/
C1---C2---M1 main
现在要让 feature 获得 main 的最新代码,可以 merge,也可以 rebase。两者都能把代码放到一起,但整理历史的方法不同。

图 4:Merge 保留分叉并产生汇合点,Rebase 把功能提交重新排到主线后面。
18. git merge 和 git rebase 有什么区别?
merge 会把两条历史接起来。发生分叉时,通常创建一个新的合并提交:
F1---F2
/ \
C1---C2---M1---M2
rebase 则把 F1、F2 暂时取下,重新放到 M1 后面:
C1---C2---M1---F1'---F2'
看起来像一条直线,但 F1'、F2' 是重新生成的提交,哈希已经改变。
面试回答:merge 通过合并提交连接两条分支历史,一般不改写原提交;rebase 会把当前分支的提交重新应用到新的基点上,使历史更线性,但会生成新的提交哈希。公共分支适合 merge,尚未共享的个人分支可以用 rebase 整理历史。
19. 什么是 Fast-forward 合并?
如果 main 停在 C2 后再也没动,而 feature 从它后面继续提交:
main
↓
C1---C2---F1---F2
↑
feature
合并时不需要创建额外提交,Git 直接把 main 指针移动到 F2。这叫 Fast-forward,也就是快进合并。
如果团队希望明确保留“这个功能分支被合并过”的记录,可以执行:
git merge --no-ff feature
面试回答:当目标分支是待合并分支的直接祖先时,Git 可以只移动目标分支指针,不创建 merge commit,这叫 Fast-forward。使用
--no-ff可以强制生成合并提交,保留功能分支的合并痕迹。
20. 代码冲突为什么会出现?怎么解决?
Git 擅长合并明确的修改。小周改了第 20 行,同事改了第 80 行,Git 通常能自动处理。可两个人都改了同一段逻辑,而且结果不一致,Git 就不知道业务上该听谁的。
冲突文件中会出现标记:
<<<<<<< HEAD
当前分支的内容
=======
另一个分支的内容
>>>>>>> feature
正确做法不是随手点“全部采用本地”,而是读懂两边意图,手动整理最终代码,然后执行:
git add conflicted-file
git commit # merge 场景
如果冲突发生在 rebase 中,则解决后执行:
git add conflicted-file
git rebase --continue
最后要运行测试。冲突标记消失,只能说明文本合上了,不代表业务逻辑一定正确。
面试回答:当不同分支对同一区域做出无法自动协调的修改时,Git 会产生冲突。开发者需要理解双方修改,手动得到最终内容,执行
git add标记冲突已解决,再继续 commit 或 rebase,并通过测试验证结果。
21. 如何中止 merge 或 rebase?
小周处理了半天,发现自己合错了目标分支。与其继续硬解冲突,不如先退回操作前:
git merge --abort
git rebase --abort
两条命令分别用于中止进行中的 merge 和 rebase,并尝试恢复开始操作前的状态。
面试回答:进行中的 merge 可以用
git merge --abort中止,进行中的 rebase 可以用git rebase --abort中止。它们会尽量把仓库恢复到操作开始前。
22. 为什么公共分支不建议随意 rebase?
假设小周和同事都基于提交 F2 开发。小周把共享分支 rebase 后,远程变成了 F1'、F2'。虽然代码看起来相似,Git 眼里它们却是另一批提交。
同事本地还保留旧历史,之后拉取和推送时,旧提交与新提交会重新碰面,可能造成重复记录和复杂冲突。
面试回答:rebase 会改写提交并改变哈希。如果其他人已经基于旧提交开发,改写后的历史会与他们的本地历史分叉,容易产生重复提交和冲突。因此不要对已经共享并被他人依赖的提交随意 rebase。
23. 如何整理多个零散提交?
功能分支上可能有这样的提交:
完成登录页
修一下
又漏了一个空指针
真的修好了
合并前可以用交互式 rebase 整理:
git rebase -i HEAD~4
常见操作有 pick、reword、squash、fixup 和 drop。例如把后三个修补提交合并进第一个,最终留下一个容易阅读、容易回滚的功能提交。
这仍然属于改写历史,只适合整理自己的未共享分支。
面试回答:可以使用
git rebase -i交互式整理提交。reword修改说明,squash或fixup合并提交,drop删除提交。因为操作会改写历史,通常只在个人分支尚未共享时使用。
24. Git Flow、GitHub Flow 和主干开发有什么区别?
这三种模式没有绝对高下,只是适合不同团队。
Git Flow 设计了 main、develop、feature、release、hotfix 等分支,适合版本发布节奏明显、需要维护多个阶段的项目,但流程偏重。
GitHub Flow 简单一些:从主分支拉功能分支,提交 Pull Request,审查和测试通过后合并,常见于持续交付团队。
主干开发要求开发者频繁向主干集成。为了不把半成品直接暴露给用户,团队通常配合自动化测试和特性开关。
面试回答:Git Flow 分支角色完整,适合发布流程较重的项目;GitHub Flow 以短期功能分支和 Pull Request 为中心,适合持续交付;主干开发强调小批量、频繁集成,通常依赖完善的自动化测试和特性开关。选择取决于团队规模和发布方式。
第四站:下午四点,撤销命令不是“后悔药大礼包”
小周终于遇到了开头那道面试题:刚提交的代码想撤销,但文件内容必须留下。
组长没有直接告诉他命令,只问了三句话:
要移动分支指针吗?
要重置暂存区吗?
要覆盖工作区吗?
只要能回答这三句,reset 的三个模式就不会再混。

图 5:先判断修改处于哪一层、是否已经共享,再选择对应的撤销或恢复命令。
25. git reset 和 git revert 有什么区别?
把提交历史想成一本已经编好页码的账本。
reset 是把当前分支的书签移回旧页。后面的提交可能不再属于当前分支,历史看起来像被改写了,适合处理尚未共享的本地提交。
revert 不撕旧账,而是在最后新增一页“冲销记录”,用反向修改抵消目标提交。旧提交还在,所以更适合公共分支。
面试回答:
git reset通过移动分支指针回到指定提交,可能改写可见历史,适合处理未共享的本地提交;git revert创建一个新的反向提交来抵消原修改,不删除历史,适合已经推送的公共分支。
26. git reset --soft、--mixed、--hard 有什么区别?
先看三层变化:
| 模式 | 移动 HEAD/分支 | 重置暂存区 | 重置工作区 |
|---|---|---|---|
--soft | 是 | 否 | 否 |
--mixed | 是 | 是 | 否 |
--hard | 是 | 是 | 是 |
撤销最近一次提交但保留暂存状态:
git reset --soft HEAD~1
撤销提交,保留文件修改但取消暂存:
git reset --mixed HEAD~1
--mixed 是默认模式,所以也常写成 git reset HEAD~1。
--hard 会同时覆盖暂存区和工作区。未提交的修改可能直接丢失,不能把它当普通清理命令随手执行。
面试回答:soft 只移动 HEAD,暂存区和工作区不变;mixed 移动 HEAD 并重置暂存区,但保留工作区,它也是默认模式;hard 会同时重置 HEAD、暂存区和工作区,可能丢失未提交修改。
27. 已经 git add,但还没提交,如何取消暂存?
文件已经装进“快递箱”,现在只想拿回桌上继续改,不想扔掉内容:
git restore --staged UserService.java
它会取消暂存,工作区修改仍然保留。老写法是:
git reset HEAD UserService.java
面试回答:可以使用
git restore --staged <file>将文件移出暂存区,同时保留工作区修改。旧版本中也常使用git reset HEAD <file>。
28. 如何丢弃工作区中尚未暂存的修改?
如果确认这些修改不要了,可以执行:
git restore UserService.java
它会用暂存区中的版本覆盖工作区文件。因为这些修改还没有进入 Git 对象库,丢弃后未必能找回,执行前最好先看 git diff。
面试回答:可以使用
git restore <file>恢复工作区文件,它通常会用暂存区版本覆盖当前修改。未提交内容可能无法恢复,因此操作前应先确认差异或临时 stash。
29. 如何撤销最近一次提交,但保留代码?
这就是小周面试时卡住的题。
如果想让代码继续留在暂存区:
git reset --soft HEAD~1
如果想保留工作区修改,同时取消暂存:
git reset HEAD~1
如果提交已经推送到团队公共分支,通常不再移动公共历史,而是:
git revert HEAD
面试回答:未推送时,可以用
git reset --soft HEAD~1撤销提交并保留暂存状态,或用默认 mixed reset 保留工作区代码但取消暂存。若提交已经共享,应优先使用git revert HEAD新增反向提交。
30. git log 和 git reflog 有什么区别?
git log 像项目正式出版的历史书,只显示从当前分支等引用能够到达的提交。
git reflog 更像本地监控录像。commit、reset、rebase、分支切换都会让引用移动,这些动作会被 reflog 记录一段时间。即使某个提交已经不在正常分支历史里,也可能从 reflog 中找到。
git reflog
reflog 主要是本地记录,不会作为普通提交历史推送给别人。
面试回答:
git log展示当前引用可达的提交历史;git reflog记录本地 HEAD 和分支引用的移动过程。误执行 reset、rebase 或删除分支后,可以通过 reflog 找到原提交。reflog 通常不会推送到远程。
31. 如何恢复误删的提交或分支?
小周误删分支后,先用 reflog 找到了原来的提交:
git reflog
假设目标哈希是 a1b2c3d,重新创建分支即可:
git switch -c recovered-feature a1b2c3d
如果提交已经存在于远程分支、Tag 或其他开发者仓库中,也可以从这些引用恢复。真正危险的是未提交的工作区内容,因为 Git 可能从未保存过它。
面试回答:先通过
git reflog、远程分支或 Tag 找到目标提交哈希,再使用git switch -c <new-branch> <commit-id>创建分支恢复。已提交内容通常有机会找回,未进入 Git 对象库的修改则不一定能恢复。
32. 代码提交到了错误分支怎么办?
假设提交本来属于 feature,却落到了 main。
先记住错误提交的哈希,然后在正确分支接收它:
git switch feature
git cherry-pick <commit-id>
再处理错误分支。如果 main 上的提交没有推送,可以谨慎 reset;如果已经共享,就用 revert 产生一条明确的撤销记录。
面试回答:可以先切到正确分支,用
git cherry-pick接收错误提交。随后处理原分支:未推送的本地提交可以 reset,已经推送到公共分支的提交应优先 revert。
第五站:准备推送,团队协作最怕“我本地明明是好的”
个人仓库里,许多操作只是自己收拾房间。公共分支则像共享办公室:移动一把椅子之前,得先看看上面有没有坐着同事。
33. 为什么不建议直接 git push --force?
普通 push 被拒绝,往往说明远程分支出现了本地没有的新提交。--force 会无视这个情况,用本地历史覆盖远程历史,同事刚推送的工作可能因此从分支上消失。
相对安全的方式是:
git push --force-with-lease
它会检查远程分支是否仍处于本地预期的位置。如果别人已经更新远程分支,推送就会被拒绝。
不过,“相对安全”不等于“可以随便用”。在受保护的主分支上,团队通常直接禁止强制推送。
面试回答:
git push --force可能覆盖远程分支上其他人的新提交。--force-with-lease会先检查远程引用是否符合本地预期,若远程已经变化则拒绝推送,因此更安全,但公共主分支仍应避免强推。
34. .gitignore 为什么对已提交文件无效?
.gitignore 像仓库门口的“以后不要收这类文件”告示。文件如果早已登记入库,贴告示不会让它自动消失。
想让 Git 停止跟踪,但保留本地文件,可以执行:
git rm --cached application-local.yml
随后把路径写入 .gitignore 并提交。若一个目录下已有大量被跟踪文件,处理前应先确认范围,避免误删索引内容。
面试回答:
.gitignore只影响尚未被 Git 跟踪的文件,对已经提交或进入索引的文件无效。可以用git rm --cached取消跟踪,再提交.gitignore规则。
35. 密码已经提交到 Git,加入 .gitignore 能解决吗?
不能。密码已经出现在历史提交中,后来忽略文件只会影响未来的跟踪行为。
处理顺序很重要。第一件事不是清理 Git 历史,而是让泄露的凭证立即失效:撤销 Token、轮换密码或更换密钥。之后再删除当前代码中的秘密,必要时使用专门工具清理历史,并通知协作者重新同步。
以后把配置改为读取环境变量或密钥管理服务,不要把真实凭证写进仓库。
面试回答:不能。
.gitignore无法删除历史中的敏感信息。发现泄露后应先吊销或轮换凭证,再从当前代码和必要的历史记录中清除秘密,通知协作者重新同步,并改用环境变量或密钥管理服务。
36. 标签和分支有什么区别?
分支会随着新提交向前移动,像阅读进度书签;标签通常固定指向某个提交,像印在书页上的版本章。
发布版本时常创建附注标签:
git tag -a v1.0.0 -m "release v1.0.0"
git push origin v1.0.0
Lightweight Tag 只是一个简单引用。Annotated Tag 还包含标签作者、时间和说明,也可以进行签名,更适合正式发布。
面试回答:分支是会随提交移动的引用,主要用于持续开发;标签通常固定指向某个提交,用于标记发布版本。Annotated Tag 包含作者、时间和说明,Lightweight Tag 则只是简单引用。
37. 拉取代码时发生冲突,应该怎么处理?
先执行:
git status
如果本地还有未完成修改,先提交或 stash,别让“旧工作没收好”和“新代码拉不下来”混成一个问题。然后 fetch,确认远程变化,再选择 merge 或 rebase。
冲突发生后,逐个理解双方代码、解决标记、加入暂存区并继续操作。最后跑测试。
面试回答:先用
git status检查本地状态,未完成修改应先提交或 stash。获取远程变化后,手动解决冲突并执行git add,再继续 merge 或 rebase。解决文本冲突后还要运行测试,确认业务逻辑正确。
第六站:线上出 Bug,Git 也能帮你找“凶手”
Git 不只负责保存代码。历史足够清楚时,它还能帮团队定位问题、自动检查提交,并管理一些特殊文件。
38. git bisect 有什么作用?
线上 Bug 在今天被发现,但团队只知道“两周前还是好的”。从几百个提交里逐个排查太慢,git bisect 会用二分查找不断缩小范围。
git bisect start
git bisect bad
git bisect good <某个正常提交>
Git 会检出中间提交。测试后告诉它当前版本是 good 还是 bad,它再选择下一半。若项目有可靠的自动化测试,还能让 bisect 自动运行脚本。
面试回答:
git bisect使用二分查找定位首次引入问题的提交。先标记一个 bad 提交和一个 good 提交,再不断判断中间版本,查找次数大约是对数级;它也可以配合测试脚本自动执行。
39. Git 为什么不适合直接保存大型二进制文件?
文本代码修改一行,Git 很容易压缩和比较。视频、压缩包、模型权重这类二进制文件稍微变化,就可能产生一个新的大对象。即使后来删除文件,旧版本仍在历史里,仓库体积不会自动缩回去。
Git LFS 的做法是:普通 Git 仓库保存一个小型指针文件,真实大文件放在单独的 LFS 存储中。
面试回答:大型二进制文件不利于差异比较和版本压缩,多次修改会让仓库历史持续膨胀。Git LFS 在仓库中保存指针,把真实大文件放到独立存储,更适合模型、视频和大型设计资源。
40. 什么是 Git Hook?
Git Hook 是在特定 Git 事件发生时运行的脚本。
例如,pre-commit 可以在提交前检查格式,commit-msg 可以验证提交说明,pre-push 可以在推送前运行测试。服务端的 pre-receive、post-receive 则能参与接收代码后的检查和部署流程。
.git/hooks 下的客户端脚本默认不会跟着仓库提交。团队若想统一规则,通常会借助 Husky、pre-commit 等工具,或者在 CI 和服务端再做一层强制校验。仅靠本地 Hook 防不住主动绕过。
面试回答:Git Hook 是在 commit、push、receive 等事件前后执行的脚本,可用于格式检查、测试和提交信息校验。客户端 Hook 默认不会随仓库分发,也可能被跳过,因此重要规则还应放在 CI 或服务端执行。
41. 什么是浅克隆?
一个项目有十年历史,但构建服务器只需要最新代码。此时可以限制克隆深度:
git clone --depth 1 <repository-url>
这样下载更快、占用更少,但本地历史不完整,某些 log、bisect、历史比较和分支操作会受限制。需要时可以继续拉深,或转换为完整克隆。
面试回答:浅克隆通过
--depth只获取有限深度的提交历史,可以减少下载时间和磁盘占用。代价是历史不完整,部分日志、比较和排错操作会受限,适合只需要当前代码的构建环境。
42. Submodule 解决什么问题?有什么代价?
公司项目依赖另一个独立维护的仓库,又希望主项目明确锁定它的某个版本,可以使用 Git Submodule。
主仓库不会直接保存子仓库全部内容,只记录子模块路径、远程地址配置以及一个特定提交。克隆后通常还要初始化和更新:
git submodule update --init --recursive
它的麻烦也很真实:切换主项目分支后,子模块可能仍停在旧状态;修改子模块后还要分别在子仓库和主仓库提交。团队需要统一操作流程。
面试回答:Git Submodule 用于让一个仓库引用另一个独立仓库的特定提交,适合依赖需要独立版本管理的场景。它能精确锁定版本,但克隆、切换、更新和提交都更复杂,团队必须管理好主仓库指针与子仓库状态。
面试前十分钟:把命令按“影响哪一层”再背一遍
命令很多,死记名字很容易串。按影响范围复习会轻松得多:
| 你的目的 | 常用命令 | 主要影响 |
|---|---|---|
| 把当前修改放进下次提交 | git add | 暂存区 |
| 取消暂存,保留代码 | git restore --staged | 暂存区 |
| 丢弃未暂存修改 | git restore | 工作区 |
| 临时收起未完成修改 | git stash | 工作区、暂存区 |
| 修改最近一次提交 | git commit --amend | 本地提交历史 |
| 撤销本地提交,代码留在暂存区 | git reset --soft | HEAD/分支 |
| 撤销本地提交,代码留在工作区 | git reset --mixed | HEAD/分支、暂存区 |
| 连工作区一起回退 | git reset --hard | HEAD/分支、暂存区、工作区 |
| 安全撤销公共提交 | git revert | 新增反向提交 |
| 只下载远程更新 | git fetch | 远程跟踪分支 |
| 下载并合入当前分支 | git pull | 远程跟踪分支、当前分支 |
再记住四组最容易混的:
add 是准备提交,commit 才是生成历史。
fetch 只取回,pull 还会继续合入。
reset 移动旧历史,revert 新增撤销历史。
merge 保留分叉,rebase 重排成直线。
最后做一次模拟追问
面试官问:“你本地有一次错误提交,还没有 push,希望撤销提交但保留代码,怎么办?”
你可以先答:
如果希望代码仍在暂存区,我会使用
git reset --soft HEAD~1;如果只想保留工作区修改并取消暂存,就使用默认的 mixed reset。因为提交还没有共享,所以可以移动本地历史。
面试官接着问:“如果已经 push 了呢?”
如果是团队公共分支,我会优先使用
git revert创建反向提交,不随意 reset 后强推,避免改写别人已经依赖的历史。
他再追问:“要是你真的 reset 错了?”
我会先停止继续改动,通过
git reflog找到 reset 之前的提交,再创建恢复分支。只要提交对象还没被清理,通常有机会找回来。
这三问其实只考一件事:你是否知道当前变化发生在工作区、暂存区还是提交历史,以及这段历史有没有被别人使用。
小周后来又参加了一次面试。面试官仍然问 reset、revert、rebase,他没有逐字背定义,而是先在脑子里画出三层区域和几根分支指针。命令终于不再是一堆长得很像的咒语。
Git 的八股,大多就是这么回事:先判断代码在哪,再决定要把哪根指针、哪一层内容,移到哪里。


459

被折叠的 条评论
为什么被折叠?



