<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Version Control on kenji.blog</title><link>http://kenji.blog/zh-tw/tags/version-control/</link><description>Recent content in Version Control on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>zh-tw</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 09:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/zh-tw/tags/version-control/index.xml" rel="self" type="application/rss+xml"/><item><title>【Git 指令】rebase 與 merge 的差異與實務上的正確使用時機</title><link>http://kenji.blog/zh-tw/p/git-rebase-vs-merge-practical-guide/</link><pubDate>Sun, 13 Sep 2026 09:00:00 +0900</pubDate><guid>http://kenji.blog/zh-tw/p/git-rebase-vs-merge-practical-guide/</guid><description>&lt;img src="http://kenji.blog/p/git-rebase-vs-merge-practical-guide/img/eyecatch.jpg" alt="Featured image of post 【Git 指令】rebase 與 merge 的差異與實務上的正確使用時機" />&lt;h1 id="1-前言為什麼要用-merge-還是-rebase是個永遠的課題">1. 前言：為什麼「要用 merge 還是 rebase」是個永遠的課題
&lt;/h1>&lt;p>Git 是現代軟體開發中不可或缺的版本控制系統。當多位開發者同時修改程式碼庫時，Git 強大的分支模型便能發揮其威力。然而，在團隊開發中，「該使用 &lt;code>merge&lt;/code> 還是 &lt;code>rebase&lt;/code>」的爭論，是從初學者到老手都經常煩惱的議題之一。&lt;/p>
&lt;p>本文將從 Git 的內部結構：DAG（有向無環圖）以及提交（Commit）雜湊（Hash）的數學性質出發，深入探討 &lt;code>git merge&lt;/code> 與 &lt;code>git rebase&lt;/code> 的機制差異。接著，將配合具體的工作流程，徹底解說在實務上該如何正確區分使用它們。這不僅僅是介紹指令，透過理解 Git 在背後進行了哪些計算，你將不再對衝突（Conflict）感到恐懼，並能建立出乾淨且可追蹤的歷史紀錄。&lt;/p>
&lt;hr>
&lt;h1 id="2-git-的內部結構提交雜湊與物件模型">2. Git 的內部結構：提交雜湊與物件模型
&lt;/h1>&lt;p>為了理解 Git 是如何整合歷史紀錄的，我們必須先了解 Git 是如何儲存資料的。Git 並不只是儲存檔案修改的差異（Patch），而是儲存特定時間點整個檔案系統的快照（Snapshot）。&lt;/p>
&lt;h2 id="21-提交雜湊的密碼學性質">2.1 提交雜湊的密碼學性質
&lt;/h2>&lt;p>Git 的每一個提交，都是透過 SHA-1（Secure Hash Algorithm 1）雜湊函數，根據其內容計算出一組 40 個字元的十六進位數字來唯一識別。提交物件（Commit Object）由以下元素組成：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>指向 Tree 物件的指標&lt;/strong>：該時間點的目錄結構與檔案（Blob）的快照&lt;/li>
&lt;li>&lt;strong>指向父提交的指標&lt;/strong>：一個或多個父提交的雜湊值（首次提交沒有父提交，而合併提交則有兩個以上的父提交）&lt;/li>
&lt;li>&lt;strong>作者資訊（Author）&lt;/strong>：撰寫程式碼的人與時間&lt;/li>
&lt;li>&lt;strong>提交者資訊（Committer）&lt;/strong>：建立與套用提交的人與時間&lt;/li>
&lt;li>&lt;strong>提交訊息（Commit Message）&lt;/strong>：說明修改意圖的文字&lt;/li>
&lt;/ol>
&lt;p>用數學來表示，對於提交物件 $C$，其雜湊值 $H(C)$ 定義如下：&lt;/p>
$$
H(C) = \text{SHA-1}( \text{tree} \parallel \text{parent} \parallel \text{author} \parallel \text{committer} \parallel \text{message} )
$$&lt;p>這裡的 $\parallel$ 代表資料的串接。由於雜湊函數的特性，即使只改變了提交訊息中的一個字元，或是父提交不同，都會產生完全不同的雜湊值。也就是說，&lt;strong>提交是不可變的（Immutable）&lt;/strong>。後文將提到 &lt;code>rebase&lt;/code> 常被說成是「改寫歷史紀錄」，實際上它是「建立內容相似但雜湊值不同的新提交」。&lt;/p>
&lt;p>雜湊空間的大小為 $2^{160}$，利用生日攻擊（Birthday Paradox）的理論，可以近似計算出發生碰撞（Collision，即不同提交擁有相同雜湊值）的機率 $P$（$n$ 為提交數量）：&lt;/p>
$$
P(\text{collision}) \approx 1 - \exp\left(-\frac{n^2}{2 \times 2^{160}}\right)
$$&lt;p>這個機率極低，在實務上 Git 的提交雜湊幾乎不可能發生碰撞。&lt;/p>
&lt;hr>
&lt;h1 id="3-圖論與-daggit-歷史的數學模型">3. 圖論與 DAG：Git 歷史的數學模型
&lt;/h1>&lt;p>Git 的提交歷史，在圖論中被模型化為「有向無環圖（Directed Acyclic Graph, DAG）」。&lt;/p>
&lt;h2 id="31-什麼是-dag有向無環圖">3.1 什麼是 DAG（有向無環圖）
&lt;/h2>&lt;p>在圖 $G = (V, E)$ 中，$V$ 是提交的集合（頂點），$E$ 是表示提交之間父子關係的有向邊集合。在 Git 中，邊的方向是「從子提交指向父提交」。這是因為新的提交會保存指向過去提交的指標。&lt;/p>
&lt;pre class="mermaid">
graph BT
A[&amp;#34;Commit A (初始)&amp;#34;]
B[&amp;#34;Commit B&amp;#34;]
C[&amp;#34;Commit C (主分支)&amp;#34;]
D[&amp;#34;Commit D (功能分支)&amp;#34;]
E[&amp;#34;Commit E (合併)&amp;#34;]
B --&amp;gt; A
C --&amp;gt; B
D --&amp;gt; B
E --&amp;gt; C
E --&amp;gt; D
&lt;/pre>
&lt;p>DAG 最大的特徵是「不存在循環（Cycle）」。因此，追溯提交歷史的演算法不會陷入無限迴圈，並且能確實到達終點（初始提交）。&lt;/p>
&lt;h2 id="32-拓撲排序與歷史順序">3.2 拓撲排序與歷史順序
&lt;/h2>&lt;p>當我們使用 &lt;code>git log&lt;/code> 等指令顯示歷史紀錄時，DAG 會透過拓撲排序（Topological Sort）演算法被排序為一維的列表。對於 DAG 中任意的有向邊 $u \to v$（$u$ 為 $v$ 的子節點），在列表中 $u$ 會被排在 $v$ 的前面。&lt;/p>
&lt;hr>
&lt;h1 id="4-git-merge-的機制與種類">4. git merge 的機制與種類
&lt;/h1>&lt;p>整合分支變更最基本的指令就是 &lt;code>git merge&lt;/code>。然而，根據目前的狀態，Git 會自動選擇不同的合併策略。&lt;/p>
&lt;h2 id="41-快轉合併fast-forward---ff">4.1 快轉合併（Fast-Forward, &amp;ndash;ff）
&lt;/h2>&lt;p>如果整合目標（例如：&lt;code>main&lt;/code>）分支是整合來源（例如：&lt;code>feature&lt;/code>）分支的直接祖先，Git 就會執行「快轉（Fast-Forward）」合併。這是一個不會建立新提交，只是單純將分支指標向前推進的操作。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
merge feature
&lt;/pre>
&lt;p>快轉合併能讓歷史紀錄保持一直線，但缺點是會失去「哪些提交是屬於同一個功能開發（feature）」的上下文資訊。&lt;/p>
&lt;h2 id="42-非快轉合併non-fast-forward---no-ff">4.2 非快轉合併（Non-Fast-Forward, &amp;ndash;no-ff）
&lt;/h2>&lt;p>如果明確指定 &lt;code>git merge --no-ff&lt;/code>，即使在可以快轉合併的情況下，也必定會建立一個新的「合併提交（Merge Commit）」。合併提交是擁有兩個父節點的特殊提交。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
commit id: &amp;#34;Main 工作 1&amp;#34;
merge feature type: NORMAL
&lt;/pre>
&lt;p>這種方法的優點是，功能分支的存在與歷史會明確地保留在 DAG 上。當發生問題時，只要執行 &lt;code>git revert -m 1 &amp;lt;合併提交的雜湊&amp;gt;&lt;/code>，就可以安全地一次將整個功能取消（Revert）。&lt;/p>
&lt;h2 id="43-三方合併3-way-merge演算法">4.3 三方合併（3-Way Merge）演算法
&lt;/h2>&lt;p>當整合目標與整合來源的分支各自擁有獨立的提交時，Git 會執行三方合併。此時，Git 會探索 DAG，並找出兩個分支的「共同祖先（Lowest Common Ancestor, LCA）」。&lt;/p>
&lt;p>找出 LCA 的演算法時間複雜度 $T_{\text{LCA}}$，相對於頂點數 $|V|$ 與邊數 $|E|$，可以在線性時間內執行完成：&lt;/p>
$$
T_{\text{LCA}} = \mathcal{O}(|V| + |E|)
$$&lt;p>Git 會比較「LCA 的狀態」、「目前分支的狀態」與「對方分支的狀態」這三者，若變更沒有衝突，就會自動產生合併提交。&lt;/p>
&lt;hr>
&lt;h1 id="5-git-rebase-的機制與歷史的重建">5. git rebase 的機制與歷史的重建
&lt;/h1>&lt;p>相較於 &lt;code>git merge&lt;/code> 是將歷史「整合」，&lt;code>git rebase&lt;/code> 則是將歷史「重建（移花接木）」。&lt;/p>
&lt;h2 id="51-rebase-背後的運作方式">5.1 Rebase 背後的運作方式
&lt;/h2>&lt;p>將 &lt;code>feature&lt;/code> 分支變基到 &lt;code>main&lt;/code> 分支（&lt;code>git rebase main&lt;/code>）時，內部的運作方式如下：&lt;/p>
&lt;ol>
&lt;li>找出 &lt;code>feature&lt;/code> 分支與 &lt;code>main&lt;/code> 分支的共同祖先（LCA）。&lt;/li>
&lt;li>將從 LCA 到 &lt;code>feature&lt;/code> 分支最前端的提交差異，儲存到暫存區域。&lt;/li>
&lt;li>將 &lt;code>feature&lt;/code> 分支的指標移動到 &lt;code>main&lt;/code> 分支的最前端。&lt;/li>
&lt;li>將儲存的差異，依序一個個套用（Cherry-Pick）到新的基底（&lt;code>main&lt;/code> 的最前端）上，並產生新的提交。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;Commit A&amp;#34;] --&amp;gt; B[&amp;#34;Commit B&amp;#34;]
B --&amp;gt; C[&amp;#34;Commit C (主分支)&amp;#34;]
B --&amp;gt; D[&amp;#34;Commit D (舊功能分支)&amp;#34;]
D -.-&amp;gt; E[&amp;#34;Commit D&amp;#39; (新功能分支)&amp;#34;]
C --&amp;gt; E
style D stroke-dasharray: 5 5, fill: #f9f9f9, color: #999
&lt;/pre>
&lt;p>這裡重要的是，透過 rebase 產生的提交 $D'$，&lt;strong>因為父提交與原本的提交 $D$ 不同，所以會有完全不同的雜湊值&lt;/strong>（請參考前面提到的雜湊函數定義 $H(C)$）。&lt;/p>
&lt;h2 id="52-互動式變基interactive-rebase">5.2 互動式變基（Interactive Rebase）
&lt;/h2>&lt;p>使用 &lt;code>git rebase -i&lt;/code>（或 &lt;code>--interactive&lt;/code>），可以隨心所欲地操作提交歷史。這是用來整理本地端歷史紀錄最強大的工具。&lt;/p>
&lt;ul>
&lt;li>&lt;code>pick&lt;/code>：直接採用該提交&lt;/li>
&lt;li>&lt;code>reword&lt;/code>：只修改提交訊息&lt;/li>
&lt;li>&lt;code>edit&lt;/code>：暫停以修改提交內容&lt;/li>
&lt;li>&lt;code>squash&lt;/code>：將此提交與前一個提交融合，並合併兩者的訊息&lt;/li>
&lt;li>&lt;code>fixup&lt;/code>：與 &lt;code>squash&lt;/code> 相同，但會捨棄此提交的訊息&lt;/li>
&lt;li>&lt;code>drop&lt;/code>：完全刪除該提交&lt;/li>
&lt;/ul>
&lt;p>從數學的角度來看，如果一個分支上有 $N$ 個提交，透過 rebase 改變順序所能產生的線性歷史排列組合（排列）$P$ 如下：&lt;/p>
$$
P = N!
$$&lt;p>Git 給予了開發者 $N!$ 種自由，使其能夠保持歷史紀錄在合乎邏輯且優美的狀態。&lt;/p>
&lt;hr>
&lt;h1 id="6-rebase-的黃金法則the-golden-rule-of-rebase">6. Rebase 的黃金法則（The Golden Rule of Rebase）
&lt;/h1>&lt;p>&lt;code>rebase&lt;/code> 雖然非常強大，但存在一個絕對的規則。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>「絕對不要對已經公開的歷史紀錄進行 rebase」&lt;/strong>
&lt;em>(Never rebase public history)&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;h2 id="61-為什麼不能對公開的歷史紀錄進行-rebase">6.1 為什麼不能對公開的歷史紀錄進行 rebase？
&lt;/h2>&lt;p>Git 是分散式的。你推送到 &lt;code>origin/main&lt;/code> 的提交，也會被複製（Clone）到其他開發者的本地端儲存庫中。如果你對已經推送的提交進行 rebase 來改寫歷史，並且使用 &lt;code>git push --force&lt;/code> 強制覆蓋，會發生什麼事呢？&lt;/p>
&lt;p>其他開發者本地端的 DAG 會與遠端的 DAG 產生根本性的分歧。當其他開發者執行 &lt;code>git pull&lt;/code> 時，Git 會試圖強制合併擁有不同歷史的提交群，導致產生大量衝突或重複的提交（內容相同但雜湊不同的提交），讓儲存庫陷入混亂狀態。&lt;/p>
&lt;p>Rebase 的鐵則就是**只能對「尚未與任何人共享的本地端分支」**執行。&lt;/p>
&lt;hr>
&lt;h1 id="7-解決衝突conflict與-git-rebase---continue">7. 解決衝突（Conflict）與 git rebase &amp;ndash;continue
&lt;/h1>&lt;p>當多個人修改了同一個檔案的同一個地方時，就會發生衝突。&lt;code>merge&lt;/code> 與 &lt;code>rebase&lt;/code> 在解決衝突的過程上有所不同。&lt;/p>
&lt;h2 id="71-merge-中的衝突解決">7.1 Merge 中的衝突解決
&lt;/h2>&lt;p>在使用 &lt;code>git merge&lt;/code> 的情況下，衝突解決&lt;strong>只會發生一次&lt;/strong>。在建立最終的合併提交之前，會一次性修正所有衝突的地方。&lt;/p>
&lt;h2 id="72-rebase-中的衝突解決">7.2 Rebase 中的衝突解決
&lt;/h2>&lt;p>在使用 &lt;code>git rebase&lt;/code> 的情況下，由於是將提交一個一個重新套用，因此&lt;strong>每個提交都有可能發生衝突&lt;/strong>。&lt;/p>
&lt;p>在 rebase 過程中若發生衝突，Git 會暫停處理。解決的流程如下：&lt;/p>
&lt;ol>
&lt;li>開啟編輯器或 IDE（例如 VS Code），手動修正衝突標記（&lt;code>&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code>、&lt;code>======&lt;/code>、&lt;code>&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code>）。&lt;/li>
&lt;li>將修正好的檔案加入索引：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add &amp;lt;修正的檔案&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>不建立提交，直接恢復 rebase 處理：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>如果是想要取消 rebase 本身並恢復到原本的狀態，則執行以下指令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --abort
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>（※如果不需要解決衝突，想直接略過該提交，則使用 &lt;code>git rebase --skip&lt;/code>）&lt;/p>
&lt;hr>
&lt;h1 id="8-實務上的正確使用時機工作流程實踐">8. 實務上的正確使用時機（工作流程實踐）
&lt;/h1>&lt;p>那麼，在實際的開發現場中，應該如何區分使用 &lt;code>merge&lt;/code> 和 &lt;code>rebase&lt;/code> 呢？這裡介紹最標準且安全的做法。&lt;/p>
&lt;h2 id="81-情境-1整理本地端的工作歷史使用-rebase">8.1 【情境 1】整理本地端的工作歷史（使用 Rebase）
&lt;/h2>&lt;p>假設在功能分支開發中，累積了許多細碎的提交（例如「修正拼寫錯誤」、「暫存」等）。在發出發布請求（Pull Request, PR）之前，為了將這些提交整理成有意義的單位，可以使用互動式變基。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 feature 分支的狀態下執行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （編輯器會開啟，使用 squash 或 fixup 來讓歷史變得乾淨）&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>這樣一來，就能建立出讓審查者更容易理解意圖、優美的提交歷史。&lt;/p>
&lt;h2 id="82-情境-2跟上最新的-main-分支使用-rebase">8.2 【情境 2】跟上最新的 main 分支（使用 Rebase）
&lt;/h2>&lt;p>當開發時間拉長，&lt;code>main&lt;/code> 分支不斷被合併了其他人的變更，自己的 &lt;code>feature&lt;/code> 分支就會變舊。在這種情況下，將 &lt;code>feature&lt;/code> 分支 rebase 到最新的 &lt;code>main&lt;/code> 上以保持同步。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 取得 main 的最新資訊&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 將 feature 分支重新接到最新的 main 之上&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase origin/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>這樣可以讓歷史保持一直線，並防止後續合併時發生衝突。同時也能避免產生多餘的合併提交（&amp;ldquo;Merge branch &amp;lsquo;main&amp;rsquo; into feature&amp;rdquo;）。&lt;/p>
&lt;h2 id="83-情境-3整合完成的功能使用-merge">8.3 【情境 3】整合完成的功能（使用 Merge）
&lt;/h2>&lt;p>當 &lt;code>feature&lt;/code> 分支上的開發完成，終於要整合到 &lt;code>main&lt;/code> 分支的階段時。這裡要使用 &lt;strong>&lt;code>git merge --no-ff&lt;/code>&lt;/strong>（這等同於在 GitHub 等的 Pull Request 中選擇「Create a merge commit」）。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge --no-ff feature -m &lt;span class="s2">&amp;#34;Merge feature: 實作使用者登入功能&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如此一來，&lt;code>main&lt;/code> 分支的 DAG 上就會留下「在這裡合併了一個功能」的歷史節點（合併提交）。日後回顧歷史時，就能以功能為單位輕鬆追蹤程式碼。&lt;/p>
&lt;hr>
&lt;h1 id="9-結論總結">9. 結論（總結）
&lt;/h1>&lt;p>在 Git 的操作中，「全部都用 Merge 解決」或是「全部都用 Rebase 弄成一直線」這類極端的做法，各自都有其優缺點。&lt;/p>
&lt;p>實務上的最佳實踐是**「本地端的私人歷史用 rebase 優美地整理，公開的整合歷史用 merge &amp;ndash;no-ff 保留上下文」**這種混合式的方法。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>本地端（個人的工作空間）&lt;/strong>：使用 &lt;code>rebase&lt;/code> 排除無謂的提交，並跟上最新的主線，維持一直線的歷史。&lt;/li>
&lt;li>&lt;strong>全域（共享的工作空間）&lt;/strong>：使用 &lt;code>merge --no-ff&lt;/code> 將功能分支的存在作為合併提交記錄在 DAG 上，讓 revert 或追蹤變得更容易。&lt;/li>
&lt;/ul>
&lt;p>透過理解 DAG 的結構與雜湊函數機制的數學及架構背景，Git 指令就不再只是單純的死記硬背，而是昇華為「有目的的歷史紀錄設計」。請遵守 rebase 的黃金法則，並根據情況選擇最合適的指令，建立出對整個團隊來說易讀且易於維護的乾淨提交歷史吧。&lt;/p></description></item><item><title>Git初學者容易犯的錯誤與解決指令集（解決衝突等）</title><link>http://kenji.blog/zh-tw/p/git-beginners-mistakes-and-solutions/</link><pubDate>Sat, 12 Sep 2026 17:00:00 +0900</pubDate><guid>http://kenji.blog/zh-tw/p/git-beginners-mistakes-and-solutions/</guid><description>&lt;img src="http://kenji.blog/p/git-beginners-mistakes-and-solutions/img/eyecatch.jpg" alt="Featured image of post Git初學者容易犯的錯誤與解決指令集（解決衝突等）" />&lt;h1 id="git初學者容易犯的錯誤與解決指令集解決衝突等">Git初學者容易犯的錯誤與解決指令集（解決衝突等）
&lt;/h1>&lt;h2 id="1-前言為什麼會在-git-中犯錯">1. 前言：為什麼會在 Git 中犯錯？
&lt;/h2>&lt;p>在軟體開發中，Git 已經成為像空氣和水一樣不可或缺的存在。然而，對於許多初學者（有時甚至是熟練者）來說，Git 感覺就像是一個「可怕的魔法黑盒子」。提交消失、將大量變更推入錯誤的分支、從未見過的衝突錯誤訊息佔滿螢幕……。當陷入這些「Git 的陷阱」時，工作進度會完全停滯，最糟糕的情況下，甚至會產生原始碼被破壞的恐懼。&lt;/p>
&lt;p>為什麼 Git 會如此困難且容易誘發錯誤呢？最大的原因是「沒有理解 Git 內部發生了什麼，只是死記硬背表面的指令來使用」。Git 是基於分散式版本控制系統（DVCS）的堅固設計理念，但它的介面（CLI）並不總是直觀的。&lt;/p>
&lt;p>在本文中，我們將把 Git 初學者在工作現場經常遇到的「失誤（常見錯誤）」進行分類，並提供每個情況的具體解決方案指令。但這不會只是一份單純的指令清單（備忘錄）。我們將透過 &lt;code>.git&lt;/code> 目錄的結構、背後運行的 Diff 演算法的數學背景，以及 Mermaid 圖解，以超過 10,000 字的篇幅徹底深入探討「為什麼會發生這個錯誤」、「執行該指令時 Git 內部資料是如何移動的」。&lt;/p>
&lt;p>當你讀完這篇文章時，你應該能從「害怕 Git」的情緒中解脫出來，反而會確信「沒有比 Git 更可靠的夥伴了」。那麼，讓我們潛入 Git 的深淵世界吧。&lt;/p>
&lt;hr>
&lt;h2 id="2-git-的深淵理解-git-目錄的內部結構">2. Git 的深淵：理解 &lt;code>.git&lt;/code> 目錄的內部結構
&lt;/h2>&lt;p>許多疑難排解變得容易的第一步，是了解 Git 如何儲存資料。存在於你專案根目錄中的隱藏資料夾 &lt;code>.git&lt;/code>，正是 Git 的心臟地帶。Git 並非只是一個依序記錄檔案差異（patch）的系統，而是將資料作為&lt;strong>快照（snapshot）流&lt;/strong>來管理。&lt;/p>
&lt;h3 id="21-物件模型blob-tree-commit">2.1 物件模型：Blob, Tree, Commit
&lt;/h3>&lt;p>Git 主要使用 3 種物件來表示儲存庫的狀態。這些物件儲存在 &lt;code>.git/objects&lt;/code> 中。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Blob (Binary Large Object)&lt;/strong>
這是儲存檔案內容本身的物件。檔案名稱或權限的資訊不包含在這裡。純粹的位元組陣列會被 zlib 壓縮，並由 SHA-1 雜湊值（40 個字元的十六進位數字）來識別。&lt;/li>
&lt;li>&lt;strong>Tree&lt;/strong>
表示目錄結構的物件。Tree 物件包含指向其他 Tree 物件（子目錄）或 Blob 物件（檔案）的指標（SHA-1 雜湊值），以及它們的檔案名稱、存取權限。它扮演類似 UNIX 目錄的角色。&lt;/li>
&lt;li>&lt;strong>Commit&lt;/strong>
包含指向特定時間點儲存庫整體頂層 Tree 物件的指標、元資料（作者、提交時間、提交訊息），以及指向前一個提交（父提交）的指標。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
Commit1[&amp;#34;提交 (Hash: 9f8a)&amp;#34;] --&amp;gt; Tree1[&amp;#34;樹 (Hash: 4b82)&amp;#34;]
Tree1 --&amp;gt; Blob1[&amp;#34;Blob (Hash: 8d7e) : index.js&amp;#34;]
Tree1 --&amp;gt; Tree2[&amp;#34;樹 (Hash: 3a2c) : src/&amp;#34;]
Tree2 --&amp;gt; Blob2[&amp;#34;Blob (Hash: 5f1b) : app.js&amp;#34;]
&lt;/pre>
&lt;h3 id="22-head-與參照refs的真面目">2.2 HEAD 與參照（Refs）的真面目
&lt;/h3>&lt;p>在 Git 中工作時，會頻繁看到 &lt;code>HEAD&lt;/code> 這個單字。這是指目前簽出（checkout）的分支（或提交）的&lt;strong>符號參照（Symbolic Reference）&lt;/strong>。
如果用文字編輯器打開 &lt;code>.git/HEAD&lt;/code> 這個檔案，會看到以下字串。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ref: refs/heads/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>這意味著「目前的狀態位於 &lt;code>main&lt;/code> 分支的頂端」。然後，當打開 &lt;code>.git/refs/heads/main&lt;/code> 時，會看到那裡寫著 40 個字元的 SHA-1 雜湊值，這就指向了最新的 Commit 物件。
Git 的分支，不過是一個指向特定提交的輕量級指標（檔案）而已。只要知道這個事實，就能消除「刪除分支是不是所有檔案都會消失？」的恐懼。&lt;/p>
&lt;hr>
&lt;h2 id="3-用數學解讀-gitdiff-演算法與雜湊函數">3. 用數學解讀 Git：Diff 演算法與雜湊函數
&lt;/h2>&lt;p>當 Git 檢測到衝突或顯示檔案差異時，內部正在運行進階的演算法。&lt;/p>
&lt;h3 id="31-myers-的-diff-演算法">3.1 Myers 的 Diff 演算法
&lt;/h3>&lt;p>Git 預設的差異檢測演算法是由 Eugene W. Myers 發明的。當有兩個文字檔 $A$ 和 $B$ 時，找到將 $A$ 轉換為 $B$ 的「最小編輯步驟（插入與刪除）」的問題，可以建模為圖論中的最短路徑問題。&lt;/p>
&lt;p>假設字串的長度分別為 $N, M$，總和為 $V = N + M$。在 Myers 演算法中，會尋找編輯距離（Edit Distance） $D$。這個演算法的時間複雜度可以用以下公式表示：&lt;/p>
$$ \mathcal{O}(V \cdot D) $$&lt;p>這裡，如果檔案之間的差異很小（也就是 $D$ 很小），演算法的執行速度會非常快，達到 $\mathcal{O}(V)$。然而，如果檔案完全不同，則 $D \approx V$，最壞情況下的時間複雜度會變成 $\mathcal{O}(V^2)$。&lt;/p>
&lt;h3 id="32-patience-diff-與-histogram-diff">3.2 Patience Diff 與 Histogram Diff
&lt;/h3>&lt;p>雖然 Myers 演算法很優秀，但當函數或類別的順序發生大幅改變時，它可能會產生對人類來說不直觀（無意義）的差異。為了解決這個問題，Git 實作了 &lt;code>Patience Diff&lt;/code> 和 &lt;code>Histogram Diff&lt;/code>。&lt;/p>
&lt;p>Patience Diff 著眼於「在兩個檔案中只出現過一次的唯一行」，並找出它們的最長共同子序列（Longest Common Subsequence: LCS）。假設唯一元素的數量為 $U$，計算 LCS 的複雜度可以用以下公式來解決：&lt;/p>
$$ \mathcal{O}(U \log U) $$&lt;p>當你覺得解決衝突很困難時，使用 &lt;code>git diff --histogram&lt;/code>，或者在合併策略中指定這個演算法（&lt;code>git merge -s recursive -X histogram&lt;/code>）是一個好方法。&lt;/p>
&lt;h3 id="33-sha-1-與碰撞機率">3.3 SHA-1 與碰撞機率
&lt;/h3>&lt;p>Git 使用 SHA-1 雜湊值來管理所有物件。雜湊空間的大小為 $2^{160}$。關於雜湊碰撞（不同的內容擁有相同的雜湊值）發生的機率，如果我們使用生日悖論（Birthday Paradox）來近似，碰撞機率 $p$ 達到 50% 所需的物件數量 $k$ 如下：&lt;/p>
$$ k \approx \sqrt{2 \ln(2)} \cdot 2^{80} \approx 1.2 \times 2^{80} $$&lt;p>這是一個天文數字，在一般的軟體開發中，無意間發生碰撞的機率幾乎為零。因此，Git 依賴雜湊值作為「絕對唯一 ID」來運作。&lt;/p>
&lt;hr>
&lt;h2 id="4-案例研究-1不小心提交到了錯誤的分支">4. 案例研究 1：不小心提交到了錯誤的分支！
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
沒注意到自己正在 &lt;code>main&lt;/code> 分支上工作，就拼命寫了新功能的程式碼，甚至還執行了 &lt;code>git commit&lt;/code>。原本應該要建立一個名為 &lt;code>feature/login&lt;/code> 的分支並在那裡工作的啊！&lt;/p>
&lt;h3 id="解決方法使用-git-reset-與建立分支">解決方法：使用 &lt;code>git reset&lt;/code> 與建立分支
&lt;/h3>&lt;p>在 Git 中，提交是獨立的物件，而分支只是指標。因此，只要執行「先建立新分支，再將目前分支的指標倒退」的操作，就能瞬間解決。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 建立一個指向目前提交（不小心建立的提交）的新分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git branch feature/login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 將 main 分支的指標倒退到前一個提交（HEAD~1）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 使用 --keep 可以保留工作目錄中未提交的變更並安全地重設。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --keep HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 切換到正確的分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="圖解內部發生了什麼事">圖解：內部發生了什麼事？
&lt;/h3>&lt;p>讓我們使用 Mermaid 的 &lt;code>gitGraph&lt;/code> 來視覺化這時分支指標的移動。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;初始提交&amp;#34;
commit id: &amp;#34;錯誤修復&amp;#34;
commit id: &amp;#34;誤操作的提交&amp;#34; type: HIGHLIGHT
branch feature/login
checkout feature/login
checkout main
&lt;/pre>
&lt;p>一開始 &lt;code>main&lt;/code> 和 &lt;code>HEAD&lt;/code> 都指向「誤操作的提交」，但透過 &lt;code>git branch feature/login&lt;/code>，在那裡建立了一個新的指標。之後透過 &lt;code>git reset&lt;/code>，只有 &lt;code>main&lt;/code> 指標回到了「錯誤修復」的位置。物件本身並沒有被刪除任何東西。&lt;/p>
&lt;hr>
&lt;h2 id="5-案例研究-2想要取消已經推送的提交">5. 案例研究 2：想要取消已經推送的提交！
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
在深夜情緒高昂時提交了充滿 bug 的程式碼，而且還用 &lt;code>git push origin main&lt;/code> 公開到了遠端儲存庫。發現重大 bug 後臉都綠了。&lt;/p>
&lt;h3 id="解決方案-1抹除歷史的-git-revert推薦安全">解決方案 1：抹除歷史的 &lt;code>git revert&lt;/code>（推薦・安全）
&lt;/h3>&lt;p>在團隊開發中，嚴禁使用 &lt;code>git reset&lt;/code> 等方法去篡改已經推送的提交歷史。這會導致與其他開發者的本機儲存庫失去一致性。正確的方法是，&lt;strong>「建立一個新的提交來完全抵消那個錯誤提交的變更」&lt;/strong>。這就是 &lt;code>git revert&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 建立一個抵消最新提交的提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git revert HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>main 7f3a8b2&lt;span class="o">]&lt;/span> Revert &lt;span class="s2">&amp;#34;錯誤提交的訊息&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="m">1&lt;/span> file changed, &lt;span class="m">1&lt;/span> insertion&lt;span class="o">(&lt;/span>+&lt;span class="o">)&lt;/span>, &lt;span class="m">10&lt;/span> deletions&lt;span class="o">(&lt;/span>-&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 推送到遠端&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;提交 A&amp;#34;
commit id: &amp;#34;提交 B (錯誤)&amp;#34;
commit id: &amp;#34;還原提交 B&amp;#34; type: REVERSE
&lt;/pre>
&lt;p>歷史會繼續前進，只有程式碼的狀態會恢復原狀。&lt;/p>
&lt;h3 id="解決方案-2篡改歷史的-git-push---force-with-lease">解決方案 2：篡改歷史的 &lt;code>git push --force-with-lease&lt;/code>
&lt;/h3>&lt;p>如果你是在只有你一個人使用的分支上，在剛推送之後，重寫歷史是可以被容許的。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在本機重設提交並進行修改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --hard HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;Correct implementation&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 強制覆寫遠端的歷史&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin feature/login --force-with-lease
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>--force-with-lease&lt;/code> 是一種安全的強制推送，可以防止意外覆寫他人工作的事故。&lt;/p>
&lt;hr>
&lt;h2 id="6-案例研究-3工作到一半想切換到另一個分支stash-的魔法">6. 案例研究 3：工作到一半想切換到另一個分支（Stash 的魔法）
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
在 &lt;code>feature/A&lt;/code> 分支實作新功能時，原始碼還處於連編譯都過不了的半途狀態。突然，主管下達指示：「&lt;code>main&lt;/code> 分支的正式環境出現緊急 bug，現在立刻修好它！」&lt;/p>
&lt;h3 id="解決方法使用-git-stash-進行暫存">解決方法：使用 &lt;code>git stash&lt;/code> 進行暫存
&lt;/h3>&lt;p>&lt;code>git stash&lt;/code> 是一個將尚未提交的變更暫存到臨時區域的指令。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 暫存工作中的變更&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash push -m &lt;span class="s2">&amp;#34;WIP: feature A partially implemented&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 現在可以切換到 main 分支了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ...（進行緊急 bug 修復作業，提交並推送）...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 作業結束後回到原本的分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. 恢復暫存的變更&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash pop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>當執行 &lt;code>git stash&lt;/code> 時，Git 內部會產生兩個特殊的提交物件，並保存在 &lt;code>refs/stash&lt;/code> 這個參照中。也就是說，Stash 說到底也只是「沒有名字的臨時提交」而已。&lt;/p>
&lt;hr>
&lt;h2 id="7-案例研究-4恐怖的-detached-head-狀態">7. 案例研究 4：恐怖的 &amp;ldquo;Detached HEAD&amp;rdquo; 狀態
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
為了確認過去某個時間點的程式碼，執行了 &lt;code>git checkout 9f8a7b6&lt;/code>。結果顯示了 &lt;code>You are in 'detached HEAD' state.&lt;/code>。就這樣直接提交了，但切換分支後，提交卻消失了！&lt;/p>
&lt;h3 id="detached-head-的機制">Detached HEAD 的機制
&lt;/h3>&lt;p>通常 &lt;code>HEAD&lt;/code> 會指向 &lt;code>refs/heads/main&lt;/code> 等分支。但是，如果直接簽出特定的提交，&lt;code>HEAD&lt;/code> 就會直接指向該提交物件。這被稱為 &lt;strong>Detached HEAD（分離的 HEAD）&lt;/strong>。&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;提交 A&amp;#34;] --&amp;gt; B[&amp;#34;提交 B&amp;#34;]
B --&amp;gt; C[&amp;#34;提交 C&amp;#34;]
C --&amp;gt; D[&amp;#34;提交 D&amp;#34;]
BranchMain[&amp;#34;分支: main&amp;#34;] --&amp;gt; D
HEAD[&amp;#34;HEAD&amp;#34;] --&amp;gt; B
style HEAD fill:#f9f,stroke:#333,stroke-width:4px
&lt;/pre>
&lt;p>在這種狀態下即使疊加提交，也沒有任何分支會追蹤那個新的提交。切換到其他分支的瞬間，新的提交就會迷路。&lt;/p>
&lt;h3 id="解決方案將其儲存為新的分支">解決方案：將其儲存為新的分支
&lt;/h3>&lt;p>只要在目前所在的位置建立新分支就能解決。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在目前的 HEAD 位置建立新分支，並切換過去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout -b feature/recovered-work
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;h2 id="8-案例研究-5合併與重定基底時的衝突解決">8. 案例研究 5：合併與重定基底時的衝突解決
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
執行 &lt;code>git merge&lt;/code> 或 &lt;code>git rebase&lt;/code> 時，顯示 &lt;code>CONFLICT (content)&lt;/code>，並且程序被中斷。&lt;/p>
&lt;h3 id="合併merge與重定基底rebase的差異">合併（Merge）與重定基底（Rebase）的差異
&lt;/h3>&lt;ol>
&lt;li>&lt;strong>Merge（合併）&lt;/strong>
使用兩個分支的最新提交與共同的祖先進行三方合併（3-way merge），並建立一個合併提交。&lt;/li>
&lt;li>&lt;strong>Rebase（重定基底）&lt;/strong>
將目前分支的提交暫存，並重新套用在目標分支的頂端。歷史記錄將會變成一條直線。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;M1&amp;#34;
commit id: &amp;#34;M2&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;F1&amp;#34;
commit id: &amp;#34;F2&amp;#34;
checkout main
commit id: &amp;#34;M3&amp;#34;
merge feature
&lt;/pre>
&lt;h3 id="衝突的解決方法">衝突的解決方法
&lt;/h3>&lt;p>發生衝突的檔案中，會插入如下的標記。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="nx">HEAD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span> &lt;span class="nx">feature&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">api&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>解決步驟非常簡單。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>刪除標記，並修正為正確的程式碼。&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NODE_ENV&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;production&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>將已解決的檔案加入暫存區（staging）。&lt;/strong>
&lt;code>git add&lt;/code> 具有「告訴 Git 衝突已經解決」的作用。
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git add index.js
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>完成程序。&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 合併的情況&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;Resolve merge conflict in index.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 重定基底的情況&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>如果感到慌亂，隨時可以使用 &lt;code>$ git merge --abort&lt;/code> 或 &lt;code>$ git rebase --abort&lt;/code> 來中止。&lt;/p>
&lt;hr>
&lt;h2 id="9-案例研究-6提交歷史亂七八糟-git-rebase--i">9. 案例研究 6：提交歷史亂七八糟！ &lt;code>git rebase -i&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
產生了大量如「修正錯字」、「還是修改一下」、「新增測試」等瑣碎的提交。如果就這樣合併到 &lt;code>main&lt;/code>，歷史記錄會變得非常髒亂。&lt;/p>
&lt;h3 id="解決方案互動式重定基底">解決方案：互動式重定基底
&lt;/h3>&lt;p>使用 &lt;code>git rebase -i&lt;/code>（interactive），可以改變過去提交的順序、將多個提交合併為一個（squash），或是修改提交訊息。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 整理最近 3 個提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase -i HEAD~3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>編輯器會開啟，並顯示如下：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 修正錯字
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 2b3c4d5 還是修改一下
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 3c4d5e6 新增測試
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>將其修改為以下內容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 實作功能 X
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 2b3c4d5 還是修改一下
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 3c4d5e6 新增測試
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>存檔並關閉後，這 3 個提交就會漂亮地整合成 1 個。&lt;/p>
&lt;hr>
&lt;h2 id="10-案例研究-7不知道什麼時候混入了-bug-git-bisect">10. 案例研究 7：不知道什麼時候混入了 bug！ &lt;code>git bisect&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【狀況】&lt;/strong>
目前的 &lt;code>main&lt;/code> 分支存在 bug，但在 1 個月前發布時是正常的。想要找出是哪個提交混入了 bug，但有超過 100 個提交，靠手動尋找根本不可能！&lt;/p>
&lt;h3 id="解決方案使用二元搜尋來定位-bug">解決方案：使用二元搜尋來定位 bug
&lt;/h3>&lt;p>Git 內建了一個能透過數學上的二元搜尋（Binary Search）來找出混入 bug 的提交的工具。因為時間複雜度是 $\mathcal{O}(\log N)$，即使有 1000 個提交，大約也只需要 10 次測試就能定位出來。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 開始搜尋&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 目前的提交有 bug（bad）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1 個月前（例如雜湊值是 a1b2c3d）是正常的（good）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good a1b2c3d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Git 會自動簽出中間的提交，所以請執行測試&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果測試成功：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果測試失敗：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>只要重複這個過程，Git 就會準確地告訴你：「這個提交是第一個 Bad 提交」。結束後使用 &lt;code>$ git bisect reset&lt;/code> 即可恢復原狀。&lt;/p>
&lt;hr>
&lt;h2 id="11-終極安全網git-reflog">11. 終極安全網：&lt;code>git reflog&lt;/code>
&lt;/h2>&lt;p>對於 Git 中所有「失誤」的最終奧義就是 &lt;code>git reflog&lt;/code>。Git 會將本機的所有操作歷史（HEAD 的移動歷史）完整記錄一段時間。即使不小心刪除了分支，或是做了錯誤的重設，只要使用 &lt;code>git reflog&lt;/code> 找出過去的雜湊值，然後對其執行 &lt;code>git reset --hard&lt;/code> 就能復原。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git reflog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">9f8a7b6 &lt;span class="o">(&lt;/span>HEAD -&amp;gt; main&lt;span class="o">)&lt;/span> HEAD@&lt;span class="o">{&lt;/span>0&lt;span class="o">}&lt;/span>: commit: Add new feature
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1a2b3c4 HEAD@&lt;span class="o">{&lt;/span>1&lt;span class="o">}&lt;/span>: reset: moving to HEAD~1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="12-結語">12. 結語
&lt;/h2>&lt;p>本文非常詳細地解說了 Git 初學者容易犯的錯誤、其背後的 Git 運作機制，以及解決方法。提交到錯誤的分支、取消已推送的提交、靈活運用 Stash、從 Detached HEAD 狀態生還，以及解決衝突。在所有這些操作中，最重要的是去想像「Git 在背後是如何操作物件和指標的」。&lt;/p>
&lt;p>由數學公式表示的嚴謹 Diff 演算法計算出檔案的差異，並透過密碼學的雜湊函數來確保歷史的一致性。只要理解了這個優美的設計理念，你就會明白 Git 絕對不是一個「莫名其妙的黑盒子」，而是能堅固保護你原始碼的最強盾牌。&lt;/p>
&lt;p>下次當你覺得「搞砸了！」的時候，請不要慌張地關閉終端機，而是深呼吸並輸入 &lt;code>git status&lt;/code>。Git 一定會為你提供復原的提示。&lt;/p></description></item></channel></rss>