昨天,突如其來的好奇充斥著我的腦袋:究竟 git push 如何通過 SSH 工作呢?由於我越來越習慣使用 strace 來折騰這類問題,所以我又嘗試用它來練練手。如果我利用 strace(跟蹤)git push 命令到這個網站的(資料)庫,會得到如下顯示:
1 |
[pid 15943] execve("/usr/bin/ssh", ["ssh", "git@github.com", "git-receive-pack 'kamalmarhubi/w"...], [/* 51 vars */]) = 0 |
所以 git push 最終會呼叫 ssh git@github.com git-receive-pack <repo-path>。然後在我的終端(terminal)嘗試輸入以下命令,得到了以下線索:
1 2 3 4 5 6 7 |
$ ssh git@github.com git-receive-pack kamalmarhubi/website 00bb2979fec627d60938c4ed2086cc60bb1 refs/heads/gh-pagesreport-status delete-refs side-band-64k quiet atomic ofs-delta agent=git/2:2.4.8~upload-pack-wrapper-script-1211-gc27b061 003f04bfcb3e238e5660ae9e71a6ce99f472211fe85f refs/heads/master 0000 |
終端依然在等待我的輸入。SSH 用來解決身份驗證和遠程控制的問題,驗證成功後,SSH 的另一端會執行一段命令來進行資料交換。而上面這幾行就是資料交換的開始。
在網上稍微搜尋了一下,我知道了這個協議是由行組成的,而每一行的 4 位前置碼正是行長度的十六進位制表示。後面跟著提交的 SHA-1 和 ref ,傳送端以一行 “0000” 作為結束識別符號。
上面的每一行對應(資料)庫裡的每一個分支:第一行自帶了一條長長的小尾巴,好像是傳送程式的自我介紹和支援的相關功能。
我在研究這些程式碼的時候,使用 xsel 命令把輸出結果複製到編輯器上面,不過令人困惑的是,我貼上得到的竟然只有第一行而不是所有後設資料。
1 |
00bb29793c39c8e4bfec627d60938c4ed2086cc60bb1 refs/heads/gh-pages |
通過 hexdump –C 檢視完整的輸出後發現,原來在 refs/heads/gh-pages 後面有一個空位元組,而且在末尾處還換行了(用星號 * 標記處):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
00000000 30 30 62 62 32 39 37 39 33 63 33 39 63 38 65 34 |00bb29793c39c8e4| 00000010 62 66 65 63 36 32 37 64 36 30 39 33 38 63 34 65 |bfec627d60938c4e| 00000020 64 32 30 38 36 63 63 36 30 62 62 31 20 72 65 66 |d2086cc60bb1 ref| 00000030 73 2f 68 65 61 64 73 2f 67 68 2d 70 61 67 65 73 |s/heads/gh-pages| 00000040 *00*72 65 70 6f 72 74 2d 73 74 61 74 75 73 20 64 |.report-status d| 00000050 65 6c 65 74 65 2d 72 65 66 73 20 73 69 64 65 2d |elete-refs side-| 00000060 62 61 6e 64 2d 36 34 6b 20 71 75 69 65 74 20 61 |band-64k quiet a| 00000070 74 6f 6d 69 63 20 6f 66 73 2d 64 65 6c 74 61 20 |tomic ofs-delta | 00000080 61 67 65 6e 74 3d 67 69 74 2f 32 3a 32 2e 34 2e |agent=git/2:2.4.| 00000090 38 7e 75 70 6c 6f 61 64 2d 70 61 63 6b 2d 77 72 |8~upload-pack-wr| 000000a0 61 70 70 65 72 2d 73 63 72 69 70 74 2d 31 32 31 |apper-script-121| 000000b0 31 2d 67 63 32 37 62 30 36 31*0a*30 30 33 66 37 |1-gc27b061.003f7| 000000c0 39 32 66 34 39 36 65 37 35 33 64 62 39 33 33 30 |92f496e753db9330| 000000d0 66 30 61 34 65 38 32 39 30 62 38 61 36 63 62 61 |f0a4e8290b8a6cba| 000000e0 38 61 62 36 64 61 62 20 72 65 66 73 2f 68 65 61 |8ab6dab refs/hea| 000000f0 64 73 2f 6d 61 73 74 65 72 0a 30 30 30 30 |ds/master.0000| 000000fe |
我在沒有仔細研究的情況下,大膽地做了一個猜想:那些在 github 上做開發的傢伙們,定義了一個相當簡單的長度字首+換行分隔協議(length-prefixed, newline-separated protocol),當他們有需要向協議裡面加入一些後設資料的時候,可以保持和老版本 git 的相容性。這個解決方案巧妙地利用了 C 語言的 0 結尾字串:把後設資料放在空位元組和新行之間。用這種方式讀取資料(讀取到新行之前位置)可以得到所有的後設資料。後設資料處理程式碼在執行時會跳過空位元組,但是現有的協議程式碼卻只能看到到空位元組之前的資料,它們對這種改動完全無感!
所以我之前用 xsel 命令複製資料時,那些空位元組後面的東東被完美地忽略了。
真相就只有一個,謎底就這樣被解開了!(其實wo真不是柯南哦)