1. 从一次“血泪史”说起:为什么你总在合并时翻车?
我敢打赌,每个用过Git的程序员,都至少经历过一次“合并灾难”。可能是你信心满满地执行了git merge,结果屏幕上蹦出一堆<<<<<<< HEAD的冲突标记,让你瞬间头皮发麻;也可能是你以为只是简单地git push,却收到了“非快进式推送被拒绝”的冰冷提示;更惨的是,你试图用git reset --hard回退版本,结果发现几天的代码“凭空消失”,欲哭无泪。
这些问题,表面上看是操作失误,但根子在于对Git底层核心原理的一知半解。很多人把Git当成一个“高级的文件复制粘贴工具”或者“带版本的文件管理器”,只记住了add、commit、push、pull这几个命令的固定组合。一旦遇到分支策略复杂、多人协作频繁或者需要处理历史记录的场景,就完全抓瞎,只能靠搜索引擎的只言片语和玄学般的尝试来解决问题,往往让情况变得更糟。
今天,我不打算再给你罗列一堆命令大全。那些东西网上一搜一大把。我想带你穿透Git那层“命令”的外壳,直接看到它的骨骼和内脏——也就是它的底层数据模型和核心对象。当你真正理解了Git在仓库里到底存储了什么、这些存储单元之间如何连接、分支和标签又是什么“廉价货”之后,你会发现,所有那些令人困惑的操作,无论是合并、变基、重置还是推送拉取,都变得无比清晰和自然。你不会再惧怕冲突,因为你知道冲突产生的精确位置和原因;你不会再搞丢代码,因为你知道每一个提交都像磐石一样稳固;你也能设计出更清晰、高效的团队协作流程。
所以,忘掉那些复杂的命令参数,让我们回到最初的地方,看看Git到底是怎么“想”的。我保证,看完这篇,Git对你而言将不再是一个黑盒,而是一个你可以清晰理解和掌控的工具。如果看完还不会,你确实可以来找我——不过我相信,那大概率是因为你想和我深入探讨更进阶的话题了。
2. 基石:拆解Git的仓库数据模型
要理解Git的一切行为,必须从它的数据模型开始。这就像学编程先理解变量和内存,学数据库先理解表和索引。Git的仓库(.git目录)内部,主要存储了四种核心对象:Blob、Tree、Commit和Tag。它们通过SHA-1哈希值相互引用,构成一个有向无环图。这个设计是Git强大、高效且分布式的根本。
2.1 四大核心对象:Git仓库里到底存了什么?
Blob对象:这是最基础的数据单元。你可以把它想象成一个“文件内容快照”。但注意,它不存储文件名,只存储文件内容。无论你有一个叫hello.txt还是main.py的文件,只要它们的内容完全相同,在Git仓库里就只会存在唯一一个对应的Blob对象。它的SHA-1哈希值就是根据其内容计算出来的。这种设计实现了高效的去重存储。
Tree对象:Tree对象解决了“文件内容有了,那目录结构呢?”的问题。它相当于一个目录的快照,记录了某个时刻目录的结构。一个Tree对象里包含了一系列条目,每个条目指向一个Blob对象(代表文件)或者另一个Tree对象(代表子目录),并且记录了对应的文件名或目录名。所以,是Tree对象将Blob对象组织成了我们熟悉的文件树结构。
Commit对象:这是版本控制的核心。一个Commit对象代表一次完整的提交,它包含了以下关键信息:
- 指向一个Tree对象:这个Tree对象就是本次提交时,整个项目工作目录的根目录快照。通过它,可以还原出提交那一刻的所有文件内容。
- 指向父提交:指向一个或多个之前的Commit对象。这形成了提交历史链。第一次提交没有父提交,合并提交则有两个或更多父提交。
- 作者和提交者信息:姓名、邮箱、时间戳。
- 提交信息:就是你
git commit -m “...”时写的内容。
Commit对象本身也由这些信息计算出一个唯一的SHA-1哈希值,我们常说的“版本号”或“commit id”就是指这个哈希值。正是这个不可变的哈希值,保证了Git历史的完整性。任何对历史的篡改都会导致后续所有提交的哈希值改变,从而立刻暴露。
Tag对象:这是一个可选的、更“重”的标签对象,通常用于标记重要的里程碑(如v1.0.0)。它指向一个特定的Commit对象,并且可以包含标签名、标签信息、签名等。我们更常用的git tag v1.0创建的是“轻量标签”,它其实只是一个直接指向Commit的引用,不创建Tag对象。
注意:这里有一个关键理解点。Git保存的不是文件的“差异”,而是每次提交的完整“快照”。当你提交时,Git会为所有有变动的文件创建新的Blob,并为包含它们的目录创建新的Tree,最终形成一个新的Commit。虽然存储的是快照,但Git在显示历史差异(
git diff)或传输数据时,会智能地计算和压缩差异,所以实际效率很高。
2.2 引用与符号引用:分支和HEAD的本质
理解了核心对象,我们再来看看每天打交道的“分支”到底是什么。在Git里,分支本质上就是一个指向某个Commit对象的、可移动的指针。
.git/refs/heads/目录下,每个文件就是一个分支。比如master分支,其实就是.git/refs/heads/master这个文件,里面简单存储了一个Commit对象的哈希值,例如a1b2c3d...。这个哈希值指向哪个提交,哪个提交就是master分支的“头”。
HEAD则是一个特殊的符号引用,它通常指向你当前所在的分支。.git/HEAD文件里的内容可能是ref: refs/heads/feature/login。这意味着你当前在feature/login分支上。当你做出新的提交时,Git会:
- 根据工作区变更创建新的Blob和Tree对象。
- 创建一个新的Commit对象,其父提交就是当前HEAD所指向的提交(即
feature/login指针指向的那个提交)。 - 将
feature/login这个分支指针(即.git/refs/heads/feature/login文件里的哈希值)更新为这个新创建的Commit。 - HEAD因为指向
feature/login,所以自动“跟”着前进了。
这个过程完美解释了为什么创建分支如此廉价——仅仅是在.git/refs/heads/下创建一个包含某个提交哈希的新文件而已。切换分支,就是改变.git/HEAD文件的内容,并据此更新工作区文件。
2.3 图解一次提交的诞生
让我们把上述过程串联起来,假设我们有一个简单的项目,首次提交一个README.md文件。
- 工作区:你创建了
README.md,内容为# My Project。 - 暂存区:执行
git add README.md。Git会:- 计算
# My Project内容的SHA-1哈希值(假设为blob_sha1)。 - 将内容压缩后,以Blob对象的形式存入
.git/objects/目录。
- 计算
- 创建Tree:执行
git commit -m “init”。Git会:- 为当前暂存区(此时只有
README.md)创建一个Tree对象。这个Tree对象包含一个条目:(mode: 100644, type: blob, sha1: blob_sha1, name: “README.md”)。这个Tree对象也会被计算哈希(假设为tree_sha1)并存入对象库。
- 为当前暂存区(此时只有
- 创建Commit:Git接着创建Commit对象,包含:
- 指向的Tree:
tree_sha1 - 父提交: 无(因为是首次提交)
- 作者/提交者信息
- 提交信息: “init” 计算这个Commit对象的哈希值(假设为
commit_A),并存入对象库。
- 指向的Tree:
- 更新分支指针:Git将当前分支(比如
master)的指针文件(.git/refs/heads/master)的内容更新为commit_A。
至此,一次完整的提交就完成了。整个仓库的数据关系可以简化为:master分支指针 -> Commit A -> Tree A -> Blob for README.md。下一次提交,新的Commit B会指向Commit A作为父提交,并指向一个新的Tree B(可能包含了变化的文件内容),然后master指针再移动到Commit B。这个链条,就是你的提交历史。
3. 分支合并的三种策略与底层抉择
理解了数据模型,我们终于可以深入最令人头疼的环节:合并。合并的本质,是将两个或多个分支的历史联系在一起。Git提供了几种主要的合并策略,每种策略在底层做的事情截然不同,产生的历史图也不同。
3.1 快进合并:当历史是线性的时候
这是最简单的情况。假设你从master分支的C1提交创建了一个feature分支,然后在feature上进行了C2和C3两次提交。在此期间,master分支没有任何新的提交。
此时的分支状态图是线性的:
C1 (master) \ C2 — C3 (feature)当你切回master分支并执行git merge feature时,Git发现master指向的C1直接就是feature分支的祖先。这意味着feature的所有新提交(C2, C3)都是在master的基础上进行的,没有分叉。
底层操作:Git根本不会创建新的合并提交。它只是简单地将master分支的指针快速向前移动,直接指向feature分支当前的提交C3。这就叫“快进”。合并后的历史仍然是一条直线:
C1 — C2 — C3 (master, feature)feature分支指针通常就完成了使命,可以被删除了。
实操心得:快进合并很干净,但有时我们可能希望保留分支存在的痕迹。这时可以使用
git merge --no-ff feature来禁止快进,强制创建一个新的合并提交。这样在历史图中就能清晰地看到一个合并节点,表明这里曾有一个特性分支被集成。在团队开发中,对长期存在的特性分支使用--no-ff是个好习惯,能让历史更清晰。
3.2 三方合并:处理分叉的历史
更常见的情况是,在你开发feature分支的同时,master分支也有其他人提交了新的修改(C4)。
C2 — C3 (feature) / C1 — C4 (master)现在,master和feature从C1开始分道扬镳了。此时在master上执行git merge feature,快进合并不再适用。Git会启动默认的“递归三方合并”策略。
底层操作:
- 寻找共同祖先:Git首先找到
master(C4) 和feature(C3) 的“最近共同祖先”,在这个例子里就是C1。这个祖先提交作为合并的“基准”。 - 计算差异:Git会分别计算:
- 差异A:从祖先C1到
master当前C4的变化(即别人在master上改了啥)。 - 差异B:从祖先C1到
feature当前C3的变化(即你在feature分支上改了啥)。
- 差异A:从祖先C1到
- 应用合并:Git尝试将这两组差异同时应用到共同祖先C1的状态上。如果两组差异修改了不同的文件,或者修改了同一文件的不同部分,Git可以自动合并,生成一个新的合并结果。
- 创建合并提交:自动合并成功后(或手动解决冲突后),Git会创建一个新的提交,即合并提交C5。这个提交的特殊之处在于它有两个父提交:C4和C3。
C2 — C3 (feature) / \ C1 — C4 ———— C5 (master)合并提交C5指向一个新的Tree对象,这个Tree反映了合并后的最终文件状态。master分支指针移动到C5。
3.3 变基:重写历史,追求线性
合并会产生分叉的历史,有些人喜欢这种能体现真实开发过程的历史图,但也有些人追求简洁的线性历史。git rebase就是后者的选择。
继续用上面的例子,你在feature分支上,历史是C1 — C2 — C3。master已经前进到了C1 — C4。
底层操作:当你在feature分支上执行git rebase master时,Git会做以下事情:
- 找到待重演的提交:找到当前分支 (
feature) 相对于目标分支 (master) 的最近共同祖先之后的所有提交。这里是C2和C3。 - 暂存这些提交的差异:Git会依次提取C2和C3相对于其各自父提交所引入的变更(即差异补丁)。
- 重置分支基底:将
feature分支的指针重置到master的最新提交C4上。注意,此时C2和C3在feature分支上暂时“消失”了。 - 重新应用提交:Git在C4这个新的基础上,依次重新应用之前暂存的C2和C3的变更。每应用一个,就创建一个新的提交。假设新创建的提交叫C2'和C3'。重要的是,C2'和C3'是全新的提交,拥有全新的SHA-1哈希值,虽然变更内容与C2、C3相同。
- 移动分支指针:最后,将
feature分支指针移动到最后一个新提交C3'上。
最终的历史图变成了:
C1 — C4 (master) \ C2' — C3' (feature)现在,feature分支的历史看起来就像是直接从最新的master上拉出来进行开发的一样,历史是一条完美的直线。此时再切回master执行git merge feature,就会触发一次快进合并,将master直接移到C3',得到一条从C1到C4到C2'到C3'的线性历史。
重大警告:变基的本质是丢弃原有提交,创建内容相似但全新的提交。这意味着你重写了提交历史。绝对不要对已经推送到远程仓库、且可能被其他人使用的提交进行变基!这会导致你的本地历史与远程历史严重分歧,在协作中引发灾难。变基只适用于你本地、尚未分享的提交,用于整理你的本地工作,使其更清晰后再推送。
3.4 冲突的产生与解决:当Git无法自动抉择时
在三方合并或变基重新应用补丁的过程中,如果Git发现两组差异修改了同一个文件的同一行代码(或相邻行),它就无法自动决定应该采用哪个版本。这时,合并过程会暂停,并标记为冲突状态。
底层发生了什么:Git会在冲突的文件中插入冲突标记:
<<<<<<< HEAD // master分支上的代码 console.log(“Hello from master”); ======= // feature分支上的代码 console.log(“Hello from feature”); >>>>>>> feature<<<<<<< HEAD和=======之间是当前分支(你执行合并命令时所在的分支,即接收合并的分支)的内容。=======和>>>>>>> feature之间是要被合并进来的分支的内容。
解决冲突的实质:就是手动编辑这个文件,删除这些冲突标记,并决定最终保留的代码应该是什么样子。可能只保留一边,也可能将两边的修改融合。解决完所有冲突文件后,你需要用git add将解决后的文件标记为已解决,然后完成合并提交(git commit)或变基操作(git rebase --continue)。
避坑技巧:遇到冲突别慌。首先,使用
git status查看哪些文件有冲突。其次,善用图形化工具(如VSCode、IntelliJ IDEA内置的合并工具)或命令行工具git mergetool,它们可以并排显示两个版本的差异,让你更直观地解决。解决后,务必进行编译和测试,确保合并后的代码能正常工作。
4. 远程协作:推送与拉取背后的数据同步
本地玩得再转,最终代码也要和团队共享。这就涉及到与远程仓库的交互:push和pull(以及fetch)。它们的底层同样是对象和引用的传输与更新。
4.1 远程引用:远程仓库的镜像指针
当你克隆一个仓库或添加远程仓库(git remote add origin <url>)后,Git会在本地创建远程分支引用。它们位于.git/refs/remotes/<remote-name>/下,例如origin/master。
这个origin/master指针,是你本地仓库对远程仓库master分支最后一次已知状态的记录。它在你执行git fetch或git pull时被更新。关键点:origin/master是一个本地引用,但它被Git特殊对待,你不能直接在它上面提交。它只是一个“书签”,标记着远程分支的位置。
4.2 推送的本质:上传对象,更新远程引用
git push origin feature这个命令做了两件核心事情:
- 传输对象:Git会找出你本地
feature分支上有,而远程origin仓库的feature分支上没有的所有Git对象(Commit, Tree, Blob, Tag)。将这些对象打包并上传到远程服务器。 - 更新远程引用:请求远程服务器,将其
feature分支的指针更新为和你本地feature分支指向相同的提交。
快进与非快进推送:这里就引出了常见的错误! [rejected]。远程仓库会检查你的推送请求是否是一个“快进”操作。即,你要求远程feature分支从旧的提交A移动到新的提交B,那么A必须是B的直接祖先。如果是,则允许快进推送。如果不是(即远程分支已经有了你本地没有的新提交),则意味着历史分叉,直接推送会覆盖别人的工作,因此默认被拒绝。
如何处理非快进推送被拒绝:
- 先拉取再合并:这是标准流程。执行
git pull origin feature(这相当于git fetch+git merge origin/feature),将远程的新变更拉取到本地并与你的修改合并,解决可能的冲突,产生一个新的合并提交。此时你的本地历史包含了远程的新内容,再推送就满足快进条件了。 - 变基后推送:如果你追求整洁历史,可以在拉取时使用
git pull --rebase origin feature,这会将你的本地提交变基到更新后的远程分支之上,然后再推送。同样,要确保你的本地提交还没有被推送到远程,否则变基会重写历史,给协作者带来麻烦。 - 强制推送:使用
git push --force或更安全的git push --force-with-lease。这会强制用你的本地分支覆盖远程分支,无视非快进限制。这是危险操作,会覆盖远程历史,只应在你完全确定可以这样做的情况下使用(例如,在个人特性分支上整理尚未合并的提交)。
4.3 拉取与抓取:获取远程更新的两种方式
很多人混淆git pull和git fetch,其实它们区别很大。
git fetch <remote>:这是一个“只读”操作。它联系远程仓库,下载所有你本地还没有的所有新对象(提交、文件等),并更新你本地的远程引用(如origin/master)。它不会动你本地的任何工作区文件,也不会动你本地的分支指针(如master)。执行完git fetch后,你可以通过git log origin/master查看远程仓库的最新进展,然后决定如何整合(merge或rebase)。git pull <remote> <branch>:这是一个“复合”操作。它等价于git fetch加上git merge(默认)或git rebase(如果配置了pull.rebase或使用--rebase选项)。即,它先执行git fetch更新远程引用,然后立即尝试将远程分支的更新合并或变基到你当前检出的本地分支上。
最佳实践建议:我个人的习惯是,几乎总是使用
git fetch而不是git pull。因为fetch更安全,它让你先看到远程发生了什么变化(git log --oneline origin/master..master可以查看本地领先了哪些提交,git log --oneline master..origin/master可以查看远程领先了哪些提交),然后再从容地决定是合并 (git merge origin/master) 还是变基 (git rebase origin/master)。直接使用git pull有时会意外地创建一个你不想要的合并提交,让历史图变得混乱。
4.4 图解一次完整的协作流程
假设你和同事都在develop分支上工作。
- 你本地
develop指向提交A,远程origin/develop也指向A。 - 你完成了工作,提交了B和C。此时你本地
develop指向C,origin/develop仍指向A。 - 你执行
git push origin develop。Git检查:A是C的祖先吗?是的(A — B — C)。所以是快进推送,允许。于是远程develop更新为C,你的本地origin/develop引用也被更新为C(在推送反馈中)。 - 与此同时,你的同事也提交了D并成功推送。现在远程
develop指向D。 - 你再次开发,提交了E。现在你想推送。执行
git push origin develop。Git检查:C(远程已知状态)是E的祖先吗?不是,因为远程已经是C — D,而你是C — E。历史分叉了,非快进,推送被拒绝。 - 你执行
git fetch origin。这会更新你本地的origin/develop指针,让它指向D。 - 现在你需要整合。你可以:
git merge origin/develop:将D合并到你的E上,可能会产生合并提交M。然后推送M。git rebase origin/develop:将你的提交E“重新播放”在D之后,产生新的提交E'。然后推送E'(此时是快进)。
- 选择一种方式整合后,再次推送,成功。
理解了这个数据流动和引用更新的过程,远程协作中的各种问题就都有了清晰的排查思路。
5. 高级操作解析:重置、检出与贮藏的底层逻辑
掌握了核心原理,我们再来看几个日常中强大但容易误用的命令,你会对它们有全新的认识。
5.1 重置:移动分支指针与操作暂存区/工作区
git reset是一个多功能命令,它的行为主要由一个参数决定:--soft、--mixed(默认)、--hard。理解它的关键在于明白它操作的是三个区域:当前分支指针(HEAD所指向的分支)、暂存区(索引)、工作目录。
假设当前分支master指向提交C,历史是 A — B — C。
git reset --soft B:这是最“温柔”的模式。它只做一件事:将当前分支指针(master)从C移动到B。暂存区和工作目录保持不变。这意味着,C引入的所有变更,现在都显示为“已暂存,待提交”的状态。这常用于将多个提交“压缩”成一个——先reset到某个早期提交,然后一次性提交所有变更。git reset --mixed B:这是默认模式。它做两件事:- 移动分支指针到B(同
--soft)。 - 重置暂存区,使其状态与B一致。工作目录保持不变。 这意味着,C引入的所有变更,现在都显示为“已修改,未暂存”的状态。这常用于撤销一次提交,但保留修改内容以便重新编辑和提交。这也是网络热词中提到的,在回退版本后,再次执行
reset --mixed到之前版本号的操作意图——它让代码回到工作区,从而可以重新提交或处理。
- 移动分支指针到B(同
git reset --hard B:这是最“强硬”的模式。它做三件事:- 移动分支指针到B。
- 重置暂存区到B的状态。
- 重置工作目录到B的状态。这意味着,C引入的所有变更,以及任何未提交的暂存区和工作区修改,都将被丢弃,且难以恢复。此操作非常危险,务必在确认不需要这些修改时使用。网络热词中提到的回退操作,第一步
reset --hard就是为了彻底回到过去的某个版本状态。
重要警告:
git reset --hard会覆盖工作目录。如果你有未提交的修改(即使是已暂存的),它们会被永久丢弃。在执行前,请务必用git status确认,或者先将工作区贮藏(git stash)备份。
5.2 检出:切换分支与还原文件
git checkout也有两个主要用途,底层逻辑不同。
切换分支:
git checkout feature。这本质上是:- 更新HEAD文件,使其指向
feature分支(符号引用)。 - 用
feature分支指向的提交对应的Tree,来更新暂存区和工作目录。因此,你的工作区文件会变成feature分支的最新样子。 - 如果当前工作区或暂存区有未提交的修改,且这些修改与要切换到的分支的内容冲突,Git会阻止你切换,除非你贮藏或提交这些修改。
- 更新HEAD文件,使其指向
检出文件/提交:
git checkout commit-hash -- file.txt。这个命令非常有用,它从指定的提交(或暂存区,如果省略提交哈希)中,取出某个文件的版本,同时更新暂存区和工作目录中的该文件。这常用于丢弃对某个文件的本地修改,将其恢复到上次提交或暂存时的状态。注意,它不移动任何分支指针。
在较新版本的Git中,推荐使用git switch来切换分支,使用git restore来恢复文件,以使命令的意图更清晰。
5.3 贮藏:临时保存工作现场
git stash是一个“救火队长”。当你在一个分支上工作到一半,需要紧急切换到另一个分支处理问题时,但当前修改又没完成、不想提交,就可以使用贮藏。
底层操作:
git stash或git stash push:Git会为你的工作目录和暂存区的修改创建几个(通常是两个)新的提交。这些提交不在任何分支上,但可以通过一个特殊的引用refs/stash来找到。然后,它会执行一个git reset --hard HEAD,让你的工作区变干净。git stash list:查看贮藏栈。git stash pop:应用最近一次贮藏的修改到当前工作区(并尝试恢复暂存状态),然后从贮藏栈中删除该记录。git stash apply:应用贮藏,但不从栈中删除。
贮藏的本质是创建游离的提交来保存你的工作状态。它是一个非常实用的功能,但不宜长期使用,因为贮藏的记录容易遗忘。处理完紧急事务后,应及时回到原分支pop出贮藏内容。
6. 实战:从原理出发,解决高频疑难杂症
理解了原理,很多网上搜到的“玄学”问题就能迎刃而解。我们分析几个网络热词中的典型问题。
问题一:cannot retrieve latest commit at this time.
这个错误常见于GitHub、GitLab等平台,或者某些IDE(如Android Studio)的Git插件中。它通常意味着你的本地Git客户端无法从远程仓库获取到最新的提交信息。
排查思路:
- 网络问题:首先检查网络连接。尝试
git fetch origin看命令行是否报更详细的错误。 - 认证问题:如果你使用SSH,检查密钥是否已添加且远程仓库有公钥。如果是HTTPS,检查密码或令牌是否过期。可以尝试
git remote -v查看远程地址,并用git fetch <remote-url>测试。 - 远程引用不一致:有时本地缓存的远程分支引用 (
origin/xxx) 可能过时或混乱。可以尝试git remote update origin --prune来更新并清理远程跟踪分支。 - 仓库权限:确认你有该仓库的读取权限。
- IDE/工具缓存:如果是IDE报错,尝试重启IDE,或使用命令行执行Git操作来绕过IDE的Git集成可能存在的缓存或bug。
问题二:idea 中将dev分支的代码合并到自己的分支
这本质上就是一个合并操作。假设你当前在自己的feature/xxx分支上。
- 标准流程:
- 确保你的分支工作区是干净的(没有未提交的修改)。可以用
git status查看。 - 首先,获取远程最新代码:
git fetch origin。 - 然后,将
origin/dev(远程dev分支)合并到你的当前分支:git merge origin/dev。 - 解决可能出现的冲突,完成合并提交。
- 确保你的分支工作区是干净的(没有未提交的修改)。可以用
- 使用变基保持整洁:如果你想让你分支的历史看起来像是基于最新的
dev开发的,可以在第3步使用git rebase origin/dev。但同样,确保你的feature/xxx分支上的提交还没有推送到远程,或者你确定协作伙伴能接受历史重写。
问题三:drop commit 怎么找回
“drop commit”通常发生在交互式变基 (git rebase -i) 中,你选择丢弃(drop)了某个提交。或者,你不小心用git reset --hard回退并丢失了提交。
找回的核心:只要这个提交曾经存在于你的仓库中(比如你提交过,但还没被垃圾回收),它就可以被找到。Git不会立即删除对象,它有“回收站”机制。
找回方法:
- 使用
git reflog:这是你的救命稻草。reflog记录了HEAD和分支指针在过去一段时间内的所有移动记录。执行git reflog,找到对应操作(如“rebase”或“reset”)之前的那个提交哈希。
上例中,a1b2c3d HEAD@{0}: reset: moving to HEAD~2 e4f5g6h HEAD@{1}: commit: Your lost commit message ...e4f5g6h就是你丢失的提交。使用git checkout e4f5g6h可以检出一个临时的游离状态来查看它,或者git branch recovery-branch e4f5g6h基于这个提交创建一个新分支来恢复它。 - 使用
git fsck --lost-found:这个命令会列出所有“悬空”的对象(即不被任何分支或标签引用的对象)。你可以在.git/lost-found/目录下找到它们,但操作相对复杂。reflog通常是更简单直接的选择。
问题四:cannot commit changes due to unresolved conflicts.
这个错误直白地告诉你:存在未解决的冲突,所以无法创建提交。
解决步骤:
- 使用
git status查看哪些文件处于“Unmerged paths”状态。 - 打开这些文件,解决其中的冲突标记(
<<<<<<<,=======,>>>>>>>)。决定保留哪部分代码,或进行融合。 - 对每个解决完冲突的文件,执行
git add <file>。这告诉Git该文件的冲突已解决。 - 当所有冲突文件都已被
add后,就可以正常执行git commit来完成合并提交了。如果你是在变基过程中,则使用git rebase --continue。
整个过程的核心,就是手动完成Git无法自动完成的三方合并决策,并通过git add来确认你的决策。