---
# System prepended metadata

title: Git 進階篇
tags: [Git]

---

# Git - 進階篇

## 使用分支（branch）

在增加新功能、修正 bug，或是想嘗試一些新做法時，都可以另外開一個分支來進行，等開發完確認沒問題之後再合併回來，就不會影響正在運行的功能。簡而言之，branch 的作用就是讓開發過程各自獨立。

![branch](https://imgur.com/wQmzfvH.png)

### 分支的本質

在 Git 裡⾯的分⽀，其實就跟**⼀張貼紙**⼀樣，它會貼在某個 Commit 上：

![branch](https://imgur.com/eAJMurC.png)

當完成了一個新的 Commit，新的 Commit 會指向它的前一個 Commit，而目前的分支，會貼到剛剛完成的新的 Commit 上。也就是說分支會隨著每次 Commit 的新增一起移動：

![branch on new commit](https://imgur.com/Edjec69.png)

在 Git 的分⽀並不是複製⽬錄或檔案來進⾏開發與修改，**分⽀就只是⼀個指標、⼀張貼紙，貼在某個 Commit 上⾯⽽已**。

### 查看專案分支

```bash
$ git branch
* master
```

### 建立分支

開發新功能之前，養成建立新分支的好習慣。

```bash
$ git branch develop
$ git branch
  develop
* master
```

### 切換分支

```bash
$ git checkout develop
Switched to branch 'develop'

$ git branch
* develop
  master
```

#### 在切換分⽀的時候發⽣了什麼事？

1. 更新暫存區以及⼯作⽬錄

    Git 在切換分⽀的時候，會⽤該分⽀指向的那個 Commit 的內容來「更新」暫存區以及⼯作⽬錄。但是**在切換分⽀之前所做的修改則還是會留在⼯作⽬錄**，也就是說換切分⽀並**不會影響已經在⼯作⽬錄的那些修改**。

2. 變更 HEAD 的位置

    除了更新暫存區以及⼯作⽬錄的內容外，同時 HEAD 也會指向剛剛切換過去的那個分⽀，也就是說 `.git/HEAD` 這個檔案會跟著被修改。

### 修改分支名稱

分支改名**不會影響到檔案或目錄**。

```bash
$ git branch -m <old branch-name> <new branch-name>
```

### 刪除分支

```bash
$ git branch feature
$ git branch
* develop
  feature
  master
$ git branch -d feature
Deleted branch feature (was babb78d).
```

如果要刪除的分支還沒被完全合併，Git 會有小提示：

```bash
$ git branch -d feature
error: The branch 'feature' is not fully merged.
If you are sure you want to delete it, run 'git branch -D cat'.
```

如果刪除了**尚未被合併完成**的分支，但事後反悔了想將檔案救回來，還是有方法可行的。

![delete unmerged branch](https://imgur.com/BOWWGhq.png)

從上圖可以看到，即便刪除了分支，但在該分支上原先的 Commit 依然保存著，那是因為**分支只是一個指向某個 Commit 的指標，刪除這個指標並不會造成那些 Commit 消失**。既然 Commit 沒有消失，就意味著還是可以重新把它找回來：

```bash
$ git branch return_dev b174a5
```

這個指令的意思是「建立一個名為 `return_dev` 的分支，讓它指向 `b174a5a` 這個 Commit」，簡單來說就是再去拿一張新的貼紙貼回去的意思。

:::warning
**補充**

如果沒有把被刪除的分支的 SHA-1 值記下來，該怎麼復原？

可以使用 **`git reflog`** 指令去查找。當 **`HEAD`** 有移動的時候（例如：切換分支或是 reset，都會造成 **`HEAD`** 移動），Git 就會在 Reflog 裡記上一筆。
:::

### 合併分支

當在 `develop` 分支開發完新功能，要將功能上線到穩定系統，也就是把 `develop` 分支合併到主要分支 `master`，需要以下步驟：

0. 使用 `git log` 指令查詢歷史紀錄：

    ```bash
    $ git log --oneline
    87e0bb8 (HEAD -> develop) <create> product management feature
    a3664cb <create> product page
    6b9bf18 (master) <create> main feature
    dbbcdd9 <create> home style
    4c6862a <create> home page
    ```

1. 切回主要分支 `master`

    ``` bash
    $ git checkout master
    Switched to branch 'master'
    ```

2. 輸入要合併的分支名稱

    ```bash
    $ git merge develop
    Updating 6b9bf18..87e0bb8
    Fast-forward
    product.html | 0
    product.js   | 0
    2 files changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 product.html
    create mode 100644 product.js
    ```

在 `develop` 分支新增的 `product.html` 與 `product.js` 兩個檔案，因為主要分支 `master` 現在已經合併了 `develop` 分支，所以現在在主要分支 `master` 上也有這兩個檔案了。

:::danger
**注意**

所謂的 Merge 並不是指分支與分支之間的合併，而是去 Merge 「**該分支所指向的那些 Commit**」。
:::

#### 快轉模式（Fast Forward）

快轉模式是指當主要分支 `master` 合併 `feature` 分支時，`master` 當前的節點一直和 `feature` 的根節點相同，沒有發生改變，此時就會採取「快轉模式」，其實就是把 `master` 這張貼紙撕起來，往前貼到 `feature` 分支所指的那個 Commit 而已。

![fast forward](https://imgur.com/gh2tuar.gif)

#### 非快轉模式（No Fast Forward）

當主要分支 `master` 當前的節點與 `feature` 分支的根節點不同時，此時合併分支並不會觸發快轉模式，會額外產出一個 Commit。

![no fast forward](https://imgur.com/kiAcFZ6.gif)

### 另一種合併方式：rebase

rebase 這個英文單字的翻譯是「重新定義分支的**參考基準**」。Rebase 合併分支跟 Merge 合併分支最明顯的差異在於，使用 Rebase 合併分支，Git 不會特別產生一個專門用來合併的 Commit。

>　A `git rebase` copies the commits from the current branch, and puts these copied commits on top of the specified branch.

![rebasing](https://imgur.com/OXgklx2.gif)

#### rebase 參考資料

[另一種合併方式（使用 rebase）](https://gitbook.tw/chapters/branch/merge-with-rebase)

### 合併時發生衝突（Merge Conflict）

Git 有能力幫忙檢查簡單的衝突，所以並不是改到同一個檔案就一定會發生衝突，但是改到**同一個檔案的同一行程式碼**就沒辦法了。Git 無法幫你選擇哪一個當作最終版本，所以當發生衝突時，只能「自己手動調整」。

假設現在有一個 `index.html` 檔案，主要分支 `master` 與 `develop` 分支都同時修改了這個檔案的同一行程式碼，如果想要把 `develop` 分支 Merge 到主要分支 `master` 上，會出現以下訊息：

```bash
$ git merge develop
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
```

使用 `git status` 指令查詢目錄狀態：

```bash
$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
```

>　You have unmerged paths.

你有一個未合併的路徑。意思就是**目前的合併尚未完成**。

進入 `index.html` 檔案會顯示衝突發生的位置，需要手動調整檔案內容：

![merge conflict message](https://imgur.com/rSN5i9L.png)

Git 會把有衝突的段落標記出來：

* 上半部是 `HEAD`，也就是目前所在的主要分支 `master`
* 中間是分隔線
* 下半部是被合併的分支，也就是 `develop` 分支

修改完發生衝突的段落後，還是要把檔案重新安置到暫存區，並完成 Commit Message，才算解決 Merge Conflict。

```bash
$ git add index.html
$ git commit -m 'conflict fixed'
[master d634e31] conflict fixed
```

![merge conflict](https://imgur.com/UWOIgkf.gif)

## 使用 GitHub 遠端共同協作

Git 是一個分散式版本控制工具，藉由它可以產生一個儲存庫（Repository），裡面存放著被 Git 版本控制的專案。

GitHub 是目前全球最大的 Git Server，許多 open-source 的專案都是使用 Github 進行程式碼的管理，可以把 GitHub 想成「**提供存放 / 使用 Git 專案儲存庫（Repository）的服務**」。

### 將本地端 Repository 推送到遠端

1. 在 Github 上建立一個 Repository

    ![create repository](https://imgur.com/zStUuVM.png)

2. 建立好遠端 Repository 後，有兩種方式將本地端 Repository 與 GitHub 的遠端 Repository 連結：

    * 如果是全新開始的專案，依照「create a new repository on the command line」指示進行
    * 如果是要上傳現有專案，依照「push an existing repository from the command line」指示進行

    ![remote guide](https://imgur.com/SXpZV0E.png)

3. 以全新開始的專案為例：

    ```bash
    $ cd /desktop          
    $ mkdir git-practice
    $ cd git-practice

    $ echo '# Git Practice' >> README.md
    
    $ git init
    Initialized empty Git repository in /desktop/git-practice/.git/

    $ git add README.md

    $ git commit -m 'initial commit'
    [master (root-commit) 542dc7e] initial commit
    1 file changed, 1 insertion(+)
    create mode 100644 README.md
    ```

4. 將本地端 Repository 推送到 GitHub

    ```bash
    $ git remote add origin https://github.com/avery210412/git-practice.git
    ```
    
    意思是「**將本地端 Repository 新增一個名為 `origin` 的遠端 Repository**」。

    * `git remote`：主要是跟遠端有關的操作
    * `add`：要加入一個遠端的節點
    * `origin`：是一個「代名詞」，指的是後面那串 GitHub 伺服器的位置

    ```bash
    $ git push -u origin master
    Enumerating objects: 3, done.
    Counting objects: 100% (3/3), done.
    Writing objects: 100% (3/3), 229 bytes | 229.00 KiB/s, done.
    Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
    To https://github.com/avery210412/git-practice.git
    * [new branch]      master -> master
    Branch 'master' set up to track remote branch 'master' from 'origin'.
    ```

    `git push` 指令其實做了幾件事：

    * 把 `master` 這個分支的內容，推向 `origin` 指向的 GitHub 伺服器位置
    * 在 `origin` 指向的伺服器上，如果 `master` 分支不存在，就建立一個叫做 `master` 的同名分支。
    * 如果伺服器上原本就存在 `master` 分支，便會移動伺服器上 `master` 分支的位置，使它指到目前最新的 Commit 上。
    * `-u`：設定 `upstream`，使本地端分支開始追蹤指定的遠端分支，也就是說現在開始，「**本地端的 `master` 分支，會去追蹤遠端 origin 上的 `master` 分支**」

5. 回到瀏覽器上重新整理頁面：

    ![push repository finished](https://imgur.com/o7Jjyn1.png)

    看到這個畫面，表示已經順利把本地端 Repository 的東西，推送到這個遠端 Repository 裡面了。

:::success
**狀況題**

如何修改已經 Push 到遠端的 Commit？

假設現在已經完成一個 Commit，並且也推送到遠端 Repository 上，如果此時專案中的檔案做了一些小修改，但是又不想在這個 Commit 以後再新增一個 Commit 記錄此次的修改，只想要修改上一次的 Commit 並且重新 Push 到遠端 Repository，可以這樣操作：

  1. 使用 **`--amend`** 參數進行 Commit，來修改上一次的 Commit，此時 SHA-1 值會重新計算

  2. 使用 **`git push --force-with-lease origin <branch>`**，將 Commit 推送至遠端 Repository
:::

#### `git push` 參考資料

[如何使用 git push 指令只 Push 部份的進度？](https://youtu.be/VShhhq_5sMc)

[怎麼有時候推不上去](https://gitbook.tw/chapters/github/fail-to-push)

[修改已經 Push 到遠端 Repository 的 Commit](https://tinyurl.com/y6gtw7s8)

---

### 將遠端 Repository 同步至本地端

#### Fetch

當本地端 Repository 完成推送到遠端 Repository 時，目前的分支圖如下：

![git branching diagram](https://imgur.com/TR4NUjH.png)

假使這個專案由多人協作開發，某位開發者 Push 了新的功能，此時遠端 Repository 就會新增一個 Commit：

![push a new feature](https://imgur.com/5Pngthu.png)

此時，若想要把遠端 Repository 的最新版本下載回本地端，但又擔心下載後，會與本地端的 Repository 發生合併衝突（Merge Conflict）的情形，此時可以使用 `git fetch` 指令。

當執行 Fetch 指令時，Git 會看一下遠端 Repository 的最新版本內容，再比較一下此時本地端 Repository 的內容，它會**把遠端有但是本地端沒有的內容**下載到本地端 Repository，但是**不會將這些內容合併到本地端 Repository**，因此分支圖還是會維持上面的情況：

![git fetch](https://imgur.com/5Pngthu.png)

由於 `origin/master` 這個遠端分支，是從本地端 `master` 分支推送到 GitHub 進而產生的，因此 `origin/master` 分支跟 `master` 分支**本是同根生**。如果要把 Fetch 得到的最新版本內容合併到本地端 Repository，會觸發快轉模式（Fast Forward），此時的分支圖如下：

![Fetch and Merge](https://imgur.com/QUQ5aAH.png)

![Fetch](https://imgur.com/tPCaQE5.gif)

#### Pull

> *git pull = git fetch + git merge*

Pull 指令就是將遠端 Repository 的最新版本下載回本地端，並且將遠端分支合併到本地分支，也就是將 `origin/master` 這個遠端分支**直接合併**到本地端 `master` 分支。

![Pull](https://imgur.com/RtefJ9S.gif)

### 從 GitHub 伺服器上取得 Repository

如果在 GitHub 上看到某個專案很有趣，想要下載到本地端研究，使用 Clone 指令就可以把整個專案複製一份到本地端了。

```bash
$ git clone https://github.com/sparanoid/chinese-copywriting-guidelines.git
Cloning into 'chinese-copywriting-guidelines'...
remote: Enumerating objects: 907, done.
remote: Counting objects: 100% (212/212), done.
remote: Compressing objects: 100% (98/98), done.
remote: Total 907 (delta 137), reused 181 (delta 113), pack-reused 695
Receiving objects: 100% (907/907), 354.90 KiB | 2.25 MiB/s, done.
Resolving deltas: 100% (529/529), done.
```

Clone 指令會把整個專案的內容複製一份到本地端，也就是你的電腦裡，這裡指的「內容」不是只有檔案，而是指**整個專案的歷史紀錄、分支、標籤等**內容都會複製一份下來。

:::danger
**注意**

如果這個專案你是第一次看到，想要下載到你的電腦裡，請使用 **Clone** 指令；如果你已經下載過這個專案，只是想要把本地端的內容，更新成最新的線上版本內容，請使用 **Pull**（**Fetch**）指令。

**Clone 指令通常只會在第一次下載時使用，Clone 之後的更新，就是 Pull / Fetch 的事了。**
:::

## 使用 GitHub Flow 參與開源專案

Git 儲存庫並沒有**權限控管**的概念。當越多人參與同一個專案，每個人都有存取這個儲存庫的權限時，可能會遇到一些協作開發上的問題，例如：每個人都可以 Commit 到專案正式上線的分支（例如：`master`），在這種情況下，不同人彼此之間的程式碼會互相干擾。

GitHub 提供了 Fork 與 Pull Request 的機制，賦予儲存庫基本的權限控管。

### GitHub Flow

![GitHub Flow](https://i.imgur.com/gns2luN.png)

GitHub Flow 是一個基於分支（branch）的輕量化工作流程，藉由分支去管理功能的開發，以及來自社群的貢獻。

遵守 GitHub Flow 進行開發有一個規則：**`master` 分支必須保持隨時可以部屬正式環境（Production Ready）的狀態**。

### GitHub Flow 步驟

#### Fork

![fork repository](https://imgur.com/Oq2TpJc.png)

如果想要參與一個你沒有推送（Push）權限的專案，可以先複製（**Fork**）一份原始專案的**副本**到自己的 GitHub 帳號底下，你對這個副本有全部的權限，之後的任何修改都在這個副本中執行。

所有人都可以 Fork 專案，對 Fork 出來的副本 Push 更新內容，然後去發送 Pull Request，來把這些更新內容貢獻回原始專案裡。

> Fork 在這邊翻譯成「複製」，但並不是這個詞的原意。在技術圈來說，這個詞使用的情境是「原作者做得不夠好，其它人覺得可以做得更好，或是想加入一些個人喜好的功能，而修改出另外的版本」。

#### Branch

![Create Branch](https://i.imgur.com/YxNhiqA.png)

將副本 Clone 到本地端，第一件事情就是**建立新分支**，之後的修改和討論都會**以這個分支為基準**。

在專案建立一個分支，代表建立了一個環境來開發新功能，在分支上所做的修改，都不會影響到 `master` 分支，所以可以自由的嘗試並提交修改。

#### Commits

![Add Commits](https://i.imgur.com/ehEeTyx.png)

在對專案進行功能開發、修改之前，執行 `git remote -v` 查看 Git 的遠端設定：

```bash
$ git remote -v
origin  https://github.com/avery210412/chinese-copywriting-guidelines.git (fetch)
origin  https://github.com/avery210412/chinese-copywriting-guidelines.git (push)
```

建議對 Git 的遠端設定進行調整，**將 fetch 的遠端位置，改成原始專案的位置**：

```bash
$ git remote set-url origin https://github.com/sparanoid/chinese-copywriting-guidelines.git
$ git remote -v
origin  https://github.com/sparanoid/chinese-copywriting-guidelines.git (fetch)
origin  https://github.com/sparanoid/chinese-copywriting-guidelines.git (push)

$ git remote set-url --push origin https://github.com/avery210412/chinese-copywriting-guidelines.git
$ git remote -v
origin  https://github.com/sparanoid/chinese-copywriting-guidelines.git (fetch)
origin  https://github.com/avery210412/chinese-copywriting-guidelines.git (push)
```

這樣設定可以達到以下好處：

* 之後會從原始專案中取得最新的專案資料（fetch），保持專案的一致性
* 所有修改的資料只會推送到自己的專案中（push），不會影響到其他人

完成設定後，就可以對專案進行功能開發。

#### Pull Request

![Open Pull Request](https://i.imgur.com/1XdLZa1.png)

功能開發完成後，將本地端的 Repository 推送到遠端的 GitHub Repository。

此時可以在 GitHub 上建立一個 Pull Request，Pull Request 提供了一個方式法來通知專案維護者，是否考慮使用你所做得修改。

這邊特別說明一件事，每個專案可能都有自己的規範，通常會寫在專案的 `README.md` 或是 `CONTRIBUTING.md`，請務必在修改或提交前，閱讀專案的規範，維持開源專案的品質。

#### Discuss And Review

![Discuss And Review](https://i.imgur.com/ser2wXQ.png)

專案維護者在檢視你所貢獻的程式碼後，可以在該 PR 中進行討論，討論程式碼內容或關於修改的建議。過程中如果有需要修改的地方，可以直接在該分支中進行修改，因為 **PR 是看分支的**，所以該分支於 PR 確認合併前，都可以新增 Commit，並納入該 PR 中。

#### Deploy

![Test Deploy](https://imgur.com/ibRwwDN.png)

正式合併到 `master` 分支之前，在這個階段可以先部署到測試環境，以進行合併前的最終測試。如果測試沒有通過或產生任何可以討論的議題，則可以取消合併，在通過一系列的測試後，才可以發佈到正式環境（Production Ready）上。

#### Merge

![Merge](https://i.imgur.com/IYJgbY5.png)

當所貢獻的程式碼經過一連串的檢閱、測試後，具有**原始專案合併權限的人**，就可以將你的貢獻合併到原始專案中了，你也成為原始專案的貢獻者（Contributions）之一了。

#### GitHub Flow 參考資料

[如何使用 GitHub Flow 來參與開源專案](https://blog.poychang.net/guide-to-use-github-flow/)

[讓我們來了解 GitHub Flow 吧！](https://reurl.cc/1ZYDqp)

## 總結

![Git Cheat Sheet](https://i.imgur.com/zzNkt5X.jpg)

### Git 版本控制參考資料

[為你自己學 Git](https://gitbook.tw/)

[CS Visualized: Useful Git Commands](https://dev.to/lydiahallie/cs-visualized-useful-git-commands-37p1)
