易學難精:Git 簡單的部分(一)

img

前言

說到 git 就很容易聯想到 SVN(Subversion 簡稱),因為同樣都是在做版本控制,所以也常常被拿來比較,同時聯想到的就是錯綜複雜的時間線圖。
img
接著我們就像物流中心的調度員一樣開始監控每台配送車的”配送路線”,當然不令人意外的客戶需要的”貨品”常常有出入,買A貨給B貨、東西少了(功能)、東西多了(Bug),最重要的是哪個環節出錯不知道。
img
查詢維基百科就可以發現它把 git 定位在”軟體”,而把 SVN 定位在”系統”,所以 SVN 無庸置疑的應該比 git 強大很多,但是現今 git 似乎快成為版控的唯一代名詞了(問了周遭朋友幾乎都只想到 git)。
img
網路上其實已經有很多專業的教學文章,例如:官網文件30 天精通 Git 版本控管,所以我們不在此討論如何使用 git,而是研究一下 git 在做什麼?

環境設置

因為筆者電腦是 Windows 10,所以就以 Windows 當作測試環境。

安裝 Chocolatey

img
開啟 Chocolatey 官網從標題就可以知道它是一個在 Windows 上的套件(軟體)管理工具,各種常見的軟體幾乎都可以透過它來快速安裝。
安裝說明頁面可以看到我們必須以系統管理員身分開啟 PowerShell 並執行下列指令就可以安裝完成。
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
img

主要指令如下
choco install [package name]:安裝套件。
choco upgrade [package name]:升級套件。
choco uninstall [package name]:移除套件。

可以透過 choco upgrade chocolatey 指令來升級 Chocolatey,透過 -y 參數來同意安裝過程的執行動作。

我們可以透過搜尋功能來找我們需要的軟體以及對應的安裝指令。
img

安裝軟體

接著我們便利用 Chocolatey 來安裝軟體,例如筆者透過下列指令一次安裝完 gitTortoiseGitposh-gitCmderVisual Studio Code
choco install git tortoisegit poshgit cmder visualstudiocode -y
img
跟我們一般安裝模式一樣,Chocolatey 所安裝的軟體也可以透過控制台來移除。
img
對於一些本身就不是標準的安裝軟體(如:Cmder、posh-git)則會被安裝到 C:\tools 資料夾內。
img
git:必要安裝軟體。
TortoiseGit:git 的 GUI 工具,有很多同類產品,選擇一套即可,TortoiseGit 也提供中文語系套件可以安裝。
img
posh-git:可以讓我們在 PowerShell 下看到目前目錄的 git 版控狀態。
img
Cmder:可以讓我們執行 PowerShell、Cmd(命令提示字元)、bash 的強大工具。
img

筆者比較喜歡安裝 Cmder-powerline-prompt 並透過 Cmder 來執行 Cmd。
img

Visual Studio Code:簡稱 VS Code,強大的文字編輯器,支援 git 版控操作。

git 版本控制

由筆者比較不熟悉 git 所以後續操作會以 GUI 工具 TortoiseGit 為主,減少初期學習時間,當然我們也會盡可能地加入對應的指令操作。

建立全域的帳號 (name、email)

git 在版控儲存時同時會記錄帳號資訊,因此必須先設定,我們可以透過 TortoiseGit 來設定,在桌面或是檔案總管內按滑鼠右鍵,選擇 TortoiseGit => 設定 => Git 即可到編輯頁面,輸入後按確定儲存設定。
img

也可透過指令 git config --global user.name [name]git config --global user.email [email] 來設定。
img

版本庫 (Repository)

首先建立一個測試用資料夾,在此筆者取名為 demo_git,接著我們利用 TortoiseGit 功能選單來建立 Repository,建立好之後可以發現資料夾圖示改變了。
img
我們可以從 TortoiseGit 設定頁面了解各種版控過程的狀態所對應的圖示,同時這代表這個資料夾納入版控,相反的如果檔案或是資料夾不是相關圖示就表示沒有納入版控。
img
開啟資料夾可以發現 git 幫你建立一個 .git隱藏資料夾,這也就是 Repository,所有的版控資訊都會紀錄在此資料夾內,也就是說當你不想版控時,把 .git 資料夾刪除掉即可,前提是要先確認你目前的版本是你所需要的最終版本
img

也可以透過指令 git init 來建立 Repository。
img
或者使用 VS Code 來建立。
img
VS Code 預設會隱藏 .git 資料夾,我們可以透過設定來取消隱藏。
img

預設分支 (master HEAD)

不論是透過 TortoiseGit 功能選單或是 Cmder 都可以發現多了一個 “master“ 的詞彙,這個 master 便是 我們所講的 HEAD。
img
在此我們先不解釋 HEAD 的功能,只要了解到 git 在建立 Repository 時會順便幫我們建立一個名叫 master 的 HEAD,這句話是對的,但同時也是錯的,對的是我們從工具上來看確實有 master,錯的是 git 如果建立 HEAD 時會在 .git\refs\heads\ 資料夾下建立同名的檔案,這時候才算真正存在的 HEAD,但是目前我們瀏覽資料夾發現目前沒有任何檔案,也就是說目前沒有任何 HEAD。
img
那 master 這個 HEAD 是怎麼出現的,系統內定的嗎?說對了一半,我們開啟 \.git\HEAD 檔案就可以發現這邊會記錄目前所參考的 HEAD 就是 master。
img
我們嘗試修改成另一個不存在的 HEAD master1,可以發現不論是 Cmder 或是 TortoiseGit 都會顯示目前的 HEAD 為 master1,也就是說 git 預設會以 \.git\HEAD 所指定的 HEAD 當作目前所在的 HEAD,但是不會檢查此 HEAD 是否有效,後續再演練切換分支時可以再看看是否會跟著改變。
img

強烈建議:所有操作都應該透過合法的工具(git 指令或 GUI 工具) 來處理,切勿自行手動修改,在此的調整單純是為了驗證。

由此可知 git init 會在建立 \.git\HEAD 同時將它指定到名為 master 的 HEAD,但是並不會建立這個 HEAD

提交 (Commit)

Commit 說是版控最重要的功能也不為過,因為所有的版控軟體(系統)都有這個功能,所以有些公司會要求員工下班前先 Commit 一次,甚至拿來當成工作日誌,資深的前輩也會提醒”有拜有保佑(有提交就有救)”,做任何危險動作之前先提交,Commit 簡而言之就是將需要版控的檔案備份起來,每一次 Commit 就會建立一個目前狀態的紀錄,這份紀錄會保存當下檔案的”版本(內容)”,隨著時間的推移我們會對文件的內容作調整,而版本控制就是讓我們可以比較甚至還原過去的內容(版本)。
接下來我們便來嘗試透過 TortoiseGit 來 Commit,但是輸入訊息紀錄後卻發現無法 Commit,怎麼第一步就踢到鐵板。
img
改透過指令 git commit -m [message] 或是 VS Code 就可以發現原來在沒有任何變更的情況下,git 預設是不允許 Commit,因為目前的版本跟上一次版本一樣,。
img
img
當然我們也可以強制提交,只要勾選”只輸入訊息”(從提示訊息可以知道就是多帶入一個參數 --allow-empty)就可以完成 Commit(提交訊息會出現一組7位的英數字 fe42787)。
img
從官方文件可以看到原來 git 是透過與”父提交(Parent Commit)”比較來判斷有無變更,但是 git 怎麼確認哪一個是 Parent Commit?
img

commit 詳細指令請參閱:https://git-scm.com/docs/git-commit

我們重新檢視 \.git\refs\heads\ 資料夾發現多了一個 master 的檔案,也就是說 master HEAD 這時候真正被建立起來了,查看內容可以發現包含一組40碼的英數字 fe42787d9357684ef05ce87362a7e3b5c86aa543,而且湊巧的前7碼與剛才 Commit 完成訊息的7碼一樣。
img
更厲害的是我們開啟 \.git\objects\ 發現多了一些資料夾,打開裡面的 fe 可以看到有一個38碼的檔案 42787d9357684ef05ce87362a7e3b5c86aa543,我們把2碼的資料夾名稱再加上38碼的檔案名稱拼起來竟然跟 \.git\refs\heads\master 內容一致,很顯然的這一切都是有關聯的存在。
img

物件 (Object)

git 其實會將特定的資料保存在 \.git\objects\ 內,保存方式是將資料的內容以 SHA-1 雜湊演算成一個40個十六進制的雜湊值,並以前2碼當作資料夾後38當作檔名的方式儲存,SHA-1 雜湊演算對於不同的內容(那怕是多一個空白)都會產生不同的雜湊值,所以可以確保所有不同的資料都可以被儲存。

史上第一例!Google破解SHA1實現碰撞攻擊:科學家花了2年的時間才能發生”刻意”的碰撞,所以正常情況下,我們可以視為不會發生。

commit 類型

接著我們可以透過 TortoiseGit 查看日誌,可以發現 Commit 許多資訊。
img
img
不過想要真正了解儲存哪些資訊就必須透過指令 git cat-file 來查閱。
git cat-file -t <object>:透過 -t 參數可以查詢 Object 的類型,我們可以看到 fe42787 的類型是 “commit”。
git cat-file -p <object>:透過 -t 參數可以查詢 Object 的內容,我們可以看到這個 commit 類型內容包含:tree 物件及其對應的雜湊值,作者資訊(名稱、郵件、日期)、提交者資訊(名稱、郵件、提交日期)、Commit 的訊息
img

git 再須帶入 object(雜湊演) 時可以只要帶入前4碼(或以上)的數列即可。

tree 類型

不意外的 tree 類型也存在於 \.git\objects\ 內。
img
目前查看 4b825dc 只能確定它是一個內容空白的 tree 類型。
img
接下來我們模擬一些需要版控的檔案,建立一個 1.txt,一個內含 a1.txt 的 a 資料夾,一個空白的 b 資料夾。
img
接著透過 TortoiseGit 來將檔案加入版控,可以發現加入完成時可以發現檔案圖示也變成 git 相關圖示,其實訊息視窗還同時提供 “提交(Commit)” 功能,也就是這些剛加入版控的檔案”內容”還沒被儲存起來,目前的動作只是將要檔案狀態記錄在位於 \.git\index 的索引檔(Index)內,
img
我們現在 Commit 時就會發現因為狀態有變更,所以提交按鈕可以直接點選。
img
但是我們發現 master HEAD 所對應的 commit 類型由原本的 fe42787 變更成 ca206d2,查看日誌也確實改變了。
img
img

我們也可以透過指令 git add [file name] 將要納入版控的檔案逐一加入,或是直接使用 git add . 讓 git 幫我們把未加入版控的檔案都加進來,接著透過 Commit 指令來提交。
img
或者利用 VS Code 提交功能來處理,它提交前景是我們那些檔案未納入版控並協助我們處理。
img

接著我們透過指令來查詢 ca206d2 內容,可以發現 tree 也由原本的 4b825dc 變成 4083cde
img
查詢 4083cde 資訊可以看到裡面記錄著檔案 1.txt 以及資料夾 a 的資訊,我們可以看到 a 資料夾又是一個 tree 類型,查詢該雜湊值 2bda749 資訊,不難想像它就包含著 a 資料夾內的 a1.txt 的資訊,所以我們可以說 tree 類型就是對應實體資料夾的類型,說它就是資料夾其實也不為過。
img
tree 類型內容包含:關聯模式(associated mode)、物件類型、雜湊值、檔案名稱

blob 類型

查詢 1.txt (aa75558)、a1.txt(3502cc3) 的資訊不難發現 blob 類型就是原始檔案的內容,注意只有內容,沒有檔名,檔名被彙整到上層的 tree 類型。
img

反向思考

比較前後差異可以得一個很複雜的示意圖,可以發現雜湊值是整個串聯的關鍵,但是 Commit 一次幾乎所有的雜湊值都變更了。
img
committreeblob 都是 git 的 Object,都儲存在 \.git\objects\ 內,還記得它的規則嗎?
先將要儲存的內容透過 SHA-1 雜湊演算出雜湊值,再以這個雜湊值當作檔名來儲存。
接下來我們一步一步說明,這個邏輯與實際運作模式會有所出入,但是筆者覺得比較容易理解:

  • 1.因為我們增加了檔案,git 會由內而外的逐一將檔案儲存到 \.git\objects\,當然到的最上層的 tree 時內容勢必會變更
  • 2.因為內容變更所以 git 會演算初不一樣的雜湊值 4083cde
  • 3.接著以 4083cde 為檔名(前2碼為資料夾),將新的 tree 內容儲存在 \.git\objects\
  • 4.最後再修正 commit fe42787 所對應的 tree。

img

  • 1.承上,變更了 commit 內的 tree。
  • 2.因為 commit 內容變更所以 git 會演算初不一樣的雜湊值 ca206d2
  • 3.接著以 ca206d2 為檔名(前2碼為資料夾),將新的 commit 內容儲存在 \.git\objects\
  • 4.最後再修正 HEAD master 所對應的 commit。

img

所以 Commit 其實不是在建立新的版本,它只是在儲存檔案以及修正雜湊值,只是當我們檔案內容或是結構變更時會造成演算出來的雜湊值與原先的布一樣,所以間接影響到 Commit 內容的修正,而 Commit 內容的變更又在影響到 HEAD 內容的修正,所以最終會讓我們感覺每一次的 Commit 都會產生一個新的版本(雜湊值)。

內容變更就會產生新的雜湊值,所以我們只要了解 git 內每個檔案類型儲存那些資料就可以了解它影響的層面。

類型 內容
HEAD commit 雜湊值
commit tree 雜湊值、作者名稱、作者郵件、發布日期、提交者名稱、提交者郵件、提交日期、commit 訊息
tree tree 或 blob 的 關聯模式(associated mode)、物件類型、雜湊值、名稱
blob 實體檔案內容

官方繁體文件 1.3 開始 - Git 基礎要點 似乎翻譯有點錯誤,”Git 會根據檔案的內容資料夾的結構來計算”,應該是”檔案的內容資料夾的結構”,檔案的內容像是 commit、blob,料夾的結構則是 tree,但是其實都是指內容的變更。
img
英文版簡體版都是用或(or)
img
img

測試

開啟 \demo_git\a\,我們將檔案 a1.txt 複製多份。
img
將這些檔案加入版控並提交出去。
img
img
查詢 \demo_git\a\ 對應的 tree 可以發現有趣的事,所有的檔案都對應到同一個雜湊值。
img
如果你充滿疑惑,那應該是把 git 想複雜了,這些 txt 檔案的內容其實都一樣,在 git 內會以 blob 類型來儲存,而 blob 檔名的雜湊演算指計算檔案內容,並沒有實體檔名,所以才會對到同一個雜湊值,當然這也意味著在 git 內只會存一份檔案,也就是相同的內容只會有一份,所以 git 比起其他版控軟體相對儲存空間較小。

線圖 (Commit History)

眼尖的人應該發現在談論 commit 類型內容時有一行資料並沒有說明。
img
這個 parent 其實就是**父提交(Parent Commit)**,就像之前說的,當 commit 內容有變更時,勢必會產生新的雜湊值,進而建立新的 commit,git 會先將目前 commit 的雜湊值加入到新的 commit 內容在做雜湊演算,因此就可以之前每個 commit 源自哪一個 commit。

追朔

查詢 \.git\refs\heads\master 可以得知目前對應的 commit 為 910d17e
img
透過指令 git cat-file -p 910d17e 查詢內容,可以看到它源自另一個 commit ca206d2
img
透過指令 git cat-file -p ca206d2 查詢內容,可以看到它源自另一個 commit fe42787
img
透過指令 git cat-file -p fe42787 查詢內容,可以看到沒有任何 parent commit,也就是代表這是第一次 commit。
img
最後打開 TortoiseGit 日誌來比較可以發現是一致的,也就說我們所看到的線圖是由後往前追朔,依照 HEAD 所記錄的 commit 當作起點,逐一往前搜尋 parent commit,一直到沒有 parent commit 為止,整個追朔過程與時間毫無關係,所以嚴格來說它不能算是 Timeline,只是因為因為現實上的時間之不可逆,所以造成 commit 與時間有著正向關係,因此說是 Timeline 也沒錯。
img

.gitkeep 與 .gitignore

如果仔細看檔案總管的圖示或是在 Commit 過程有注意到有哪些變更就可以發現另一個問題,b 資料夾並沒有被納入版控,因為 git 預設並不會將空資料夾納入版控。
img
如果希望將空資料夾加入版控,解決的方式其實就是加入一個檔案讓它不再是空資料夾,在習慣上我們會建立一個 .gitkeep 的空檔案來填塞,它並沒有任何特殊義務,只是名稱上容易理解。
img
有強制加入的需求相對的就有強制不加入的需求,如果我們希望有些檔案或是資料夾不要納入版控,可以在加入一個檔案 .gitignore,並在內部編寫不希望納入版控的資料。
我們已 Angular 專案為例,透過 Angular CLI 所建立的專案預設會在 \src\assets\ 內建立一個 .gitkeep 檔案,這樣 assets 資料夾就可以被自動納入版控,而在最外層資料夾又建立一個 .gitignore 檔案,預設已經加入許多要排除的檔案及資料夾,例如 node_modules 就是 Node.js 套件的存放路徑,因為這些套件本身就透過 npm 來管理,所以正常大家都不會將它納入版控。
img

建立新分支 (Branch)

接著利用 TortoiseGit 來建立一個 h1 的分支。
img
從日誌可以看到 h1 分支被建立而且與 master 對應同一個 commit 雜湊值。
img
所以建立分支時預設會複製當前的 HEAD 所對應的 commit 雜湊值當作新分支的預設 commit 雜湊值。
img

也可以透過指令 git branch [branch name] 來建立新的分支。
img
另一個比較不正式的做法就是自己在 \.git\refs\heads\ 內建立一個檔案(這邊以 h2 為例),並手動填入完整的 commit 雜湊值。
img

切換分支 (checkout)

同樣的利用 TortoiseGit 來切換到 h1 分支。
img
從日誌或是 Cmder 上都可以看到確實切換成 h1(TortoiseGit 會以紅色背景來表示目前分支)。
img

也可以透過指令 git checkout [branch name] 來建立新的分支。
img

分道揚鑣

目前我們已經切換到 h1 分支,接著建立一個 h1.txt 檔案並 Commit 一次。
img
從日誌可以看到 h1 所對應的 commit 已經改成 976bfb2,與 master 分支已經錯開。
img
再次切換回 master 分支,可以發現 h1.txt 檔案不見了,確實恢復到 master 分支所對應 commit 的狀態。
img
接著在這個 master 分支上建立一個 master.txt 檔案並 Commit 一次。
img
從日誌發現 h1 分支不見了。
img
勾選”所有分支”,這時 h1 分支才出現,但是線圖上 masterh1 確實開始出現”分支“了。
img

後記

這邊我們只提到一些最常使用的功能,並概略的說明它背後的運作模式,其他的進階功能很多也都是從這些基礎的概念演變而來的,後續有時間我們再陸續補充。