删掉的代码还能捞回来?聊聊 Git 这个记性过分好的家伙
上周三下午,我干了一件蠢事。
手一抖,rm -rf 敲下去,一个写了三天的模块没了。屏幕上光标还在闪,像个没事人。
心跳大概停了两秒。然后我想起来——这玩意儿我提交过。
五个字母,一条 checkout,文件回来了。那一刻我真有点想给当年发明 Git 的人鞠个躬。
所以今天想聊聊这东西。不讲课,就说说它到底怎么回事,以及我这些年踩过的坑。
那个一直被忽略的隐藏文件夹
很多人用 Git,用得跟网盘一样:写完了,push 一下,收工。至于项目根目录里那个叫 .git 的隐藏文件夹?从来不看一眼。
可它才是主角。
你所有提交过的代码、每一次改动的痕迹、每一条分支的来龙去脉,全塞在那个文件夹里。它不是缓存,不是临时文件,不是删了会自动重建的东西。你把 .git 删掉,哪怕代码文件一个没少,这个项目也彻底失忆了——它不再是一个仓库,只是一堆躺在硬盘上的文本。
反过来讲,一个项目如果只剩 .git,代码是能全须全尾捞回来的。这就是为什么换电脑的时候,克隆一下比拷贝文件夹靠谱得多。
它不是在记你改了什么,是在拍照片
这是最容易被误解的一点。
大多数人的直觉是:Git 存的是差异。第 3 行加了个逗号,第 7 行删了个括号,像 Word 的修订模式那样。其实不是。至少不完全是。
Git 每次 commit,是在给整个项目拍一张快照。所有文件的当时状态,一张照片,封存。下一个 commit 又是一张。前后两张之间怎么变的?它自己算,算给你看,但存的还是照片。
那仓库为什么没被撑爆?因为它够聪明。内容一样的文件,它只留一份。你改了一百次,一百个版本里没动过的那些文件,全指向同一块存储。只有真正动过的,才新增。
这就像你每天给房间拍照。听起来存了一百张照片,可那面墙根本没动过,照片里那面墙就共用同一张底片。只有桌上那堆杂物是新的。
理解了这个,一堆事情就顺了。为什么 git log 里每条记录都有个长长的哈希值——那是照片编号。为什么你说不清“哪一行变了”,得靠 diff 去比——因为人家给你的是两张整照片,比对是事后做的。

分支,其实只是一张贴纸
我见过对分支有莫名恐惧的同事。觉得“建分支”是个大动作,会复制一份代码,占空间,很危险。
真不是。
Git 里的分支,本质上就是一个写着字符串的小文件。里面存的是某个 commit 的哈希值。就这么点东西,几十个字节。你建一百个分支,硬盘都不会喘一下。
真正让很多人懵的是 HEAD。
你把分支理解成便利贴,贴在某个 commit 上。HEAD 呢,是“你人站在哪张便利贴旁边”。切换分支,就是挪一下位置。你在哪个分支上提交,那张便利贴就往前挪一格,贴到新拍的照片上去。
所以偶尔会看到一种诡异状态:HEAD 不指向任何分支,直接指向某个 commit。学名叫“分离头指针”。听着吓人,说白了就是——你人站在那儿了,可附近没有便利贴。这时候提交的东西,回头容易找不着。

我第一次撞见这东西的时候慌得不行,以为把仓库搞坏了。其实就是自己走到了一个没插旗子的地方,插一面就是了。
几个我踩过的坑,你最好绕开

密码那事,说来惭愧。配置文件里塞了数据库密码,一提交一推送,等于贴公告栏。更麻烦的是,你后来某次提交把密码删了也没用——历史记录里那条还在,别人翻提交记录照样翻得出来。
正确的做法是用环境变量,或者干脆把配置文件丢进 .gitignore。已经提交了?那就得上 filter-branch 或者 BFG 这种重写历史的狠活儿,完了还得通知所有人重新克隆。麻烦得要命。所以,第一次提交之前,先看一眼你有没有带着不该带的东西。
还有件更蠢的,node_modules。
我把整个文件夹提交过一次。那次 push 卡了快十分钟,我以为是公司网络炸了。后来才发现是几万个文件在往上爬。删掉、加忽略规则,仓库从几百兆瘦回几兆。Git 不是网盘,别什么都往里塞。
还有一种情况更隐蔽:只用图形界面,从没在命令行敲过 git status。工具是好东西,但出问题的时候,你得知道底下发生了什么。至少得看懂那几行红字绿字在说什么。不然就只能干瞪眼,或者把仓库删了重新克隆——我见过有人这么干的,不止一个。
说到底,它记性好得有点过分
Git 最厉害的地方,不是帮你保存代码。
是它几乎不忘记任何事。
你删掉的文件,它记着。你反悔的那次改动,它记着。你凌晨三点提交的那条备注写着“真的最后一版”的 commit,它也老老实实记着,永远挂在那儿,等着哪天被哪个同事翻出来。
对写代码的人来说,这是一种很踏实的感觉。意味着你可以放心试错,可以大胆重构,可以把一个功能整个推倒重来。因为你知道,不管走多远,回头总有一条路。
人情上的事它管不了。你改了三个月又全部回滚,它不会笑你。但同事会。
对了。今天下班前,记得 commit 一下。
别问我为什么这么说。