
1. 從“秒懂”說起為什么Git值得被講清楚很多人第一次接觸版本控制腦子里冒出來的畫面大概是這樣的一堆文件夾名字叫“最終版”“最終版2”“最終版真的不改了”“這個(gè)才是最后一版”然后某天想找回三天前刪掉的一段代碼翻遍硬盤也找不到。這種痛幾乎每個(gè)寫過代碼的人都經(jīng)歷過。Git這個(gè)東西就是專門來終結(jié)這種混亂的。它做的事情說起來很簡單——記錄每一次改動(dòng)讓你隨時(shí)能回到任何一個(gè)歷史節(jié)點(diǎn)同時(shí)還能讓多個(gè)人在同一份代碼上協(xié)作而不互相踩腳。但它的“身世”其實(shí)挺有意思理解了它是怎么來的、為什么長成現(xiàn)在這個(gè)樣子很多命令就不用死記硬背了。這篇內(nèi)容適合誰看如果你是剛學(xué)編程的新手被git add、git commit、git push這一套搞得云里霧里那這篇就是寫給你的。如果你已經(jīng)用了一段時(shí)間Git但只會(huì)clone和pull遇到?jīng)_突就慌那這篇也能幫你把底層邏輯理順。我盡量不堆術(shù)語用生活化的類比把Git的設(shè)計(jì)思路講明白讓你看完之后不只是“會(huì)敲命令”而是真正“秒懂”它為什么這么設(shè)計(jì)。核心關(guān)鍵詞就幾個(gè)版本控制、分布式、快照、分支、提交歷史。這幾個(gè)詞貫穿全文理解了它們Git的傳奇身世也就清楚了。2. Git的誕生背景與核心設(shè)計(jì)哲學(xué)2.1 一個(gè)“被迫營業(yè)”的項(xiàng)目如何改變了世界Git的誕生故事在技術(shù)圈流傳很廣但我不想把它講成神話。簡單說就是當(dāng)年Linux內(nèi)核這個(gè)超大型開源項(xiàng)目的代碼管理遇到了瓶頸。早期用的那套工具是免費(fèi)軟件但用著用著發(fā)現(xiàn)限制太多社區(qū)里有人覺得不爽于是決定自己造一個(gè)。這個(gè)任務(wù)落到了一位以脾氣火爆和執(zhí)行力驚人著稱的開發(fā)者身上。他花了大概兩周時(shí)間寫出了Git的第一個(gè)可用版本。兩周一個(gè)后來統(tǒng)治了全球代碼托管領(lǐng)域的工具就這么誕生了。但這里有個(gè)關(guān)鍵點(diǎn)很多人忽略Git不是憑空設(shè)計(jì)出來的它是被“逼”出來的。Linux內(nèi)核項(xiàng)目有幾個(gè)硬性需求——速度要快、必須支持分布式、要能扛住成千上萬人的并發(fā)提交、還要保證數(shù)據(jù)完整性。這些需求直接決定了Git的架構(gòu)。比如“分布式”這個(gè)選擇就是因?yàn)榧惺较到y(tǒng)一旦服務(wù)器掛了所有人都沒法干活而分布式意味著每個(gè)人本地都有一份完整的倉庫副本服務(wù)器只是用來同步的“中轉(zhuǎn)站”。這個(gè)設(shè)計(jì)在當(dāng)時(shí)是顛覆性的。2.2 快照思維Git和傳統(tǒng)工具的根本分歧要理解Git先要理解它和早期版本控制工具比如SVN的根本區(qū)別。SVN這類工具是“基于差異”的它記錄的是文件每次改了什么比如“第3行刪了兩個(gè)字第7行加了一句話”。而Git是“基于快照”的每次提交它都會(huì)給當(dāng)前所有文件拍一張“全景照片”記錄下每個(gè)文件在這一刻的完整狀態(tài)。這個(gè)區(qū)別聽起來很技術(shù)但影響巨大?;诓町惖姆绞侥阆肟茨硞€(gè)歷史版本得從第一個(gè)版本開始把所有的差異一層層疊加起來才能還原出當(dāng)時(shí)的全貌。版本越多疊加越慢。而基于快照的方式每個(gè)版本都是獨(dú)立的、完整的想看哪個(gè)版本直接拿出來就行不需要“倒帶”。這就是為什么Git切換分支、回退版本的速度快得離譜——它根本不需要重新計(jì)算只是換了一張“照片”而已。當(dāng)然Git也不是傻到每個(gè)版本都存一份完整拷貝那樣倉庫會(huì)爆炸。它內(nèi)部做了大量優(yōu)化沒改動(dòng)的文件會(huì)直接復(fù)用之前的存儲(chǔ)只對(duì)改動(dòng)的文件生成新快照。這個(gè)機(jī)制叫“內(nèi)容尋址存儲(chǔ)”后面會(huì)細(xì)說。2.3 分布式架構(gòu)帶來的連鎖反應(yīng)“分布式”這三個(gè)字是Git身世里最核心的基因。它帶來的連鎖反應(yīng)包括離線可用你在飛機(jī)上、地鐵里沒有網(wǎng)絡(luò)照樣可以提交、查看歷史、切換分支。因?yàn)樗胁僮鞫际窃诒镜氐耐暾麄}庫上進(jìn)行的。速度快絕大多數(shù)操作不需要聯(lián)網(wǎng)自然快。git log、git diff、git commit這些命令幾乎都是瞬間完成。容錯(cuò)性強(qiáng)每個(gè)人的本地都是一份完整備份任何一臺(tái)機(jī)器壞了代碼都不會(huì)丟。服務(wù)器掛了隨便找一個(gè)人的本地倉庫重新推上去就行。協(xié)作模式靈活你可以直接和同事的本地倉庫同步不一定非要經(jīng)過中央服務(wù)器。這種“點(diǎn)對(duì)點(diǎn)”的協(xié)作方式在開源社區(qū)里特別常見。但分布式也不是沒有代價(jià)。最直接的問題就是倉庫體積會(huì)變大因?yàn)槊總€(gè)人都存了完整歷史。另外權(quán)限管理比集中式復(fù)雜因?yàn)闆]有一個(gè)天然的“中心”來統(tǒng)一控制。不過這些代價(jià)在絕大多數(shù)場景下都是值得的。3. 核心機(jī)制拆解Git內(nèi)部到底在干什么3.1 三個(gè)工作區(qū)域理解Git的第一把鑰匙Git把代碼的流轉(zhuǎn)分成了三個(gè)區(qū)域這是理解所有命令的基礎(chǔ)。我用一個(gè)生活化的類比來解釋想象你有一張書桌工作區(qū)桌上攤著你正在寫的稿子。旁邊有一個(gè)文件柜暫存區(qū)你寫完一段覺得滿意就先放進(jìn)文件柜里。最后文件柜里的內(nèi)容會(huì)被正式歸檔到檔案室本地倉庫每個(gè)檔案都有一個(gè)編號(hào)可以隨時(shí)調(diào)取。對(duì)應(yīng)到Git工作區(qū)你實(shí)際編輯文件的地方就是你能看到的文件夾。暫存區(qū)一個(gè)中間地帶你通過git add把改動(dòng)放進(jìn)去相當(dāng)于“確認(rèn)這些改動(dòng)是我要提交的”。本地倉庫通過git commit把暫存區(qū)的內(nèi)容正式記錄成一個(gè)歷史版本。為什么要有暫存區(qū)這個(gè)“多此一舉”的設(shè)計(jì)因?yàn)楹芏鄷r(shí)候你改了好幾個(gè)文件但只想提交其中一部分。暫存區(qū)讓你可以精確控制每次提交的內(nèi)容把相關(guān)的改動(dòng)放在一起形成一個(gè)邏輯清晰的提交記錄。沒有暫存區(qū)的話你要么全部提交要么全部不提交非常僵硬。3.2 提交對(duì)象與內(nèi)容尋址Git的“身份證系統(tǒng)”每次git commitGit都會(huì)創(chuàng)建一個(gè)提交對(duì)象。這個(gè)對(duì)象里包含了幾樣?xùn)|西指向當(dāng)前文件快照的指針、作者信息、提交時(shí)間、提交說明以及一個(gè)指向“父提交”的指針。這個(gè)“父提交”指針很關(guān)鍵它把所有提交串成了一條鏈這就是所謂的“提交歷史”。Git用SHA-1哈希值來標(biāo)識(shí)每一個(gè)對(duì)象。你可以把哈希值理解成對(duì)象的“身份證號(hào)”它是根據(jù)對(duì)象內(nèi)容計(jì)算出來的。內(nèi)容一樣哈希值就一樣內(nèi)容變了一個(gè)字節(jié)哈希值就完全不同。這個(gè)機(jī)制保證了數(shù)據(jù)的完整性——如果你不小心改壞了某個(gè)文件Git一算哈希值對(duì)不上立刻就能發(fā)現(xiàn)。內(nèi)容尋址的好處是同樣的內(nèi)容在倉庫里只會(huì)存一份。比如你有兩個(gè)文件內(nèi)容完全一樣Git只會(huì)存一個(gè)對(duì)象兩個(gè)文件名都指向它。這在大型項(xiàng)目里能省下大量空間。3.3 分支的本質(zhì)一個(gè)輕量級(jí)的指針很多人覺得分支很神秘其實(shí)Git里的分支簡單到令人發(fā)指——它就是一個(gè)指向某個(gè)提交對(duì)象的指針本質(zhì)上是一個(gè)包含40個(gè)字符哈希值的文件。創(chuàng)建分支就是創(chuàng)建一個(gè)新指針切換分支就是讓HEAD指向不同的指針。正因?yàn)榉种н@么輕量Git才鼓勵(lì)你“隨便開分支”。修bug開一個(gè)加功能開一個(gè)實(shí)驗(yàn)性的想法再開一個(gè)。用完就刪沒有任何負(fù)擔(dān)。對(duì)比一下SVN創(chuàng)建分支相當(dāng)于復(fù)制整個(gè)目錄又慢又占空間所以大家都不愿意開分支。這個(gè)設(shè)計(jì)差異直接影響了團(tuán)隊(duì)的工作流程。HEAD是一個(gè)特殊的指針?biāo)赶蚰惝?dāng)前所在的分支。當(dāng)你提交時(shí)HEAD指向的分支會(huì)自動(dòng)向前移動(dòng)。理解HEAD的概念對(duì)后面理解reset、checkout這些命令至關(guān)重要。4. 從零開始的實(shí)操流程把理論跑一遍4.1 環(huán)境準(zhǔn)備與初始配置先確認(rèn)你的機(jī)器上裝了Git。打開終端輸入git --version如果顯示版本號(hào)說明已經(jīng)裝好了。沒有的話去官網(wǎng)下載對(duì)應(yīng)系統(tǒng)的安裝包一路下一步就行。裝好之后第一件事是配置用戶名和郵箱。這兩個(gè)信息會(huì)出現(xiàn)在每次提交記錄里方便別人知道是誰改的git config --global user.name 你的名字 git config --global user.email 你的郵箱--global表示全局配置對(duì)這臺(tái)機(jī)器上所有倉庫生效。如果某個(gè)項(xiàng)目想用不同的身份可以在項(xiàng)目目錄里不加--global再配一次項(xiàng)目級(jí)的配置會(huì)覆蓋全局配置。提示用戶名和郵箱不需要和任何賬號(hào)綁定Git只是把它們當(dāng)作標(biāo)識(shí)信息記錄在提交里。但在團(tuán)隊(duì)協(xié)作中建議用真實(shí)可識(shí)別的信息方便追溯。再配一個(gè)常用的別名能省不少打字時(shí)間git config --global alias.st status git config --global alias.co checkout git config --global alias.br branch git config --global alias.ci commit git config --global alias.lg log --oneline --graph --all這樣以后敲git st就等于git status敲git lg就能看到漂亮的提交圖譜。4.2 創(chuàng)建倉庫與第一次提交進(jìn)入你的項(xiàng)目文件夾執(zhí)行g(shù)it init這個(gè)命令會(huì)在當(dāng)前目錄下創(chuàng)建一個(gè)隱藏的.git文件夾所有版本信息都存在里面。注意.git文件夾就是Git的“大腦”刪掉它這個(gè)目錄就變回普通文件夾了所有歷史都會(huì)丟失?,F(xiàn)在創(chuàng)建一個(gè)文件比如readme.md隨便寫點(diǎn)內(nèi)容。然后git status你會(huì)看到readme.md顯示為紅色表示它處于“未跟蹤”狀態(tài)。Git看到了這個(gè)新文件但還沒有把它納入版本管理。接下來把它加入暫存區(qū)git add readme.md再執(zhí)行g(shù)it status文件名變成綠色表示已經(jīng)暫存。最后提交git commit -m 初始化項(xiàng)目添加readme-m后面跟的是提交說明。提交說明很重要寫清楚這次改了什么、為什么改將來回頭看歷史的時(shí)候會(huì)感謝自己。4.3 查看歷史與回退版本提交完成后用git log查看歷史git log --oneline--oneline讓每個(gè)提交只顯示一行包含短哈希值和提交說明看起來清爽很多。如果想看某個(gè)提交具體改了什么git show 提交哈希值回退版本用git reset它有三種模式區(qū)別在于對(duì)工作區(qū)和暫存區(qū)的影響命令暫存區(qū)工作區(qū)適用場景git reset --soft 目標(biāo)保留保留想重新提交但保留所有改動(dòng)git reset --mixed 目標(biāo)重置保留默認(rèn)模式想重新add和commitgit reset --hard 目標(biāo)重置重置徹底放棄目標(biāo)之后的所有改動(dòng)注意--hard會(huì)永久丟棄工作區(qū)的改動(dòng)用之前一定要確認(rèn)沒有未保存的重要內(nèi)容。我踩過這個(gè)坑辛辛苦苦寫的一下午代碼一個(gè)--hard全沒了那種感覺不想再體驗(yàn)第二次。如果只是臨時(shí)想看看某個(gè)舊版本的樣子不想改變當(dāng)前狀態(tài)用git checkout 提交哈希值進(jìn)入“分離頭指針”狀態(tài)看完再git checkout main回來就行。4.4 分支操作創(chuàng)建、切換、合并創(chuàng)建一個(gè)新分支并切換過去git checkout -b feature-login這個(gè)命令等價(jià)于先git branch feature-login再git checkout feature-login。在新分支上正常提交不會(huì)影響主分支。改完代碼、提交完成后切回主分支git checkout main然后把feature分支合并進(jìn)來git merge feature-login如果兩個(gè)分支沒有沖突Git會(huì)自動(dòng)完成合并創(chuàng)建一個(gè)新的“合并提交”。如果有沖突Git會(huì)提示哪些文件沖突了你需要手動(dòng)打開這些文件找到、、這些標(biāo)記決定保留哪部分代碼刪掉標(biāo)記然后重新git add和git commit。合并完成后如果不再需要feature分支可以刪掉git branch -d feature-login-d是安全刪除如果分支還沒合并會(huì)提示你。如果確定要強(qiáng)制刪除用-D。5. 常見問題與排查技巧實(shí)錄5.1 提交錯(cuò)了怎么辦amend與revert場景一剛提交完發(fā)現(xiàn)提交說明寫錯(cuò)了或者漏加了一個(gè)文件。用git commit --amend這個(gè)命令會(huì)把暫存區(qū)的內(nèi)容合并到上一次提交里同時(shí)允許你修改提交說明。注意如果上一次提交已經(jīng)推送到遠(yuǎn)程倉庫amend后需要強(qiáng)制推送這在團(tuán)隊(duì)協(xié)作中要謹(jǐn)慎因?yàn)闀?huì)改寫歷史。場景二某個(gè)提交已經(jīng)推送了發(fā)現(xiàn)有問題想撤銷。不要用reset因?yàn)闀?huì)改寫歷史影響其他人。用git revert 提交哈希值revert會(huì)創(chuàng)建一個(gè)新的提交內(nèi)容是“反向操作”把之前那個(gè)提交的改動(dòng)抵消掉。這樣歷史是向前的不會(huì)影響別人。5.2 沖突排查速查表問題現(xiàn)象可能原因解決思路merge時(shí)提示沖突兩個(gè)分支改了同一文件的同一區(qū)域手動(dòng)編輯沖突文件保留正確內(nèi)容后addcommitpush被拒絕遠(yuǎn)程有本地沒有的提交先pull解決可能的沖突再push文件莫名其妙消失可能被reset --hard或checkout覆蓋用git reflog查看操作歷史找到丟失前的提交提交歷史混亂頻繁merge產(chǎn)生大量合并提交考慮用rebase整理歷史但不要在公共分支上rebase倉庫體積過大歷史中包含了大量二進(jìn)制文件用git gc清理或考慮用git-lfs管理大文件git reflog是一個(gè)救命命令它記錄了HEAD的所有移動(dòng)歷史包括那些看起來“已經(jīng)丟失”的提交。只要提交過哪怕分支被刪了reflog里通常還能找到。找到哈希值后用git checkout或git branch恢復(fù)就行。5.3 幾個(gè)容易踩的坑第一個(gè)坑在錯(cuò)誤的分支上寫了代碼。比如本來應(yīng)該在feature分支上改結(jié)果忘了切換直接在main上改了。解決辦法先git stash把改動(dòng)暫存起來切到正確的分支再git stash pop把改動(dòng)放出來。第二個(gè)坑.gitignore沒配好把不該提交的文件比如編譯產(chǎn)物、本地配置文件、密鑰文件提交進(jìn)去了。一旦提交這些文件就進(jìn)入了歷史即使后來刪掉歷史里依然存在。如果密鑰泄露必須立刻更換密鑰因?yàn)闅v史記錄可能已經(jīng)被別人拉取了。第三個(gè)坑在公共分支上使用rebase。rebase會(huì)改寫提交歷史如果別人已經(jīng)基于你的舊提交做了工作rebase后他們的歷史就和你的對(duì)不上了會(huì)產(chǎn)生大量沖突。記住一個(gè)原則只在自己的私有分支上rebase公共分支永遠(yuǎn)只用merge。6. 進(jìn)階思路從“會(huì)用”到“用得好”6.1 工作流的選擇Git Flow與Trunk Based團(tuán)隊(duì)協(xié)作時(shí)分支策略很重要。常見的有兩種Git Flow維護(hù)兩條長期分支main和develop功能分支從develop切出完成后合并回develop。發(fā)布時(shí)從develop切出release分支修完bug后合并到main和develop。這套流程適合版本發(fā)布周期長、需要同時(shí)維護(hù)多個(gè)版本的項(xiàng)目。Trunk Based所有人都在main或trunk上開發(fā)功能分支生命周期很短通常不超過一天。通過特性開關(guān)來控制未完成功能的可見性。這套流程適合持續(xù)交付、發(fā)布頻率高的項(xiàng)目。沒有哪種一定更好關(guān)鍵看團(tuán)隊(duì)規(guī)模和發(fā)布節(jié)奏。小團(tuán)隊(duì)、快速迭代Trunk Based更輕快大團(tuán)隊(duì)、嚴(yán)格版本管理Git Flow更穩(wěn)妥。6.2 提交歷史的整理藝術(shù)一個(gè)清晰的提交歷史本身就是最好的文檔。幾個(gè)原則一個(gè)提交只做一件事不要把“修bug”和“重構(gòu)代碼”混在一個(gè)提交里。提交說明寫清楚“為什么”代碼本身說明了“做了什么”提交說明應(yīng)該解釋“為什么這么做”。善用交互式rebase整理歷史git rebase -i可以合并、拆分、重排提交在推送前把歷史整理干凈。交互式rebase的界面里每個(gè)提交前面有一個(gè)命令詞pick保留這個(gè)提交reword保留提交但修改說明squash把這個(gè)提交合并到上一個(gè)提交drop刪除這個(gè)提交我個(gè)人的習(xí)慣是在功能分支上隨便提交完成后再用rebase -i整理成幾個(gè)邏輯清晰的提交最后合并到主分支。這樣主分支的歷史看起來非常干凈。6.3 遠(yuǎn)程協(xié)作的注意事項(xiàng)遠(yuǎn)程倉庫比如團(tuán)隊(duì)自建的Git服務(wù)是協(xié)作的樞紐。幾個(gè)實(shí)用建議push前先pull養(yǎng)成習(xí)慣每次push之前先pull一下減少?zèng)_突概率。用git fetch而不是git pullfetch只下載遠(yuǎn)程改動(dòng)不自動(dòng)合并給你一個(gè)檢查的機(jī)會(huì)。確認(rèn)沒問題后再手動(dòng)merge。保護(hù)主分支在遠(yuǎn)程倉庫設(shè)置里把main分支設(shè)為受保護(hù)禁止直接push必須通過合并請(qǐng)求Merge Request的方式合入。這樣能保證主分支的代碼始終是經(jīng)過審查的。提示如果團(tuán)隊(duì)用的是自建的Git服務(wù)通常都支持合并請(qǐng)求功能。合并請(qǐng)求不僅是代碼審查的工具也是知識(shí)分享和討論的平臺(tái)值得好好利用。7. 我個(gè)人在實(shí)際操作中的體會(huì)用了這么多年Git最大的體會(huì)是命令可以查但思維模式必須內(nèi)化。什么叫思維模式就是你知道每次操作會(huì)影響哪個(gè)區(qū)域、哪個(gè)指針知道歷史是可以被改寫但改寫有代價(jià)的知道分支只是指針?biāo)钥梢噪S便開。有了這個(gè)思維模式遇到?jīng)]見過的問題你也能推理出大概的解決方向。另一個(gè)體會(huì)是不要怕犯錯(cuò)。Git的設(shè)計(jì)本身就考慮到了容錯(cuò)只要提交過的東西幾乎都能找回來。reflog就是你的后悔藥。真正危險(xiǎn)的操作只有少數(shù)幾個(gè)比如reset --hard、push --force、刪除未合并的分支。對(duì)這些操作保持敬畏其他的大膽試。最后分享一個(gè)小技巧如果你經(jīng)常需要同時(shí)處理多個(gè)任務(wù)試試git worktree。它允許你在同一個(gè)倉庫上檢出多個(gè)工作目錄每個(gè)目錄對(duì)應(yīng)一個(gè)分支。比如一個(gè)目錄跑測試一個(gè)目錄寫代碼互不干擾不用來回切換分支。這個(gè)功能用的人不多但在多任務(wù)場景下非常香。