---
# System prepended metadata

title: Git 版本控管實戰：新手進階篇
tags: [Git]

---

---
tags: Git
---
# Git 版本控管實戰：新手進階篇
## Git 內部架構解析
### 版本控管（Version Control）
* 完整記錄軟體變化的過程（人、事、時、地、物）
* 紀錄版本變化而𧗠生出
  * 查詢歷史紀錄
  * 復原變更
  * 比對差異
  * 標記版本
  * 變更追蹤
* 多人版控進一步衍生出
  * **協同作業**
  * 分支合併
  * **版控流程**
  * 發行管理

### 分散式版本控管（DVCS）
* 優點
  * 本地端的工作區會保有完整的儲存庫
    * 每個人都擁有一分完整的儲存庫備分（Full Backup） => 分散式
    * 不需要 Server 支援即可運作版控 => 不用網路連線，大幅節省開發時間
    * 操作都在本地儲存庫中 => 快、離線的版本、完整的歷史紀錄
  * 擁有強悍的**合併追蹤**能力
    * 取得他人變更版本後，可透過合併方式進行整合
    * 合併多人的版本只要有存取共用儲存庫的權限或管道即可
* 缺點
  * 無法**鎖定**版控策略（僅能使用合併策略）==> **專注在自己的分支**
  * 無法針對專案進行**精細的權限控管**
    * 若要鎖定特定資料夾，可透過 Submodules 實現 (Owner 完整的 repo，其他人則在 Submodules 內進行)

### 工作區（Workspace / Work Tree）
https://gitbook.tw/chapters/using-git/working-staging-and-repository.html

* 頻繁異動的開發目錄
* 在工作區執行任意 git 命令
* 內含 .git 隱藏資料夾(本地儲存庫)
* 沒有 .git 目錄 => `git init`
* 刪掉 .git 目錄 => 脫離版控
* 擁有 .git 目錄 == 擁有所有原始碼
* 工作區底下檔案的狀態
  * untracked
  * tracked
	* unmodified
	* modified
	* staged

### 儲存庫（Repository）
* 本地儲存庫（Local Repository）
  * 預設位於 .git 資料夾
* 遠端儲存庫（Remote Repository）
  * 僅**儲存庫** (Bare Repository)
  * 實際上是將本地的 .git 資料夾上傳到遠端儲存庫
  * 透過 `git clone --bare` 僅下載遠端儲存庫回來
* 共用儲存庫（Shared Repository）
  * `git init --bare` 建立共用儲存庫
  * 並不是僅限於git Server，只要是能遠端同步的工具，都可以當作遠端儲存庫使用
    * Dropbox、Google Drive ...etc
    * 再用 `git clone file:///path/to/repo.git/` checkout 即可

#### `--bare`
* https://moelove.info/2016/12/04/Git-%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93%E5%92%8C%E8%A3%B8%E4%BB%93%E5%BA%93/
* 在預設的情況下，不管是 `git init` 或 `git clone` 都會一併建立[工作區（Workspace / Work Tree）](#%E5%B7%A5%E4%BD%9C%E5%8D%80%EF%BC%88Workspace--Work-Tree%EF%BC%89)
* 包含 tags、local branches，但不包括 remote tracking branches
* 若不須要 git workspace，搭配 `--bare` 參數即可乎略（不建立）git work tree
* 若在沒有 work tree 的 repo 進行操作，則會出現訊息：`fatal: this operation must be run in a work tree`

#### `--mirror`
* https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---mirror
* https://stackoverflow.com/questions/3959924/whats-the-difference-between-git-clone-mirror-and-git-clone-bare
* 含蓋所有的 refs（即包含 tags、local branches、remote tracking branches）

### 了解 Git 資料結構
#### 物件（Object）：immutable
* [Git Internals - Git Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)
* 用來保存**儲存庫**中所有的**檔案**與**版本紀錄**
* 類似當下狀態的 snapshot
* 區分四種物件類型
  * blob
    * 檔案內容
    * `git hash-object <fileName>` => 根據檔案內容產生物件名稱
  * commit => 提交
    * author 和 committer 可以不一樣（ex. 重新 commit：rebase、`commit --amend`）
  * tree => 目錄
    * 檔案名稱不變，內容改變 => 新的 tree
    * 檔案名稱改變，內容不變 => 新的 tree
  * tag
    * annotated tag 為物件型態（with `-a` property）：以二進制檔案型式儲存
    * lightweight tag 為參考型態（without `-a` property）：以文字檔案型式儲存 (不會產生 tag 物件)
  * 可使用 [`git cat-file`](https://git-scm.com/docs/git-cat-file) 可以知道 git objects 的類型
    * `git cat-file` 看到的 ID 為檔案**內容**所運算（SHA-1 hash）而來，若 git object 檔名相同（即 ID 相同），則代表檔案內容一致
      * `-t` : type 類型
      * `-p` : print 內容
      * `-s` : size 檔案大小
    * 以下圖為範例
      ```shell=
      $ git cat-file -t a821ff3dfa531821967cc0380570bb01356c05a2
      tree

      # 放置 git object 之資料夾名稱 = a8
      # git object 檔案名稱（ID） = 21ff3dfa531821967cc0380570bb01356c05a2
      # 使用 sha1 值前 2 碼當作目錄名稱，目的在於提升讀取效率
      ```
      ```
      .git
      |
      ...
      |
      |- objects  // 所有資料都在 objects 底下
      |  |- a8
      |  |        21ff3dfa531821967cc0380570bb01356c05a2
      |  |
      |  |- f5
      |  |        eea678d87a8664e4c76e12d3ef5c4ff775ad58
      |  |
      |  |- fb
      |  |        8821d9a4cd57a2e6c5e4ae6a6706bcb89f2ce7
      |  |- info
      |  '- pack
      '- refs
          |- heads
          '- tags
      ```
    * **頻繁變動的大檔案**不應 commit 到 Git (因為占空間)
      * 1g 的檔案，就會產生 1g 的 blob => 使用 [Git Large File Storage (LFS)](https://git-lfs.github.com/) 解決 (需 server 有支援)

#### 索引（index）
* 記錄有哪些檔案即將要被提交到下一個 commit 版本中 (保存要進儲存庫之前的**所有檔案狀態**)
* `git add` 會將 tracked file 記錄到 git index file（git file status => Staged），並且建立一個 blob 物件（[Blob 對象](http://www-cs-students.stanford.edu/~blynn/gitmagic/intl/zh_tw/ch08.html#_blob%E5%B0%8D%E8%B1%A1)）
  * working directory => index => object storage
  * ~~俗話說的好：有 `git add` 有保佑~~，有 `git add` 過都會有檔案的 blob，即使沒有 commit 指向它
* `git commit` 時，會建立 commit object file，並指向 git index file 中異動檔案的 blob 物件
* 屬於一種「可變的」(mutable) 檔案類型
* 主要位於 `.git/index` 檔案
* 包含了所有**版控中**的檔案（可利用 .gitignore 排除檔案受到 index 版控）
* 空資料夾是無法加入版控的 (通常會用 `.gitkeep` 空檔使資料夾加入版控)
* 工作區與索引的檔案為一對一Mapping
* 索引有可能錯亂 XD (壞掉時將 index 砍掉重建就好了)

### Git 物件結構的優點
* 有效率的處理大型專案
  * 以**檔案內容**換算 SHA1 Hash => 絕對不可能發產生衝突
* 歷史紀錄保護
  * 每個版本包含上一個版本的 hash 值
  * 檢查 git 儲存庫的完整性：`git fsck`
* 定期的封裝物件
  * 對於不常用的物件會自動進行壓縮處理
    * `git gc` => 壓縮到 objects/pack 目錄下，同時會將不常用到的物件刪除
      * 使用 `git gc --prune=now` 會立刻清除 Unreachable 物件
        * 等價於 `git gc` + `git prune --expire=now`
    * ex: `git add .` 後又修改了同一份檔案後再次 `git add .` ==> 產生兩個 blob 物件，但 commit 僅參考到最後一份 blob 物件，第一份 blob 物件會在 `git gc` 時被清除

### 儲存庫、工作目錄與索引的關係圖
```
   |                            |                       |               |
   |------------------ git commit -a ------------------>|               |
   |                            |                       |               |
   |------- git add (-u) ------>|----- git commit ----->|               |
   |                            |  commit / tree / tag  |               |
   |                            |                       |-- git push -->|
   |                            |                       |               |
工作目錄                        索引                 本地儲存庫      遠端儲存庫
   |                            |                       |               |
   |<---------------------------- git pull -----------------------------|
   |                            |                       |               |
   |                            |                       |<- git fetch --|
   |                            |                       |               |
   |<----------- git checkout <branch_name> ------------|               |
   |                            |                       |               |
   |<-- git checkout -- file ---|                       |               |
   |                            |                       |               |
   |                            |-- git diff --cached --|               |
   |                            |                       |               |
   |--------- git diff ---------|                       |               |
   |                            |                       |               |
```

### git checkout
從索引內取得檔案 / 資料夾內容

#### `--`
請參考：[還原](#還原)
* [git checkout -- . vs git checkout](https://stackoverflow.com/questions/41101998/git-checkout-vs-git-checkout)
* [Why do we use double dash in “git checkout -- .”?](https://stackoverflow.com/questions/23208156/why-do-we-use-double-dash-in-git-checkout)

### Git Reset 應用技巧
* https://dotblogs.com.tw/wasichris/2016/04/29/225157
* 主要用途：將當前分支復原變更
* `--mixed` | `--soft` | `--hard`
  * `--mixed` [預設值] 工作目錄變更會保留，也就是保留 working directory 中的異動內容 (僅更新索引及指標位置)
  * `--soft` 工作區目錄變更會保留，同時也保留 index 中的異動內容
  * `--hard` 工作區檔案內容未 commit 的會被直接還原到指定版本
  * [`--soft vs. --hard`](https://stackoverflow.com/questions/3528245/whats-the-difference-between-git-reset-mixed-soft-and-hard)
    ![](https://i.stack.imgur.com/qRAte.jpg)
    
  |Reset 模式|所在位置（HEAD）|變更狀態紀錄（INDEX）|工作目錄|說明|
  |--|--|--|--|--|
  |soft|changed|unchanged|unchanged|僅移除 **commit** 變成新版未 commit，內容仍是新版的|
  |mixed|changed|changed|unchanged|index 移除 **staged** 標記，變成 **Modified** or **Untracked**，內容是新版的|
  |hard|changed|changed|changed|回到上一版版本，其間變更完全移除（接近 svn revert），內容及狀態皆是上一版|
  ![](https://trello-attachments.s3.amazonaws.com/583c75c90173173906e0b4ce/1023x654/ad3080ce58b146f8f0b5e343771a8a17/_output_gitlifecycle.png)

* 取得檔案變更應使用 `git checkout` 而非濫用 `git reset`
* `git reset -p`: 互動式選擇還原區塊

#### 復原最近一次重置 (reset)、合併 (merge) 或 重訂基底 (rebase)
* `git reset --hard ORIG_HEAD`: 復原**重大變更**的前一版

#### `git reflog`
* 利用 git commit object 在 `git gc` 之前都存在的特性，將因 reset 而沒有指向的 commit 調出來
  * `reflog` 列出來的內容是做完指令的結果，所以要還原必須再往前找一版

#### `commit --amend`
* 重新 commit 最近的 commit
  * 修改 commit 的 Git user name、email：
    * [`--reset-author`](https://git-scm.com/docs/git-commit#git-commit---reset-author)
    * [`--author=<author>`](https://git-scm.com/docs/git-commit#git-commit---authorltauthorgt)：`git commit --amend --author="Author Name <email@address.com>" --no-edit`
    * https://help.github.com/articles/setting-your-commit-email-address-in-git/
    * https://stackoverflow.com/questions/3042437/change-commit-author-at-one-specific-commit
    * https://stackoverflow.com/a/1320317/8530187
    * https://yulun.me/2014/git-tips-change-author-and-email-in-previous-commits/

## Git 分支合併技巧
### 分支 (Branching)
* 一個指向某個 commit id 的 pointer
* 每一個 commit 是將 index 的內容寫入 respository（請參考索引一節）
* branch、lightweight tag（非 `-a`）、HEAD 都是一種指向 commit 的**指標**（以文字檔案型式儲存著 commit id）
* 分支圖是從 refs 中找出所有儲存指標的檔案，並將它們所指向的 commit 及其 parents 繪製出來
* HEAD 指向一個沒有 branch 指向的 commit => 稱為 detached HEAD
* 分支刪除並不會保留任何紀錄

### 孤兒分支（orphan）
* `git checkout --orphan <branch_name>`
* 沒有 parent 的 commit object
* 一個 repo 中可以有多個 root commit
* 範例：
  * GitHub Pages
  * [台北捷運](https://github.com/othree/taipei-mrt)
  * 設計師將寫好的版型放到 orphan 分支，前端工程師比對差異進行套版

### 關於「分支」的真正意義
* 標記"時間維度"的指標
* 「分支 Branch」是一個會隨時間演進的指標，當下時間點的索引狀態 (開發用)
  * 任何一條分支的異動，都不會影響其他分支
* 「標籤 Tag」是一個不會隨時間演進的指標 (參考用)
* 「切換分支 Checkout」等同於「控制時間維度」

### 檢示所有分支詳細資訊
* https://git-scm.com/docs/git-branch#git-branch--v

### 分支更名（改名）
* https://git-scm.com/docs/git-branch#git-branch---move
* `git branch --move [<oldbranch>] <newbranch>`
* 重新命名 `.git/refs/heads/<oldbranch>` 分支

### 合併「不同世界」的分支
* `git merge --ff`（default）＝> 快轉 (Fast Forward)：快速合併，直接使用每一次異動的 commit，最好使用在同一分支的情況下，ex. 本地 merge 遠端
  ```
  C <-- develop        C <-- develop, test
  |                    |
  B <-- test    ==>    B
  |                    |
  A                    A
  ```
  * 只有異動 commit 的父 commit 才能使用 Fast Forward 至異動（同一條 branch）
* `git merge --no-ff`=> No Fast Forward：將多個異動併為一個 commit
  * 兩條意義不同的分支合併應該使用此合併策略，使線圖清晰
  * master 每一個版本都會是穩定版

  ```
                          M
                          | \ <-- develop
                          | o <-- test
                          | o
  M   <-- develop         | o
  | o <-- test     ===>   o |
  o o                     o |
  o o                     o |
  o /                     o /
  I                       I
  ```
* `git merge --ff` vs. `git merge --ff-only`
  * `--ff`：預設行為，若不行使用 Fast Forward 則用自動轉為 No Fast Forward
  * `--ff-only`：明確要求必須使用 Fast Forward，若不能使用 Fast Forward 時則拒絕操作
* `git pull` == `git fetch` + `git merge`
  * 所以遠端比本地新旳時候會無法 `git push`
* `git pull --rebase`
  * https://ihower.tw/blog/archives/3843
  * 加上 rebase 的意思是，會先
    1. 把本地 repo 從上次 pull 之後的變更暫存起來
    2. 回復到上次 pull 時的情況
    3. 套用遠端的變更
    4. 最後再套用剛暫存下來的本地變更
  * [`git pull --rebase --autostash`](https://git-scm.com/docs/git-pull#git-pull---autostash)
* 無法 Fast Forward 的情形，本地端與遠端版本已經不是同一條分支了
  ```
    O <-- origin/master
  M | <-- master
  o o
  o /
  I
  ```
    * 解法一：`git cherry-pick <commit_id>`
    * 解法二：`git rebase` (等同於 reset 後再做 cherry-pick)


### 合併沒有共同祖先的分支 (即 orphan branch)
* 透過 `git merge --allow-unrelated-histories <orphan_branch_name>` 合併孤兒分支

### `--no-ff` vs. `--no-ff --no-commit` vs. `--squash`
* [What are the differences between `--squash` and `--no-ff --no-commit`?](https://stackoverflow.com/questions/11983749/what-are-the-differences-between-squash-and-no-ff-no-commit)
* `--no-commit`：套用合併內容而不直接 commit，待使用者自行 commit
  * 合併但不 commit (會同時 add to index)
  * 合併成功 != 建置成功，確定可正常建置再自行 commit
* http://www.jianshu.com/p/58a166f24c81
* `--no-ff` 產生的 merge commit 上不會有 diff 資訊，要自行到被併入的 branch 中查看各個 commit

#### `--no-ff`
`(master) $ git merge --no-ff topic`

```
       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /            \
D -- E -- F -- G    master        D -- E -- F -- G -- H  master
```

#### `--no-ff --no-commit`
```bash
(master) $ git merge --no-ff --no-commit topic
Automatic merge went well; stopped before committing as requested
(master) $ git status
On branch master
Your branch is up to date with 'origin/master'.

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

	modified:   README.md
	modified:   package.json
...
```

```
       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /            \
D -- E -- F -- G    master        D -- E -- F -- G -- H  master
```

#### `--squash`
```bash
(master) $ git merge --squash topic
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
(master) $ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   README.md
	modified:   package.json
...
```

```
       A -- B -- C  topic                A -- B -- C     topic
      /                     ===>        /
D -- E -- F -- G    master        D -- E -- F -- G -- H  master
```
  * 壓縮合併，但不產生線圖
  * 等價於 cherry-pick & rebase -i squash

### 合併策略
* https://git-scm.com/docs/git-merge#_merge_strategies
* https://git-scm.com/docs/git-rebase#_merge_strategies
* https://git-scm.com/docs/merge-strategies
* https://www.jianshu.com/p/58a166f24c81
* https://www.cnblogs.com/chaoguo1234/p/5347623.html
* `git [merge | rebase] [-X ours | theirs] <commit>`
  * `-X ours`：以目前 head 所在的 branch 為主
  * `-X theirs`：以不是 head 所在的 branch 為主
* `git checkout [--ours | theirs]`
  * https://gitbook.tw/chapters/branch/fix-conflict.html
  * https://nitaym.github.io/ourstheirs/
* 衝突方向
  * 目前 head 指向的分支內容：current change
  * 對方分支內容：incoming change
  * 例如：
    * `(dev)$ git merge master`：dev <- current（在 dev 上建立 merge commit 將 master 內容 merge 至 dev，會把 merge commit 的另一個 parent 指向 master）
    * `(dev)$ git rebase master`：master <- current（將 dev 上的 commit 一個一個重新 commit 到 master，head 指向 master 且會一個一個移動到後方重新 commit 的新 commit）

### 列出已合併/未合併的分支
* 已合併: `git branch --merged`
    * 刪除已合併的分支 `git branch --merged | egrep -v "(^\*|master|dev)" | xargs git branch -d`
        * egrep 排除 當前分支、master、dev
        * https://stackoverflow.com/questions/6127328/how-can-i-delete-all-git-branches-which-have-been-merged
* 未合併: `git branch --no-merged`

## Rebase
git rebase 的重新 commit 為：
1. 建立一個沒有名字的 branch
2. 在此 branch 提交
3. 將原 branch 指向此 branch

https://blog.yorkxin.org/2011/07/29/git-rebase

```
        A -- B -- C  topic                          A' -- B' -- C'  topic
      /                      ===>                  /
D -- E -- F -- G     master        D -- E -- F -- G                 master
```
* 在 topic 下指令 `git rebase master`
* 移花接木，將 master 的 commit 接在 topic 的 commit之前（**不影響 master 之線圖**）
  1. 先 `checkout` 到指定 commit
  2. 後 `cherry-pick` 到一個沒有 branch 指向的 commit
  3. 再 `checkout` 回自己 commit 後 `reset` 回沒有名字的 branch
* rebase 的目的在於避免 merge 衝突
  * 遠端有更新時，應當立刻 rebase！！
* 通常有遠端的時候才會需要用到
* [`git rebase --autostash`](https://git-scm.com/docs/git-rebase#git-rebase---autostash)

### `git rebase --skip`
* https://git-scm.com/docs/git-rebase#git-rebase---skip
* https://stackoverflow.com/questions/9539067/what-exactly-does-git-rebase-skip-do
* 跳過（略過）目前衝突的 commit，會放棄此 commit 的異動

### `git rebase -i`
* `git rebase -i <base_commit_id>`（可修改 base commit 後的 commit）
  * 參數 i 代表：Interactive Mode 互動模式
  * pick = 要這條 commit ，什麼都不改
  * reword = 要這條 commit ，但要改 commit message
  * edit = 要這條 commit，但要改 commit 的內容
    * 修改完 commit 內容後可以再繼續新增其他 commit (於既有歷史紀錄中插入新版本)
  * squash = 要這條 commit，但要跟前面那條合併，並保留這條的 messages
    * 於 commit 時加上 `--squash=<commit>` 標註要壓縮哪個版本
    * `git rebase -i --autosquash` 會自動將修正版本接在 commit 版本之後並設為 squash
    * [git rebase --autosquash](https://git-scm.com/docs/git-rebase#git-rebase---autosquash)
  * fixup = squash + 只使用前面那條 commit 的 message ，捨棄這條 message
    * 於 commit 時加上 `--fixup=<commit>` 標註要修正哪個版本 (效果同 squash)
  * exec = 執行一條指令（如執行 git 指令）
  * drop = 不要這條 commit
  * 還可以調整 commits 的順序，直接剪剪貼貼，改行的順序就行了
* rebase 修訂衝突後，應使用 `git add <file>` 標註衝突已解決，並使用 `git rebase --conitnue` 完成 (**而不是增加額外的commit**)
* 修改第一個初始 commit
  * `git rebase -i --root`
  * 最早可跳至初始 commit 後的狀態
  * https://stackoverflow.com/questions/2119480/edit-the-root-commit-in-git
  * https://git-scm.com/docs/git-rebase#git-rebase---root

### `git rebase --onto`
* https://blog.yorkxin.org/2011/07/29/git-rebase
* https://git-scm.com/docs/git-rebase
* 指定要 rebase 的 base commit
* 可用於要 rebase 已 merge 的 branch，解決只用 rebase 可能會無法正確 rebase 的情形
* `git rebase --onto <new base-commit> <current base-commit> <target-branchName>`
* `git rebase --onto <new base-commit> <current base-commit>`（rebase 目前的分支）

## `git cherry-pick`
* `--no-commit`：先不 commit，可利用此參數來多次 cherry-pick 不同的 commit 內容
  * 在真正 commit 之前，都是屬於 cherry-pick 的過程
  * 過程中可以 `git cherry-pick --abort` 來中斷 cherry-pick
* 可以跨 repository 挑選需要的 commit

### `git cherry-pick` 多個 commit
* 多個 commit：`git cherry-pick commit1 commit2`
* 多個連續 commit：`git cherry-pick preStartCommit..endCommit` 或 `git cherry-pick startCommit^..endCommit`
  * https://git-scm.com/docs/git-cherry-pick#git-cherry-pick-codegitcherry-pickmaintmasternextcode
  * https://stackoverflow.com/questions/1670970/how-to-cherry-pick-multiple-commits
  * https://www.jianshu.com/p/08c3f1804b36
  * https://segmentfault.com/q/1010000010185984
  * `preStartCommit..endCommit` 代表從 `preStartCommit`（不包含此 commit 之異動內容）到 `endCommit` 為止的所有 commit
  * `startCommit^..endCommit` 代表從 `startCommit`（包含此 commit 之異動內容）到 `endCommit` 為止的所有 commit
* cherry-pick 整個分支：`git cherry-pick parentBranchName..targetBranchName`

### 反向撿櫻桃 (Revert)
* `git revert <commit_id>`

## HEAD
* https://git-scm.com/docs/git-rev-parse#_specifying_revisions
* https://stackoverflow.com/questions/2221658/whats-the-difference-between-head-and-head-in-git
* **HEAD^**
  一個 ^ 表示為第一個分支上的前一版，一個 ^ 就代表切換一次分支，後面接的數字則代表往前一版的第幾個分支
  * Note: Windows command 需打 `HEAD^^` (`^`為跳脫字元 [Escape Characters](http://www.robvanderwoude.com/escapechars.php))
* **HEAD~**
  一個 ~ 表示為前一版，預設都是於左數來第一個分支上往上找，後面接的數字則代表往前幾版
* **HEAD~** vs. **HEAD^**
  ![](https://i.stack.imgur.com/pDAzG.png)

### detached HEAD
* https://www.git-tower.com/learn/git/faq/detached-head-when-checkout-commit

### origin/HEAD
* [Re: [問題] 請問 git中 origin/HEAD 指的是](https://www.ptt.cc/bbs/Linux/M.1406900317.A.D51.html)
* [How does origin/HEAD get set?](https://stackoverflow.com/questions/8839958/how-does-origin-head-get-set)
* origin/HEAD 代表遠端的 default branch，也就是你 clone（與一些其他操作）時預設會切換到的 branch。預設會指到 origin/master

### ORIG_HEAD
https://gitbook.tw/chapters/branch/how-git-know-what-current-branch-is.html

> 當你在做一些比較「危險」的操作（例如像 `merge`、`rebase` 或 `reset` 之類的），Git 就會把 HEAD 的狀態存放在這裡，讓你隨時可以跳回危險動作之前的狀態

## 還原／清除工作目錄檔案
### 還原
* `git reset`
  全部 Staged -> Unstaged，可搭配 `hard`、`soft`、`mixed` 等參數
* `git reset [--] <file>`
  Staged -> Unstaged
  `--`：代表將 `--` 後方的每個參數皆一率視為檔案路徑
  * https://git-scm.com/docs/git-reset#git-reset-Resetasinglefileintheindex
  * https://stackoverflow.com/questions/7147270/hard-reset-of-a-single-file
  * https://stackoverflow.com/questions/6561142/difference-between-git-checkout-filename-and-git-checkout-filename/6561160#6561160
  
* `git reset <commitCode>`
  會 checkout 至指定 commit，並將差異還原成 Unstaged
  一樣可搭配 hard、soft、mixed 等參數
  但要小心每一個參數處理差異的方式並不同
* `git checkout <file>`
  Staged 中有記錄：以 Staged 來還原
  Staged 中沒有記錄：以目前 commit 來還原，Unstaged 內容會消失

### 清除
* `git clean`
  刪除 untracked 的檔案
  * `git clean -n` 只列出將會清除的清單（Dry run, 不搭配其他參數則列出 non-ignored 的 untracked 檔案）
  * `git clean -f` 執行清除檔案（不搭配其他參數則清除 non-ignored 的  untracked 檔案）
  * 參數 n、f 可以搭配以下參數：
    * `git clean -X` 要清除 untracked 檔案（ignored files）
    * `git clean -x` 要清除所有 untracked 檔案（ignored and non-ignored files）
    * `git clean -d` 要同時清除 untracked 目錄
  * 可指定路徑

## 比對差異
* https://git-scm.com/docs/git-diff

### 不同的 git diff 方法
* 工作目錄(Work Tree) vs 更新索引(Index): `git diff`
* 工作目錄(Work Tree) vs 最新版本(HEAD): `git diff HEAD`
* 工作目錄(Work Tree) vs 歷史版本(Commits): `git diff <commit>`
* 更新索引(Index) vs 最新版本(HEAD): `git diff --cached <HEAD>` (HEAD 可省略)
* 更新索引(Index) vs 歷史版本(Commits): `git diff --cached <commit>`
* 歷史版本(Commits) vs 歷史版本(Commits): `git diff <src-commit> <target-commit>`

### 比對二進位檔案之間的差異
* `git diff --binary`
  * 用於產生 patch

### 比對兩個版本之間的檔案異動清單與狀態
* `git diff --name-only`: 僅列出工作目錄與索引間異動的檔案
* `git diff --name-status`: 列出工作目錄與索引間異動的檔案及狀態
* `git diff --name-status <src-commit> <target-commit>`: 列出兩版本間異動的檔案及狀態
  * 搭配 `--diff-filter` 指定特定狀態，配合 shell script 自動修正佈署環境檔案

### 使用 git diff 產生 patch 修補檔與套用修補檔的方法
https://juejin.im/post/5b5851976fb9a04f844ad0f4

1. 建立 patch: `git diff <src-commit> <target-commit> > my-patch.patch`
  * 若包含二進位檔案須加上 `--binary`
2. 套用 patch: `git apply my-patch.patch`
  * 套用前可先使用 `git apply --check` 檢查套用 patch 過程是否會發生衝突

#### 正式環境上版與退版
* 上版: `git apply <patch_file>`
* 退版: `git apply --reverse <patch_file>`

#### `format-patch` vs. `diff >`
https://yodalee.blogspot.com/2017/03/git-patch.html

> 常見的 diff 其實也就是git diff 生成的 patch，內容就是：這幾行刪掉，這幾行加上去，用 git diff > commit1.patch 就能輕鬆生成。git patch system 則是用 git format-patch 來產生，它提供比 diff 更豐富的資訊

## git stash
* git stash 也是一種分支（refs/stash）
* [`git stash show`](https://git-scm.com/docs/git-stash#git-stash-showltstashgt)
* `git stash pop <n>`：n 是離現在最近的第幾個 stash（0-base）

## git show
* https://git-scm.com/docs/git-show
* [`git show --patch`](https://git-scm.com/docs/git-show#git-show---patch)
* [Generating patches with -p](https://git-scm.com/docs/git-show#_generating_patches_with_p)

## git add / checkout --patch
https://zlargon.gitbooks.io/git-tutorial/content/advanced/add_checkout_part_of_file.html

## 檔案 / 資料夾大小寫更名
1. `git config --local core.ignorecase false` 將 git 設定為大小寫敏感
2. `git mv client/themeCore/components/core/productCard temp`
3. `git mv temp client/themeCore/components/core/ProductCard`

## Git 遠端儲存庫管理
* `git remote`
  * [`--verbose`](https://git-scm.com/docs/git-remote#git-remote---verbose)
  * `git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git`
  * Syncing a fork
    * http://fred-zone.blogspot.tw/2015/09/git-fork.html
    * https://gitbook.tw/chapters/github/pull-from-github.html
    * https://gitbook.tw/chapters/github/syncing-a-fork.html
    * https://www.peterdavehello.org/2014/02/update_forked_repository/
    * `$ git pull upstream master`
* 一般建議使用 `git pull --rebase`
  * 衝突時，先 `git rebase --abort` 再說
* `git push [<遠端倉庫名稱>] [<本地分支名稱>][:<遠端分支名稱>]`
  * `git push origin local-name`
  * `git push origin local-name:remote-name`
  * https://git-scm.com/docs/git-push#git-push-codegitpushorigincode
  * https://git-scm.com/docs/git-push#git-push-ltrefspecgt82308203
  * [git push --force and how to deal with it](https://evilmartians.com/chronicles/git-push---force-and-how-to-deal-with-it)
  * [GIT: PUSHING TO A REMOTE BRANCH WITH A DIFFERENT NAME](https://penandpants.com/2013/02/07/git-pushing-to-a-remote-branch-with-a-different-name/)
  * [將本地分支 push 到遠端](https://git-scm.com/docs/git-push#_description)
  * https://help.github.com/articles/pushing-to-a-remote/
* `git push --force`
  * https://git-scm.com/docs/git-push#git-push---force
* `git push --force-with-lease`
  * https://git-scm.com/docs/git-push#git-push---no-force-with-lease
  * https://blog.csdn.net/wpwalter/article/details/80371264
* `git branch -u <遠端倉庫名稱>[/<遠端分支名稱>] [<本地分支名稱>]` == `git branch --set-upstream-to=<遠端倉庫名稱>[/<遠端分支名稱>] [本地分支名稱]`
  * [追蹤遠端分支](https://git-scm.com/docs/git-branch#git-branch--ultupstreamgt)
  * https://zlargon.gitbooks.io/git-tutorial/content/remote/upstream.html
* `git checkout -b [<本地分支名稱>] --track <遠端倉庫名稱>[/<遠端分支名稱>]`
  * 建立新的追蹤分支（tracking branch），為 `git checkout <分支名稱>` 的[預設行為之一](https://git-scm.com/docs/git-checkout#git-checkout-emgitcheckoutemltbranchgt)（如果有同名的遠端分支名稱）
* `git push -u <遠端倉庫名稱> <本地分支名稱>` == `git push --set-upstream <遠端倉庫名稱> <本地分支名稱>`
  * `git branch -u <遠端倉庫名稱>[/<遠端分支名稱>] [<本地分支名稱>]`
* 刪除遠端分支
  * https://git-scm.com/book/en/v2/Git-Branching-Remote-Branches
  * https://git-scm.com/docs/git-push#git-push---delete
  * `git push origin --delete <branchName>`
      * 會同時刪掉遠端追蹤分支
  * 其他人則透過 `git fetch --prune` 刪掉遠端分支
* 刪除本地之遠端追蹤（remote-tracking）分支
  * `git branch -r` 列出遠端追蹤分支
  * https://git-scm.com/docs/git-branch#git-branch---delete
  * https://git-scm.com/docs/git-branch#git-branch---remotes
  * `git branch --delete --remotes origin/<branchName>`

## 標記版本
* 指向特定 commit 版本
* 標籤建立後通常不會再異動
* https://git-scm.com/docs/git-tag

### 建立與刪除輕量標籤 (lightweight tag)
* 輕量標籤: 就只是個指標，並不會建立任何物件
* 建立標籤: `git tag <tag_name> <commit>`
  * 顯示訊息時只會顯示該版本的 commit 內容
* 刪除標籤: `git tag -d <tag_name>`
  * 或手動刪除檔案 `.git/refs/tags/<tag_name>`

### 建立與刪除標示標籤 (annotated tag)
* 建立標籤: `git tag -a <tag_name> <commit>`
  * 需輸入版本訊息 (使用 `tag -n` 顯示 tag 訊息)
  * 通常用於 release
* 刪除標籤: `git tag -d <tag_name>`

### 遠端標籤
* 推送遠端標籤: `git push --tags`
* 刪除遠端標籤: `git push --delete origin <tag_name>`
* 取得遠端標籤: `git fetch`
  * 若遠端標籤已不存在，要同時刪除本地標籤: `git fetch --prune-tags`

### 取出特定標籤的完整原始碼
* `git checkout <tag_name>`: 實際上是把標籤指向的版本取出 (因為標籤不是分支，所以會跑去 detached HEAD)
  * 因此應加上 `-b` 同時建立分支, ex `git checout -b hotfix/6.0.1 6.0.1`

## Submodule
https://blog.chh.tw/posts/git-submodule/

## Git 協同作業實戰 ==> 好的分支合併流程！！
1. 集中式版控流程
    * 共用儲存庫
    * 類似 SVN
2. 整合式管理版控流程
    * blessed repository 對每個人都是唯讀
    * devloper 將修改 pull request 給 integration manager，通過後再合併
    * 類似 GitHub fork
    * pull 都從 blessed repository，push 則 push 自己的
      * 透過 `git remote set-url --push origin <URL_TO_MY_REPO>` 設定
3. 獨裁者與副手工作流程
    * 類似前一步，只是再加上副手幫忙 code review

## 認識 GitHub Flow
* https://guides.github.com/introduction/flow/
* http://calvert.logdown.com/posts/2014/09/21/understanding-the-github-flow
* http://blog.krdai.info/post/17485259496/github-flow

## Git Commit Message
* http://karma-runner.github.io/5.0/dev/git-commit-msg.html
* http://blog.fourdesire.com/2018/07/03/%E6%92%B0%E5%AF%AB%E6%9C%89%E6%95%88%E7%9A%84-git-commit-message/
* https://juffalow.com/other/write-good-git-commit-message
* https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/

## 其他
* 命令列呈現線圖 `git log --oneline --graph -n`
    * `--oneline`: 僅顯示第一行訊息，即 commit log
    * `--graph`: 以線圖呈現
    * `-n`: 最近幾版
* 實用的 git alias
  ```
  git config --global alias.co   checkout
  git config --global alias.ci   commit
  git config --global alias.st   status
  git config --global alias.sts  "status -s"
  git config --global alias.br   branch
  git config --global alias.re   remote
  git config --global alias.di   diff
  git config --global alias.type "cat-file -t"
  git config --global alias.dump "cat-file -p"
  git config --global alias.lo   "log --oneline"
  git config --global alias.ll   "log --pretty=format:'%h %ad | %s%d [%Cgreen%an%Creset]' --graph --date=short"
  git config --global alias.lg   "log --graph --pretty=format:'%Cred%h%Creset %ad |%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset [%Cgreen%an%Creset]' --abbrev-commit --date=short"

  # Windows 限定 (因為要 TortoiseGit)
  git config --global alias.tlog "!start 'C:\PROGRA~1\TortoiseGit\bin\TortoiseGitProc.exe' /command:log /path:."
  ```

---
## Git 工具
* [必要安裝] [Git 命令列工具](https://git-scm.com/)
* [推薦安裝] [TortoiseGit](https://tortoisegit.org/) ( Windows only )
* [推薦安裝] [SourceTree](https://www.sourcetreeapp.com/)
* [Visual Studio Code](https://code.visualstudio.com/)
* [GitHub Desktop](https://desktop.github.com/)
* [Fork](https://git-fork.com/) ( Mac only )
* [SmartGit](http://www.syntevo.com/smartgit/)
* [GitExtensions](https://gitextensions.github.io/) ( Windows only )
* [tig](https://jonas.github.io/tig/) (Command Line)
* [GitKraken](https://www.gitkraken.com ) ( Commerical )
* [Tower](https://www.git-tower.com/) ( Commerical )

### 環境建立
* [Git 版本控管實戰：新手進階篇 - 範例版本庫 (Windows)](
https://gist.github.com/doggy8088/491d9da6b2452437f4af487137dd0340)
  * [Start-Sleep](https://ss64.com/ps/start-sleep.html) 必須使用 PowerShell 才能使用
* [Git 版本控管實戰：新手進階篇 - 範例版本庫 (macOS)](
https://gist.github.com/doggy8088/e535363a8f5c8ef3aeb31a1d234d4937)

### Git 教學影片(課後問卷)
* [認識 Git 資料結構中的物件資料庫與物件之間的關係](https://www.youtube.com/watch?v=PZbSRy_ow0U)
* [認識 Git 資料結構中的索引與檔案狀態的變化關係](https://www.youtube.com/watch?v=5c_7v0cIFk4)
* [如何在 GitHub 使用 Fork / Pull Request 功能 (以 VS2013 為例)](https://www.youtube.com/watch?v=NTLAfy6lcdQ)
* [透過 Visual Studio 2013 匯入方案到 Git 儲存庫的正確做法 ](https://www.youtube.com/watch?v=97BVjQyK8ag)
