Medium 好讀版點此。
在本地端可以用 git branch
產生新分支、可以用 git merge
合併兩個分支;在遠端(如 GitHub)也可以開新分支與用發 pull request(PR)的方式合併分支,那在本地端與遠端進行這兩項操作,效果究竟是否相同?
在《深入一點點認識 Git》系列文的最後一篇,我們將透過觀察 .git/
資料夾的變化,找出以上問題的答案。
首先在本地端建立第一筆 commit:
echo "hello, world" > hello_world.txt
git add hello_world.txt
git commit -m "Initial commit"
得 .git/
結構如下:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ └── heads/
│ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│ └── HEAD # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│
├── objects/
│ ├── 4b/ # blob物件
│ │ └── 5fa63702dd96796042e92787f464e28f09f17d
│ ├── 89/ # tree物件
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── 97/ # commit物件
│ │ └── 16f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ └── tags/
│
├── COMMIT_EDITMSG # Initial commit
├── ...
└── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
圖示如下:
接著把遠端倉儲網址設定為 remote
:
git remote add origin https://github.com/ruifuhong/Day30.git
把剛剛在本地的專案推到遠端 GitHub 倉儲:
git push -u origin main
仿照 Day 29,我們於 GitHub 上建立新的 README.md
檔案:
簡單輸入 README.md
內容後,點選右上角的 Commit changes... 按鈕:
Commit 訊息使用預設的不變,但這次我們改選「創建一個新分支」,並將新分支命名為 feature
,如下圖紅框處:
因為 GitHub 假設這個新分支後面要合併進 main
分支,所以會開啟一個 pull request(PR),我們不改動該 PR 內容,直接點選下圖紅箭頭所指的「Create pull request」按鍵:
現在遠端有 main
與 feature
兩分支,但在本地端只有 main
分支,遠端並有一個 PR 待合併:
經過以下指令:
git pull
把遠端倉儲的版本拉下來後,本地端的 .git/ 資料夾長這樣:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│ │ └── remotes/
│ │ └── origin/
│ │ ├── feature # 0000000000000000000000000000000000000000 4d977033b1474489164a0489ba324d9ef40c3615 Ralph <ralph@ralphmail.com> 1757131386 +0800 pull: storing head
│ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072641 +0800 update by push
│ └── HEAD # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│
├── objects/
│ ├── 4b/ # 舊有blob物件
│ │ └── 5fa63702dd96796042e92787f464e28f09f17d
│ ├── 4d/ # 新增commit物件
│ │ └── 977033b1474489164a0489ba324d9ef40c3615
│ ├── 31/ # 新增tree物件
│ │ └── 161639bcde43c9e4b599faab2107fd98948f0e
│ ├── 52/ # 新增blob物件
│ │ └── 9b7254396083b8c44da1197fe19e470b79f89e
│ ├── 89/ # 舊有tree物件
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── 97/ # 舊有commit物件
│ │ └── 16f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ ├── remotes/
│ │ └── origin/
│ │ ├── feature # 4d977033b1474489164a0489ba324d9ef40c3615
│ │ └── main # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ └── tags/
│
├── COMMIT-EDITMSG # Initial commit
├── ...
├── FETCH_HEAD # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 branch 'main' of https://github.com/ruifuhong/Day30
│ # 4d977033b1474489164a0489ba324d9ef40c3615 not-for-merge branch 'feature' of https://github.com/ruifuhong/Day30
├── HEAD # ref: refs/heads/main
├── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
└── ORIG_HEAD # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
在遠端開分支後再經過 git pull
,本地端資料夾共有以下變化:
logs/remotes/origin
:除了 main
分支外,還多了遠端建立的 feature
分支歷史紀錄,且該歷史紀錄並無上代,表示 feature
分支的第一個 commit 就是 4d97703...
。objects/
:遠端建立 commit 形成的 blob、tree 與 commit 物件也通通出現。refs
:遠端參考一樣多了 feature 分支,指向 4d97703...
這個 commit;然而,本地端目前依然只有 main
分支。FETCH_HEAD
:main
的指向 9716f8a...
、feature 的指向 4d97703...
且標示 not-for-merge
。index
:仍為 4b5fa63...
這個 blob 物件。ORIG_HEAD
:指向 9716f8a...
。圖示如下:
看來遠端開分支帶來的變化都有被帶到本地,但如果輸入 git branch 查看本機目前有的分支,會看到什麼呢?
為何沒有 main
分支?其實如果回頭看 .git/
資料夾,也確實會發先 main
分支只存在於 /remotes
中,表示本地端是沒有 feature
分支的:
├── refs/
│ ├── heads/
│ │ └── main # 本地端沒有feature分支
│ ├── remotes/
│ │ └── origin/
│ │ ├── feature # 遠端才有feature分支
│ │ └── main
│ └── tags/
如果要查看不限於本地的所有分支,要在 git branch
指令加上 -a
如下:
git branch -a
這樣就能看到那個遠端的 feature
分支了:
那要怎麼在本地端切換到 feature
分支呢?
其實一樣可以直接下 git switch
,這樣在本地端就自動長出 feature
分支了呢:
再回去觀察 .git/
資料夾,可發現本地端的 feature
分支長出來了:
├── refs/
│ ├── heads/
│ │ ├── feature # git switch後,本地端也長出feature分支
│ │ └── main
│ ├── remotes/
│ │ └── origin/
│ │ ├── feature
│ │ └── main
│ └── tags/
另一方面,工作目錄在 git pull
後還沒有在 feature
分支上的 README.md
檔案,經過 git switch
,這檔案也出現了:
而如果是在空資料夾透過 git clone
把整份專案克隆下來,本地的 .git/
資料夾如下:
.git/
├── ...
│
├── logs/
│ └── refs/
│ ├── heads/
│ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757135590 +0800 clone: from https://github.com/ruifuhong/Day30
│ └── remotes/
│ └── origin/
│ └── HEAD # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757135590 +0800 clone: from https://github.com/ruifuhong/Day30
│
├── objects/
│ ├── info/
│ └── pack/ # pack-52f61e3d010cb84169d7a15a7ef53a4d5409b7e6.idx
│ # pack-52f61e3d010cb84169d7a15a7ef53a4d5409b7e6.pack
│
├── refs/
│ ├── heads/
│ │ └── main # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ ├── remotes/
│ │ └── origin/
│ │ └── HEAD # ref: refs/remotes/origin/main
│ └── tags/
│
├── ...
├── HEAD # ref: refs/heads/main
├── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# 4d977033b1474489164a0489ba324d9ef40c3615 refs/remotes/origin/feature
# 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 refs/remotes/origin/main
本地資料夾變化整理如下:
logs/
:不論本地端或遠端,都只有包含 9716f8a...
這筆 commit 的紀錄。objects/
:如 Day 29 說明,使用 git clone
得到的物件是打包過的,透過 git verify-pack -v .git/objects/pack/pack-*.idx
指令檢視物件內容,可得全部六個物件:refs/
:本機端的是 main
指向 9716f8a...
,但遠端的是寫 HEAD
指向 main
分支。packed-refs
:遠端 feature
分支指向 4d97703...
、遠端 main
分支指向 9716f8a...
。那這時候本地端有 feature
分支嗎?
答案依然是沒有的,但透過 git switch
切換分支,就一樣可以建立本地的 feature
分支:
此時 .git/
資料夾的 refs/
如下:
├── refs/
│ ├── heads/
│ │ ├── feature # 新分支,指向4d977033b1474489164a0489ba324d9ef40c3615
│ │ └── main
│ ├── remotes/
│ │ └── origin/
│ │ └── HEAD
│ └── tags/
點選這個「Merge pull request」按鍵,把 GitHub 上的 feature
分支合併進 main
分支:
我們使用預設的合併訊息,但刪除延伸說明,直接點 Confirm merge 完成合併:
完成合併!
若回到本地端原始狀態(尚未把遠端 feature
分支拉下來),直接使用 git pull
這個合併完的分支,會發生什麼事呢?
.git/
資料夾變化如下:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│ │ │ # 新增9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 be0d15a10a7609be326fc0de996486fc6c64f665 Ralph <ralph@ralphmail.com> 1757137350 +0800 pull: Fast-forward
│ │ └── remotes/
│ │ └── origin/
│ │ ├── feature # 0000000000000000000000000000000000000000 4d977033b1474489164a0489ba324d9ef40c3615 Ralph <ralph@ralphmail.com> 1757137350 +0800 pull: storing head
│ │ └── main # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072641 +0800 update by push
│ │ # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 be0d15a10a7609be326fc0de996486fc6c64f665 Ralph <ralph@ralphmail.com> 1757137350 +0800 pull: fast-forward
│ │
│ └── HEAD # 0000000000000000000000000000000000000000 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 Ralph <ralph@ralphmail.com> 1757072239 +0800 commit (initial): Initial commit
│ # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086 be0d15a10a7609be326fc0de996486fc6c64f665 Ralph <ralph@ralphmail.com> 1757137350 +0800 pull: Fast-forward
│
├── objects/
│ ├── 4b/
│ │ └── 5fa63702dd96796042e92787f464e28f09f17d
│ ├── 4d/
│ │ └── 977033b1474489164a0489ba324d9ef40c3615
│ ├── 31/
│ │ └── 161639bcde43c9e4b599faab2107fd98948f0e
│ ├── 52/
│ │ └── 9b7254396083b8c44da1197fe19e470b79f89e
│ ├── 89/
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── 97/
│ │ └── 16f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
│ ├── be/ # 新增commit物件
│ │ └── 0d15a10a7609be326fc0de996486fc6c64f665
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # be0d15a10a7609be326fc0de996486fc6c64f665
│ ├── remotes/
│ │ └── origin/
│ │ ├── feature # 4d977033b1474489164a0489ba324d9ef40c3615
│ │ └── main # be0d15a10a7609be326fc0de996486fc6c64f665
│ └── tags/
│
├── COMMIT-EDITMSG # Initial commit
├── ...
├── FETCH_HEAD # be0d15a10a7609be326fc0de996486fc6c64f665 branch 'main' of https://github.com/ruifuhong/Day30
│ # 4d977033b1474489164a0489ba324d9ef40c3615 not-for-merge branch 'feature' of https://github.com/ruifuhong/Day30
├── HEAD # ref: refs/heads/main
├── index # 100644 529b7254396083b8c44da1197fe19e470b79f89e 0 README.md
│ # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
└── ORIG_HEAD # 9716f8a33bf4ffdcb0f75fe6fe6f65c00d7fb086
圖示如下,可發現新增了一個合併的 commit 物件,且本地與遠端的 main
分支都指向它:
因為已經合併,我們就不用再理會 feature
分支,也不用下 git switch
在本地端建立這條分支。
.git/
├── ...
│
├── logs/
│ └── refs/
│ ├── heads/
│ │ └── main # 0000000000000000000000000000000000000000 be0d15a10a7609be326fc0de996486fc6c64f665 Ralph <ralph@ralphmail.com> 1757167054 +0800 clone: from https://github.com/ruifuhong/Day30
│ └── remotes/
│ └── origin/
│ └── HEAD # 0000000000000000000000000000000000000000 be0d15a10a7609be326fc0de996486fc6c64f665 Ralph <ralph@ralphmail.com> 1757167054 +0800 clone: from https://github.com/ruifuhong/Day30
│
├── objects/
│ ├── info/
│ └── pack/ # pack-106eec24ca3fb261662c7299d2939e60a9dbe379.idx
│ # pack-106eec24ca3fb261662c7299d2939e60a9dbe379.pack
│
├── refs/
│ ├── heads/
│ │ └── main # be0d15a10a7609be326fc0de996486fc6c64f665
│ ├── remotes/
│ │ └── origin/
│ │ └── HEAD # ref: refs/remotes/origin/main
│ └── tags/
│
├── ...
├── HEAD # ref: refs/heads/main
├── index # 100644 529b7254396083b8c44da1197fe19e470b79f89e 0 README.md
│ # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# 4d977033b1474489164a0489ba324d9ef40c3615 refs/remotes/origin/feature
# be0d15a10a7609be326fc0de996486fc6c64f665 refs/remotes/origin/main
logs/
:不論本地端或遠端,都從合併的 commit be0d15a...
開始記錄。objects/
:一樣是打包過的物件,透過 git verify-pack -v .git/objects/pack/pack-*.idx
指令檢視物件內容,可得全部七個物件,包含合併物件 be0d15a...
(反白處):refs/
:本機端的是 main
已改指向 be0d15a...
,遠端依然是寫 HEAD
指向 main
分支。packed-refs
:遠端 feature
分支仍指向 4d97703...
、遠端 main
分支已改指向合併的 commit be0d15a...
。因為合併 commit 是在 GitHub 上做的,一如 Day 29 文章觀察,這個合併 commit 也帶有自己的簽章:
若在遠端開分支再 git pull
或 git clone
到本地,則 .git/
資料夾會分開記錄遠端跟本地的分支紀錄,而在本地端用 git switch
等指令切換到遠端新增的分支前,本地紀錄不會有這新分支。
在遠端以 pull request 的方式合併分支,其形成的 git 物件結構與在本地用 git merge
相似,都是產生新的合併 commit,上代為合併相關兩分支原本最新的 commit,而這個新合併 commit 因為在遠端產生,所以預設自帶簽章。
總而言之,在遠端開分支與合併分支的效果與在本地進行類似,比較大的差別是:如果拉或克隆下來後,沒有在本地切換到新分支,則本地端不會產生在遠端才出現的新分支。