MRT & GIT

今年 COSCUP Lightening Talk 分享的主題是 GIT & MRT,主題就是紀錄我之前用 GIT 線圖畫台北捷運路線圖的過程,投影片在上面,這篇文章是補充一下被省略的部分和講不完的部分,當天我有用 Ricoh Theta 錄影,也丟上 YouTube 了:

接著要來說一下當天省略的部分吧,就是git branchgit log我所學到的東西,不過要把這內容講清楚,需要先介紹一下.git這個資料匣,這是 git repository 的所有資料儲存的地方,包括所有的 branch、commit 資訊、檔案和目錄結構等等都在裡面,一般git clone也是先把這個目錄的內容抓下來,然後才把從裡面把工作目錄(working directory)的檔案弄出來,那麼,如果現在已經把.git的資料都抓下來後,要怎麼取出工作目錄的檔案呢,首先要看.git/HEAD這個檔案,一般而言,內容會是:

ref: refs/heads/master

意思是 reference 到.git/refs/heads/master,所以就看一下這個檔案的內容,以 immutable-quadtree-js 為例,.git/refs/heads/master的內容如下:

4b5c03ea81e0e24714bea66de3892a40165fe56f

這是一段 sha-1 hash,那麼要到哪裡找這個 hash 的內容呢?其實這個檔案的位置就在:

.git/objects/4b/5c03ea81e0e24714bea66de3892a40165fe56f

檔案(又稱為 object)內容是用 zlib 壓縮過的,git 有簡單的工具可以幫忙檢視內容,就是git cat-file,只要給他 hash 它就會自動去定位了,例如:

$ git cat-file -p 4b5c03ea81e0e24714bea66de3892a40165fe56f

或是

$ git cat-file -p 4b5c03

就會把上面那個 object 的內容印出來:

tree 96602375208c5d4f028bdbcd7873f049a70d9287
parent 280142567c56235af3fc7c35513edeb978c1b465
author othree <othree@gmail.com> 1429321581 +0800
committer othree <othree@gmail.com> 1429321581 +0800

Use same name in node and global module

這就是一個 commit 的內容,記錄了現在這個 commit 的檔案狀態(tree)、上一個 commit(parent)、還有作者、提交者及 commit message,要把工作目錄的內容抓出來會需要的是 tree,後面的值也是一個 object hash,所以就來看看內容吧:

100644 blob 31843d5c03e1a3a0618b40bdd2034b0c492f2132    .gitignore
100644 blob 20fd86b6a5bee335c75b4efea34312ff7f3a039e    .travis.yml
100644 blob fd7bce4d5095540e4cb83f56276b9cd831f9ec76    DOC.md
100644 blob 3c983c1efb8ab258bca8b7a0aed13bd97cf1b47f    LICENSE
100644 blob c11ab5fc05c2cc398f2be094451f34bf428e0a9e    Makefile
100644 blob fa3477c3305c08116408aea0ad30520627598804    README.md
040000 tree 61cc224cb5de89713ac3bdaddc7835ec2dbea129    dist
100644 blob 3ceec31471c4e4d8e38e0f0e506762feda7b852c    package.json
040000 tree 48614476be4f27aad0137d57170852a9d5bdd227    src
040000 tree 05181d9234207ffcc34e0bc4574bd9fb4e3210ad    test

很容易可以理解這個內容的意思,它其實就是這個 commit (HEAD),根目錄下的檔案和目錄,分成四個 column 紀錄,第一欄是檔案權限、第二欄是 object type,第三欄是 hash,最後一欄是檔案/目錄名稱,其中的 object type 如果是tree就是子目錄,blob就是檔案,然後每一筆資料都透過 hash 指引到一個 object 檔案,如果是 tree object 就會看到一樣結構的內容,如果是 blob object 就是該檔案本身,所以就可以透過遞迴操作來把整個工作目錄取出(這是我理解 git 資料結構後認為的作法,不是去看 git 原始碼得知的)。

再回到前面的 commit object,裡面的 parent 屬性就是唯一可以連結每個 commit 之間關係的屬性,如果是 merge point 則會有多個 parent,例如 taipei-mrt 中的南京復興站:

$ git cat-file -p f747e466dd826ff6b81550505412f7bf4875fd68

tree d6d7e908638af9bb8759870723c0c6deed174faf
parent 460ff499e9917051e747a5305daa19bcd80a86ea
parent 5f913238492d3b8ea5a15893b463492681ae59ff
author othree <othree@gmail.com> 1455717204 +0800
committer othree <othree@gmail.com> 1455717204 +0800

南京復興

所以可以理解的是,git 不是紀錄每個 commit 間的 diff,而是記錄每個 commit 的狀態,然後 commit 間的關係只能透過 parent 來連接,是個單向的關係,每個 commit 都只能往上尋找自己的祖先,沒辦法從 commit 連結到自己的子孫,而這個單向的關係,也影響了 branch 的定義。

Git 的 branch 嚴格說起來,其實只是進入 commit tree 的入口,一開始預設只有 master branch 這個入口,就如同上面介紹要怎麼把工作目錄的資料抓出來時一樣,git 會先去看.git/HEAD,HEAD 是一個特殊的參考(reference),預設會指到 master branch,上面介紹時,master branch 的 reference 檔案位置在.git/refs/heads/,事實上,所有的 branch 的 reference 都在這個位置:

$ ll .git/refs/heads/

total 72
drwxr-xr-x  11 othree  staff  374 Sep 10 00:02 .
drwxr-xr-x   5 othree  staff  170 Feb 15  2016 ..
-rw-r--r--   1 othree  staff   41 Aug 14 23:45 blue
-rw-r--r--   1 othree  staff   41 Sep 10 00:02 brown
-rw-r--r--   1 othree  staff   41 Feb 16  2016 green
-rw-r--r--   1 othree  staff   41 Feb 15  2016 green-a
-rw-r--r--   1 othree  staff   41 Feb 15  2016 orange
-rw-r--r--   1 othree  staff   41 Sep 10 00:02 orange-a
-rw-r--r--   1 othree  staff   41 Sep  9 00:06 orange-b
-rw-r--r--   1 othree  staff   41 Sep 10 00:02 red
-rw-r--r--   1 othree  staff   41 Sep 10 00:02 red-27

每個檔案內容都是一個 commit hash:

$ cat .git/refs/heads/*

1873fddf2b760cbb753f6d81076e50a079acd687
1873fddf2b760cbb753f6d81076e50a079acd687
91631c62df972ee49f7699eb6b15cbda913f50e3
a245d40e28f75e3bed1b7a8d726906eb230bd6fe
f861f243e367c15b681919ebc2b1d9d9ceccde17
e73351a53945ebb3fcf0f9bbc078969bdb6dc29b
c13ea61e77de42bc579ecf2a1cf32aa2a4a9d6e0
9ad00b514fccdec7f7f5bb0a91e6d5cb678c2a6f
99e31bceea7e9446e37603c00cafa04f8c4827a2

有這些入口後,就可以進到編輯歷史裡,然後透過每個 commit 的 parent 來串連起 branch 的修改歷史。事實上這種設計並沒有限制 branch 之間一定要有相連,所以就有了可以完全獨立的 orphan branch,最常見被用來做 Github Pages,也可以用來放一些相關的資料,不過實務上,會需要放相關資料在 repository 李的話,通常會在根目錄先建立好不同用途的子目錄,不然要查詢時還要切換 branch 實在也不方便,不過要畫捷運路網時, orphan branch 卻是幫助很大,可以簡單的建立不同路線的起點。

在瞭解 branch 之後,就可以來用 Git 線圖建立路網的交錯了,其實原理很簡單,就是先把兩個分支 branch merge 起來,並且讓兩個 branch 都指向到同一個 merge commit 上,作法就是兩個 branch 互相 merge:

git merge orphan --allow-unrelated-historie
git co orphan
git merge master --allow-unrelated-historie

在 Git 2.9 之後需要加上--allow-unrelated-historie這個參數,第一個 merge 完成會變這樣:

M─┐ [master] Merge branch 'orphan'
│ I [orphan] Commit B
I Commit A

第二個 merge 會是 fast-forward ,然後結果就變成:

M─┐ [orphan] [master] Merge branch 'orphan'
│ I Commit B
I Commit A

可以看到兩個 branch 現在都指向到同個 commit 了,然後分別在兩個 branch 建立新的 commit,就可以分支成兩條線出去了:

o [orphan] Commit D
│ o [master] Commit C
M─┘ Merge branch 'orphan'
│ I Commit B
I Commit A

不過由於 Git 的歷史紀錄只能靠 parent 屬性建立,branch 也只是一個進入編輯歷史的入口,所以追塑到 merge commit 後,就無法分辨更之前的 commit 是屬於那個 branch 了,我的 MRT 線圖也是如此,其實只能維持到 topology 正確,而沒辦法讓所有的 commit 都能分辨出來是哪條線的。

第二個要說的則是git log的部分,當天的分享有講到用 git log range 來查詢捷運路線,主要用的是A..BA...B,其實在一般只有一條線的情形下用,.....都可以用來擷取 A 和 B 兩個 commit 中間的變化,但是用來查捷運路線,就會發現很多時候結果不如預期,事實上,這兩個 syntax 都有其代表的邏輯操作,如同上面所說,git 歷史的記錄只能靠 parent 來建構起來,一般的git log就會從 HEAD 指到的 commit 開始,透過 parent 屬性,把所有可以連結到的 commit 都列出來,而E..B的意思,則是從 E 開始,列出所有 E 能連結到的 commit,但是不列出 B 能連結到的 commit,只有一條線的情境來看:

E-D-C-B-A

E 能連結到的包括了:

E-D-C-B-A
  ^ ^ ^ ^

B 能連結到的包括了:

E-D-C-B-A
        ^

把上面的扣掉下面的就變成:

E-D-C-B-A
  ^ ^ ^

看起來就剛好是 B 到 E 中間的變化歷程,至於...又有點不同,它的定義是列出所有 B 和 E 可以到達的 commit,但是不列出兩者都可以到達的 commit,單一條線的時候,結果會和..是一樣的,所以差異是在有分支的情形才會出現,舉例來說:

o [master] <E> Commit E
│ o [orphan] <F> Commit F
│ o Commit D
o │ Commit C
M─┘ Merge branch 'orphan'
│ I Commit B
I Commit A

如果用...看 E 到 F 的話:

$ git log E...F --oneline --graph

* 8d6c4d5 Commit E
* d2f593f Commit C
* f4dad66 Commit F
* c3ac3ca Commit D

就可以達成類似從 E 站到 F 站的效果,不過由於 Merge commit 是兩邊都可以到達的 commit,所以不會出現,另外就是出現的順序不會正確。所以我在未來發展那邊有提到可以開發一個工具來專門查詢捷運路線,大概要用的方法就是要從每個 branch 進去找,然後還要尋找 merge commit,再從 merge commit,實際上還要仔細想想才能確定查詢的流程就是。

以上就是這次學習到的 git 相關知識,當時投影片上寫著下略五千字,沒想到寫起來真的蠻多的,這篇文章的字數統計還真的有達標。