这几年我见过太多人在版本控制这件事上栽跟头:代码写了一半不敢删、重要的修改被别人覆盖、两个人协作结果互相把对方的成果顶掉。到头来发现,大部分人缺的不是写代码的能力,而是一套"有后悔药、可追溯、可并行"的工作方式。Git就是这套方式里被验证最多的一个方案。这篇文章不打算只罗列命令,而是从Git的底层模型讲起,把"为什么这么设计"讲透,然后带你完整走一遍从初始化、提交、分支合并,到远程协作、冲突解决、事故恢复的链路。内容覆盖新手入门到熟练工进阶,哪怕你只是听说过Git,顺着读也能建立起完整的心智模型。
1. 版本控制到底在"控"什么:Git的设计源头与底层模型
1.1 先忘掉命令,回到问题本身
在没有版本控制的年代,一个项目走到后期通常是这样的:桌面上堆着"论文终版.doc""论文终版2.doc""论文真终版.doc""最终版打死也不改.doc"。代码比文档更惨,因为代码要按照目录结构来组织,你没法把整个项目复制成"jquery最终版全套2"。于是有人选择把改动过的文件手动复制到备份目录,有人用网盘的同步功能"碰运气",还有人靠着记忆硬扛。
这些做法的本质问题是一致的:你无法回到任意历史时刻,也无法安全地让多条修改线并行前进。Git要解决的就是这两件事——历史的可追溯性,以及并行工作的安全性。理解了这一点,后面所有命令都有了归属感,你不会再问"Git为什么要分工作区、暂存区、版本库",你会反过来想:如果没有这三个区域,撤销和并行就无从谈起。
1.2 快照思维:Git和传统版本控制系统拉开的差距
早期版本控制系统多数采用"差异文件"思路,也就是只保存每次修改的增量,比如第一版是完整文件,第二版只记录"第100行删了5个字"。好处是省空间,坏处是你要想还原某个历史版本,必须从第一版开始逐条叠加差异,一旦中间某个差异损坏,后面全完蛋。
Git换了一种方式:快照。每次提交时,Git把当前所有文件的状态打包成一个快照保存下来,同时为了省空间,如果某个文件没有变化,新的快照直接引用之前的文件,不重复复制。这种设计让"切换历史版本"变成了一次简单的指针跳转,速度飞快,而且任何一个时间点的状态都是完整、可独立检出的。
这张对比表可以帮你快速建立判断:
| 维度 | 差异存储 | Git快照式存储 |
|---|---|---|
| 还原历史速度 | 慢,需要叠加多次差异 | 快,直接取快照 |
| 数据完整性 | 中间差异损坏会导致全部失效 | 每个快照独立完整性校验 |
| 空间占用 | 理论最优 | 无变化文件共享引用,实际也很省 |
| 分支成本 | 分支常需要复制整份文件 | 分支只需新建一个指针 |
实际工作中你会发现,Git无论在大型仓库还是小型项目上,切分支都像呼吸一样自然,靠的就是这套快照模型。你不需要关心底层存储细节,但知道这个原理之后,就能解释为什么Git仓库会有一个笨重的.git目录,为什么删除.git你得到的是一个普通的文件夹。
1.3 对象模型:blob、tree、commit的一次说清
Git内部的核心对象只有四种:blob、tree、commit、tag。用珠宝店来打比方会很直观。
假设你有一块玉石要放进保险柜。blob就是"玉石本身",它只关心内容,不关心这块玉叫什么名字。tree就像"保险柜里的抽屉标签",记录着抽屉里每件东西的名字、编号和位置。commit则是一张"封条",它记录了三件事:这一次封存是什么时候、是谁封存的、它的上一张封条编号是多少。tag比较特殊,相当于你在某些重要封条上贴的"纪念标签",比如"拍卖会专用"。
当你执行git commit,Git做的就是这三件事的连续动作:把当前文件内容打包成blob,把目录结构和文件对应关系整理成tree,再生成一张commit封条把整棵tree永远冻结起来。因为每个commit都记录了parent提交的编号,所以所有提交串成了一条条可回溯的时间线。这就是"链式提交"的含义。
每个对象都有一个40位的十六进制哈希值,这个值由文件内容计算得出。这不是为了炫技,它同时承担了寻址和完整性校验两个功能:内容变了哈希就变,哈希对不上说明仓库被篡改过。你以前可能听过"Git不可能被篡改历史",它依赖的正是这套哈希机制。
2. 从初始化到第一次提交:把"提交"这件事做到位
2.1 初始化之前,先把"你是谁"配好
很多新手第一次装完Git就开始初始化仓库,结果提交时报错或者提交人信息是空的。原因很简单:每一次提交都要把身份信息写入commit对象,而Git不负责帮你猜身份。
打开终端,先做全局配置:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"这两行命令是几乎所有Git操作的起点。user.name和user.email最终会被写进每一个提交对象,团队协作时,其他人就是靠这个信息知道某行代码是谁改的。如果不设置,Git会尝试从系统账户里猜一个,猜不到就直接拒绝提交。
还有几个配置建议从一开始就养成习惯:
# 让Git在push前自动检查是否有未push的提交 git config --global push.default simple # 让Git命令输出带颜色,区分不同状态 git config --global color.ui auto # 配置常用编辑器,比如VS Code里提交信息时会自动弹出编辑器 git config --global core.editor "code --wait"我在实际项目里见过太多因为身份信息混乱导致的问题:有人用笔记本提交了一次,身份显示"root",追查改动的对应需求都得靠猜。身份信息这件事越早配好越省心。
2.2 暂存区不是多余的一步,而是一道"精装修"工序
初始化仓库后,你有了.git目录,但还没有任何历史。接着做第一次提交,命令链看起来很简单:
git init git add . git commit -m "init commit"但这里最值得理解的是git add这一步,为什么Git要把"提交"拆成"添加到暂存区"和"正式提交"两步?
用一个生活的类比:整理房间。git add相当于把所有杂物先放到"待收纳区",你可以在放进收纳盒之前检查一遍哪些要留、哪些不要;git commit才相当于亲手封箱。假如只有"封箱"没有"待收纳区",你就无法做到只封一部分东西,或者封箱前检查自己要封的到底是什么。
实际使用时,这个设计的作用非常巨大。比如你改了三个文件,其中两个是功能改动,一个是调试用的临时改动,你可以只git add前两个文件,提交后临时改动还留在工作区,完全不影响提交的整洁性。又比如git diff --cached可以专门查看暂存区里将要被提交的内容,这在提交前自查时是必经步骤。
一句话记住:暂存区是工作区和版本库之间的安检口。善于使用它的项目,提交历史往往干净利落;跳过它的人,提交里经常混进调试代码、临时配置和一堆无关紧要的空白符改动。
2.3 提交信息怎么写,决定了你三个月后骂不骂自己
提交信息是Git里面最容易"无所谓"也最容易"要命"的细节。我见过很多仓库,提交信息写着"111""aaa""修改""修复",三个月后回来看历史时完全不知所云。
好的提交信息至少要做到两点:一是让人不打开diff也能看懂这次改动做了什么;二是让人知道"为什么要这么改",而不只是"改了什么"。
我常用的格式是:
<类型>(<影响范围>): <简述> <详细说明> <关联需求或单号>类型可以约定为feat(新功能)、fix(修复)、docs(文档)、refactor(重构)、test(测试)、chore(杂务)。举两个例子:
git commit -m "fix(login): 修复重置密码链接过期时间过短的问题 - 将令牌有效期从30分钟延长到2小时 - 在过期前5分钟向用户发送续期提示 - 修复了手机端样式覆盖导致的按钮不可见问题 - 关联需求单 #88421"第一行的fix(login)让浏览历史的人一眼锁定提交的类型和范围;下面每一条都说明一个具体变化;最后关联单号把代码和需求管理串起来。这个习惯在团队协作里几乎能拯救所有人的时间,自己以后回来排查问题也会轻松很多。
2.4 .gitignore:提前挡住不该进仓库的文件
做过几个项目后你会意识到,Git仓库里最没价值的提交往往来自那类文件:node_modules、编译生成的dist目录、IDE的配置文件夹、本地的环境变量文件。这些文件庞大、易变、且只对你本机有意义,放进仓库只会让你的提交历史充斥噪音,还可能把敏感信息带进共享仓库。
解决办法是在仓库根目录创建.gitignore文件,声明哪些路径要被Git忽略:
# 依赖目录 node_modules/ vendor/ # 构建产物 dist/ build/ *.log # 编辑器与系统文件 .idea/ .vscode/ .DS_Store # 本地环境配置 .env.local注意.gitignore匹配的规则其实不复杂:*匹配任意字符,/放在开头表示相对于仓库根目录,目录后面加/表示忽略整个目录。你可以用git check-ignore node_modules/来验证某个路径是否被成功忽略,这个命令很实用。
还有两个容易踩的坑。第一,如果你在加入.gitignore之前已经把node_modules提交进了仓库,那么直接写进.gitignore是没用的,因为Git只忽略未跟踪文件,已跟踪文件继续被Git管理。你需要先git rm -r --cached node_modules把它从索引里移除,再提交一次。--cached参数保证了只移出索引而不删除工作区的文件,这套操作值得记下来。第二,某些敏感文件被误提交后,只从仓库删除并写入.gitignore还不够,因为历史记录里还残留着,需要做历史改写,这个我在踩坑部分会细讲。
3. 分支的真相与合并机制:指针、快进与被误解的冲突
3.1 分支不是一个目录副本,而是一个移动的指针
新手对分支最常见的误解是:分支是不是把代码复制一份出来,然后各改各的?如果你这么理解,会发现Git的分支操作快得不可思议——创建一个分支只需要一瞬间,哪怕仓库里有几万个文件。
真相是:分支不过是一个指向commit对象的指针。git branch feature这条命令做的事,就是在当前commit上贴一个新的标签。因为快照机制的存在,"基于当前状态开一个新分支"天然就是O(1)的,不需要复制任何文件。
HEAD是一个特殊的指针,它指向你当前所在的分支,而所在分支又指向具体的commit。每提交一次,当前分支指针就自动向前移动到新commit。当你git checkout另一个分支时,Git做的事是:把HEAD指针移过去,然后让工作区里的文件内容跟着改变。这听起来很玄幻,但验证起来很简单:
git switch feature ls你会立刻看到工作区变成了feature分支的内容。现在再切换回main,文件又变回main的样子。这个机制是所有分支操作的核心,理解它之后,你对"切分支要不要备份""切分支会不会丢失改动"这类问题的判断就会清晰很多。
3.2 三种合并旅途:快进、递归与冲突
分支存在的意义就是让工作并行,但它终归要"合流"。git merge feature的含义是:把feature分支上的改动合并到当前分支上。合并的具体结果会根据提交历史的不同走向出现三种情况。
第一种是快进合并。假设你在main的提交A上创建了feature分支,main一直没动,feature一路提交到了B、C,此时在main上执行git merge feature,Git发现main的历史完全包含在feature的历史里,没必要产生一个专门的合并提交,直接把main指针向前滑动到C就行,这就是fast-forward合并。干净、线性、没有多余的合并节点。
第二种是递归合并。如果main分支在A之后又产生了一个D提交,而feature也从A走到了C,两条历史在A处分岔了。Git需要把D和C的改动同时整合进一个"合并提交M"里,它会对比三处内容:共同祖先A、当前分支的D、目标分支的C。Git能自动处理那些只在一侧修改过的文件,并生成一个merge commit把两条线连接起来。你会在历史里看到一个有两个parent的提交,这是合并的标志。
第三种是冲突合并。如果文件同一处位置在D和C里都被修改了,Git无法判断你真正想要哪份内容,只能停下来把问题交给你。此时仓库进入冲突状态,你在git status里能看到"both modified"字样,需要手动介入。很多人把冲突当作Git不够聪明的表现,其实这正是它谨慎的体现——与其猜错,不如让你决策。
3.3 学会读冲突标记,比背一百条命令更有用
冲突发生后,打开冲突文件,你会看到类似这样的标记:
<<<<<<< HEAD 这是当前分支的版本 ======= 这是被合并分支的版本 >>>>>>> feature中间的=======把两边的改动分隔开来。你要做的不是选一边,而是综合两边的意图,形成最终合理的代码,然后把所有标记行删除。通常我的做法是:先读两遍两边的改动,理解各自的意图,再亲手整合出一版,然后执行git add把文件标记为已解决,全部处理完后再git commit完成这次合并提交。
注意:解决冲突后不要立刻急着删除另一方的逻辑。很多冲突之所以难解,是因为双方各自依赖了对方不知道的约定。如果你对冲突区域没有十足把握,宁可和改动相关的人对齐一次,也不要闷头选择一边。
4. 撤销与回滚:reset、revert、checkout的边界与选择
4.1 三个区域决定撤销的层级
Git的日常操作始终在三个区域之间流转:工作区(文件系统的可见状态)、暂存区(下一次提交的候选集)、版本库(已提交的历史)。撤销的本质,就是决定在哪个层级上"回退"。
有一个很形象的扒皮比喻。工作区相当于你摊在桌面上没归档的文件;暂存区是已经装进信封准备寄出的文件;版本库是已经密封入库的档案。撤销可以是:把桌面上的文件撕了重写(影响工作区),把已经装进信封的抽出来改(影响暂存区),把已经入库的档案作废(影响版本库)。命令的选择就看你想扒到哪一层。
4.2 一句话区分checkout、restore、reset
Git多年来命令不少,新手很容易在撤销场景里选错。我用最直白的语言做一个对应:
- 只想丢弃工作区的改动,让文件回到暂存区或HEAD的状态:使用git restore(或老一点的git checkout -- )。这不会改动任何提交历史,也不影响暂存区。
- 已经git add了,想撤出暂存区:使用git restore --staged ,让文件恢复为未暂存状态,但工作区的内容还在,改动的内容不会丢。
- 要回退提交历史本身:使用git reset或git revert,这是更高层级的操作,需要谨慎区分场景。
这组操作里最容易出事故的是git reset,它有三种模式:
git reset --soft HEAD~1 # 只移动分支指针,暂存区和工作区都保留 git reset --mixed HEAD~1 # 移动指针,同时把暂存区清理掉,工作区内容保留 git reset --hard HEAD~1 # 移动指针,暂存区和工作区全部回退,改动直接消失用生活化语言解释:--soft是"反悔提交但东西全部留在桌上",--mixed是"反悔提交并且把装好的信封拆开",--hard是"反悔提交且把桌面也清理干净"。--hard最危险,因为它会直接丢弃工作区里的未提交修改,一旦操作前没备份,改动可能永远找不回来。我见过不止一个人把git reset --hard当成"刷新页面"随手按,结果辛苦写了两天的代码直接蒸发。
4.3 已经push到远端的提交,请用revert而不是reset
区分reset和revert有一条铁律:如果提交已经push到了远端而且不只是你一个人在用,禁止用reset改写历史,改用revert产生反向提交。
原因是reset会移动分支指针,相当于"把这段历史从时间线上抹去"。如果远端已经有人基于它拉过分支、改过代码,你一旦reset再force push,所有人的本地历史都会错乱,协作现场会迅速变成事故现场。
而git revert的特点是:它不动历史,只是在历史末尾追加一个"撤销某个提交"的新提交。比如你要撤销最近的两次提交:
git revert HEAD~2..HEAD执行完后,历史记录里会多出两个revert提交,它们清晰记录着"某年某月某日,我们撤销了某次改动"。这种可追溯性在正式项目里非常宝贵,因为"为什么撤销"本身也是项目历史的一部分。
选择逻辑其实很清晰——未push的提交,大胆用reset;已push且多人共享的分支,老老实实用revert。这个习惯能让你少经历至少三次午夜救火。
4.4 reflog:被错误重置后,Git给你留的后门
前面说--hard很危险,但Git其实给自己留了后门:reflog会记录所有指针的移动历史,包括分支头部的每一次变化。也就是说,即便你执行了git reset --hard,之前的commit依然存在于对象库里,只是没有任何分支指向它,成了"悬空提交"。
恢复方式极其简单:
git reflog # 输出示例 # abc1234 HEAD@{2}: checkout: moving from feature to main # def5678 HEAD@{1}: commit: 修复登录状态异常 # 1234abc HEAD@{2}: reset: moving to HEAD~1 git reset --hard def5678git reflog会列出每次HEAD移动的摘要和对应的commit哈希。你找到那次误操作之前的哈希,然后reset回去,一切恢复如初。平时看似只有十几行历史的reflog,实际可能保存了几十天甚至几个月的引用记录,只要你不主动清理,它基本就是这个项目的"后悔药房"。在误操作场景里,别慌,先看reflog。
5. 远程协作的完整工作流:从clone到冲突解决
5.1 本地分支与远程分支的数据关系
单人开发只需要本地仓库就够,但协作必须有远程仓库作为"公共枢纽"。远程仓库通常是部署在服务器上的裸仓库,它不含工作区,只保存Git对象和分支指针。你本地的分支称为local branch,对应远程的跟踪分支通常命名为origin/feature。
每次git fetch,是把远端的分支状态拉取到本地的跟踪分支,但不改动你的工作区。git pull则是fetch加merge的组合——它先拉取远端状态,再把远端的改动合并到当前分支。我见过很多团队连pull的概念都没有统一,有些人以为pull自动解决冲突,发现冲突后不知所措,其实pull的底层仍然是fetch+merge。
以下是远端操作里最常用的几个命令及其职责:
git clone <仓库地址> # 从远端拉取整个仓库并建立工作区 git remote -v # 查看已配置的远端仓库地址 git remote add origin <地址> # 给当前仓库添加一个远端,习惯命名为origin git push origin main # 把本地main分支推送到远端main分支 git fetch origin # 同步远端最新状态到本地的origin/*跟踪分支 git pull origin main # fetch + merge 的合并操作5.2 两种主流协作模型,选错会很难受
团队协作有两大流派。一种叫"共享仓库"模型,成员都直接往同一个远端分支推送,适合小规模团队或老项目,特点是流程简单、冲突常见;另一种叫"分叉协作"模型,每个人先复制一份远端仓库到自己的账号下(fork),在复制出来的仓库里开分支开发,完成后再向原仓库发起合并请求(merge request或pull request),由负责人审阅后合入,适合中大型团队、开源项目,特点是流程可靠、有审查关卡。
我个人的经验是:哪怕团队只有三个人,也建议至少保留合并请求机制。它不是为了走形式,而是给每个改动提供一个"停下来思考"的节点——提交说明是否清楚、测试是否通过、有没有误入临时文件。多一道审查,仓库质量就会往上涨一截。
5.3 冲突解决的完整实操流程
冲突避不开,但可以靠好习惯减少,靠清晰的流程解决。完整流程我整理如下:
- 先处理自己这一侧的改动,保证本地的未提交修改不会干扰冲突处理。
- git pull(或git fetch + merge)拉取远端最新代码,让冲突暴露出来。
- git status查看哪些文件出现冲突,git diff可以查看冲突详情。
- 逐一打开冲突文件,阅读冲突标记两边的代码,理解双方的意图。
- 整合两边的合理内容,删除冲突标记,完成手工合并。
- 对每个处理过的文件执行git add,标记冲突已解决。
- 最后执行git commit完成这次合并提交,提交信息可以直接用默认的Merge相关说明,说明冲突处理的大致思路更好。
冲突处理的高频错误是:看到冲突标记就崩溃,然后强行选择某一侧内容覆盖,结果另一侧的功能被静默丢失。建议遇到拿不准的区域,就把两边的改动都读一遍,找到改动涉及的功能边界。如果冲突区域涉及好几个文件并且互有关联,找个工作的人一起过一遍比独自硬扛更稳妥。
5.4 保持历史整洁:merge与rebase的取舍
同样是把两条分支合流,merge会产生合并提交,rebase会把当前分支的提交"移植"到另一个分支的最新提交之后,从而拼出一条线性历史。对比一下:
| 维度 | git merge | git rebase |
|---|---|---|
| 历史形态 | 保留分岔,增加合并提交 | 线性历史,看起来更干净 |
| 提交顺序 | 按实际时间混排 | 按移植后顺序重排 |
| 安全性 | 不改写已有提交,安全 | 改写提交,严禁用于已push的共享分支 |
| 冲突处理 | 一次合并处理一次冲突 | 每个提交移植时可能连续处理多次冲突 |
日常开发里,从主分支同步更新功能分支时,我倾向于用rebase,因为它让我在真正合并前把本地提交"垫"到最新代码之上,后续的合并就会干净很多。但一旦我的功能分支已经push到远端并且有其他人在上面工作,就绝不能强制rebase改写它。这条线划清楚,团队协作才会稳定。
6. 高手武器库与踩坑恢复:进阶技巧和现场救火
6.1 交互式rebase:把杂乱提交理成一条清晰线索
开发一个功能时,你往往会产生一连串质量参差的提交,比如"wip""修复笔误""改一下样式""再试一次"。这些提交如果原样进入主分支,历史会变得非常难读。用交互式rebase可以把它们组织成形:
git rebase -i HEAD~10执行后Git会打开编辑器,列出最近10个提交,你可以用几个简写指令来整理它们:
pick 1abc111 第一版实现 squash 2def222 修点小问题 fixup 3ghi333 改测试 reword 4jkl444 调整文档 drop 5mno555 临时提交不要了pick表示保留,squash表示把这个提交合并到上一个提交里并重新写提交信息,fixup和squash类似但丢弃当前提交信息,reword用来改提交信息,drop直接删除。这套操作对于保持主干整洁极有价值,我通常会在功能合入主分支前做一次完整整理。再次强调:这条命令只用于尚未push到范围共享的提交,否则等于重写历史,会伤害所有依赖这段历史的人。
6.2 cherry-pick:精准移植单次提交
有时你不需要合并整条分支,只想把某个提交从别的分支拿过来,比如"修复上线用的紧急bug"和"开发中的功能分支"之间,你只想移植修复提交而不把整个功能带过去。
git cherry-pick <commit哈希>这个命令会把指定提交的改动应用到当前分支上,并生成一个新提交。实际上它相当于一次"手工补丁"操作,只是不需要手动复制粘贴内容。使用频率虽然没有merge和rebase高,但它确实是处理"一个萝卜一个坑"式需求时的利器。需要注意的是,cherry-pick后如果原提交后面又有修复,得记得同步把修复也移植过来,不然同一个bug会在两个地方表现出不一致。
6.3 stash:临时保存现场,比乱commit体面得多
切分支时如果工作区还没有提交完,Git通常会拒绝切换,或者把未提交的改动一起带过去,这可能造成混乱。更体面的做法是用stash临时"冻住"当前现场:
git stash push -m "登录页样式未完成" git stash list git stash pop # 恢复到工作区并删除stash记录 git stash apply # 恢复到工作区但保留stash记录stash适合的场景包括:突然需要修一个线上紧急bug,手上的半成品代码不想提交;或者要在一个干净的工作区上进行实验性操作。stash pop和apply的区别在于是否保留缓存记录,如果你可能还需要对比多个stash,或者怕切来切去时弄丢代码,就选apply,确认无误后再手动清理。
6.4 bisect:用二分法在几十个提交里定位BUG来源
有一个场景非常虐心:功能上线后突然某天崩了,你只知道"上周还好好的",但改了几十个提交。逐个看diff显然不现实,git bisect可以帮你自动缩小范围。
git bisect start git bisect bad # 当前这个提交是坏的 git bisect good <某个已知正常的commit哈希>Git会像一个"二分查找算法"一样自动帮你切换到一个中间提交,然后你运行测试或手工验证,告诉它git bisect good还是git bisect bad。每次判定都会砍掉一半范围,通常在十几次判定内就能锁定引入bug的那个提交。我实际用下来,这个命令的精准程度往往让人惊讶——很多看起来无关的提交,恰恰是bug的源头。值得记住一个细节:git bisect提供的每个中间版本都只会切换代码,不会改变你当前的stash,所以操作前最好保证工作区干净或先stash。
6.5 高威救火:误删分支、敏感信息、大文件
最后一个部分,聊聊我踩过的坑以及对应的恢复手段。
误删分支。不小心git branch -D删掉了还有内容的分支,第一反应不要慌,用git reflog找到该分支最后一次指向的commit,然后基于那个commit重建分支即可。reflog恢复的适用范围很广,它几乎就是Git世界里"意外险"的存在。
敏感信息误提交并且已经push。这种情况最麻烦,简单的revert并不会把文件从历史中抹掉,任何人都还能从旧历史里挖出密码。你需要历史改写工具来重写所有涉及的历史提交,同时建议把泄露的密码立即作废换新,而不是只删除文件。写过历史后,还要让所有协作者重新同步,因为历史哈希都变了,常规pull会产生大量冲突,所以这个操作只能由有权限的负责人统一执行。实际上只要经历一次这种事故,你就会养成"敏感文件一律不进仓库"的习惯。
大文件进了仓库。即使从当前版本删除了大文件,它依然存在于历史中,仓库体积会持续膨胀。解决路径是用专门的大文件管理方案,比如Git LFS,它把大文件实际内容放到远端存储,仓库内只保留一个指针文件。换句话说,从一开始就把构建产物、压缩包、音视频资源等大文件排除在常规Git管理之外,才是唯一干净的策略。
最后分享一点个人体会
Git学到最后,真正区分高手的不是会背多少命令,而是遇到"要不要reset""能不能rebase""冲突怎么办"这类问题时,心里有一套清晰的是非判断。这套判断全部可以回溯到这篇文章开头那一问:你的操作是在帮助建立可追溯的历史,还是在给未来的自己挖坑?我在实际项目里看到的绝大多数版本控制事故,都不是因为某条命令太难,而是因为操作者在动手之前没想清楚自己正处于工作区、暂存区、版本库的哪一层,以及这次改动到底要影响哪一层。把这套思维内化之后,你会发现Git不仅不可怕,甚至可以说是所有开发工具里设计最自洽的那一个。