Git 合併方向不是由分支名稱的地位決定,而是由執行合併時所在的分支決定。功能分支已經包含主線,不等於主線已經包含功能分支。要確認工作有沒有整合到預定位置,應比較兩個分支的提交端點,而不是只讀「沒有需要更新」的訊息。
本文從本地 Branch 筆記的方向提醒出發,以全新離線儲存庫建立合成歷史。範例只使用空提交,不建立業務資料、不連接遠端,也不處理既有專案。目標是讓合併方向、快轉與分歧拒絕都有可觀察的判定;這不是重構衝突解法、部分暫存技巧或分支制度的完整設計。
接收分支才是合併的目的地
合併前應先確認目前分支,再寫出要接收哪個來源分支的歷史;合併之後讀回雙方端點。來源名稱出現在指令中,不代表它就是被更新的目的地,這是最容易混淆的地方。
把需求寫成「讓 main 包含 feature 的歷史」,比「把兩邊合起來」更容易驗收。前者已指定接收者、來源及完成條件;後者沒有說明要移動哪個分支。本文第一項測試故意停在 feature,執行接收 main 的動作。由於 main 本來就是 feature 的祖先,這次操作沒有推進 main,也沒有新增整合成果。
Git 官方 merge 文件把操作描述為將指定提交的變更整合到目前分支。這個定義支持我們先問「目前在哪裡」,再問「來源是誰」。在示範歷史中,分支名只是可讀的標籤;把 main 改成其他名字,不應改變我們對接收方向的判斷。合併語意依據[1]
來源筆記把新分支描述成複製歷史版本,適合初步理解,但本文不把它解讀為複製整個資料夾,也不把任何預設分支名稱說成永久規則。筆記真正值得保留的是操作位置與合併方向必須一致;歷史模型與指令語意則由官方文件及下方測試補充。
端點相等與歷史包含是不同問題
確認整合方向需要分開檢查端點是否相等,以及一方是否已經包含另一方的歷史。前者適合驗收本例的快轉,後者適合判斷祖先關係;不要拿檔案畫面相同代替歷史證據。
本例故意使用空提交,所以各個分支的檔案內容沒有差異,但它們仍可具有不同提交端點。這個安排讓「檔案沒有變,所以沒有新歷史」的推論失效。讀者不需要建立一段應用程式,也能看見歷史拓樸與檔案內容是兩個驗收面向。
git merge-base --is-ancestor A B 回答 A 是否為 B 的祖先。官方規定成立回傳零、不成立回傳一,其他非零值表示錯誤。因此檢查程式不能把所有非零都叫做正常分歧。本文第三項測試要求明確的一,而不是接受任意失敗。祖先判斷依據[2]
建議驗收紀錄至少保存接收分支名稱、操作前端點、來源端點與操作後端點。這些值應在每次測試中實際取得,不必把本文某次執行的提交識別碼硬編碼成常數。相同腳本在不同時間建立提交,仍應用相對關係判定,而非要求識別碼和文章示例完全相同。
快轉是端點推進,不是自動建立合併提交
當接收端位於來源歷史之中,快轉可以直接把接收端推進到來源端點,不需要額外整合提交。使用快轉限定模式,能讓不符合這項假設的情況停下來,而不是默默改採另一種歷史形狀。
本文第二項測試先切回 main,再接收 feature。通過條件不是終端出現成功字樣,而是 main 端點等於先前保存的 feature 端點,同時 feature 本身不變。這組檢查直接回答「誰移動、移到哪裡、誰不應被改動」,比只看提交數量更貼近本題。
官方文件區分快轉、禁止快轉與只接受快轉的選項。本文採用 --ff-only 表達明確限制,並在分歧案例驗證拒絕;沒有把預設設定當作每個環境都相同的前提。這也是腳本會隔離全域設定的原因:要觀察的是明示選項及合成歷史,而不是未揭露的個人設定。快轉選項依據[4]
若工具介面只顯示「已更新」,仍需回到端點與歷史確認方向。尤其在教學或交接時,兩個分支名稱相似、視窗開啟多個資料夾,都可能讓人把正確指令放在錯誤位置。本文不提供修復既有錯誤歷史的強制命令;先在新測試區理解行為,再為個別情境訂定處理方式。
分歧拒絕是預期訊號,不是需要隱藏的錯誤
兩個端點從共同基準各自新增提交時,快轉限定模式應拒絕整合。測試必須同時確認拒絕狀態及原有分支不變,再另外驗證明確允許的合併,不能把任何錯誤都當作成功的負向案例。
第三項測試從同一基準建立 left 與 right,各自增加一筆不同訊息的空提交。腳本先確認 left 不是 right 的祖先,接著在 left 執行快轉限定合併。它保存錯誤碼,要求非零,並重新讀回兩個端點及乾淨工作區。若指令意外成功,或任一端點改變,整體測試立即失敗。
拒絕確認之後,腳本才以明確的非快轉合併建立整合提交,並檢查第一個父提交是原 left、第二個父提交是原 right,來源 right 仍停在原處。這是同一測試資料中的對照,不代表遇到拒絕就應自動改用另一種策略。選項改變之前,讀者必須重新確認想保留的歷史形狀。
這個案例沒有檔案衝突,也沒有測試應用程式能否執行。空提交讓我們只研究分支端點與父提交;它不證明文字合併、資料格式或函式契約正確。若問題是重構後程式失效,應另外使用整合後的行為測試,不能用本例的乾淨工作區取代。
驗證與重現:完整離線測試腳本
請在可寫入的新測試區執行完整腳本,逐項核對三組通過訊息與整體退出狀態。腳本只建立自己的儲存庫,所有提交都是合成資料;任一版本、端點或父提交檢查失敗,都應停止判定。
固定實測基準是 Windows、Git for Windows 2.53.0.windows.3、GNU Bash 5.2.37(1)-release。這是本次執行版本,不是最新版本聲明,也不是要求讀者降級。前置條件是 Git Bash、git、mktemp、cygpath 可呼叫,以及明確可寫入的 scratch 資料夾。其他工具鏈應建立獨立紀錄。
- 執行
git --version與bash --version,確認固定基準,將完整程式存為git_merge_direction_lab_20261012.sh。在 Git Bash 設定TMPDIR為自己已建立的 scratch 資料夾,使用 Windows 正斜線路徑;不可指向既有專案的工作樹。 - 執行
bash git_merge_direction_lab_20261012.sh。檢查第一組訊息及第二組訊息,分別代表反方向接收沒有推進 main,以及正確快轉只推進接收分支。每項訊息都在端點比較通過後才輸出。 - 檢查第三組訊息、祖先退出碼及快轉拒絕退出碼。預期祖先碼是一、快轉拒絕為非零,最後出現整體通過且腳本退出零。拒絕的 fatal 訊息是預期負向案例;若沒有最後成功結果,仍不能視為完成。
git --version
bash --version
mkdir -p "$HOME/merge-direction-scratch"
export TMPDIR="$(cygpath -m "$HOME/merge-direction-scratch")"
bash git_merge_direction_lab_20261012.sh
#!/usr/bin/env bash
#New offline repositories only; preserve all generated evidence.
set -euo pipefail
version=$(git --version)
test "$version" = 'git version 2.53.0.windows.3'
: "${TMPDIR:?Set TMPDIR to a writable scratch directory}"
lab=$(mktemp -d "$TMPDIR/merge-direction-XXXXXXXX")
lab=$(cygpath -m "$lab")
mkdir "$lab/empty" "$lab/repo"
for name in ${!GIT_@}; do unset "$name"; done
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$lab/config"
export GIT_TERMINAL_PROMPT=0
export GIT_AUTHOR_NAME=Sandbox GIT_AUTHOR_EMAIL=sandbox@example.invalid
export GIT_COMMITTER_NAME=Sandbox GIT_COMMITTER_EMAIL=sandbox@example.invalid
touch "$lab/config"
g() { git -c core.hooksPath="$lab/empty" -c commit.gpgsign=false -c merge.gpgsign=false "$@"; }
cd "$lab/repo"
g init --quiet --template="$lab/empty" -b main
g commit --quiet --allow-empty -m base
base=$(g rev-parse HEAD)
g switch --quiet -c feature
g commit --quiet --allow-empty -m feature
feature=$(g rev-parse HEAD)
test "$(g branch --show-current)" = feature
g merge --ff-only main
test "$(g rev-parse main)" = "$base"
test "$(g rev-parse feature)" = "$feature"
printf '%s\n' 'PASS 1: merging main into feature leaves main unchanged'
g switch --quiet main
g merge --ff-only feature
test "$(g rev-parse main)" = "$feature"
test "$(g rev-parse feature)" = "$feature"
printf '%s\n' 'PASS 2: fast-forward advances only the receiving branch'
g switch --quiet -c left "$base"
g commit --quiet --allow-empty -m left
left=$(g rev-parse HEAD)
g switch --quiet -c right "$base"
g commit --quiet --allow-empty -m right
right=$(g rev-parse HEAD)
g switch --quiet left
set +e
g merge-base --is-ancestor left right
ancestor_exit=$?
g merge --ff-only right
refusal_exit=$?
set -e
test "$ancestor_exit" -eq 1
test "$refusal_exit" -ne 0
test "$(g rev-parse left)" = "$left"
test "$(g rev-parse right)" = "$right"
test -z "$(g status --porcelain)"
g merge --no-ff --no-edit -m 'combine synthetic histories' right
test "$(g rev-parse HEAD^1)" = "$left"
test "$(g rev-parse HEAD^2)" = "$right"
test "$(g rev-parse right)" = "$right"
test -z "$(g status --porcelain)"
printf '%s\n' 'PASS 3: divergence refused, then explicit merge records ordered parents'
printf 'git=%s\nancestorExit=%s\nffOnlyExit=%s\n' "$version" "$ancestor_exit" "$refusal_exit"
printf '%s\n' 'PASS all: synthetic empty commits; no remote or existing repository used'
修正 Windows 路徑表示法後,上述完整腳本已在本次環境實際執行,整體退出碼為零,三組檢查均通過;祖先查詢為 1,快轉拒絕為 128。128 是本次觀察,不是跨版本保證,腳本只要求該拒絕非零。首次試跑發現 shell 路徑不能直接作為原生 Git 範本路徑,因此加入 cygpath -m,並以明確 scratch 路徑重跑;本文採用修正後版本。
如何記錄結果而不擴大結論
測試報告應區分工具版本、歷史關係、真實退出碼與未涵蓋條件。保存完整腳本與所有檢查結果,能讓後續審查者重跑相同案例;只有一張成功畫面,無法證明每個負向條件都被檢查。
這次沒有計時、沒有下載外部程式碼,也沒有量測吞吐量或推估節省工時。結果只支持三個合成歷史案例。它沒有驗證遠端同步、多人同時操作、子模組、衝突解決或任何現有應用程式的相容性。讀者若增加這些條件,應增加相應測試,而不是沿用本例通過紀錄。
提交身分採用不可投遞的示例地址,沒有使用真實帳號。腳本清除繼承的 Git 環境設定、指定空範本與空 hooks 目錄,並讓操作只發生在新建資料夾。它保留測試資料、不自動刪除,便於檢查;這不代表可以在既有工作樹內照貼內部指令。
實作檢查表可以很短:目前分支是否正確?來源端點是否已保存?預期是無動作、快轉還是新增合併提交?來源分支是否維持不變?負向測試是否確認錯誤類別?有沒有把工作區乾淨誤當成應用程式正確?這些問題應在操作前寫清楚,而不是事後挑選支持成功的畫面。
來源界線與結論
可靠的合併驗收從明確接收者開始,以端點讀回及父提交關係完成。筆記提供方向問題,官方文件確認工具語意,離線測試提供實際證據;三者各有責任,不能把其中一項當成全部保證。
本篇與既有 Git 語意衝突文章的問題不同:該文研究合併後呼叫契約是否仍成立,本篇研究哪個分支被推進,以及歷史限制是否拒絕分歧。部分暫存與 index 快照文章關注提交候選的內容,補丁文章關注變更交換格式,版本控制治理文章則討論團隊責任;本文不重新包裝那些搜尋意圖。
官方核對文件包括 git-merge 2.53.0、git-merge-base 與 git-branch。文件均固定於 v2.53.0 原始專案標籤;快轉選項另查核同版本 merge-options 原始文件。測試版本仍以上方明列值為準。branch 手冊支援目前分支名稱的觀察方式。[3]
最後的實用原則是:不要問「兩邊是否合過」,而要問「指定接收分支是否到達預期歷史」。在這個問題有清楚答案之前,指令成功、檔案相同與工作區乾淨都只是部分證據。結構化資料建議只使用 BlogPosting,不附加不存在的常見問答,也不承諾搜尋呈現結果。
Sources
[1] https://github.com/git/git/blob/v2.53.0/Documentation/git-merge.adoc [2] https://github.com/git/git/blob/v2.53.0/Documentation/git-merge-base.adoc [3] https://github.com/git/git/blob/v2.53.0/Documentation/git-branch.adoc [4] https://github.com/git/git/blob/v2.53.0/Documentation/merge-options.adoc