自從 Git 從 2005 年出現後,到現在幾乎所有軟體開發者都離不開他,加上 GitHub 等服務讓 Git 甚至能當成網頁空間或線上文件共筆平台等。但 Git 初學時可能有許多人不知道如何入門,本篇將簡單整理個人的 Git 學習方式,使用建議與需要注意的概念等,並不會詳細說明 Git 指令等內容。

什麼是 Git

總括來說,Git 是一套版本控制軟體(Version Control System, VCS)。在開發軟體時常常會有不同的需求以及問題,開發者會希望能控制軟體版本並且能回復到之前的狀態,讓軟體開發更順利。古人常說工欲善其事,必先利其器,而 Git 就是能讓我們工作或處理其他事項與分享知識等更方便與有效的

過去也有許多版本控制軟體如 CSV 以及 Subversion 等,但在 Git 問世並隨著 GitHub 等服務在全世界引起風潮,現在的 Git 已經不只侷限在軟體開發,甚至能拿來建立靜態網頁(GitHub Page)以及共筆寫作等,應用層面已遠遠超過原先預設的範圍了。

Git 簡史

Git 的原始開發者就是 Linux Kernel 的開發者 Linus Torvalds,只要是資訊界的人應該都聽過他的大名。我們每天使用的網路系統基底有許多都是使用 Linux 系統服務,也因為有他才能使現在資訊生活越來越便捷。

原先 Linux kernel 並不是使用 Git 來開發,而是使用商用軟體 BitKeeper 來進行版本控制。但在 2005 年時因一些問題使得 Bitkeeper 公司收回了軟體使用權。而在找不到適合的替代軟體的情況下 Linus 決定自己寫一個版本控制系統,在花了 10 天後 Linus 帶著初版 Git 來到世界上。

雖然一開始 Git 只為了 Linux Kernel 的開發而撰寫,但 Git 強大的功能讓其他開發者也逐漸在自己的系統上使用它,而 GitHub 等服務的出現,讓現在 Git 幾乎成為了版本控制的代名詞。除了 Google 將自己的許多開源專案的平台都放在 GitHub 上之外,Microsoft 也將內部的版本控制系統轉換到 Git 中了。

同一時間也有另一名工程師 Matt Mackall,在不知道 Linus 開發 Git 的情況下另外開發了 Mercurial 這套版本控制系統想作為 Linux Kernel 的開發使用。但後來 Linux Kernel 開發社群採用了 Git。

個人如何應用 Git

個人最先使用的版本控制軟體是 Subversion,但當時也只把它當做備份軟體用。而當開始轉換到 Git 並去了解內部運作方式後,才真的了解到 Git 的設計顛覆了許多對版本控制系統的理解,並開始應用在各個方面中,如

  • 程式碼管控

Git 最主要的功能了,除了在工作上使用外,也會用 GitHub 或 GitLab 分享自己的程式內容與成果。

  • 日常文件撰寫與紀錄

在紀錄事情方面,剛開始時是使用 Google Doc 來紀錄,但後來考慮到需透過瀏覽器才能編輯,也無法自由轉換到其他平台上,現在主要透過 Visual Studio Code 或 Vim 等編輯軟體,並以 Markdown 格式來紀錄每天所得項目,並利用 GitLab 來備份內容。

  • 建立個人專屬網站

GitHub 或 GitLab 都有提供免費靜態網頁空間的功能,能當成個人的網頁空間來使用。現在所看到的 Blog 就是透過 Hugo 這套靜態網頁產生器(Static Site Generator) 來產生網頁 HTML 檔案後,使用 Git 將其上傳到 GitHub 並透過 GitHub Pages 的功能來顯示網頁內容。

除了上面的應用外,也有人透過 Git 來共筆使用 Latex 撰寫論文 等。

如何學習 Git

由於網路上已經有著許多豐富的 Git 學習資源,所以個人不會再詳細描述 Git 功能與操作方式,而會提供一些初學者可能搞混得的以及 Git 使用上的建議。初學者可參考下面列出的網站學習 Git 的使用

其中個人最推薦的是 Pro Git 一書,該書現在出到第二版(繁體中文部分翻譯),其中對於了解 Git 的大小事情,包括 Git 的歷史,檔案儲存方式,Repository 結構,Garbage Collection,Git Hooks 等項目都有詳細的介紹。

Git Tips

若已對 Git 的概念與操作有一定的認識後,下面是一些個人認為有用的概念要點與進階內容。

熟悉 Command Line,避免依賴 IDE

許多新手在初學 Git 時因對該流程與概念不熟悉,或是不熟悉 command line 操作等因素,常常會使用如 SourcetreeFork 等 GUI 軟體來進行 Git 的操作。個人認為雖然一開始使用這些套裝軟體無傷大雅,但長久來看導致許多問題存在。

使用 IDE 軟體常常會不了解 Git 真正的執行步驟,且沒支援的功能就無法使用,因此要真正理解 Git 所提供的各項功能就必須熟悉 command line。此外如果是使用 Terminal 在遠端 Server 中工作時,就必須使用 command line 來進行 Git 操作,無法透過 IDE 來處理,因此非常建議一定要習慣使用 command 進行各種的操作。

SHA-1 Hash

我們常常在 Git 中看到一常串的文字

a906cb2a4a904a152e80877d4088654daad0c859

初學者可能會很困惑這些奇怪的文字是什麼,實際上該文字是 SHA-1 hash function 所產出結果的的 16 進位表示字元。而 Git 對所有物件的索引值都是採用檔案內容的 SHA 1 編碼結果,如一個 README.md 的檔案經過 SHA-1 計算後,得到的索引可能是

a906cb2a4a904a152e80877d4088654daad0c859

而當對 README.md 的內容做修改後,其 SHA-1 結果可能變化成為

8f94139338f9404f26296befa88755fc2598c289

因此一個檔案的 SHA-1 可以代表該檔案是否發生變化。Git 所有內容包含 commit object 以及資料夾結構物件 tree object 都是使用 SHA-1 作為索引值。要計算檔案對應的 SHA-1 可用 git hash-object <file> 指令

因 SHA-1 產出的結果為 160 bits 資訊,其 16 進位表示法結果都為 40 chars。此外 SHA-1 可能會發生兩個不同檔案產生同樣的結果,也就是發生碰撞(collision),但這個機率其實非常低,低到 Git 使用到現在還沒發生碰撞的問題。

了解 .git 資料夾結構

當執行 git init 初始化指令後,會在該資料夾下面新增一個 .git 的資料夾,該資料夾就是 Git Repository 的本體所在。其中包含 blob objects, branch, remote, config 設定檔等。.git 儲存了 Git 所有資訊。在 Local 端對 Git 的所有操作,也都是反映在這個資料夾中。

.git 內包含幾個部分

  • HEAD - 目前所在的 branch。
  • config - 該 Git 資料夾的區域設定檔。
  • index - 資料暫存區
  • objects - Git 所有資料的 blob objects 儲存處,主要包含三種類型
    • commit objects - 每筆 commit 的資訊,包含 author, parent commit object, commit message 以及所指向的 tree object 資訊。
    • tree objects - 記錄資料夾的檔案結構,包含檔案的路徑,名稱,模式以及對應的 blob objects
    • blob objects - 記錄實際檔案物件的二進位內容。

當我們每次進行 branch 切換時,Git 會去 objects 資料夾中找出該 branch 對應的 commit object,從 commit object 中取得對應的 tree object 後,再由 tree object 所記錄的檔案結構將資料夾回復到該 branch 所對應的狀態。

Git objects model (From: Pro Gti) Git objects model (From: Pro Gti)

可以在進行 Git 操作看看 .git 內做了哪些什修改。

更詳細的內容可參考 Pro Git 中 Git Internal 的章節。

Branch 只是輔助,Commit 才是本體

Git 的歷史是由 commit 與其內部紀錄的 parent commit 所連接起來,而 branch 只是參照到的特定 commit 的紀錄。當在一個 branch 上執行 commit 時,是新增一個 commit object 並將現在的 branch 指向新建立的 commit。

我們可以在 .git 資料夾中看到紀錄對應的檔案。如 maseter branch 對應的 commit 就紀錄在 .git/refs/head/master 的檔案中。查看當中內容可以看到

cat .git/refs/head/master
ff5f583564a6108407d9a2304af839e877d80f3c

這樣的內容,代表 master branch 對應到 ff5f583564a6108407d9a2304af839e877d80f3c 的 commit。

在 Git 中,對 Git Branch 的操作都可以用 commit 的 SHA-1 編號代替。假如我們想加入另一個 commit a7ccca6fd65bc347a065d83a8f32fd347bab544f 的修改內容,可以執行

# a7ccca6fd65bc347a065d83a8f32fd347bab544f 可用前面的編號取代完整的 SHA-1 編碼
git cherry-pick a7ccca

來進行 cherry-pick 操作

以 commit 歷史與內容來思考要進行的操作

當在一個 branch 上開發軟體時,假如有其他 branch 有我們想要套用的修改,或是想回復檔案狀態等操作。在過去可能會想要將檔案直接複製文件中。而在 Git 上可使用 reset, revert, checkout, rebase, cherry-pick 等指令改變 commit tree 或直接將變動套用到現在的程式碼中,這會比用複製檔案來的省事且不容易出錯。如下面的範例

# 切換到特定 commit
git checkout <commit>

# 取出由特定 commit 的 file 內容
git checkout <commit> <file>

# 將現在的 branch 切換到特定 commit
git reset <commit>

# 將 stage 的檔案回復到 unstage 狀態
git reset HEAD <staged file>

Git 中有許多類似的指令,能夠讓使用者非常方便的做內容變動與修補,因此只要能更理解 Git 的運作架構並且善用對 commit tree 的操作,就更能發揮 Git 強大的功能。

使用 GitLab 或 GitHub 等程式碼代管服務

除了 Local 端的操作外,Git 的價值之一就在多人協作非常便利,因此誕生了如 GitHub 或 GitLab 的原始碼代管平台。開發者可以將自己的 Git Repository 推送(push)遠端平台進行備份與管理。

這些平台除了提供程式碼代管外,也提供如 Issue, Milestone, Pull Request(Merge Request) 等多人協作所需功能,甚至能透過 Webhook 等呼叫 CI/CD 服務進行自動化測試部署,甚至透過所提供的 Page 功能當作是靜態網頁空間使用。

選定適合的開發模式

Git 在使用上非常自由,不論是輕量的 branch 或是快速的 merge 甚至對 commit 歷史都能進行操作,但也因為如此團隊開發時需要一套統一的開發流程與規範,才能在開發時才不會造成混亂。

Vincent Driessen 在 2010 在一篇名為 A successful Git branching model 提出了一個稱為 Git Flow 的開發方式,透過將 branch 分為 masterdevelop 等主要分支,以及 feature, releasehotfix 等支援分支來管理開發版本,並使用一定的開發流程如 feature 為由 develop 上建立並加入功能的 branch,而 release 為由 develop 分出來進行 debug 後再併入 master 發佈該版本等流程來使開發流程更加有組織性。

而除了 Git Flow 外,也有其他如 GitHub FlowGitLab Flow 等開發流程,團隊可根據需求與習慣選擇適合的開發模式。

正確的 commit message 格式

Git 的 commit message 是留下修改訊息的重要內容,當在與團隊協同開發,清晰易懂的 commit message 能讓其他人快速了解到該 commit 做過哪些改變,特別是為什麼(Why)要做以及做了什麼(What),而不是如何(How)做的訊息(從 Code 中就能了解)。

而如何寫出適合的 commit message 可參考 How to Write a Git Commit Message 這篇文章,中文翻譯可在 如何寫一個 Git Commit Message 這篇文章中找到。

當然這些並沒有一定必須如此撰寫的規定,一切都看團隊如何這定一個共通的標準。

減少 clone repository 所佔用的空間與時間

在執行 git clone 指令時,會將目標的 repository 全部抓下來,但如果該 repository 太大, 許多過去的資料可以不用抓下來節省空間與傳輸時間,這時可以透過 --depth <depth> 的參數來建立 shallow clone,如下面的指令

git clone --depth 1 GIT_REPO

執行後 Git 只會抓取 master branch 中最新一筆 commit 資料,可大幅節省儲存空間與 clone 的時間。

Garbage Collection

在許多 Git 教學中都提過,當使用 Git 將檔案加入後會儲存檔案的完整內容。但假如只修改一小部分卻需要保留整個檔案,將會使 blob objects 的資料儲存空間的使用非常沒有效率。因此 Git 可以執行 Packfiles 動作,將許多的離散資料打包成依照差異組成的單一 Packfile 以節省空間消耗。

Packfiles 動作通常在檔案數量多到一定程度(約 7000 objects)時會自動執行,而我們也可透過 git gc 指令手動執行 Packfiles。

更詳細說明可參閱 Pro Git - Packfiles 的章節。

程式模組管理 - Submodule 與 Subtree

在開發程式有時會需要引入其他外來的程式庫,或是將自己在撰寫的程式庫釋放出去。但當對方或自己修改程式時就需要重新下載或是釋放一次,因此當其他使用了你的程式庫做了修改後,要修補時會非常不方便。這時我們可以使用 submodulesubtree 的功能來將內部的資料夾作為一個獨立的 Git Repository 個體來管理做 branch, commit, push, pull 等功能。雖然 submodule 與 subtree 都是來做模組管理功能,但實現方式不一樣。

submodule 會在資料夾的 .gitmodule 文件中加入該模組的名稱,位置,對應的 Git Repository 等資訊。當使用 clone 功能把該 Git Repository 抓下來後再更新管理的 module。而 subtree 則是直接將該 Git Repository 的內容加入到現在的 Git 歷史中,當作原始碼的一部分管理。往後只要將該 Git Repository 抓下來不需要在更新管理的 module。兩種方式各有各的好處,可依照各自的需求決定使用的方式。

Reference