Medium 好讀版點此。
要從遠端倉儲「下載」檔案下來,可分為「本地有較舊版本」或「本地完全沒有這份專案」兩種情境,前者用 git fetch
或 git pull
、後者用 git clone
。本文除了比較兩情境、三指令的使用時機,還將仔細觀察 .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 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ └── HEAD # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│
├── objects/
│ ├── 4b/ # blob物件
│ │ └── 5fa63702dd96796042e92787f464e28f09f17d
│ ├── 23/ # commit物件
│ │ └── b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── 89/ # tree物件
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ └── tags/
│
│
├── ...
└── index
圖示如下:
為了避免之後每次與這個倉儲都要貼完整網址,我們可以先把該倉儲網址取名為 origin
如下:
git remote add origin https://github.com/ruifuhong/Day29.git
接著把專案推上 GitHub 倉儲:
git push -u origin main
成功推送:
我們來看看終端機顯示的訊息透漏哪些資訊:
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 215 bytes | 215.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/ruifuhong/Day29.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
Enumerating objects: 3, done.
:git 找到三個物件要推出去。Counting objects: 100% (3/3), done.
:git 再數一次,確認有三個物件。Writing objects: 100% (3/3), 215 bytes | 215.00 KiB/s, done.
:要把三個物件寫進 GitHub 伺服器,一共 215 個位元組,傳輸速率為每秒 215.00 千位元組。Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
:一共傳送 3 個物件(0 個以 delta 物件傳送)、重複使用 0 個已被壓縮的物件(0 個為 delta 物件)、沒有重複使用任何已打包的物件。所謂「delta 物件」指示的是這個物件只記錄跟其他物件的差別,而非完整物件,當物件量大的時候,可以節省儲存空間。
* [new branch] main -> main
以及 Branch 'main' set up to track remote branch 'main' from 'origin'.
:本地端送上去的是 main
分支,但 GitHub 預設只有 master
分支而沒有 main
分支,因此送一個新的 main
分支上去。觀察 .git/refs
資料夾可發現多了一個 remotes/
參考:
├── refs/
│ ├── heads/
│ │ └── main # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── remotes/
│ │ └── origin/
│ │ └── main # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ └── tags/
表示在 GitHub 這遠端倉儲(命名為 origin
)中的 main
這個參考也是指向 23b21f6...
這個 commit。
點擊下圖中紅圈處 Add a README
新增一個 README.md
檔案:
點擊紅圈處的 Add a README
在下圖左邊橘色箭頭處以 markdown 語法寫個簡單的 README
內容後,點擊右上角黃色箭頭指的 Commit changes...
我們使用預設的 commit 訊息 "Create README.md"
,並讓這變化直接發在 main
分支上,因此直接點選下圖右下角的 Commit changes:
從 GitHub 倉儲的歷史紀錄來看,多了一個雜湊碼為 d07c6d9
的 commit:
現在 GitHub 倉儲中已有 d07c6d9...
這筆 commit,但本地端還沒有,於是需要把遠端的進度同步(sync)到本地端。
同步的方式有二,一是 git fetch、二是 git pull,但兩者有些許不同。
指令如下:
git fetch
終端機顯示訊息如下:
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 953 bytes | 32.00 KiB/s, done.
From https://github.com/ruifuhong/Day29.git
23b21f6..d07c6d9 main -> origin/main
Git 伺服器掃完四個物件後,決定送三個物件下來(可能因第四個物件本地已經有了),從 main
分支抓到本地的 main
分支。
完成後,本地 .git/
資料夾如下:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ │ └── remotes/
│ │ └── origin/
│ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756952891 +0800 update by push
│ │ # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756968286 +0800 fetch: fast-forward
│ └── HEAD # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│
├── objects/
│ ├── 4b/
│ │ ├── 5fa63702dd96796042e92787f464e28f09f17d # 舊有blob物件
│ │ └── 266ffcba8cec812fd6e6c622f3d8cb5e3aed64 # 新增tree物件
│ ├── 23/ # 舊有commit物件
│ │ └── b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── 89/ # 舊有tree物件
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── d0/ # 新增commit物件
│ │ └── 7c6d941fe83fc72141bd5454b49a62502780bc
│ ├── e2/ # 新增blob物件
│ │ └── f685524b151f5f4457b58d72f6947153966f1d
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── remotes/
│ │ └── origin/
│ │ └── main # d07c6d941fe83fc72141bd5454b49a62502780bc
│ └── tags/
│
├── COMMIT-EDITMSG # Initial commit
├── ...
├── FETCH_HEAD # d07c6d941fe83fc72141bd5454b49a62502780bc branch 'main' of https://github.com/ruifuhong/Day29.git
├── HEAD # ref: refs/heads/main
└── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
值得觀察的點如下:
logs/
:本地的 main
跟 HEAD
都沒有記錄到 d07c6d9...
這在 GitHub 上新增的 commit,但是遠端的 remotes/origin/main
有。objects/
:新增在 GitHub 上做出的 blob、tree 與 commit 物件。ref/
:遠端的 remote/origin/main
已經指向新 commit d07c6d9...
,但本地的 heads/main
依然指向舊 commit 23b21f6...
。COMMIT-EDITMSG
:依然為舊 commit 的 Initial commit
。FETCH_HEAD
:表示在遠端的 HEAD
指向哪個 commit 時抓下來。index
:預存區存的是舊 commit 中對應的 blob 物件。圖示如下:
很明顯,雖然新的 commit 已經被抓下來了,但是本地的紀錄依然指著舊 commit,用 git log
也能觀察到,甚至透過 ls
觀察工作目錄,也還沒有 README.md
:
因此,還要再把當前本地 main
分支合併到遠端的 main
分支(也就是 FETCH_HEAD
指的地方)。我們以下列指令完成這件事:
git merge FETCH_HEAD
合併時的終端機畫面顯示:
Updating 23b21f6..d07c6d9
Fast-forward
README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 README.md
因為 d07c6d9...
為 2321f6...
往後一個 commit,使用的合併方式是快轉模式(Fast-forward),也就是 main
分支的參考移到 FETCH_HEAD
所指的地方:
首先可發現工作目錄已經出現 README.md
檔案:
合併完之後的 .git/
資料夾如下:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ │ │ # 新增23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756969313 +0800 pull: fast-forward
│ │ └── remotes/
│ │ └── origin/
│ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756952891 +0800 update by push
│ │ # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756968286 +0800 fetch: fast-forward
│ └── HEAD # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ # 新增23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756975923 +0800 merge d07c6d941fe83fc72141bd5454b49a62502780bc: Fast-forward
│
├── objects/ # 不變
│ ├── 4b/
│ │ ├── 5fa63702dd96796042e92787f464e28f09f17d
│ │ └── 266ffcba8cec812fd6e6c622f3d8cb5e3aed64
│ ├── 23/
│ │ └── b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── 89/
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── d0/
│ │ └── 7c6d941fe83fc72141bd5454b49a62502780bc
│ ├── e2/
│ │ └── f685524b151f5f4457b58d72f6947153966f1d
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # 變成d07c6d941fe83fc72141bd5454b49a62502780bc
│ ├── remotes/
│ │ └── origin/
│ │ └── main # d07c6d941fe83fc72141bd5454b49a62502780bc
│ └── tags/
│
├── COMMIT-EDITMSG # Initial commit
├── ...
├── FETCH_HEAD # d07c6d941fe83fc72141bd5454b49a62502780bc branch 'main' of https://github.com/ruifuhong/Day29.git
├── HEAD # ref: refs/heads/main
├── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
│ # 新增100644 e2f685524b151f5f4457b58d72f6947153966f1d 0 README.md
└── ORIG_HEAD # 新增檔案,內容為23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
主要的變化包含:
logs/
裡本地相關者新增從 23b21f6...
長出 d07c6d9...
commit 的紀錄。refs/heads/main
:變成指向最新 commit d07c6d9...
。index
:多出新增的 blob 物件 e2f6855...
。ORIG_HEAD
:新增檔案,指向原本的 commit 23b21f6...
。圖示如下:
那如果用以下指令把專案「拉」下來呢?
git pull
終端機顯示訊息如下:
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 953 bytes | 43.00 KiB/s, done.
From https://github.com/ruifuhong/Day29.git
23b21f6..d07c6d9 main -> origin/main
Updating 23b21f6..d07c6d9
Fast-forward
README.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 README.md
上半段是剛剛下 git fetch
時顯示的內容、下半段是剛剛 git merge FETCH_HEAD
時顯示的內容,那 .git/
裡面長怎樣呢?
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ │ │ # 新增23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756969313 +0800 pull: fast-forward
│ │ └── remotes/
│ │ └── origin/
│ │ └── main # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756952891 +0800 update by push
│ │ # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756968286 +0800 fetch: fast-forward
│ └── HEAD # 0000000000000000000000000000000000000000 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 Ralph <ralph@ralphmail.com> 1756950578 +0800 commit (initial): Initial commit
│ # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756969314 +0800 pull: Fast-forward
│
├── objects/
│ ├── 4b/
│ │ ├── 5fa63702dd96796042e92787f464e28f09f17d
│ │ └── 266ffcba8cec812fd6e6c622f3d8cb5e3aed64
│ ├── 23/
│ │ └── b21f6eddfd80fa9fdadc602516d460f7cbeaa4
│ ├── 89/
│ │ └── 34288024c536ae07113abd94e0975284a707ac
│ ├── d0/
│ │ └── 7c6d941fe83fc72141bd5454b49a62502780bc
│ ├── e2/
│ │ └── f685524b151f5f4457b58d72f6947153966f1d
│ ├── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ └── main # d07c6d941fe83fc72141bd5454b49a62502780bc
│ ├── remotes/
│ │ └── origin/
│ │ └── main # d07c6d941fe83fc72141bd5454b49a62502780bc
│ └── tags/
│
├── COMMIT-EDITMSG # Initial commit
├── ...
├── FETCH_HEAD # d07c6d941fe83fc72141bd5454b49a62502780bc branch 'main' of https://github.com/ruifuhong/Day29.git
├── HEAD # ref: refs/heads/main
├── index # 100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 hello_world.txt
│ # 100644 e2f685524b151f5f4457b58d72f6947153966f1d 0 README.md
└── ORIG_HEAD # 23b21f6eddfd80fa9fdadc602516d460f7cbeaa4
除了 logs/HEAD
最新一筆紀錄不包含合併外,其他通通相同!
由此實驗可知,git pull
其實就是同時 git fetch
(把遠端倉儲內容抓下來)與 git merge FETCH_HEAD
的綜合體,因此在把遠端進度同步到本地端時,通常都是使用比較簡便的 git pull
。
不管哪種方式,同步後使用 git log
都會觀察到如下終端機畫面:
不論 git fetch
或 git pull
,都僅限本地端有某個版本的專案時才能使用,如果是要把 GitHub 上的專案整包放到一個空資料夾,則要用 git clone
指令。
現在我們在一個空白的目錄中,使用以下指令,把 GitHub 上的專案克隆下來:
git clone https://github.com/ruifuhong/Day29.git
終端機顯示訊息如下:
Cloning into 'Day29'...
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 6 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (6/6), done.
Git 找到六個物件克隆下來,這時我們打開 .git/ 資料夾看看:
.git/
├── ...
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ └── main # 0000000000000000000000000000000000000000 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756956014 +0800 clone: from https://github.com/ruifuhong/Day29.git
│ │ └── remotes/
│ │ └── origin/
│ │ └── HEAD # 0000000000000000000000000000000000000000 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756956014 +0800 clone: from https://github.com/ruifuhong/Day29.git
│ └── HEAD # 0000000000000000000000000000000000000000 d07c6d941fe83fc72141bd5454b49a62502780bc Ralph <ralph@ralphmail.com> 1756956014 +0800 clone: from https://github.com/ruifuhong/Day29.git
│
├── objects/ # 空資料夾
│ ├── info/
│ └── pack/ # pack-c4af0a359f99a2244bcc5807b33d508b4ef880b9.idx
│ # pack-c4af0a359f99a2244bcc5807b33d508b4ef880b9.pack
│
├── refs/
│ ├── heads/
│ │ └── main # d07c6d941fe83fc72141bd5454b49a62502780bc
│ ├── remotes/
│ │ └── origin/
│ │ └── HEAD # ref: refs/remotes/origin/main
│ └── tags/
│
├── ...
├── index
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# d07c6d941fe83fc72141bd5454b49a62502780bc refs/remotes/origin/main
可發現幾個有趣的現象:
logs/
從 git clone
發生起算0000000...
長出來,表示把倉儲克隆下來之後,在本地的歷史紀錄就從 d07c6d9...
開始,而沒有更早的 23b21f6...
。使用 git reflog
觀察也確實如此:
objects/
中的物件被打包了、還多了 packed-refs
檔案
克隆下來的物件是打包過的,所以沒有鬆散物件(loose objects),只有在 objects/pack
裡裝有 Day 27 提過的索引(以 .idx
為副檔名) 及被打包的物件(以 .pack
為副檔名)。
多了遠端資訊
如果使用 git log
觀察,會發現 d07c6d9...
出現 origin/main
跟 origin/HEAD
,表示在遠端倉儲 origin
中,HEAD
也指向 main
分支、而 main
分支也指向 d07c6d9...
這個 commit:
在 .git/
資料夾中,則可在 refs/remotes/origin/HEAD
找到 origin
這個遠端倉儲的 HEAD
正指向 main
分支:
├── refs/
│ ├── heads/
│ │ └── main
│ ├── remotes/
│ │ └── origin/
│ │ └── HEAD # ref: refs/remotes/origin/main
│ └── tags/
至於遠端的 main
分支指向什麼 commit,則記錄在 packed-refs
裡:
├── index
└── packed-refs # pack-refs with: peeled fully-peeled sorted
# d07c6d941fe83fc72141bd5454b49a62502780bc refs/remotes/origin/main
在進行實驗時,我發現一件有趣的事情。
因為新建 README.md
的 commit 是直接在 GitHub 上進行,而非在本地寫完後推上去,這類直接在 GitHub 上完成的 commit,commit 者(committer)就是 GitHub,而如果使用 git cat-file -p
觀察 commit 細節,還會發現多一個 gpgsig
簽章:
但如果是在本地寫完才推上去的,就不會有簽章:
這是因為 GitHub 預設在網站上建立 commit 時,就自帶 GPG 簽章,但在本地端建立 commit 就沒有這個機制。當然要在本地建立簽章也不是不行,只是要另外設定,相關說明可參考 GitHub 官方文件。
如果想要把遠端倉儲的版本同步到本地端,而本地已經有該倉儲較舊版本,可以使用以下兩種指令:
git fetch
:把遠端最新資訊抓下來,後面還要使用 git merge FETCH_HEAD
合併進度。git pull
:直接做完 git fetch
與 git merge FETCH_HEAD
兩件事。不論使用這兩種方式的哪一種,在物件數不多時,抓或拉下來的都是 git 的鬆散物件。
而如果本地端完全沒有這個遠端倉儲的專案,則要使用 git clone
,但這樣整包抓下來的專案就會經過打包,而非鬆散的 git 物件。