---
# System prepended metadata

title: Git 基礎篇
tags: [Git]

---

# Git - 基礎篇

## 前言

在軟體開發領域，對原始碼進行**版本控管**是非常重要的一件事。Git 是一套分散式版本控管系統（Distributed Version Control System），支援本地操作、遠端協作、彈性的分支與合併、原始碼審核（Code Review）等功能，對專案原始碼的品質管理很有幫助。

## What is Git？

一個檔案可能會經過多次的修改，如果將每一次的修改都保存下來，這個行為就是「版本控制」。一般使用者在進行版本控制的時候，大多都是以「資料夾」為單位，如下圖所示：

![version control by folder](https://imgur.com/CWDES3g.png)

現在有一個名為 `resume` 的目錄，目錄最初存放了 3 個檔案。在此之後也許會對 `resume` 目錄中的檔案有些操作，不論是新增、刪除檔案，或是修改檔案內容，只要「`resume` 目錄的**狀態有變化**」，就稱之為一個「**版本**」。

![version control](https://imgur.com/dr2C7BQ.png)

以日期為目錄名稱進行版本控制，雖然可以知道在什麼時間點修改過檔案，但是卻不能在第一時間反應，在那個時間點對檔案做了什麼變動，也無法比較兩個時間點間的檔案差異。

Git 就是一種版本控制系統，清楚地記錄每個檔案在什麼時候被新增、被修改或被刪除，透過 Commit Message 可以看到以前的歷史紀錄、和上一個版本差異；在多人協作方面，Git 擁有快速的分支與合併機制，強力支援分散式開發模式，有效率的處理大型專案。

## Git 基本指令

### 初始化目錄

```bash
$ cd /desktop              # 切換至 /desktop 目錄
$ mkdir git-practice       # 建立 git-practice 目錄
$ cd git-practice          # 切換至 git-practice 目錄
$ git init                 # 初始化目錄，讓 Git 對這個目錄開始進行版控
Initialized empty Git repository in /desktop/git-practice/.git/
```

`git init` 會在目錄建立一個 `.git` 隱藏目錄，整個版控的精華就在這裡面。

### 查詢目錄狀態

```bash
$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)
```

在這個目錄裡，除了 Git 初始化目錄後產生的 `.git` 隱藏目錄外，其他什麼都沒有，所以上面這段訊息的意思是「現在沒有東西可以提交（nothing to commit）」。

:::danger
**注意**

**空的目錄**是無法被 Commit 的，因為 Git 在計算、產生物件的時候，是根據「**檔案變化的內容**」去做計算的，**內容不可以為空**。
:::

### 加入版控追蹤

在目錄裡新增一個 `index.html` 檔案，使用 `git status` 指令查詢目錄狀態：

```bash
$ touch index.html
$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        index.html

nothing added to commit but untracked files present (use "git add" to track)
```

`index.html` 檔案目前的狀態是 **Untracked files**，意思是這個檔案**尚未被加到 Git 版控系統**裡，還沒開始正式被 Git「追蹤」，它只是剛剛加入這個目錄而已。

使用 `git add index.html` 指令，將 `index.html` 檔案交給 Git，讓 Git 開始追蹤它。再次使用 `git status` 指令查詢目錄狀態：

```bash
$ git add index.html
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   index.html
```

`index.html` 檔案的狀態從 **Untracked files** 變成 **new file** 狀態了，表示這個檔案已經被安置到**暫存區**（**Staging Area**），等待稍後跟其它檔案一起被存到**儲存庫**（**Repository**）裡。

:::success
**狀況題**

如果在 **`git add`** 之後，又修改了那個檔案的內容？

在 **`index.html`** 檔案被安置到**暫存區**後，又對這個檔案進行了修改，使用 **`git status`** 指令查詢目錄狀態：

  ```bash
  $ git status
  On branch master

  No commits yet

  Changes to be committed:
    (use "git rm --cached <file>..." to unstage)

          new file:   index.html

  Changes not staged for commit:
    (use "git add <file>..." to update what will be committed)
    (use "git restore <file>..." to discard changes in working directory)

          modified:   index.html
  ```

  對 Git 來說，**`index.html`** 檔案修改後的內容並沒有被加到暫存區裡，所以這時候在暫存區裡的 **`index.html`** 檔案是維持在**修改前**的狀態。如果確定檔案修改完畢，再次使用 **`git add index.html`** 指令，將 **`index.html`** 檔案安置到暫存區中。
:::
  
### 把暫存區的內容提交到儲存庫裡存檔

要讓暫存區的內容永久地保存下來，需要使用 `git commit` 指令：

```bash
$ git commit -m '<create> index.html'
[master (root-commit) 7f7ec9c] <create> index.html
 1 file changed, 9 insertions(+)
 create mode 100644 index.html
```

對 Git 來說，`git commit` 的意思是「**把暫存區（Staging Area）的東西存放到儲存庫（Repository）裡**」，對開發者而言，就是「我完成一個存檔（備份）的動作了」，這樣就建立了一個新的版本。

在 `git commit` 後面加上 `-m '<create> index.html'` 的用意是說明「在這次的 Commit 做了什麼事」。在 Commit 的時候，如果沒有輸入這個訊息，Git 預設是不會讓你完成此次的 Commit。Commit Message 最主要的目的就是告訴自己以及其它人「這次的修改做了什麼」。

![Git all state](https://imgur.com/Nb1sc7J.png)

:::danger
**注意**

要完成 **`git commit`** 指令，才是**完整建立了一個版本**的流程。每一次的 Commit 都只會處理暫存區裡的內容。
:::

### 檢視提交的歷史記錄

```bash
$ git log
commit 7f7ec9cf6b7d1bdbc3972c8882f1278b9414d35e (HEAD -> master)
Author: AveryChen <avery210412@gmail.com>
Date:   Thu Nov 2 15:03:16 2023 +0800

    <create> index.html
```

最新提交的 Commit 會先被列出來，歷史紀錄會列出每筆 Commit 的 SHA-1 校驗碼、作者名字與電子郵件、寫入日期以及 Commit Message 等資訊。

### Git 檔案追蹤機制

![Git file tracking](https://imgur.com/eY3vFN8.png)

* Untracked file：尚未被追蹤的檔案
* Changes to be committed：已加入暫存區，準備變成一個 Commit 的檔案
* Changes not staged for commit：尚未加入到暫存區，但有被加入到追蹤的檔案

### 刪除檔案 / 變更檔名

#### 刪除檔案

在 Git 裡，不管是刪除檔案或是變更檔名，對 Git 來說都是一種「修改」。

```bash
$ git rm index.html
$ git status
On branch master

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

        deleted:   index.html
```

`index.html` 檔案目前的狀態是 **deleted**，而且已經被安置到暫存區中，可以直接進行 Commit。

如果只是想把檔案從 `.git` 目錄中移除，**不再進行 Git 版本控制**，可以在 `git rm <file>` 指令後面加上 `-–cached` 參數：

```bash
$ git rm index.html --cached
rm 'index.html'
```

這個訊息並不是真的把 `index.html` 檔案刪除了，只是把檔案從 Git 裡移除而已。使用 `git status` 指令查詢目錄狀態：

```bash
$ git status
On branch master

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

        deleted:    index.html

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        index.html
```

`index.html` 檔案的狀態從原本已經在暫存區裡的 **Staged** 變成 **Untracked**，這個檔案就沒有在 Git 版本控制的範圍內了。

#### 變更檔名

```bash
$ git mv index.html home.html
$ git status
On branch master

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

        renamed:   index.html -> home.html
```

`index.html` 檔案目前的狀態是 **renamed**，而且已經被安置到暫存區中，可以直接進行 Commit。

### 取消刪除 / 修改檔案

在 Git 裡面如果不小心把檔案或目錄刪掉是救得回來的，而當你修改某個檔案但後悔了，也可以把檔案回復成它上一次 Commit 的狀態。

使用 `git checkout <file>` 指令，可以把檔案從 `.git` 目錄裡拉一份到目前的工作目錄（Working Directory），也就是說這個指令**會把暫存區裡的內容，拿來覆蓋工作目錄的內容**，它會把檔案回復到上一次 Commit 的狀態。

* 刪除專案裡所有 HTML 檔案

```bash
$ rm *.html
$ git status
On branch master

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

        deleted:   about.html
        deleted:   home.html
        deleted:   index.html
        deleted:   product.html

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

* 把被刪除的 `home.html` 檔案救回來：

```bash
$ git checkout home.html
Updated 1 path from the index
```

* 把所有被刪掉的檔案救回來：

```bash
$ git checkout .
Updated 3 paths from the index
```

### 修改 Commit 紀錄

原本的歷史紀錄是這個樣子：

```bash
$ git log --oneline
e624291 (HEAD -> master) <update>
7f7ec9c <create> index.html
```

需要修改**最近一期**的 Commit Message，在 `git commit` 指令後面加上 `--amend` 參數：

```bash
$ git commit --amend -m '<update> rename index.html to home.html'
[master 51032f3] <update> rename index.html to home.html
  Date: Thu Nov 2 15:34:16 2023 +0800
  1 file changed, 0 insertions(+), 0 deletions(-)
  rename index.html => home.html (100%)
```

再次使用 `git log` 指令查詢歷史紀錄：

```bash
$ git log --oneline
51032f3 (HEAD -> master) <update> rename index.html to home.html
7f7ec9c <create> index.html
```

雖然只是修改 Commit Message，其它檔案內容都沒有修改，但對 Git 來說，因為 **Commit Message 的內容**改變了，所以 Git 會重新計算並產生一個全新的 Commit。

:::danger
**注意**

盡量不要在已經 Push 出去之後再修改 Commit Message，否則可能會造成其它開發者的困擾。
:::

如果檔案臨時做了一些小修改，不想再提交一個新的 Commit；或者是需要追加檔案到**最近一期**的 Commit，例如新增一個 `style.css` 檔案到目錄中：

```bash
$ touch style.css
$ git status
On branch master

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        
        style.css

nothing added to commit but untracked files present (use "git add" to track)
```

可以使用 `--amend` 參數進行 Commit，將 `style.css` 檔案併入最後一次的 Commit：

```bash
$ git add style.css
$ git commit --amend -m '<update / create> index.html / style.css'
[master c40dda8] <update / create> index.html / style.css
  Date: Thu Nov 2 15:42:46 2023 +0800
  2 files changed, 0 insertions(+), 0 deletions(-)
  rename index.html => home.html (100%)
  create mode 100644 style.css
```

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

```bash
$ git log --oneline
c40dda8 (HEAD -> master) <update / create> index.html / style.css
7f7ec9c <create> index.html
```

### Git 版本還原

當送出 Commit 之後卻後悔了，有可能是發現了修改的檔案裡有 bugs，因此想要取消這次的 Commit，可以使用 `git reset` 指令。

Reset 這個英文單字的翻譯是「重新設定」，但事實上 `git reset` 指令用中文來說比較像是「前往」，也就是「go to」的概念。因為實際上 `git reset` 並不是真的刪除或是重新設定 Commit，只是「前往」到指定的 Commit，那些看起來好像不見的東西只是暫時看不到，但隨時都可以再撿回來。

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

```bash
$ git log --oneline
9dcfb12 (HEAD -> master) <create> database.yml in config folder
babb78d <create> product.html
d2ba818 <create> index.html
9d5b7d1 <create> about.html
c49e0a8 <crete> gitignore
c40dda8 <update / create> index.html / style.css
7f7ec9c <create> index.html
```

如果想要拆掉**最後一次**的 Commit，也就是回到前一次 Commit，可以使用「**相對**」或「**絕對**」的做法。

```bash
# 使用相對作法
$ git reset 9dcfb12^
$ git reset master^
$ git reset HEAD^

# 使用絕對做法
$ git reset babb78d
```

### Reset 的模式

| 模式 | mixed | soft | hard |
|-----|-----|-----|-----|
| 工作目錄（Untracked） | 不變 | 不變 | 不變 |
| 工作目錄（Modified） | 不變 | 不變 | 捨棄 |
| 暫存區 | 丟回工作目錄 | 不變 | 捨棄|
| 拆分 Commit，從儲存庫被拆出的檔案 | 丟回工作目錄 | 丟回暫存區 | 捨棄 |

#### Soft reset

![soft reset](https://imgur.com/sFl6rsZ.gif)

#### Hard reset

![hard reset](https://imgur.com/rOFASNC.gif)

### 忽略不要版本控制的檔案

一些比較機密的檔案，例如：資料庫的存取密碼、AWS 伺服器的存取金鑰；或是一些程式碼編譯的中間檔、暫存檔，不想放在 Git 裡面一起備份，只要在專案目錄裡放一個 `.gitignore` 檔案，並且設定想要忽略的規則就行了。

```bash
# .gitignore.text

# 忽略 secret.yml 檔案
secret.yml   

# 忽略 config 目錄下的 database.yml 檔案
config/database.yml

# 忽略所有 db 目錄下附檔名是 .sqlite3 的檔案
/db/*.sqlite3

# 忽略所有附檔名是 .tmp 的檔案
*.tmp
```

:::danger
**注意**

**`.gitignore`** 檔案設定的規則，只針對在規則設定**之後**有成效，對於在 **`.gitignore`** 檔案設定規則以前，就已經存在的檔案就像既得利益者一樣，這些規則是對他們沒有效果的。
:::

### 拿到一個新專案的步驟

1. 加入版本控制 `git init`
2. 建立 `.gitignore` 排除需要忽略的檔案，`.gitignore` 檔案也需要加入版控（其他協作者才知道）
3. 使用 `git add .` 將檔案加入版控，因為是**新建立的檔案**，不能直接使用 `git commit -am`（新增的檔案還不在暫存區）
4. 如果是**已經加入版控**的檔案，可以直接使用 `git commit -am` 合併指令
5. 建立 `develop`、`feature` 分支，進行開發作業
