# Git 與團隊協作
該文件簡介 Git 的使用、協作的相關知識、專案管理者與協作人員的相關行為
## 安裝 Git
請直接到[官方網站](https://git-scm.com/download/win)下載。假設你是 Windows 用戶,請確保 Git bash 也有被一起安裝。
除了Git軟體以外,你也可以安裝如 [Github Desktop](https://desktop.github.com/download/)、[Git Kraken](https://www.gitkraken.com) 等有 GUI 界面的相關軟體
## 簡介
一個 Git 倉庫(Repository) 的用途是儲存程式碼與歷史版本的地方。包含相關的文件與每個文件的修改歷史,使得開發者得以查看、回溯、管理不同版本的原始碼,僅需要知道幾個重要概念
你可以在[該處](https://www.w3schools.com/quiztest/quiztest.asp?qtest=GIT)檢驗你是否已經會 Git 的基本操作
如果你已經會 Git 的相關操作,請直接查看最後兩個章節 **給協作人員的建議**、**給管理人員的建議**
### 暫存區(Staging Area)
Git 的一大特點是可以快速暫存部分文件並提交,而無需提交工作目錄中所有已修改的文件,或在提交時列出它們。
這使得你可以僅暫存文件的部分修改,避免了不相關的修改被一起提交的情況。你可以根據需要分別暫存多個不同的修改。

當然,如果不需要這種精細控制,在提交命令 _`git commit`_ 中添加 `-a` 參數,將所有文件的所有更改直接添加到暫存區。
### 本地倉庫(Local Repository)
存儲在你的本地計算機上的完整的 Git 倉庫,包括所有的提交記錄、分支和標籤。
你在本地進行的所有操作(如添加、提交、合併等)都會影響本地倉庫。
### 遠端倉庫(Remote Repository)
存儲在遠程服務器上的 Git 倉庫。通常用於團隊協作開發,團隊成員可以通過推送(git push)和拉取(git pull)與遠程倉庫同步,
以共享和獲取最新的代碼。通常會使用 Github 或是 Gitlab 作為遠端的 Git 倉庫

在稍後,會簡單介紹 Github 與 Gitlab 用的 **整合式管理員** 工作流程
## 常用指令
* `git init` - 初始化一個新的 Git 倉庫
* `git clone` - 從一個遠端倉庫(通常是 GitHub 或 GitLab)複製一個完整的 Git 倉庫到本機
* 如果是拉取大型的倉庫(比方說 Chromium 或是Linux Kernal),通常會加上 `--depth=1` 僅拉取最近1次的提交紀錄
* 使用 `--depth` 參數複製的倉庫不包含完整的提交歷史
* `git status` - 顯示當前工作目錄的狀態
* 當前所在的分支
* 哪些文件被修改了但還沒有提交
* 哪些文件被添加到**暫存區**
* `git log` - 顯示倉庫的提交歷史。預設情況下會顯示每個提交的 Hash、作者、日期和提交信息
* `git log --pretty=<format>`, 使用指定格式顯示提交紀錄(`oneline`, `short`, `medium`, `full`)
* `git log -n` 顯示最近的幾筆提交
* `git remote` - 管理和查看與遠端倉庫的連接
* `git remote add` 用來添加一個新的遠端倉庫
* `git remote -v` 查看當前的遠端倉庫列表
### 變更倉庫狀態
* `git add` - 把檔案加入到**暫存區**
* 可以是單個文件,也可以是一組文件的目錄
* `git commit` - 把變更從**暫存區**寫入至倉庫中
* `git push` - 將本地的提交推送到遠端倉庫
* `git pull` - 從遠端倉庫拉取更新,並合併到本地倉庫
* `git merge` - 將另一個分支的改動合併到當前分支
### 常見範例
一個常見的工作流程如下,假定你已經有 Github 帳號,且帳號名稱是 `GitUser`
```shell
# 1. 初始化本地倉庫
git init my-project
cd my-project
# 2. 建立一個 `README.md`
echo "# My Project" > README.md
# 3. 添加到暫存區
git add README.md
# 4. 提交變更
git commit -m "Initial commit: Add README.md"
# 5. 連接遠端倉庫
git remote add origin https://github.com/GitUser/my-project.git
# 6. 推送到遠端
git push -u origin main
```
## 設定遠端認證
由於經常使用的線上軟體原始碼代管服務平台為 GitHub 或是 GitLab,這裡使用 GitHub 作為範例。
請先 [GitHub 網站](https://github.com/) 建立一個帳號
### 建立 SSH Key
SSH(安全外殼協定)是一種用於網路管理、遠端檔案傳輸和系統存取的安全協定。它使用一對 SSH 金鑰(公鑰和私鑰)來建立經過身份驗證和加密的安全連接,從而在不安全的開放網路上進行安全通訊。
公鑰(Public Key):共享給遠端方,類似鎖。
私鑰(Private Key):保留在安全的地方,類似鎖的鑰匙。
SSH 金鑰是透過安全演算法產生的,使用質數和大隨機數,確保公鑰可以從私鑰派生,但反之不行。
以下操作請在 `~/.ssh` 目錄下進行,如果你是 Windows 用戶,安裝Git時應該有順便安裝 Git Bash 或是 Bash,建議之後的操作都使用這個終端機
```bash
# 建立 ~/.ssh 資料夾
mkdir -p ~/.ssh
# 切換目錄到 ~/.ssh
cd ~/.ssh
```
使用以下指令生成一個 ssh 金鑰對
`ssh-keygen` 在 Windows 與 Linux 上皆有該指令, -C 後添加的是金鑰的註解(comment),另一個常用的選項是 `-t`,指定金鑰的加密方式。
若無添加預設使用 RSA 加密法,另一種常用的加密法是 ed25519
```shell=
#生成一對金鑰
ssh-keygen
# 生成一對金鑰,並添加一個註解
# 該註釋通常用來標識該密鑰的用途或擁有者,方便日後管理和辨識。
ssh-keygen -C "username@example.com"
# 使用 ed25519 作為加密方法
ssh-keygen -t ed25519
```
輸入 `ssh-keygen` 指令後,會詢問你要儲存的檔案名稱,若沒有輸入的話,預設會是 `id_<algorithm>`, 比方說 `id_rsa`
該指令會生成兩個檔案,分別是 `<filename>` 與 `<filename>.pub`,象徵SSH的私鑰與公鑰。`<filename>` 是第一階段輸入的檔案名稱
`passphase` 通常會跳過,直接輸入兩次 Enter 即可
### 添加 SSH Keys 到 Github 上
登入Github 後,請點選右上角的選單,找到 "Settings" 選項

在 "Settings" 頁面中,找到 "SSH and GPG Keys" 選項

點擊 "New SSH Key" 按鈕

"Title" 是這個 Key 的名稱,隨你喜好輸入即可
"Key" 的內容請你輸入 `*.pub` 的內容,如果你上一章節沒有輸入檔案名稱,預設應該是 `id_rsa.pub` 或是 `id_ed25519.pub`

此時,你有幾種方法驗證SSH認證是否添加成功
```bash
# 假設你沒有修改金鑰的名稱,預設會嘗試以下幾個檔案
# ~/.ssh/id_rsa , ~/.ssh/id_dsa , ~/.ssh/id_ecdsa , ~/.ssh/id_ed25519
ssh -T git@github.com
# 倘若你修改了金鑰的名稱,比方說 `mykey`,使用 -i <私鑰的位置>
ssh -T git@github.com -i ~/.ssh/mykey
# 倘若你修改了金鑰的名稱,比方說 `mykey`
ssh-add ~/.ssh/mykey #添加私鑰到 SSH-Agent
ssh -T git@github.com
```
### SSH Config
在上個小節中,我們簡單介紹了 SSH 添加到 Github 認證。主要依賴的就是 SSH 金鑰的認證功能
這裡簡單補充兩個比較常見的 SSH 金鑰管理功能
#### SSH Agent
SSH-Agent 是一個用於管理 SSH 密鑰的程序,主要功能是存儲私鑰並在需要時自動提供,這樣用戶就不必每次連接時都手動輸入`-i`指令需要使用的私鑰。
可以簡單理解成該程式會自動幫你嘗試使用所有已添加的私鑰來登入
```shell=
ssh-add <私鑰> #添加一組私鑰
ssh-add -l #查看已添加的私鑰
```
但是 SSH-Agent 是儲存私鑰在記憶體中的,也就當使用者登出,或是機器關機後就會重置。
另一個常用的作法是 SSH Config
#### Config File
SSH Config 是一個配置文件,通常位於 `~/.ssh/config`,用於設定 SSH 客戶端的行為和參數。這個文件允許你為不同的主機設定特定的選項,簡化連接過程。
一個標準的 SSH Config 檔案配置如下
```txt
Host <hostname>
HostName <actual_host>
User <username>
IdentityFile <path_to_private_key>
Port <port_number>
```
* Host - 要連線的名稱,可以隨意輸入
* HostName - 要連線的目標位置,ip 或是 網域
* User - 登入的使用者名稱
* IdentityFile - 私鑰的位置(建議使用絕對路徑,比方說 `~/.ssh/mykey`)
* Port - 連線埠,預設是 22
通常 Port 選項是可以跳過不設定的。
SSH Config 在你有多個 SSH 連線需要設定的時候非常方便,以下假定我有兩組github帳號,`Alex` 與 `Bob`:
* Alex 的金鑰對分別是 `alex.pem` 與 `alex.pub`
* Bob 的金鑰對分別是 `bob.pem` 與 `bob.pub`
以上檔案都處存在 `~/.ssh` 目錄下,建立 `~/.ssh/config` 寫入以下資訊:
```txt
Host alex
HostName github.com
User git
IdentityFile ~/.ssh/alex.pem
Host bob
HostName github.com
User git
IdentityFile ~/.ssh/bob.pem
```
都設定完成了,那我就可以使用以下方式存取 SSH 遠端
```bash
# 以下指令與 "ssh git@github.com -i ~/.ssh/alex.pem" 相同
ssh -T alex
# 以下指令與 "ssh git@github.com -i ~/.ssh/bob.pem" 相同
ssh -T bob
```
#### 複製一個遠端Git倉庫
這裡使用 [CMake](https://github.com/Kitware/CMake) 的Git倉庫作為範例

當點選 "Code" 時,會看到 HTTPS / SSH / GitHub CLI 選項。點選 "SSH" 選項,會看到一組字串
該字串就是 Git 倉庫的存放位置
```txt
git@github.com:Kitware/CMake.git
```
該字串使用 `:` 分隔,左邊是連線的資訊,右邊是倉庫的擁有者與倉庫名稱
```txt
<User>@<Hostname>:<Owner>/<Repository>
```
* User - 固定為 `git`
* Hostname - 此處是 `github.com`
* Owner - 此處是 `Kitware`
* Repository - 此處是 `CMake`
`<User>@<Hostname>` 可以替換成你在 `SSH Config` 小節設定的連線名稱
```bash
git clone alex:Kitware/CMake # 以 Alex 的身份複製 CMake 的原始碼到本地端
git clone bob:Kitware/CMake # 以 Bob 的身份複製 CMake 的原始碼到本地端
```
### 總結
認證環節的說明比較冗長,讀者把握幾個關鍵即可
* git 可以使用 SSH 作為認證的方法
* SSH 的金鑰對如何建立
* 如何添加 Public Key 到 Git 上
* 使用 SSH Config 管理連線資訊
## 多個人員協作的情況
多個人員的情況比較複雜,因為要討論到分支的情況。這裡不特別提一些底層的知識,但要先知道幾個概念
* 分支的用途
* 本地倉庫與遠端倉庫
* **整合式管理員** 工作流程
### 分支的管理與使用
分支是 Git 中的核心概念之一,讓你可以在項目中創建、管理和切換不同的工作線,以便進行並行開發或試驗新功能,而不會影響主線代碼,可以理解成「一條獨立的開發線路」

當你在自己的電腦上使用 git 或其他版本控制時,先不考慮遠端倉庫的存在。
你的工作流程應該如下所示
1. 建立新的程式碼 or 修改原有的程式碼
2. 把這些檔案添加到暫存區 (`git add`)
3. 本次的目標完成了,撰寫一個提交訊息,並且儲存這些變更(`git commit`)
4. 重複步驟 1~3,直到完成你的專案
每一次抵達步驟3時,相當於產生了一個新的版本,而 git 會幫你生成一個版本資訊
以上圖來說,`Version <N>` 就是你每次變更的檔案資訊。
假設開發者需要做出一些修改,且這些修改可能會與現在的程式碼沒有相關,那就可以使用分支功能
```bash
git switch <branch_name> # 切換到一個已存在的分支
git switch -c <branch_name> # 建立一個新的分支,並且切換過去
```
分支的是從某個節點切出一條獨立的 Workflow:

上圖的例子中,`0b743`是最初的版本,`a6b4c`是產生分支的版本,兩個分支分別是 `master` 與 `ruby_client`
* 在 `master` 分支中,有 1 個新的提交:`f42c5`
* 在 `ruby_client` 分支中,有 2 個新的提交:`e43a6`、`5ddae`
當你處於 `ruby_client` 分支的時候,你是看不到 `f42c5` 的變更。
而當你處於 `master` 分支的時候,你無法看見 `e43a6`、`5ddae` 的變更。
兩個分支就好像被「隔離」了,彼此都無法看見互相的邊更,但是他們都可以看到 `0b743` 與 `a6b4c` 的資訊
對於`master` 分支來說,他看見的版本是:
* `0b743`
* `a6b4c`
* `f42c5`
對於`ruby_client` 分支來說,他看見的版本是:
* `0b743`
* `a6b4c`
* `e43a6`
* `5ddae`
這對於開發來說至關重要,代表每個人都可以獨立工作而不會受到彼此的干擾。
另一個例子如下:

假訂你目前在 `master` 分支上,並且已經提交 3 次了,
此時你要修正一個編號53的問題,你可能會建立一個新的分支如下:
```bash
git switch -c iss53
```

因為此時你尚未提交新的版本,所以 `iss53` 與 `master` 都會看到一樣的版本紀錄,
當你在 `iss53` 提交了新的版本之後:

此時,若你發現有一個更嚴重的問題需要修理,但是你的 issue 53 又還沒完成,
可以先回到`master`分支,然後建立一個新的分支 `hotfix`,然後提交新的版本。
```bash
git switch master
git switch -c hotfix
```
這樣 `hotfix` 分支暫時看不到 `iss53` 的變更,可以安心的提交新的版本

當你完成了`hotfix`的任務,就可以對分支進行合併:
```bash
git switch master
git merge hotfix
```

因為對於 `master` 分支來說,`hotfix` 分支就好像是延續原本的紀錄繼續修改,
你應該會看到 git 提示你 "Fast-forward",代表直接把`master`指標往前更新。
`hotfix`分支的任務已經完成了,此時你可以把該分支刪除,並且回到 `iss53` 分支繼續工作
```shell=
git branch -d hotfix #刪除 hotfix 分支
git switch iss53 #回到 iss53 分支
```

當我們都完成 `iss53` 上的所有工作,準備合併兩個分支:
這次 `hotfix` 不同,無法直接從 `C4` 往前到 `C5` 這個版本(Fast-forward)

Git就會把 `C4`, `C3`, `C5` 合併後的結果建立一個新的提交(此處為`C6`)並且指向該提交
這稱為**合併提交**,特殊之處在於 `C6` 有兩個父節點

以上就是分支的使用情境
### 本地倉庫與遠端倉庫
在該小節,還要提及 `push` 與 `pull` 的用途
複製一個遠端倉庫:
```shell=
# git clone <Repository URL>
git clone alex:Kitware/CMake
git clone https://github.com/Kitware/CMake
```
以上兩個指令都可以成功把`CMake`的原始碼複製到本地端,但是他們之間有個微小的差異,就是remote的差異
當開發者使用 `clone` 指令複製一個 Git 倉庫時,會把遠端的URL設定在 `origin` 底下。
```bash
git remote -v
```
該指令可以看到遠端的名稱,兩個 clone 方式會看到不同的結果。
`alex:Kitware/CMake` 所看到的內容是
```txt
origin alex:Kitware/CMake (fetch)
origin alex:Kitware/CMake (push)
```
`https://github.com/Kitware/CMake` 所看到的內容是
```txt
origin https://github.com/Kitware/CMake (fetch)
origin https://github.com/Kitware/CMake (push)
```
`remote -v` 指令看到的內容是
```txt
<name> <url>
```
預設情況下,`<name>` 會看到 origin,而 `<url>` 會跟你在 `clone` 指令後的URL相同
remote 的意義指的是你追蹤的一組**遠端分支**。
### 整合式管理員開發流程

在 GitHub 上或是類似的 Git 託管平台上,每個使用者都擁有自己倉庫(Repository)的讀寫權限,以及對於他人公開倉庫的讀權限。如上圖所示,「Blessed Repository」是一個代表官方的倉庫,一般用戶無法直接對該專案進行貢獻,所以會Fork(分叉)該倉庫到自己的公開倉庫。
接下來,開發者會把公開倉庫的專案clone到本地倉庫,你會對這些專案的原始碼做出一些修改、測試,之後推送到公開的倉庫,最後請求官方的維護者拉取更新,維護者可以把你的倉庫作為遠端倉庫添加進來,並且在本地測試你的變更,最終合併到主分支後,推送至官方的公開倉庫。
其開發流程如下:
* 專案維護者推送至雲端的倉庫
* 其他貢獻者分叉倉庫到自己的倉庫

* 貢獻者從自己的雲端倉庫上複製(clone)到本地端
* 貢獻者做出一些修改、添加功能等
* 推送至自己的雲端倉庫
* 通知官方維護者拉取自己的雲端倉庫
* 官方維護者把程式碼拉取至本地端後,測試完成後可以合併至主分支
* 推送變更到官方倉庫上
除了程式修改以外的流程,在 Github 或是 Gitlab 平台都可以直接在網頁使用對應的UI進行操作,而不需要繁瑣的步驟。
這是大型Git託管平台常用的工作流程,方便一般的使用者派生官方的專案到自己的公開倉庫,並向自己的公開倉庫提交變更,且可以為所有人所見。方便你持續的工作,且主倉庫的維護者可以隨時拉取你的變更,雙方可以按照自己的節奏進行工作,不必互相等待。
## 給協作人員的建議
該小節整理一些開發上的注意事項,不論你是專案的管理者還是底下的協作人員,都應該遵守相關的規定。
其中比較重要的事項如下:
* 不要直接在主分支上工作
* Commit Message資訊含糊不清
* 推送瑣碎的小變更
* 謹慎使用 `push -f`
以上幾個是常見的情況,務必注意一下
### Workflow

這裡主要提及的是如何維護工作上的分支,這裡提及幾個重要的概念:功能分支與主要分支
主要分支指的不只是 master 或 main ,而是一些應該長期存在的分支。
功能分支則是因應一些特定目標,而開啟的短期分支,如 feature、hotfix 等
舉例來說,倘若我們的目標是開發一個線上商城,而此時需要開發新功能:購物車、搜尋結果顯示、折扣抵用
新功能稱為feature,一般會建立對應的分支,比方說 `feature/shopping-cat` 等
```bash
git switch -c feature/shopping-cat
```
對於所有的工作,協作者都應該建立一個新的分支,等到完成之後,推送至雲端上,在讓專案的負責人進行檢查與合併
除了 feature 分支之外,還有可能會建立如 `hotfix` 等分支,用來處理臨時的BUG修復
而等到新功能開發完成,或是BUG修復之後,這些分支就可以刪除了,因此也被稱作短期分支。
以我個人的開發情況,通常保留 `main` 以及 `develop` 當作長期分支就好,`main` 是穩定的分支,經過測試、準備發布的版本會合併到該分支,而 `develop` 可以視作「合併前用來測試」的準分支(相當於Beta)版本
```shell=
git switch develop
git merge featureA #先合併到 develop 分支上
<經過一些測試等>
git switch main
git merge develop #最終合併到主分支上
```
一個比較重要需要提醒的事情是,當你進行 **Hotfix** 的時候,務必先切回去 `main` 或是 `develop` 分支在建立新分支
一些人會忘記,從`feature`直接建立新的`hotfix`分支,這會導致當你合併到主分支時,意外把可能未完成的功能也合併進主分支
### 重訂基底 (Rebase) 與 合併(Merge)
`rebase` 和 `merge` 是 Git 用來整合分支的不同方法,已官方的範例來說:

假定從 C2 建立了兩個分支: `experiment` 與 `master`,並且都各有一個新的提交。
無論要merge哪個分支, git 都無法使用 fast-forward 模式,所以會建立一個新的快照,並且把`C2`, `C3`, `C4` 進行合併並提交

`C5` 就是`C2`, `C3`, `C4` 進行合併並提交後的節點
以該例子來說,`C4` 是基於 `C2` 進行分支的,但是可以通過 `rebase`,讓 `C4` 看起來就像是從 `C3` 進行分支的:

```shell=
git switch experiment
git rebase master #在 experiment 分支中,重新指定 master 為基底
```
這個 `C4'` ,就是經過 `rebase` 後的結果,就好像 `experiment` 是從 `C3` 這個提交分支的

此時 git 就可以進行一個 fast-forward 合併。
對於最終程式的結果來說,`C4'` 與 `C5` 是一樣的,但是提交的歷史紀錄會變得比較簡潔。使用 `rebase` 是為了遠端分支的歷史紀錄更加簡潔,比方說你在準備合併到`origin/master`之前,可以先進行`rebase origin/master`,然後在本地上測試確認,這樣最終維護者就不用處理整合事宜,直接fast-forward即可。
而一個極端重要的事情是:**不要對已經提交到雲端的提交執行rebase**
你會有大麻煩,同時你的同事也會鄙視你,人民群眾會仇恨你,你的朋友會嘲笑你,你的家人則會唾棄你。
而原因在下一小節一起說明
### 強制推送 (push -f)
`rebase` 的意義是丟棄一些現有的提交,然後建立一些內容一樣但是實際上不同的提交。如果你已經把提交推送到雲端的倉庫上,其他人也基於這些提交進行開發,此時就會出現一些問題:

雲端上目前僅有 `C1` 這個提交,你在 clone 之後進行了一些修改,並且提交了`C2`、`C3`,此時倉庫的狀態如上所示
假設有其他人提交到雲端上,且完成了合併,而且你也把些變更抓取下來了:

對於你來說,你所看到的事情如下:
* 你從 `C1` 開始進行開發,並提交了`C2`, `C3`
* 你抓取了遠端的分支,並且合併到你的開發分支(可以想像成 `git merge teamone/master`),所以產生了 `C7` 提交
* 而你的好同事,把合併給回溯,改成上一節提到的`rebase`合併,並且使用 `push -f` 強制覆蓋雲端上的提交歷史
* 最後,你嘗試抓取雲端的更新

這就是最後雲端與你本地端提交的歷史紀錄圖,你的本地端突然多了一個 `C4'` 提交紀錄,且 `C4` 與 `C6` 依然存在。
如果你此時使用 `git pull`,相當於嘗試把雲端的內容更新到你的分支,也就是把 `C4'` 與 `C7` 進行合併。在前面的章節提到,如果沒辦法使用 Fast-Forward 合併,就會先建立一個新的快照,然後嘗試進行三方合併後提交,這個新節點就是 `C8`

如果你又重新把這些變更推送到雲端(`git push`),那雲端的紀錄就會非常混亂:
事件順序如下:
* 你和好同事分別把專案 clone 到自己的電腦上
* 你進行了一些開發,把`C2`, `C3` 提交到本地端
* 好同事推送了一些提交,然後你 pull 到本地端
* 本地端發現雲端有更新過,嘗試`merge`之後產生 `C7` 紀錄
* 你的好同事想把 `C4` 跟 `C6` 清除,所以直接 `rebase` 後直接推送到雲端上,導致 `C4'` 的產生
* 而你不知道這件事情,都測試完了,想要把提交推送上去
* git會提示你雲端有更新過,請你先 `pull`,本地會把 `C7`跟`C4'`再次合併,產生`C8`
對於你的好同事來說,他想要把`C4`跟`C6` 清除,而你不知道這件事情,又把`C4`跟`C6`推送上雲端,整個紀錄就會亂糟糟的。
這些原因都是因為對於已推送的提交進行`rebase`,然後又強制覆蓋。
但也不是說`push -f`這指令就不能用,比方說以下情況:

`featureA` 跟 `featureB` 都是從 `1edee` 上進行分支的,但是當你要推送的時候,發現維護者已經先合併其他的提交了
假設維護者在拉取`featureA`的時候,發生了衝突,此時可以由你來解決:
```bash
git switch featureA
git rebase origin/master
git push -f myCloud featureA
```

相當於你重新把紀錄移送到最新的更新,然後在重新把你的變更合併。
> origin 指的是官方倉庫
> myCloud 是你從官方倉庫 Fork 後,你自己的公開倉庫
> 此例子因為不會有其他人拉取你的倉庫做變更,因次你可以放心的 rebase 與 push -f 修改自己雲端倉庫的提交紀錄
總之 `rebase` 跟 `push -f` 要謹慎使用,遇到衝突不會處理之類的叫你的主管或同事協助就好
### 提交信息
提交紀錄就是你在 `git commit` 時需要撰寫紀曆,依照 `Tim Pope` 當初撰寫的模板如下:
```txt
Capitalized, short (50 chars or less) summary
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.
Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
or "Fixes bug." This convention matches up with commit messages generated
by commands like git merge and git revert.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, followed by a
single space, with blank lines in between, but conventions vary here
- Use a hanging indent
```
大致上就是以下格式:
```txt
<Header> - 摘要本次變更的內容
(option)<Body> - 詳細說明本次變更的內容
(option)<Footer> - 額外的註記
```
而常見的的作法,會在 Header 上在加入一些分類(`category`)作為前綴,我們團隊常用的是:
* chore - 一些例行的任務之類的
* 也可以用 misc 替代
* deprecate - 註記一些功能是應該被淘汰的
* feat - 新增一個新的功能
* fix - 修復一些錯誤或 bug
* release - 一些與版本有關的註記
比方說:
```txt=
feat($browser): add onUrlChange event (popstate/hashchange/polling)
New $browser event:
1. forward popstate event if available
2. forward hashchange event if popstate not available
3. do polling when neither popstate nor hashchange available
Breaks $browser.onHashChange,which was removed (use onUrlChange instead)
```
可以使用 `category(scope)` 的方式,意思是對於哪些部份做了調整,但是常見使用 `category` 就好
* Header 作為摘要不要過長
* Body 則可以描述這次提交你做了什麼
* Footer 是個可選的區塊,如果有使用 Redmine 之類的專案管理軟體,可以註記 issue 編號
當然,你也可以引入一些其他的分類:
* docs - 程式碼文件相關的修改(CHANGELOG、README、或是其他文件等)
* style - 不影響程式的邏輯,調整原始碼(code format,空格數量、加上遺漏的分號等)
* refactor - 不添加新功能或是修復 bug 的情況,重構程式碼(可能是拆分成更小的模組、又更優美的寫法...等)
* perf - 效能改善,或者是添加一些追蹤效能的程式碼
* test - 新增測試文件
* ci - 對於 CI 相關設定的調整
這些並沒有硬性要求,團隊成員討論好即可,可以查看[相關工具的說明](#Git-相關工具)
## 給管理人員的建議
最後這節是寫給專案的PM或是維護人員的,主要是幫助你管理團隊成員
### 對於雲端倉庫的權限
這個至關重要,不要讓所有人都可以對主分支推送提交(通常是 `main` 或是 `master`)
這裡使用私人倉庫作為例子,因為一般私人倉庫都只會包含擁有者跟協作人員。
先找到 Settings -> Rules -> Rulesets ,然後選擇 "New branch ruleset"

在該頁面中,會看到以下的介面:

* Ruleset Name - 該規則集的名稱,隨意取就好
* Enforcement stats - 是否啟用規則(記得選到Active)
* Bypass list - 誰可以無視這些規則,通常會設定 `Repository Admin` 也就是倉庫的建立人
* 如果是建立Organization 的儲藏庫,可以在細分 `Maintainer` 跟 `Writer` 的權限
* 建議把 **Always Allow** 改成 **Allow for pull requests only**,也避免管理人員直接推送到主分支上
* Target - 哪些分支適用該規則(受保護的分支)
* 這個規則可以選擇套用所有分支,或是使用 Pattern
* Pattern 可以設定如 "`main`" 或是 "`feature/*`" 這樣的寫法
* 該範例是撰寫 `main`, `master` 當作受保護的分支
---
接下來可以套用規則:
先假定 Target 中的規則是 `main`, `issue*`
* Restrict creations - 無權限的用戶是否可以建立與 Target 中的匹配模式分支
* 一般人員不可以建立 `main` 和 `issue` 開頭的分支到雲端
* Restrict updates - 無權限的用戶是否可以推送提交到 Target 中的匹配的分支
* 一般人員不可以直接 `push` 提交到對應的分支
* Restrict deletions - 無權限的用戶是否可以刪除 Target 中的匹配的分支
* 一般人員不可以刪除對應的分支
* Require linear history - 是否僅允許線性歷史紀錄
* 是指前面提到的 `rebase` 合併,如果強制要求線性歷史可以防止協作者推送`合併提交`
* 這相當於所有的 pull request 都要使用 `merge --squash` 或是 `rebase` 進行合併
* 這對於復原變更相當有效,細節可以參考[這裡](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges)
* Require deployments to succeed - 合併分支之前,可以要求先成功部署到特定環境,CI/CD相關的設定
* Require signed commits - 要求簽名提交,也就是開發者必須先設定GPG Key
* Require a pull request before merging - 對於目標分支的所有更改都必須經過 `Pull Request`
* 具權限者可以要求所有的`Pull Request`在合併到受保護分支之前,必須先經過特定數量的審查(review)
* 假設某個人員提出 `Request Change`,那提出者必須要同意才能實施合併

* (可選) 如果有新的變更出現,則以前批准的審查會全部被取消
* 比方說 featureA 分支目前有 A, B, C 三個提交,且已經有 3 個審查通過
* 在被合併前,featureA 推送一個新的 D 提交,則之前的 3 個審查通過視為無效
* 在被合併前,原先的分支先合併了其他提交,也會導致目前的審查視為無效
* (可選) 要求代碼所有者的審查同意後才允許合併
* 假設 `file.cc` 是由 A 擁有的,那如果該次Pull Request 有修改到該檔案,就必須要有A的同意才能合併
* 假設 `file.cc` 是由 A~E 5人團隊擁有的,那如果該次Pull Request有修改到該檔案,就必須要有A~E其中一人的同意才能合併
* 代碼所有者(Code Owner) 可以建立 `CODEOWNER` 檔案決定所有者,具體參考[該指南](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners)
* (可選) 至少要有最後一個推送者以外的審查批准才行
* (可選) 要求在合併之前,必須解決所有的評論
* Require status checks to pass - 要求該合併之前,必須通過所有的 CI 測試
* Block force pushes - 禁止 force push 到受保護的分支
* Require code scanning results - 如果有設定 code scanning,必須通過才允許合併
---
以上就是所有的規則,基本上以下的規則都建議開啟:
* Restrict creations - 禁止一般人員建立特定名稱的分支
* Restrict updates - 禁止一般人員 push 變更到特定分支
* 通常勾選了 "Require a pull request before merging" 就可以不勾選,因為該選項是要求「所有的提交都應該推送到保護分支以外的分支」
* 如果你需要限制擁有特定權限的人員(於Bypass list)中才能合併,就可以把該選項勾起來,這個會在後面說明
* Restrict deletions - 禁止一般人員刪除特定分支
* Require a pull request before merging - 要求倉庫的管理員才能合併分支到特定分支
* Required approvals - 至少要幾個人審查過後才能合併,根據團隊規模設定
* Dismiss stale pull request approvals when new commits are pushed - 有新的push 就應該要重新審查
* Require approval of the most recent reviewable push - 除了最後一個push的人,至少還要有其他的人審查通過
* Block force pushes - 禁止強制推送
此刻,你的規則應該如下所示:

### 審查機制
當你設定完成了相關的權限後,接下來是審查相關的功能,假定你是一般的開發者,複製了雲端倉庫
```shell
git clone <Repository>
```
此時你會建立一個新的分支,用來修改、新增新的代碼
```shell
# 一般來說使用 feature/<功能名稱> 或是 hotfix/<BUG名稱>
# 如果使用 Redmine 、JIRA 之類專案管理軟體,也可能是 issue<編號> 當作分支名稱
git switch -c featureA
```
當你開發完成了,可以到 github 上的 `Pull Request` 頁籤點擊 `New Pull Request`

對於管理人員來說,應該收到 github 的信件,通知有人發出 Pull Request,此時可以點開 `Pull Request` 頁籤,點選對應的請求,可以打開Request頁面,會看到該 Pull Request 的相關資訊 - Request 的標題、相關的提交等

點選上方的 `Files Changed` 頁籤,可以開始進行審查

你可以點擊修改的程式碼旁邊的行號,添加特定的回應之類的,如果你有添加相關的回應,之後點選`Start Review`,就可以在右上方點擊按鈕,然後送出審查結果,此處假設送出的是「Request Chages」

此時協作者就可以看到審查的結果,並根據指示進行修改

要注意的是,即使已經通過審查,預設情況下還是無法 `Merge`,對於管理人員來說,必須手動勾選「Merge without waiting for requirements to be met (bypass branch protections)」,然後才能進行分支合併
管理者界面:

一般人員界面:

> "Restrict updates" 規則確保程式碼在合併進入受保護分支之前滿足某些標準。
> 但GitHub 為具有管理權限的使用者提供了一個選項,以便在合併時繞過這些分支保護規則。
> 看到有關「Merging is blocked」的警告或通知是分支保護規則下的預期行為,
> 即使對於儲存庫所有者或有權繞過這些規則的人也是如此。
如果期望某些使用者或儲存庫擁有者在完全沒有看到此警告的情況下合併 PR,則 GitHub 目前的實作可能不會在不顯示警告的情況下直接支援該工作流程。此通知旨在提醒您已採取的保護措施,並確保有意識地做出繞過這些保護的決定。
這就是前面提及的「Restrict updates」會限制特定的人員才能進行合併,即使沒有勾選該規則,但是有勾選「Require a pull request」,也無法 push 提交到保護分支
git會告訴你「Changes must be made through a pull request.」

倘若沒有限制要特定的使用者才能合併分支,可以選擇取消「Restrict updates」

這樣只要滿足條件,任何人都可以合併該分支
### CODEOWNERS 設定
> 可以在公開倉庫中使用 GitHub Free 和 GitHub Free 為組織定義程式碼擁有者
> 也可以在公開和私人倉庫中使用 GitHub Pro、GitHub Team、GitHub Enterprise Cloud 和 GitHub Enterprise Server 定義程式碼擁有者。
CODEOWNERS 文件是一個用於指定誰對倉庫中的特定文件或目錄擁有審查責任的配置文件。它能自動分配代碼審查責任,確保每次提交都由適當的成員審查。
---
CODEOWNERS 文件位於儲藏庫的根目錄或 .github、docs 或 .gitlab 等目錄中。文件的每一行包含一個文件路徑模式和一個或多個 GitHub 使用者或團隊的組合。
基本上語法和 `.gitignore` 文件類似,但是有些許不同:
* 使用 \ 對於 # 開頭的模式進行轉義,就可將其視作模式而不是註解
* 使用 ! 否定模式
* 使用 [ ] 定義字元範圍
```txt
#CODEOWNERS 檔案
<文件路徑模式> <用戶名或團隊名>
```
模式後接一個或多個使用標準 `@username` 或 `@org/team-name` 格式的 GitHub 使用者名稱或團隊名稱。
使用者和團隊必須具有對儲存庫的明確 write 存取權限,即使團隊成員已具有存取權限也是如此。
路徑區分大小寫,如果要符合兩個或多個具有相同模式的程式碼擁有者,所有的程式碼擁有者必須位於同一行。
如果程式碼擁有者不在同一行,模式會僅符合最後提到的程式碼擁有者。
```txt
# 這是一個註解。
# 每行是一個文件模式,後面跟著一個或多個擁有者。
# 除非後面的匹配模式優先級更高,這些擁有者將是儲存庫中所有內容的默認擁有者。
# 當有人發起 Pull Request 時,@global-owner1 和 @global-owner2 將被請求進行審查。
* @global-owner1 @global-owner2
# 順序很重要;最後匹配的模式具有最高優先級。
# 當有人打開一個僅修改 JS 文件的Pull Request時
# 只有 @js-owner 而不是 Global Owner 被請求進行審查。
*.js @js-owner #這是一個內聯註解。
# 也可以使用電子郵件地址
*.go docs@example.com
# 團隊也可以被指定為代碼擁有者。
# 團隊應該以 @org/team-name 的格式標識。
# 團隊必須對儲存庫具有明確的寫入訪問權限。
# 在此示例中,octo-org 組織中的 octocats 團隊擁有所有 .txt 文件。
*.txt @octo-org/octocats
# 在此示例中,@doctocat 擁有儲存庫根目錄中的 build/logs 目錄及其所有子目錄中的任何文件。
/build/logs/ @doctocat
# `docs/*` 模式將匹配文件,如
# `docs/getting-started.md` 但不包括更深層次的文件,如
# `docs/build-app/troubleshooting.md`。
docs/* docs@example.com
# 在此示例中,@octocat 擁有儲存庫中「任何位置」的 apps 目錄中的任何文件。
apps/ @octocat
# 在此示例中,@doctocat 擁有儲存庫根目錄中的 `/docs` 目錄及其所有子目錄中的任何文件。
/docs/ @doctocat
# 在此示例中,`/scripts` 目錄中的任何更改都需要 @doctocat 或 @octocat 的批准。
/scripts/ @doctocat @octocat
# 在此示例中,@octocat 擁有 `/logs` 目錄中的任何文件,例如
# `/build/logs`、`/scripts/logs` 和 `/deeply/nested/logs`。
# 任何在 `/logs` 目錄中的更改都需要 @octocat 的批准。
**/logs @octocat
# 在此示例中,@octocat 擁有儲存庫根目錄中的 `/apps` 目錄中的任何文件
# 除了 `/apps/github` 子目錄,因為它的擁有者是空的。
/apps/ @octocat
/apps/github
# 在此示例中,@octocat 擁有儲存庫根目錄中的 `/apps`
# 目錄中的任何文件,除了 `/apps/github` 子目錄,因為該子目錄有其自己的擁有者 @doctocat。
/apps/ @octocat
/apps/github @doctocat
```
一個很簡單的作法是,撰寫以下的CODEOWNERS檔案:
```txt
* @<你的用戶名稱>
```
然後把以下選項勾選起來:
* [x] Require a pull request before merging
* [x] Require review from Code Owners
比方說我的用戶名稱是`silent4v`,則
```txt
* @silent4v
```
那麼在 Pull Request 的提示,會出下以下資訊:

> Waiting on code owner review from \<???>
只要滿足條件,就可以進行合併

以該例子來說:
* 那如果是其他人提交變更,則`silent4v`必須通過審查才能合併
* 如果是 `silent4v` 自己提交變更,則需要其他人通過審查才能合併
但是要注意,因為不能自己審查自己發起的`Pull Request`,所以請不要幫別人發起`Pull Request`
假設 `silent4v` 推送`featureC`分支上去,但是由 `another` 發起 `featureC` 的 `Pull request`
* 因為 `CODEOWNERS` 是 `silent4v`,所以一定要他的審查同意
* `another` 無法審查自己發起的 `Pull Request`
* `silent4v` 是最後的推送者,規則要求得要 `silent4v` 以外的人發起 `Pull Request`
如果該團隊沒有其他人,就會導致該 PR 無法通過
### 原始碼審查
剛剛 Review 都是在 Web 上進行的,但是一般來說會拉取到本地端上進行 Review,比方說要審查的是來自 `featureA` 分支的 PR
```bash
git pull origin featureA
# 如果有使用 github-cli
gh pr checkout <PR編號>
```
另一種作法就是點開 `Pull Request` 旁的建立 Codespace,這會開啟一個線上的編輯器,該界面類似 Visual Studio Code,但是額外整合了撰寫 Review comment 的功能

### 其他事項
請一定要寫 `.gitignore` 跟協助組員處理 `Merge Conflict`,被`push -f` 覆蓋過一次工作進度就會記得一輩子
然後分支保護很重要,記得要開。Github跟Gitlab都有這功能,其他建議與內容摘要就是:
* 使用`feature`分支而不是直接在主分支上提交。
* Tag由Developer設置,而不是 CI 設置。
* Release基於Tag。
* 測試所有提交,不僅僅是主分支上的提交。
* 優先修復主分支中的錯誤,其次是`Release`分支中的錯誤。
* Commit Message應反映該次提交的意圖。
關於 `Merge` 跟 `Rebase`,大致分成兩種看法:
* 有一種觀點認為,倉庫的提交歷史即是**記錄實際發生過什麼**。
> 它是針對歷史的文檔,本身就有價值,不能亂改。
從這個角度看來,改變提交歷史是一種褻瀆,你使用謊言掩蓋了實際發生過的事情。
如果合併產生的提交歷史是一團糟怎麼辦?
既然事實就是如此,那麼這些痕跡就應該被保留下來,讓後人能夠查閱。
* 另一種觀點則正好相反,他們認為提交歷史是**專案過程中發生的事**。
> 沒人會出版一本書的第一版草稿,軟體維護手冊也是需要反覆修訂才能方便使用。
> 持這看法的人會使用 rebase 及 filter-branch 等工具來寫故事,怎麼方便後來的讀者就怎麼寫。
到底是`Merge`還是`Rebase`好,這並沒有一個簡單的答案。
Git 是一個非常強大的工具,它允許你對提交歷史做許多事情,但每個團隊、每個專案對此的需求並不相同。
既然你已經分別學習了兩者的用法,相信你能夠根據實際情況做出明智的選擇。
但記得,**只對尚未推送或分享給別人的本地**修改執行`rebase`操作清理歷史,絕對不要對已推送至別處的提交執行`rebase`!
我個人也是認為雲端上的紀錄應該要簡潔,如果已經 merge 了就不要 rebase,但是自己本地的分支,或是雲端上自己的工作的分支(若沒有其他成員依賴該分支的提交),則可以適當使用`rebase`把一些瑣碎的提交重新合併後提交
## Git 相關工具
### Git Hooks
在 `.git` 資料夾底下,有一個 `hooks` 資料夾,這裡面存放一些 hooks 專用的腳本,以`.sample`結尾,倘若把`.sample`去除,則該hook啟用
Ex. 把 `commit-msg.sample` 修改成 `commit-msg`,則會啟用 `commit-msg` hook
```txt
.git/hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── prepare-commit-msg.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── push-to-checkout.sample
└── update.sample
```
所謂的 `hooks`,指的是當特定情況時,會執行的腳本。對於系統來說,執行後的結果會有自己的狀態碼,正常退出的情況下為`0`
```c
int main() {
/* My source code... */
return 0;
}
```
因此在不少`C` 或是 `C++` 的教材中,`main` 函式都會回傳 0,意義就是告訴系統該程式是正常退出的
而 Git Hooks 也是一樣,倘若該 shell 腳本執行後的狀態碼不為 0,則git會拒絕接下來的操作
在[官方文件](https://git-scm.com/book/zh-tw/v2/Customizing-Git-Git-Hooks)提及了不少 Hooks,但此處只提及一些最常用的 Hooks: `pre-commit`, `commit-msg`, `pre-push`
* pre-commit
* 在提交變更到倉庫前運行。可以用來檢查代碼風格、執行測試等。
* 希望在每次提交之前自動檢查代碼是否符合特定標準,或者運行單元測試以確保程式碼的品質時。
* commit-msg
* 在提交消息被輸入之後但在提交完成之前運行。可以用來檢查提交消息的格式。
* 希望確保每次提交都有統一且清晰的提交消息格式時。
* pre-push
* 在推送代碼到遠程倉庫之前運行。可以用來運行測試、檢查代碼風格等。
* 希望在推送代碼之前自動檢查代碼的質量,或者運行完整的測試套件以確保代碼穩定時。
不過通常使用 `pre-commit` 跟 `commit-msg` 就好
* pre-commit 執行一些測試、Code Format 之類的操作(比方說C/C++ 的 `clang-format`, JavaScript 的 `prettier`)
* commit-msg 用來檢查Commit Message
以 `prettier` 來說,就可能會包含以下的 `pre-commit` 腳本:
```sh
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0
# Prettify all selected files
echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write
# Add back the modified/prettified files to staging
echo "$FILES" | xargs git add
exit 0
```
* 首先使用 `git diff` 找出此次變動的文件,然後執行`sed`指令,把文件中的空白轉譯
* 執行 `prettier` 格式化代碼
* 如果有新增的檔案,加入追蹤
要注意的是,不要在 `commit-msg` 中進行會對代碼修改的操作,比方說格式化之類的
考慮以下情況,該專案包含四個檔案:
```txt
A.c
B.c
C.c
D.c
```
本次提交 `A.c` 跟 `B.c`,然後在 `commit-msg` 階段執行 `clang-format`,倘若剛好修改到 `A`,則:
* 本次修改的`A.c`會被提交
* 經過排版後的`A.c`會留在 `staging area`,需要等到下一次 `commit` 才會被提交
所以應該在 `pre-commit` 中執行相關操作,而在 `commit-msg` 專注檢查提交者撰寫的訊息
### Git Archive & Git Bundle
`git archive` 和 `git bundle` 是 Git 中用於處理打包的兩個工具
#### Git Archive
git archive 用於創建項目某個版本的壓縮包(如 .tar 或 .zip 文件),不包括版本控制的元數據。這通常用於發布或分發源代碼。
```shell=
git archive --format=<format> -o <filename> <tree-ish>
```
* `<format>` 打包的格式: `tar`, `zip`, `tar.gz`
* `<filename>` 輸出的檔案名稱,通常是檔案名稱加上 `<format>` 作為副檔名
* git archive --format=zip -o backup.zip
* git archive --format=tar -o backup.tar
* `<tree-ish>` 可以是 `tag`, `commit-id`, `branch name`
```bash
git archive --format=tar -o backup.tar master # 把 branch `master` 打包
git archive --format=tar -o backup.tar dev # 把 branch `dev` 打包
```
可以加上 :`<path>` 指定要打包的目錄,比方說:
```shell
# 打包`master`分支中,專案目錄下的 `App` 資料夾
git archive --format=tar -o backup.tar master:./App
# 與上面的指令相同
git archive --format=tar -o backup.tar master:App
```
另外一種方式就是使用 `HEAD`,`HEAD` 的意思是指向目前的提交,可以使用 `HEAD~<N>` 來選擇往前 N 個版本
比方說目前有三個提交:
```txt
644d679 (HEAD -> master, origin/master, origin/HEAD)
2f3cedf
7811cff
123a747
```
* `HEAD` 指向 `644d679`
* `HEAD~1` 指向 `2f3cedf`
* `HEAD~2` 指向 `7811cff`
同理,也可以使用如 `master~1` 或是 `{branch name}~{N}` 的用法
#### Git Bundle
原則上,`git bundle` 的用途原則上跟 `git archive` 很類似,不過 `git archive` 不會打包git metadata,也就是 `.git` 內的內容。而 `git bundle` 可以處理
```bash
git bundle create master.bundle <branch>
git bundle create master.bundle HEAD~10..HEAD #近十個提交
```
然後可以使用諸如 `clone` 或 `pull` 等操作
```bash
# - case1
git clone ./master.bundle #建立儲存庫
# - case2
cd <專案目錄>
git pull ./master.bundle #將 bundle 文件中的提交合併到當前分支
# - case3
cd <專案目錄>
git bundle unbundle ./master.bundle #將 bundle 文件的內容添加到當前存儲庫中
```
要注意 `git bundle unbundle` 跟 `git pull` 看起來很像,不過 `git bundle unbundle` 只是把內容直接更新到當前的 Repo中
不會進行合併操作,也不會自動更新當前的分支
| | `git archive` | `git bundle` |
| - | - | - |
| 內容 | 工作文件中的內容,不包含 Git Metadata | 整個儲存庫,包含commit、branch、tag 等資料 |
| 用途 | 發布原始碼 | 傳輸和備份 Git Repo |
| 格式 | `.tar`, `.zip` 等壓縮格式 | Git 專用的 `.bundle` 文件 |
| 使用時機 | 建立原始碼的快照 | 離線傳輸或備份存儲庫 |
### Git Diff & Git Patch
`git diff` 和 `git patch` 是 Git 中用於處理變更的兩個工具
#### git diff
git diff 用於顯示文件的變更,通常用來比較不同版本之間的差異。它可以顯示未提交的變更、兩個提交之間的差異,或是工作目錄和索引(暫存區)之間的差異。
```bash
git diff #查看目前工作目錄與 `staging area` 的差異
git diff --cached # `staging area` 與最新一次提交的差異
git diff <commit1> <commit2> 兩個提交之間的差異
git diff <commit> 該提交與最新一次提交的差異
```
如上個章節所說,可以使用 `{rev}~{N}` 查看近幾次的提交,因為 `git diff <commit1> <commit2>` 是查看兩個版本之間的提交
又已知 `HEAD` 是最新一次的提交,因此以下兩個指令等價:
```shell
git diff HEAD~5
git diff HEAD~5 HEAD
```
一個很好用的指令是,`git diff --name-only {Version}` 會列出所有變更的檔案,倘若只是要整理變更的檔案
可以配合使用 `--diff-filter` 參數過濾變更檔案的狀態,通常包含以下幾點:
* Added (A)
* Copied (C)
* Deleted (D)
* Modified (M)
* Renamed (R)
因此若使用
```bash
git diff --name-only --diff-filter=ACMR
```
就是把除了刪除操作以外的檔案列出,可以配合如 `tar` 或是 `zip` 等指令,打包變更的檔案
```bash
git diff --name-only --diff-filter=ACMR HEAD~1 | tar czvf update.tgz -T -
git diff --name-only --diff-filter=ACMR HEAD~1 | zip -9rv update.zip -@
```
不過要注意, `--diff-filter` 不可以包含D狀態,因為D的意思是`Deleted`,所以該檔案已經不存在於工作區了
如果讓 `zip` 或是 `tar` 嘗試打包一個不存在的指令,會出現問題
#### git format-patch 與 git am
git format-patch 用於生成補丁文件。這些補丁文件是包含一個或多個提交變更的文本文件,通常用於分享和應用變更。
```bash
git format-patch [options] <commit1>..<commit2>
```
這會包含多個 `.patch` 檔案,也可以設定一些參數:
* `-N` 生成最新N筆提交的補丁
* `-1` 生成1筆
* `-3` 生成3筆
* `--root` 從首次提交到最近一次提交的所有補丁
* `-o` 生成補丁到指定的資料夾
* `--stdout` 輸出到`stdout`,而不是寫入到文件
```bashaaa
# 建立最近三次提交的補丁
git format-patch -3
# 建立指令範圍的補丁
git format-patch HEAD~3..HEAD
# 建立指令範圍的補丁,並輸出檔案到 patches/ 資料夾
git format-patch HEAD~3..HEAD -o patches
```
這指令會產生 `XXXX-<name>.patch`, `XXXX` 是補丁編號,比方說 `0001`~`0018`
最後就可以使用 `git am` 套用補丁
```bash
git am <file>
git am *.patch # 一次套用多個補丁
git am dir/*.patch # 一次套用某個路徑下的多個補丁
```
有時候也可能發生補丁無法順利運作的情況,可以使用另外的參數,例如:
* `--reject` 當補丁無法套用時,建立.rej檔案,方變開發者手動處理
* `--3way` 使用三路合併,協助解決衝突
* `--abort` 如果補丁套用時,遇到無法解決的問題,則撤銷該補丁的變更
* `--skip` 跳過當前的補丁,並套用後續的補丁
### 快速整理
* Hooks 是常用來做 code format、檢查 commit message 很好用的工具
* Archive/Bundle 用來發布原始碼很有用
* Patch 現在比較少用到,因為有 github / gitlab 這種託管平台,但是在特殊的網路環境 Ex. 限制對外的內部開發環境、有IP限制的雲端等,用來進行手動更新很好用