
1. 為什么你需要合并提交如果你用過 Git大概率遇到過這種情況為了修復(fù)一個(gè) Bug你連續(xù)提交了七八次每次的提交信息都是“修復(fù)了一個(gè)小問題”、“再改一下”、“好像還有問題”、“這次應(yīng)該對了”。一周后你看著這條像貪吃蛇一樣又長又亂的提交歷史自己都想不起來當(dāng)時(shí)到底改了啥?;蛘咴谙蜷_源項(xiàng)目提交 Pull Request 之前你希望將一系列實(shí)驗(yàn)性的、瑣碎的中間提交整理成幾個(gè)邏輯清晰、意義明確的提交塊讓維護(hù)者一眼就能看懂你的工作。這就是git rebase合并提交也稱為“壓縮提交”大顯身手的時(shí)候。它不是什么高深莫測的黑魔法而是一個(gè)強(qiáng)大的歷史編輯工具。簡單說它能讓你把多個(gè)連續(xù)的提交合并成一個(gè)或幾個(gè)更整潔的提交。這不僅僅是讓提交記錄“好看”它直接提升了代碼歷史的可讀性、可維護(hù)性也是在團(tuán)隊(duì)協(xié)作中體現(xiàn)專業(yè)性的一個(gè)細(xì)節(jié)。想象一下你是在撰寫一份清晰的修改文檔而不是留下一堆草稿紙。很多人對rebase望而卻步覺得它會(huì)“重寫歷史”很危險(xiǎn)。確實(shí)如果對已推送到公共分支的提交進(jìn)行rebase可能會(huì)給協(xié)作者帶來麻煩。但對于尚未推送的本地提交或者你個(gè)人特性分支上的提交使用rebase來整理歷史是一種非常推薦的最佳實(shí)踐。今天我們就拋開恐懼手把手、一步步地把多個(gè)提交合并這件事弄得明明白白。2. 理解 Rebase 的“互動(dòng)模式”-i 參數(shù)是關(guān)鍵git rebase命令本身用于重新應(yīng)用提交而合并提交這個(gè)特定功能主要通過其交互模式來實(shí)現(xiàn)也就是加上-i參數(shù)。核心命令格式git rebase -i [commit-ish]這里的[commit-ish]可以是一個(gè)提交哈希、分支名或者像HEAD~3這樣的相對引用。這個(gè)參數(shù)指定了rebase 操作的起點(diǎn)。更準(zhǔn)確地說Git 會(huì)列出從[commit-ish]之后不包含該提交一直到當(dāng)前HEAD的所有提交供你編輯。一個(gè)必須理解的概念HEAD~n這是指定提交范圍最常用的方式。HEAD指向你當(dāng)前所在的提交。HEAD~1表示當(dāng)前提交的父提交HEAD~2表示祖父提交以此類推。所以git rebase -i HEAD~4意味著“我要重新審視并編輯最近的 4 個(gè)提交”。當(dāng)你執(zhí)行這個(gè)命令后Git 會(huì)打開你配置的默認(rèn)文本編輯器如 Vim、VSCode、Nano展示一個(gè)類似這樣的列表pick a1b2c3d 第一次提交添加用戶登錄功能 pick e4f5g6h 第二次提交修復(fù)登錄按鈕樣式 pick i7j8k9l 第三次提交補(bǔ)充登錄失敗提示 pick m1n2o3p 第四次提交優(yōu)化登錄接口性能 # Rebase x0y1z2a..m1n2o3p onto x0y1z2a (4 commands) # # Commands: # p, pick commit use commit # r, reword commit use commit, but edit the commit message # e, edit commit use commit, but stop for amending # s, squash commit use commit, but meld into previous commit # f, fixup commit like squash, but discard this commits log message # x, exec command run command (the rest of the line) using shell # b, break stop here (continue rebase later with git rebase --continue) # d, drop commit remove commit # l, label label label current HEAD with a name # t, reset label reset HEAD to a label # m, merge [-C commit | -c commit] label [# oneline] # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line, THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted.這個(gè)界面就是你的“操作臺(tái)”。最上面是按時(shí)間順序列出的提交最新的在最下面這是個(gè)歷史習(xí)慣。每一行以命令開頭默認(rèn)是pick然后是提交哈希和提交信息。注意這個(gè)編輯界面里的提交順序是從舊到新排列的。最上面一行是最早的提交最下面一行是最新的提交。這一點(diǎn)在規(guī)劃合并操作時(shí)至關(guān)重要因?yàn)閟quash和fixup是向上合并的。3. 實(shí)戰(zhàn)演練一步步合并你的提交現(xiàn)在我們進(jìn)入實(shí)戰(zhàn)環(huán)節(jié)。假設(shè)我們有一個(gè)簡單的項(xiàng)目為了開發(fā)一個(gè)“計(jì)算器”功能我們提交了以下歷史add: 創(chuàng)建 calculator.py 框架feat: 實(shí)現(xiàn)加法函數(shù) add()fix: 修正 add 函數(shù)參數(shù)校驗(yàn)feat: 實(shí)現(xiàn)減法函數(shù) sub()docs: 為 calculator.py 添加注釋我們的目標(biāo)是將這5個(gè)提交合并成2個(gè)邏輯清晰的提交一個(gè)包含加法的所有工作提交123一個(gè)包含減法和文檔工作提交45。3.1 第一步啟動(dòng)交互式 Rebase我們想合并最近5個(gè)提交所以從第5個(gè)提交的父提交開始操作即HEAD~5。git rebase -i HEAD~5執(zhí)行后編輯器會(huì)打開顯示如下內(nèi)容pick abc1234 add: 創(chuàng)建 calculator.py 框架 pick def5678 feat: 實(shí)現(xiàn)加法函數(shù) add() pick ghi9012 fix: 修正 add 函數(shù)參數(shù)校驗(yàn) pick jkl3456 feat: 實(shí)現(xiàn)減法函數(shù) sub() pick mno7890 docs: 為 calculator.py 添加注釋3.2 第二步規(guī)劃并編輯命令現(xiàn)在我們需要修改每行開頭的命令詞來告訴 Git 我們想怎么做。pick保留該提交不做改動(dòng)。squash(或縮寫s)將該提交合并到前一個(gè)提交中并且會(huì)進(jìn)入下一步讓你編輯合并后的新提交信息。fixup(或縮寫f)將該提交合并到前一個(gè)提交中但丟棄當(dāng)前提交的提交信息。當(dāng)你有一些“修正打字錯(cuò)誤”、“微調(diào)格式”的提交時(shí)用這個(gè)最合適可以自動(dòng)融入前一個(gè)提交不產(chǎn)生多余的提交信息編輯步驟。根據(jù)我們的目標(biāo)提交1框架作為加法功能的起點(diǎn)我們保留它。提交2和提交3是加法功能的實(shí)現(xiàn)和修正應(yīng)該被“壓縮”進(jìn)提交1。提交4減法作為減法功能的起點(diǎn)我們保留它。提交5文檔是針對整個(gè)文件的我們可以把它“壓縮”進(jìn)提交4。修改命令列表如下pick abc1234 add: 創(chuàng)建 calculator.py 框架 squash def5678 feat: 實(shí)現(xiàn)加法函數(shù) add() squash ghi9012 fix: 修正 add 函數(shù)參數(shù)校驗(yàn) pick jkl3456 feat: 實(shí)現(xiàn)減法函數(shù) sub() squash mno7890 docs: 為 calculator.py 添加注釋這里有一個(gè)非常重要的操作細(xì)節(jié)squash和fixup是“向上合并”。也就是說標(biāo)記為s或f的提交會(huì)被合并到它上面一行的提交中。你不能把第一個(gè)提交標(biāo)記為squash因?yàn)樗厦鏇]有提交可以合并。理解了這一點(diǎn)你就能正確規(guī)劃順序。3.3 第三步編寫新的提交信息保存并關(guān)閉第一步的編輯界面后Git 開始執(zhí)行 Rebase 操作。當(dāng)它遇到squash命令時(shí)會(huì)再次打開編輯器讓你為合并后的新提交編寫提交信息。對于我們的例子Git 會(huì)先處理前三個(gè)提交的合并。編輯器里可能會(huì)顯示類似這樣的內(nèi)容# This is a combination of 3 commits. # This is the 1st commit message: add: 創(chuàng)建 calculator.py 框架 # This is the 2nd commit message: feat: 實(shí)現(xiàn)加法函數(shù) add() # This is the 3rd commit message: fix: 修正 add 函數(shù)參數(shù)校驗(yàn) # Please enter the commit message for your changes. Lines starting # with # will be ignored, and an empty message aborts the commit.你可以看到它列出了所有將被合并的提交的原始信息?,F(xiàn)在你需要?jiǎng)h除所有行或者保留以#開頭的注釋行然后編寫一個(gè)新的、概括性的提交信息。一個(gè)好的提交信息應(yīng)該簡明扼要地說明這個(gè)提交塊做了什么。例如我們可以寫feat: 實(shí)現(xiàn)加法計(jì)算功能 - 創(chuàng)建 calculator.py 基礎(chǔ)模塊框架 - 實(shí)現(xiàn) add(a, b) 函數(shù)支持兩數(shù)相加 - 為 add 函數(shù)添加基本的參數(shù)類型校驗(yàn)保存并關(guān)閉這個(gè)編輯器。接著Git 會(huì)繼續(xù)處理后面兩個(gè)提交減法與文檔的合并并再次彈出編輯器讓你編寫第二個(gè)新提交的信息比如feat: 實(shí)現(xiàn)減法功能并補(bǔ)充文檔 - 實(shí)現(xiàn) sub(a, b) 函數(shù)支持兩數(shù)相減 - 為 calculator.py 模塊添加完整的函數(shù)注釋3.4 第四步完成與驗(yàn)證所有編輯步驟完成后Git 會(huì)完成整個(gè) Rebase 過程。你可以使用git log --oneline --graph來查看新的提交歷史* 5f6g7h8 (HEAD - feature/calculator) feat: 實(shí)現(xiàn)減法功能并補(bǔ)充文檔 * a1b2c3d feat: 實(shí)現(xiàn)加法計(jì)算功能 * x0y1z2a ... (之前的提交歷史)看原來雜亂無章的5個(gè)提交現(xiàn)在變成了兩個(gè)清晰、獨(dú)立的特性提交。整個(gè)代碼變更內(nèi)容一點(diǎn)沒少但歷史記錄清爽多了。4. 核心命令詳解pick, squash, fixup 的選擇藝術(shù)在交互式 Rebase 的編輯界面里選擇正確的命令是成功的關(guān)鍵。我們來深入理解一下這幾個(gè)最常用的命令。pick這是默認(rèn)命令。簡單來說就是“保留這個(gè)提交原封不動(dòng)”。當(dāng)你希望某個(gè)提交獨(dú)立存在時(shí)就用pick。通常你會(huì)pick那些代表一個(gè)完整邏輯步驟、值得單獨(dú)保留的提交。squash與fixup如何選擇這兩個(gè)命令都能合并提交核心區(qū)別在于如何處理被合并提交的日志信息。squash合并提交并且保留被合并提交的提交信息在下一步中這些信息會(huì)一起呈現(xiàn)給你供你編輯整合成一個(gè)新的信息。適用于多個(gè)提交共同完成一個(gè)功能且每個(gè)提交的信息都有參考價(jià)值你想在最終信息中體現(xiàn)它們。例如feat: A、feat: B、test: add for AB可以合并成一個(gè)大的特性提交。fixup合并提交但完全丟棄被合并提交的提交信息。適用于那些“修正前一個(gè)提交中的小錯(cuò)誤”的提交比如fix typo、adjust format。你肯定不希望最終的提交歷史里留下一堆“修復(fù)錯(cuò)別字”的記錄用fixup可以讓它們無聲無息地融入前一個(gè)提交。一個(gè)實(shí)用技巧fixup的自動(dòng)化如果你已經(jīng)提交了代碼突然發(fā)現(xiàn)有個(gè)小地方要改比如一個(gè)拼寫錯(cuò)誤傳統(tǒng)的流程是修改 -git commit --fixup TARGET_COMMIT_HASH。這個(gè)命令會(huì)創(chuàng)建一個(gè)提交其信息自動(dòng)標(biāo)記為fixup! 原提交信息。 之后當(dāng)你執(zhí)行g(shù)it rebase -i --autosquash HEAD~n時(shí)Git 會(huì)自動(dòng)為你將這些fixup!提交排序并設(shè)置為fixup命令極大地簡化了操作。這是保持歷史整潔的利器。reword這個(gè)命令非常有用。它允許你修改某個(gè)提交的提交信息而不改變其內(nèi)容。比如你pick了一個(gè)提交但后來覺得它的信息寫得不清楚就可以把pick改成reword。在 Rebase 過程中Git 會(huì)在應(yīng)用到那個(gè)提交時(shí)暫停讓你重新編輯提交信息。edit這個(gè)命令更強(qiáng)大。它會(huì)在應(yīng)用這個(gè)提交時(shí)暫停允許你修改提交的內(nèi)容比如增刪文件修改完后用git commit --amend提交修改然后用git rebase --continue繼續(xù)。通常用于拆分提交或修改舊提交中的代碼。5. 必須掌握的注意事項(xiàng)與避坑指南git rebase功能強(qiáng)大但使用不當(dāng)也會(huì)帶來麻煩。下面這些坑我?guī)缀醵疾冗^希望你能避開。5.1 黃金法則只 Rebase 未推送的本地提交這是最重要的一條規(guī)則。絕對不要對已經(jīng)推送到遠(yuǎn)程倉庫如 GitHub、GitLab的提交進(jìn)行 Rebase如果其他人可能已經(jīng)基于這些提交進(jìn)行了工作。為什么因?yàn)?Rebase 的本質(zhì)是“丟棄舊的提交創(chuàng)建一系列內(nèi)容相同但哈希值全新的提交”。對于本地分支這沒問題。但對于遠(yuǎn)程分支如果你強(qiáng)制推送 (git push --force) 這些新提交就會(huì)覆蓋遠(yuǎn)程歷史。其他協(xié)作者如果已經(jīng)拉取了你舊的提交他們的本地歷史會(huì)與遠(yuǎn)程歷史產(chǎn)生分歧在下次拉取或推送時(shí)遇到非常棘手的沖突通常需要他們手動(dòng)重置自己的分支這會(huì)給團(tuán)隊(duì)協(xié)作帶來災(zāi)難。安全的工作流在個(gè)人特性分支上盡情使用rebase來整理提交。在準(zhǔn)備合并如發(fā)起 Pull Request前確保你的分支是基于目標(biāo)分支如main的最新代碼rebase過的。如果在此期間目標(biāo)分支有更新使用git pull --rebase而不是git pull來合并更新這樣可以保持你的提交歷史是線性的避免不必要的合并提交。只有在你確認(rèn)你的分支歷史是整潔的、線性的并且只有你一個(gè)人在這個(gè)分支上工作時(shí)才考慮使用git push --force-with-lease比--force更安全來更新遠(yuǎn)程分支。在團(tuán)隊(duì)協(xié)作中對共享分支應(yīng)盡量避免強(qiáng)制推送。5.2 處理 Rebase 過程中的沖突在 Rebase 過程中當(dāng) Git 嘗試應(yīng)用某個(gè)提交時(shí)如果該提交的修改與當(dāng)前代碼狀態(tài)沖突它會(huì)暫停下來讓你解決沖突。這時(shí)你會(huì)看到類似這樣的提示Auto-merging calculator.py CONFLICT (content): Merge conflict in calculator.py error: could not apply abc1234... add: 創(chuàng)建 calculator.py 框架 Resolve all conflicts manually, mark them as resolved with git add/rm conflicted_files, then run git rebase --continue. You can instead skip this patch with git rebase --skip. To abort and go back to the original state, run git rebase --abort.解決步驟不要慌。使用git status查看哪些文件有沖突。打開沖突文件你會(huì)看到這樣的標(biāo)記。手動(dòng)編輯文件解決沖突保留你想要的代碼刪除這些標(biāo)記。解決完所有沖突后用git add file或git add .將文件標(biāo)記為已解決。運(yùn)行g(shù)it rebase --continue讓 Rebase 繼續(xù)。如果這個(gè)沖突的提交你不想處理了比如它已經(jīng)無關(guān)緊要可以用git rebase --skip跳過這個(gè)提交。慎用因?yàn)檫@等于丟棄了這個(gè)提交的所有更改。如果沖突太復(fù)雜你想放棄整個(gè) Rebase 操作回到開始之前的狀態(tài)運(yùn)行g(shù)it rebase --abort。這是你的安全繩。5.3 后悔了怎么辦使用 Reflog 救命萬一 Rebase 操作搞砸了或者合并后效果不理想是不是就完蛋了并不是。Git 有一個(gè)“時(shí)光機(jī)”叫做reflog。git reflog命令會(huì)顯示 HEAD 指針的所有移動(dòng)記錄。每一次提交、合并、rebase、reset 操作都會(huì)被記錄下來。找到你開始 Rebase 之前的那個(gè)狀態(tài)通常顯示為rebase -i (start)之前的一次操作記下它的哈希值或引用如HEAD{2}。然后簡單地使用git reset --hard HEAD{2}將2替換成對應(yīng)的數(shù)字就可以將你的分支硬重置到 Rebase 之前的狀態(tài)。這是一個(gè)非常強(qiáng)大的回退工具能讓你在誤操作后從容恢復(fù)。6. 進(jìn)階技巧更復(fù)雜的提交歷史整理掌握了基礎(chǔ)合并后你可以嘗試一些更高級(jí)的操作讓你的提交歷史像藝術(shù)品一樣精致。6.1 拆分提交有時(shí)一個(gè)提交包含了兩個(gè)不相關(guān)的修改你想把它拆成兩個(gè)獨(dú)立的提交。這需要用到edit命令。在交互式 Rebase 列表中找到你想拆分的提交將其命令從pick改為edit。Rebase 過程會(huì)在應(yīng)用這個(gè)提交后暫停。運(yùn)行g(shù)it reset HEAD~。這是一個(gè)混合重置它會(huì)撤銷提交但保留所有更改在工作目錄中?,F(xiàn)在你可以選擇性地添加文件到暫存區(qū)。例如先把與功能A相關(guān)的文件git add進(jìn)去然后git commit -m “feat: add A”。接著再把與功能B相關(guān)的文件git add進(jìn)去然后git commit -m “feat: add B”。完成拆分后運(yùn)行g(shù)it rebase --continue繼續(xù)后續(xù)的 Rebase 步驟。6.2 重新排序提交在交互式 Rebase 的編輯界面里你完全可以通過移動(dòng)行來改變提交的順序。比如你把一個(gè)修復(fù) Bug 的提交移到引入該 Bug 的提交之前這在邏輯上是不通的Git 會(huì)在應(yīng)用時(shí)產(chǎn)生大量沖突。但如果你把幾個(gè)獨(dú)立的、修改不同文件的提交調(diào)整順序通??梢皂樌M(jìn)行。這在你希望按邏輯主題而非時(shí)間順序組織歷史時(shí)很有用。6.3 徹底刪除提交如果你想完全丟棄某個(gè)提交及其帶來的所有更改在交互式 Rebase 列表里直接刪除那一整行即可?;蛘吣阋部梢詫⒚罡臑閐rop。保存退出后那個(gè)提交就會(huì)從歷史中消失。這是一個(gè)破壞性操作確保你真的不需要那些更改。7. 與其它工作流的結(jié)合讓 Rebase 成為習(xí)慣git rebase不是孤立使用的它應(yīng)該融入你日常的 Git 工作流中。git pull --rebase代替git pull。git pull的默認(rèn)行為是fetchmerge這會(huì)在你的歷史中產(chǎn)生一個(gè)多余的合并提交。而git pull --rebase則是fetchrebase它會(huì)將你的本地提交“變基”到遠(yuǎn)程分支的最新提交之上從而保持歷史是一條干凈的直線。你可以通過git config --global pull.rebase true將其設(shè)為默認(rèn)行為。在特性分支開發(fā)中從main分支切出新分支feature/x。在feature/x上進(jìn)行多次小提交。在完成開發(fā)后、合并回main之前使用git rebase -i整理和合并提交。切換到main分支拉取最新代碼 (git pull --rebase)。切換回feature/x執(zhí)行g(shù)it rebase main將你的特性分支變基到main的最新提交上解決可能出現(xiàn)的沖突。這確保了你的特性分支是可以干凈合并的。切換到main執(zhí)行g(shù)it merge feature/x如果允許可以使用--no-ff保留分支信息。由于你已經(jīng) rebase 過這通常是一個(gè)快進(jìn)合并非常干凈。這個(gè)過程確保了主分支的歷史清晰、線性并且每個(gè)合并進(jìn)來的特性都經(jīng)過了整理易于追溯和回滾。經(jīng)過這樣一番操作你的 Git 提交歷史將不再是雜亂無章的日記草稿而是一份結(jié)構(gòu)清晰、目的明確的工程日志。這不僅僅是個(gè)人習(xí)慣的優(yōu)化更是在團(tuán)隊(duì)協(xié)作中傳遞專業(yè)性和尊重的一種方式。剛開始可能會(huì)覺得步驟繁瑣但一旦形成肌肉記憶它將成為你開發(fā)流程中自然而然、不可或缺的一環(huán)。記住工具是為人服務(wù)的大膽去用謹(jǐn)慎操作遇到問題還有reflog這把萬能鑰匙。