Git 版本控制 › 第 8 章

遠端協作

2026-06-26  ·  remote · origin · fetch · pull · push · tracking branch · PR

前七章的 Git 都在一台機器上運作——單人、單一版本庫、沒有網路。現實世界的工作幾乎都是多人協作:你有一份本地版本庫,伺服器上有一份共享版本庫,同事各自有一份。本章揭開「遠端」的運作原理:remote 不是神秘的雲端,它只是你本地版本庫裡記錄的一個 URL 別名。fetchpullpush 這三個指令做的事是在兩個版本庫之間搬運 Git 物件——搬的仍然是第 2 章學過的 blob、tree、commit。搞懂傳輸的方向與時機,就不會再搞混 fetch 和 pull。

8.1remote 與 origin

remote 是 Git 給「另一個版本庫的 URL」取的短名。你可以有多個 remote, 每個各有一個名字。origin 只是 git clone 時 Git 自動取的預設名稱, 它沒有任何魔法,只是慣例。

指令作用
git remote -v列出所有 remote 及對應 URL(fetch / push 各一行)
git remote add <name> <url>新增一個 remote
git remote rename origin upstream重新命名
git remote remove <name>移除

Remote 的資料儲存在 .git/config 裡,格式如下:

[remote "origin"]
    url = https://github.com/you/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*

那行 fetch = ... 叫做 refspec:它說「把 origin 的 refs/heads/* 全部對映到本地的 refs/remotes/origin/*」。這就是 remote-tracking branch (遠端追蹤分支)的由來——它們住在 refs/remotes/ 命名空間,是「上次 fetch 時遠端長什麼樣」的快照, 你無法直接 commit 到上面,只能由 fetch 自動更新。

remote-tracking branch 示意圖 本地版本庫含 main、feature 分支與 origin/main、origin/feature 追蹤分支 本地版本庫 (.git/) A 1a2b B 3c4d C 5e6f main origin/main HEAD refs/heads/ refs/remotes/origin/ fetch 更新追蹤分支 遠端版本庫 (origin) A 1a2b B 3c4d main
圖 8-1fetch 把遠端的 commit 下載到本地,並更新 refs/remotes/origin/main 追蹤分支;本地的 main 不動
origin/main 是唯讀的快照:它告訴你「上次 fetch 時遠端 main 在哪裡」。 你不能切換到它再 commit——那沒意義。它的唯一用途是讓你知道「本地 main 落後遠端幾個 commit」。

8.2fetch vs pull vs push

三個指令都在搬運物件,差別在方向是否自動合併:

指令方向合併本地分支?適合時機
git fetch 遠端 → 本地物件庫
(只更新 origin/*)
❌ 不動你的分支 想先看看遠端多了什麼,再決定如何整合
git pull 遠端 → 本地物件庫
自動 merge(或 rebase)
✅ 會動你的 HEAD 分支 確定要整合、快速同步
git push 本地 → 遠端 — (更新遠端分支) 把本地 commit 分享出去

git pull 其實就是 git fetch + git merge 的組合拳。 想改成 fetch + rebase 模式可加 --rebase 旗標,或設定:

git config pull.rebase true    # 這台機器所有 repo 都用 rebase 模式
pull 會直接改動你的工作目錄——但「工作目錄一髒就一定被擋下」是常見誤解, merge 與 rebase 兩種模式的門檻差很多:
  • 預設的 merge 模式只在髒檔案正好是這次要更新的檔案時才拒絕 (Your local changes to the following files would be overwritten by merge)。 改的若是無關的檔案,pull 照常完成,你的改動原封不動留著。 注意這是檔案層級的判斷、不是內容層級:即使你改第 1 行、遠端改第 50 行, 兩者不可能衝突,Git 一樣擋——它只問「這個檔要不要被覆寫」。
  • --rebase 模式嚴格得多:工作目錄只要有任何未 commit 的改動就拒絕 (cannot pull with rebase: You have unstaged changes),哪怕跟遠端變更八竿子打不著。 所以把 pull.rebase 設成 true 之後,你會更常被要求先 commit 或 stash。
養成「先 fetch → 查看差距 → 再 merge/rebase」的習慣,比盲目 pull 安全。
點選操作,觀察物件傳輸方向
🖥 本地版本庫
main5e6f
feature9a0b
origin/main3c4d
☁️ 遠端版本庫 (origin)
main5e6f
feature
點選上方按鈕查看說明…
fetch / pull / push 三種操作的傳輸方向 三行並排,各顯示一種操作的方向與效果 本地 遠端 (origin) fetch origin/main 更新 物件下載 main 不變 pull main 前移 / merge fetch + merge main 不變 push main 不變 物件上傳 main 前移
圖 8-2fetch / pull / push 三種操作的傳輸方向與效果對照

8.3追蹤分支(tracking branch)與 upstream

每條本地分支都可以設定一條「upstream」(上游),指向某個 remote 的某條分支。 設定之後,git statusgit pushgit pull 就知道「預設對象是誰」。

動作指令
查看各分支的 upstreamgit branch -vv
設定 upstream(第一次 push 時常用)git push -u origin feature
手動設定 upstreamgit branch --set-upstream-to=origin/main main
查看追蹤分支的差距git log main..origin/maingit fetch; git status

git clone 時,Git 自動把 main(或 master)的 upstream 設好—— 這就是為什麼 clone 完直接 git pull 就能用。自己建的分支沒有 upstream, 第一次 push 要加 -u:

git switch -c feature         # 建新分支
# ... 寫程式、commit ...
git push -u origin feature    # -u = --set-upstream,同時推上去並設定追蹤
git branch -vv 輸出範例:方括號裡就是「本地相對 upstream 的差距」,五種狀態認一次——
  experiment 8464d13 先做個實驗
* feature    9a0bc12 [origin/feature: ahead 2] 加入登入功能
  hotfix     1457d78 [origin/hotfix: ahead 1, behind 3] 修正 header 樣式
  main       5e6fa8d [origin/main] 更新 README
  release    3c2b1a0 [origin/release: behind 2] 準備 v1.2
  • [origin/main] 只有名字、沒有數字 = 完全同步,沒事做。
  • ahead 2 = 本地多 2 顆,push 就同步。
  • behind 2 = 遠端多 2 顆,pull 就同步(能 fast-forward,不會有 merge commit)。
  • ahead 1, behind 3 = 分岔(diverged)—— 雙方各有對方沒有的 commit。這是最容易卡住的狀態:直接 push 會被拒, 得先決定用 merge 還是 rebase 把兩邊接起來(第 7 章的主題)。
  • experiment 那行整個方括號都不見 = 還沒設 upstream,正是上面 -u 要解決的事。
upstream 追蹤關係圖 本地 feature 分支透過 upstream 設定追蹤 origin/feature A 1a2b B 3c4d C 9a0b D b1c2 main feature upstream 追蹤 origin/feat B 3c4d origin/main 本地 (ahead 2) 遠端 origin
圖 8-3feature 分支比 origin/feature 多 2 個 commit(ahead 2),push 前的狀態

8.4典型協作流程(feature branch + PR)

絕大多數團隊採用 feature branch 工作流:每個功能或修復各開一條短命分支, 完成後透過 Pull Request(PR)/Merge Request(MR) 合回主線。 這讓 main 永遠保持可部署狀態,且每次合併都有 code review 記錄。

1
同步主線
git switch maingit pull
確保本地 main 與遠端一致再開分支
2
建立功能分支
git switch -c feature/login
命名慣例:feature/fix/chore/ 前綴讓一眼看出用途
3
開發 + commit
git add ...git commit -m "..."
每個 commit 保持小而聚焦,方便日後 review 與 cherry-pick
4
推上遠端 + 開 PR
git push -u origin feature/login
到 GitHub/GitLab 點「New Pull Request」,選 base branch = main
5
Code review + 修改
根據 reviewer 意見繼續 commit、push;PR 頁面自動更新
可隨時 git fetch + git rebase origin/main 把最新 main 納入
6
合入主線 + 清理
PR 通過後由平台合入(Merge / Squash merge / Rebase merge)
刪遠端分支:git push origin --delete feature/login
刪本地分支:git branch -d feature/login
feature branch + PR 協作流程圖 main 線在下、feature 線在上、push 後有 PR 合入 feature main M1 同步 M2 PR merge F1 開分支 F2 commit F3 review 修 switch -c push + PR
圖 8-4feature branch 工作流:從 main 分出 → 開發 → PR review → merge 回 main
push 被拒絕:non-fast-forward
如果遠端 main 在你 push 前被別人更新了,Git 會拒絕你的 push(避免覆蓋別人的 commit)。 解法:先 git fetch + git rebase origin/main(或 git merge origin/main) 把遠端新 commit 納入,再 push。絕對不要用 --force 解決 main 上的非快進問題 ——那會讓別人的 commit 消失。

8.5force push 的安全網:--force-with-lease

改寫已推送的歷史(rebase、amend)之後,普通 push 會被拒,你只能強推。 但「強推」有兩個層級,差別不只是打字長度——它們問的問題根本不同:

--force--force-with-lease
它問的問題 沒問題,照我說的做 遠端還停在我上次看到的位置嗎?
比對對象 本地的 origin/<branch> vs 遠端實際的值
同事剛推了新 commit 直接蓋掉,他的 commit 消失 擋下(! [rejected] (stale info))

「lease」(租約)這個字是關鍵:你上次 fetch 時,遠端在某個位置,Git 把這個位置當成你「租」下的憑證。 push 時它先驗這張租約——遠端還在原位,才讓你蓋。有人動過,租約就過期(stale info),擋下。 --force完全不驗,誰的 commit 都照輾。

--force-with-lease 有個致命陷阱:git fetch 會把租約重新簽一次。
它比對的是你本地的 origin/<branch>,而不是「你真正看過的東西」。 fetch(或 pull)正好會更新 origin/<branch>——於是同事的新 commit 悄悄變成你的新租約,驗證照樣通過,他的工作照樣被輾掉。 最陰的是編輯器的背景自動 fetch:你什麼都沒做,保護就沒了。

Git 2.30 起補上了 --force-if-includes:它改問「我的分支真的建立在遠端現況之上嗎?」 ——也就是你是否真的把那些 commit 納入過,而不只是「本地有那個 ref」。搭配使用才是完整的安全網:

git push --force-with-lease --force-if-includes    # 背景 fetch 也擋得住
同事推了一顆新 commit 之後,三種強推的下場
# 我沒 fetch,還不知道同事推了東西
$ git push --force
 + d5fb4eb...956d470 main -> main (forced update)
   ↑ 同事的 commit 沒了,Git 一聲都沒吭

$ git push --force-with-lease
 ! [rejected] main -> main (stale info)
   ↑ 擋下了,同事的工作保住

# 但只要 fetch 過一次(或編輯器背景自動 fetch)……
$ git fetch
$ git push --force-with-lease
 + 0300603...0802030 main -> main (forced update)
   ↑ 租約被刷新,照樣輾過去——同一個指令,結果相反

$ git push --force-with-lease --force-if-includes
 ! [rejected] main -> main (remote ref updated since checkout)
   ↑ 這才擋得住
實務結論:把 --force 從肌肉記憶裡刪掉,改用 --force-with-lease (可設個 alias)。但別把它當成免死金牌——它防的是「你不知道遠端動過」, 不是「你不該改寫這條分支」。第 7 章的黃金法則仍然成立: 共用分支就別改寫歷史,再好的安全網也只是最後一道防線。

8.6本章小結

概念一句話記住
remote / originURL 的別名;存在 .git/config;origin 是 clone 時的預設名稱
remote-tracking branchorigin/main 是「上次 fetch 時遠端長什麼樣」的唯讀快照
fetch下載物件 + 更新追蹤分支,不動你的本地分支
pullfetch + merge(或 rebase),會動你的 HEAD 分支
push把本地 commit 傳上遠端;需確保 fast-forward 或有強制旗標
upstream / -u設定本地分支的追蹤對象,之後 push/pull 就知道預設目標
feature branch + PRmain 永遠可部署;新功能在獨立分支開發、review、再合入
--force-with-lease強推前先驗「遠端還在我上次看到的位置嗎」;但 fetch 會刷新這張租約,要配 --force-if-includes 才完整
本章把 Git 從單機帶進了網路世界。下一章(第 9 章)回溯與救援處理「改壞了怎麼辦」: reset 三模式、revertrestore,以及用 reflog 找回以為永遠消失的 commit——這是讓你真正無後顧之憂使用 Git 的最後一塊拼圖。