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