第一次带人看仓库我问了个特别基础的问题你敲下git commit那一瞬间文件到底被存到哪儿去了对方愣了几秒说“存到 Git 里了吧”。这个回答不能算错但基本等于没说。后来我发现绝大多数人用 Git 三五年仍然把 Git 当成一个“能上传代码的网盘”所以他们会在合并冲突面前手足无措会在误删分支后冷汗直流会搞不懂为什么同事的仓库和自己的对不上。这些麻烦的根子几乎都能追回到对工作流、分布式版本控制、版本库这三个基本概念的理解偏差上。这篇是我打算写的 Git 基础系列第一篇只谈概念不堆命令。目标很明确把 Git 的三个区域怎么流转、分布式到底“分布”了什么、版本库里那些目录和文件各管什么事一次讲透。适合刚接触 Git 的新手也适合用了很久但一直靠“记住命令”过日子的老手——后者往往更需要重新校准一遍底层模型。我会尽量用生活里的类比来解释抽象结构同时把每个设计背后的“为什么”交代清楚毕竟 Git 的命令可以查文档但为什么要这么设计文档通常懒得告诉你。1. 三个区域与一条命令链Git 到底把你的代码放在了哪里1.1 工作区、暂存区、版本库三个抽屉的分工我习惯把 Git 的三个区域比作办公桌上的三个抽屉。工作区Working Directory就是你眼前这个能看见、能编辑的文件夹你在这里改代码、删文件、加注释所有动作都是“草稿”状态Git 未必知道也未必在意。暂存区Staging Area也叫索引 index是第二个抽屉你把想提交的改动一件件放进去摆好顺序相当于把要寄出的东西装进快递盒。版本库Repository就是那个.git目录是第三个抽屉东西一旦装进去封好就有了编号、有了时间戳、有了不可篡改的记录。这个比喻里最容易被忽略的一点是三个抽屉之间不会自动同步。你在工作区改了十个文件暂存区里可能一个都没有你往暂存区放了三处改动版本库里可能还是上一次提交的样子。很多人刚上手时的困惑——我明明改了怎么git status显示没变化、我提交了怎么服务器上没有——本质上都是没意识到自己在跟三个独立的区域打交道。还有一层暂存区并不是一个“文件夹备份”它记录的是路径到文件内容的映射关系外加每个文件的元信息大小、修改时间、inode 等。这些元信息的存在是为了性能git status判断文件有没有变先看大小和 mtime一致就直接跳过不一致再算哈希比对内容。所以你偶尔会看到“touch 了一下文件Git 就说它变了”的现象那是元信息变了触发了重新比对但比对完发现内容其实没变最终不会产生差异。1.2 一次 git commit 在内部走过的完整路径把这条链路走一遍你对 Git 的理解会立刻上一个台阶。假设你改了app.js然后执行git add app.js。这一步 Git 做的事情远比“标记一下”复杂它读取文件内容在前面拼上头信息blob 字节数\0对整个字符串算一次 SHA-1新版本仓库也支持 SHA-256得到一个四十位的十六进制字符串。然后它检查.git/objects下有没有这个哈希对应的文件没有就把内容压缩后存进去文件名就是那个哈希。接着它更新.git/index把app.js这个路径指向刚算出来的哈希。再执行git commit -m ...。这一步 Git 先根据 index 里的路径和哈希构造出目录树结构写成一个tree 对象然后构造一个commit 对象里面记着这个 tree 的哈希、父提交的哈希、作者和提交者信息、时间戳、提交说明最后把当前分支的引用比如refs/heads/main指向这个新 commit 的哈希。整个过程中你的原始文件从来没被“移动”过被搬来搬去的只是内容和指针。所以一条重要的认知是commit 不是差异包是快照。每次提交都对应一棵完整的目录树只是 Git 用内容寻址加对象复用让没变的文件在物理上只存一份。你git log -p看到的 diff是 Git 在展示时现算出来的不是存储格式。这个区别直接决定了后面很多行为切分支为什么那么快、为什么历史可以任意跳、为什么仓库体积不会随提交次数线性膨胀。1.3 暂存区为什么不是多余设计新手最常见的抱怨是为什么不能git commit直接提交所有改动非要中间加一道git add这不是历史包袱是刻意设计。暂存区给了你在提交之前重新编排内容的能力。举个真实场景你为了修一个线上 bug顺手把旁边一段丑代码格式化了还删了两行调试日志。这时候如果整体提交code review 的人要在一堆无关改动里找那三行修复内容评审效率直接崩掉。有了暂存区你可以只git add那三行先提一个干净的修复提交剩下的格式化单独提一个style:提交。更进一步git add -p部分暂存能让你在文件内部按“块”选择同一个文件的不同改动可以拆进不同提交。这个功能我第一次用的时候觉得很别扭用了半年之后彻底离不开了——它把“提交”从“保存进度”变成了“讲故事”一个提交讲一件事历史读起来才顺畅。代价是你得多敲几次交互式确认但换来的是半年后你自己回看git log时不需要猜“这次到底改了啥”。还有个小细节值得记住git add之后的文件如果被再次修改暂存区里保留的仍是 add 那一刻的内容。这时候git status会同时显示“已暂存的更改”和“未暂存的更改”git diff看的是工作区与暂存区的差异git diff --cached或--staged看的才是暂存区与最后一次提交的差异。这两个命令搞混是排查“我提交的东西怎么不对”这类问题的第一大原因。2. 分布式版本控制的真实含义本地那份完整历史值多少钱2.1 集中式模型的日常与它的软肋要理解分布式得先想清楚它对比的是什么。集中式版本控制典型代表是早期那批以服务端为唯一权威的工具的模型很简单仓库只有一份放在中央服务器上每个人检出的是一个工作副本包含最新版本的文件但不含历史。你想看三个版本之前的代码长什么样得连服务器你想提交得连服务器服务器维护窗口期所有人都只能干等着。这个模型在带宽紧张、磁盘昂贵的年代非常合理它把存储成本压到了最低。但它的软肋也很明显所有操作都被网络绑架。出差在高铁上想提交一个阶段性成果做不到想比较两个历史版本得先把数据拉下来服务器硬盘坏了又没有可靠备份整个项目历史就没了。更隐蔽的问题是分支成本——在集中式模型里分支通常体现为服务器上的一个目录副本创建一个分支意味着复制整份代码成本高到让人不敢随便开分支于是所有人都挤在主干上改冲突频发。我在一个老项目里见过这种工作方式的后果因为分支太贵团队养成了“改完直接在主干上覆盖”的习惯谁改坏了谁负责回滚回滚靠的是手工备份的压缩包。那套流程下没人敢做超过半天的重构技术债越滚越大。后来迁到 Git第一件事就是把“开分支”的成本降到零团队的重构意愿肉眼可见地变强了。2.2 克隆之后你手里到底有什么git clone和“下载代码”是两件完全不同的事。克隆完成后你的本地磁盘上有一份完整的版本库全部历史提交、全部分支引用、全部标签、全部对象数据。这意味着断网状态下你能做几乎所有事情——查看任意历史版本、对比任意两个提交、创建分支、提交改动、合并分支、查看谁在什么时候改了哪一行。网络只在需要与他人交换数据时才登场。这个特性的价值在几个具体场景里体现得特别明显。代码审查时你可以把同事的分支拉下来在本地慢慢看、慢慢跑测试、慢慢改不需要占用任何服务器资源。排查线上问题时你可以 checkout 到任意一个历史版本本地复现二分定位git bisect到底是哪次提交引入的问题整个过程几十次 checkout 全部在本地完成速度取决于磁盘而不是网速。做实验性重构时你可以在本地开十个分支试十种方案失败了直接删掉服务器上什么痕迹都不留。需要澄清一个常见误解分布式不等于没有中央仓库。实际团队协作里几乎都会指定一个“origin”作为共享的权威仓库大家往上推、从上面拉。分布式的真正含义是“每个节点都持有完整数据”而不是“不需要中心”。中心依然存在但它从“唯一的数据源”降级成了“一个大家约定好的交换点”。这个身份变化很关键——中央仓库挂了任何一个人的本地仓库都能立刻顶上去重建数据不会丢。2.3 离线提交与本地分支带来的工作方式变化分布式对工作习惯的影响比想象中深远。最直接的一条是提交变成了纯本地行为。你可以在飞机上、在地铁里、在服务器维护期间随时随地git commit一次提交只写本地磁盘几毫秒完成。提交频率因此可以大幅提高——不再是“攒一天然后推一次”而是“做完一个小逻辑就提一次”。高频提交带来的好处是回滚粒度细、问题定位快坏处是如果不管提交信息质量历史会变成一堆“fix”“update”的垃圾。第二条影响是分支的使用方式彻底变了。本地开分支几乎是零成本的本质就是往.git/refs/heads/写一个四十字节的文件所以你可以给每个想法都开一个分支试完不满意直接删。我自己的习惯是哪怕只是改一行配置也开个分支改完合并回去再删掉。这么做的理由很实际主线永远保持随时可发布的状态任何半成品都隔离在分支上。第三条影响比较隐蔽历史是可以被整理的。因为提交发生在本地、还没推给别人你可以随意改写——合并几个零碎提交、修改提交信息、调整提交顺序、把误提交的大文件从历史里剔掉。一旦推送到共享仓库这些操作就该停了因为别人可能已经基于那些提交开始工作了。这条“本地随便改、推送要谨慎”的边界是分布式协作里最重要的一条默契也是新手最容易越界的地方。2.4 分布式的代价没有锁只有约定说完好处得说说代价否则就是片面宣传了。集中式系统通常提供文件锁我要改这个文件先锁定别人就改不了冲突从源头上被避免。Git 没有这套机制两个人可以同时改同一个文件的同一行谁先推谁赢后推的人必须先拉取、解决冲突、再推。这个设计不是偷懒是权衡的结果。锁机制在文件粒度上工作适合二进制文件和大型资源文件但对源代码来说代价太高——你锁了一个文件别人可能只是想改里面另一个函数却被迫等待。Git 选择了“允许并行修改冲突在合并时解决”的路线把冲突从“事前阻塞”变成了“事后处理”。代价是每个人都得会解决冲突收益是并行度大幅提升。与之配套的是约定文化。既然没有技术手段强制那就靠流程约束推送前先拉取、别对已推送的提交做变基、分支命名要有意义、提交信息要写清楚动机。这些东西 Git 本身不管但团队不管就会乱。我见过最混乱的一个仓库主干上同时存在三种命名风格的分支、几十个已经合并但没人删的僵尸分支、以及大量信息为空的提交——技术上完全合法协作上是一场灾难。3. 版本库的解剖.git 目录里究竟躺着什么3.1 HEAD、index、refs、objects 四个角色把.git目录打开看一眼大部分人会有点懵一堆看不懂的目录和文件。其实核心角色只有四个。objects是所有数据的最终归宿——文件内容、目录结构、提交记录、标签全部以对象形式压缩存储在这里用哈希命名。refs存放各种引用refs/heads/下每个文件就是一个本地分支文件内容就是它指向的提交哈希refs/tags/下是标签refs/remotes/下是远程分支的本地快照。HEAD是一个指针的指针通常内容是ref: refs/heads/main意思是“我现在在 main 分支上”。index就是前面说的暂存区二进制格式记录路径到对象的映射以及文件元信息。理解这四个角色的关系能解释很多“诡异”现象。比如为什么git checkout切分支那么快因为它只改了两样东西HEAD的内容以及把 index 和工作区同步成目标提交的树。几万个文件的项目切分支也就一两秒因为没变动的文件根本不用碰。再比如为什么删掉一个分支后提交还在因为分支只是一个指针文件删指针不等于删对象只要还有别的引用指向那条链或者你记得哈希数据就一直躺在objects里等着被垃圾回收。再补一个实用知识点HEAD处于“分离”状态detached HEAD是什么意思就是HEAD里直接写了一个提交哈希而不是指向某个分支。这时候你做的提交不属于任何分支切走之后就很难找回除非记下哈希。这个状态本身不可怕查看历史、跑测试时经常需要但千万别在分离状态下做正式开发。我见过有人在分离状态下写了半天代码切回主线后发现改动“消失”了其实是提交还在只是没有分支指向它。3.2 blob、tree、commit 三种对象的拼装关系Git 的对象模型非常简洁只有四种类型blob、tree、commit、tagannotated tag。blob存文件内容注意它不含文件名、不含路径、不含权限纯粹是字节流。tree存一层目录结构每一条记录包含模式文件还是目录、是否可执行、对象类型、哈希、名字。commit存一次提交的元信息指向一个 tree、指向零个或多个父 commit、作者、提交者、时间、说明。这个分层设计的妙处在于内容和名字彻底解耦。同一个文件在不同目录下出现只要内容一样就是同一个 blob物理上只存一份。重命名一个文件Git 层面其实是一次“删旧路径 加新路径”但因为新路径指向的还是同一个 blob存储开销几乎为零。你在git log --follow里看到的“重命名追踪”是展示时通过内容相似度算出来的不是存储时记录的。这也解释了一个经典困惑为什么 Git 不记录重命名因为它压根不需要记内容寻址天然处理了这件事。commit 之间的父子关系构成了一张有向无环图而不是一条直线。合并merge产生的提交有两个父提交这就是“分叉又汇合”的图形来源。理解这一点你就明白为什么git log --graph画出来的线会分叉和汇合也明白为什么有些操作需要指定“相对于哪个父提交”——因为合并提交确实有两个来路。3.3 内容寻址哈希不是校验码是地址很多人把哈希理解成“校验码”用来验证数据没坏。这个理解不算错但漏掉了关键的一半在 Git 里哈希同时是寻址方式。你要取一个对象不是靠“第几个文件”“什么路径”而是靠“算出它的哈希去objects目录里按哈希找”。这就叫内容寻址。内容寻址带来几个必然结果。第一相同内容永远只存一份天然去重这也是为什么往仓库里放一堆重复的小文件不会让体积翻倍。第二内容不可篡改改一个字节哈希全变等于换了一个对象。第三历史链自校验因为 commit 对象里包含父提交的哈希改动任何一个历史提交它之后所有提交的哈希都会变整条链对不上立刻能发现。第三点在协作场景下特别有价值。你拉下来的历史和远端的历史只要最新提交的哈希一致就能确信中间每一个字节都没被动过。这也解释了为什么强推force push改写历史会引发那么大麻烦——所有人的本地历史都跟远端对不上了必须手工处理。理解了这个机制你就不会觉得“不要强推主干”是一条死板的规矩而是一个数学上的必然要求。3.4 动手验证用底层命令把一次提交拆开看光看文字印象不深建议亲手拆一次。下面这串命令都是只读的随便找个仓库跑一遍不会有任何风险。# 看当前提交对象的完整内容 git cat-file -p HEAD # 输出大致长这样 # tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 # parent 8a1f2c... # author 某开发者 devexample.com 1700000000 0800 # committer 某开发者 devexample.com 1700000000 0800 # # feat: 增加配置加载逻辑 # 顺着 tree 往下看目录结构 git cat-file -p HEAD^{tree} # 看某个具体文件对应的 blob 内容 git cat-file -p HEAD:README.md # 看暂存区里每个路径指向哪个对象 git ls-files -s # 看某个对象的类型 git cat-file -t HEAD跑完这几条你会对“提交是一棵树的根”这件事有非常具体的感受。HEAD^{tree}那个语法意思是“取 HEAD 指向的提交里的 tree 对象”HEAD:README.md意思是“取 HEAD 那棵树里路径为 README.md 的 blob”。这些语法平时用得不多但排查疑难问题时很有用比如你想确认某个文件在某个历史版本里到底是什么内容又不想切分支破坏当前工作区。再补一个实用技巧git rev-parse HEAD能拿到完整哈希git rev-parse --short HEAD拿短哈希。写脚本或者在文档里引用某个提交时短哈希更好读但要注意短哈希在超大仓库里可能冲突正式场合还是用完整的。4. 从单人脚本到多人协作四种工作流的适用边界4.1 集中式工作流主干直推的适用场景Git 支持的第一种工作流恰好是跟集中式工具最像的那种所有人往同一条主干分支上提交推送前先拉取变基。它的优点是简单到不需要培训——没有分支概念要学没有合并策略要定新人第一天就能上手。适用边界比想象中窄。团队人数少比如三五个人、彼此改动区域不重叠、有自动化测试兜底、发布节奏快这套模式的效率其实很高。我待过的一个小工具项目就是这样三个人共用一个主干每天推十几次几乎没出过事。但只要出现两个人在同一块代码上并行改动超过半天冲突就会频繁到让人烦躁而且主干随时处于“可能编译不过”的状态。用这套流程有个硬性前提推送前必须本地跑通测试。因为主干是共享的你推上去一个坏提交所有人的拉取都会被污染。这就要求团队有比较强的自律或者有个足够快的 CI 在推送后几分钟内给出反馈。没有这两样集中式工作流很快就会退化成“谁都不敢推”的僵局。4.2 功能分支工作流隔离与评审的最小成本方案这是目前最主流的做法每做一个需求或修一个 bug从主干切一个分支出来做完推上去发起合并请求评审通过后合回主干。它的核心价值是隔离——半成品不会影响别人坏代码在合并前能被拦下来。这套流程真正的成本不在 Git 操作而在分支的生命周期管理。一个分支活得太久超过一周合并时的冲突量会指数上升评审的人也难以理解上下文。我的经验是把分支控制在“一到三天能合回去”的粒度如果一个需求太大就拆成几个可以独立合入的小步骤每步都保持主干可运行。还有一个细节合并方式的选择会显著影响历史可读性。三种常见做法——普通合并保留分叉图形、压缩合并把分支上所有提交压成一个、变基合并把分支提交搬到主干末尾保持线性。团队内部必须统一否则历史图会变得杂乱。我倾向于对功能分支用压缩合并对长期分支用普通合并理由是前者关心“做了什么功能”后者关心“什么时候集的成”。4.3 Git Flow 与主干开发重与轻的两端Git Flow 是一套分支模型定义了主干、开发、功能、发布、热修五类分支各自有明确的创建和合并规则。它诞生的背景是需要维护多个并行版本的桌面软件——你不能让新功能的代码混进当前版本的补丁里。这套模型的分支数量多、流转规则复杂用在小项目上属于典型的过度设计我见过一个四人团队硬上 Git Flow结果每天花在分支同步上的时间比写代码还多。主干开发trunk-based走的是另一个极端所有人往主干提交分支存活时间以小时计未完成的功能用开关feature flag藏起来。它对工程能力要求最高——必须有完善的自动化测试、快速的 CI、可靠的回滚机制。但它也是持续交付场景下效率最高的模式因为不存在“合并地狱”集成的痛苦被摊薄到每一次提交里。选择哪个不是技术品味问题是团队现状问题。判断标准可以简化成三个问题需要同时维护几个发布版本团队规模多大自动化测试覆盖到什么程度三个问题的答案基本能定位到合适的模型。硬套一个“先进”的模型往往比用个笨办法更痛苦。4.4 四种工作流的对照与选型建议把前面几种放在一起对比边界会更清楚。工作流主干状态适合团队规模关键前提典型痛点集中式频繁变动2-5 人强自律或快 CI冲突频繁主干不稳定功能分支始终可发布5-50 人有代码评审机制长命分支导致合并困难Git Flow严格受控需要多版本并行专职发布管理流程重分支多易混乱主干开发始终可发布任何规模高测试覆盖 特性开关对工程基建要求极高我个人的实践路径是新项目先上功能分支工作流跑顺了再根据发布节奏决定要不要往两端调整。从小团队起步的很少需要 Git Flow追求快速交付的早晚会走向主干开发。最忌讳的是“照搬大厂流程”——人家的流程是为人家的问题设计的你的问题可能完全不同。5. 从零建库并跑通一次完整流转可复现的实操序列5.1 初始化与那三行必须提前做的配置新建一个仓库git init一条命令就够。但在这之前有三项配置建议先做掉能省掉后面很多莫名其妙的麻烦。# 身份信息提交记录里会用到写错了要改历史很麻烦 git config --global user.name 你的名字 git config --global user.email youexample.com # 默认分支名跟团队保持一致避免每次 init 完手动改名 git config --global init.defaultBranch main # 换行符处理跨平台协作必配 # 看你团队的习惯选Windows 上用 true跨平台混合用 input git config --global core.autocrlf input第一项是刚需。我见过有人提交了几十次之后才发现作者信息是默认的机器名加内网邮箱想改就得重写历史团队里所有人都要重新拉取。第二项看起来无关紧要但如果团队约定是main而你本地默认生成master第一次推送就会多出一个分叉徒增困惑。第三项是跨平台协作里最容易被忽略的坑。换行符在不同系统上不一致会导致“我只改了一行但 diff 显示整个文件都变了”这种诡异现象。配置对了之后仓库内部统一用 LF检出时按平台转换问题就消失了。注意这个配置对已有文件不会自动生效历史遗留问题需要单独处理。初始化完成后第一件事是写.gitignore。等提交了一堆编译产物再想清理就得动历史了成本高得多。常见的忽略项依赖目录、构建输出、日志、本地配置文件、编辑器缓存目录。原则是“凡是可以由源码重新生成的都不入库”。5.2 三区流转的最小命令集把一个改动从工作区送进版本库最小路径是四步。下面这段可以直接照着敲。# 1. 看看现在什么状态 git status git status -s # 紧凑格式日常用这个更快 # 2. 看看具体改了什么 git diff # 工作区 vs 暂存区 git diff --cached # 暂存区 vs 最后一次提交 # 3. 挑要提交的改动放进暂存区 git add src/config.js git add -p # 交互式按块选择强烈推荐 # 4. 提交 git commit -m feat: 增加配置加载逻辑支持环境变量覆盖 # 5. 看结果 git log --oneline --graph --all -10这套流程里最值得养成习惯的是git add -p。它把“提交”这个动作拆成了“挑拣”和“封存”两步逼你在提交前重新审视一遍自己的改动。我用了几年之后发现光是这个动作就能拦下不少误提交——比如调试用的console.log、临时改的测试地址、注释掉的整段代码。提交信息也值得单独说一句。feat:、fix:、refactor:、docs:这类前缀不是形式主义它让git log变成可检索的。半年后你要找“哪个提交改了这个配置项的默认值”一条git log --oneline --grepconfig就能定位。标题行控制在五十字符内正文说清楚“为什么这么改”而不是“改了哪些行”——后者 diff 里已经写着了。5.3 几个高频误操作与恢复手法概念清楚了恢复操作其实很直观无非是把某个区域的内容替换成另一个区域的。但实际操作里几个命令长得太像必须分清。场景命令影响范围是否丢数据撤销暂存改动还在git restore --staged file只动暂存区不丢撤销工作区改动git restore file只动工作区会丢慎用撤销最后一次提交但保留改动git reset --soft HEAD~1动版本库和暂存区不丢修改最后一次提交信息或补充文件git commit --amend重写最后一次提交已推送时慎用把误加入版本库的文件移出跟踪git rm --cached file只动 index工作区文件保留最容易出事的是第二个。git restore file直接丢弃工作区改动没有确认、没有备份敲完就没了。我自己的习惯是在任何可能丢数据的操作前先git stash一下哪怕只是改了两行。stash 是本地的一个临时栈不进历史、不影响别人用完git stash pop拿回来成本极低收益是心里踏实。另一个高频问题是“提交了不该提交的文件”。如果只是刚提交还没推git rm --cached加git commit --amend就能解决。如果文件已经进了历史深处比如一个几十兆的二进制包那就得用历史重写工具清理而且所有协作者都要重新克隆。所以再次强调.gitignore要趁早大文件要在第一次提交前就拦下来。5.4 我踩过的坑与长期有效的习惯说几个纯经验的东西文档里一般不会写。第一个坑是在.git目录里手动操作。早期我为了“清理空间”手动删过objects里的文件结果仓库直接损坏git fsck报了一堆缺失对象最后只能重新克隆。.git目录对 Git 来说是不透明的二进制存储除了解剖学习时只读查看任何写操作都应该通过 Git 命令完成。第二个坑是把版本库放在同步盘里。把工作目录放进某些自动同步工具会在 Git 写对象的同时触发同步导致 index 文件被并发写入、对象文件只写了一半。表现是随机出现的“仓库损坏”或“文件锁冲突”。如果确实需要多设备正确做法是用远程仓库同步而不是同步.git目录。第三个习惯是提交前必看git status -s和git diff --cached。前者告诉你哪些文件在哪个状态后者告诉你即将提交的具体内容。这两个命令加起来不到两秒能拦掉大部分“提交了不该提交的东西”的事故。第四个习惯是给每个分支起有意义的名字。fix/login-timeout、feat/config-loader这种命名比dev2、test、new有信息量得多。分支名会出现在合并提交里、出现在代码评审页面上、出现在别人的git branch输出里它是给未来的人看的包括三个月后的你自己。最后一个也是我觉得最值钱的遇到不确定的操作先复制一份目录再动手。整个仓库复制一份的成本可能就几百兆但能让你在搞砸之后十分钟内恢复如初。等你对 Git 的对象模型足够熟了这个习惯自然会消失但在那之前它是比任何回滚命令都可靠的保险。回到最开始那个问题git commit那一瞬间文件到底去了哪儿现在你应该能给出一个像样的答案——内容被算成哈希存进对象库路径和哈希的映射写进索引目录结构被组织成 tree最后生成一个 commit 指向这棵树和它的父亲然后分支指针往前挪一格。整个过程没有“上传”没有“覆盖”只有对象的写入和指针的移动。把这套模型在脑子里建起来之后你会发现 Git 的命令不再是需要背诵的咒语而是一组可以推导的操作——想清楚要动哪个区域、要移动哪个指针命令自然就出来了。
阅读完成 · 觉得有帮助?