---
# System prepended metadata

title: GitHub 版本控制

---

# GitHub 版本控制

## 建立本地儲存庫(Local repository)
進入到工作目錄時，可以使用
```bash=
git init
```
![](https://i.imgur.com/MS50saR.png)

便會得到上方圖片的樣貌，`master`會是目前**工作目錄**的各種狀態。

當你打 git status 時，會說您是在哪一個分支一樣。
```bash=
git status
```

## 在本機建立一個共用的儲存庫(Shared repository)

共用儲存庫 (shared repository) 是指建立一個 Git 儲存庫但不包含工作目錄。較常使用在Linux環境下，多人共用同一台主機。

如果要建立共用儲存庫
```bash=
git init --bare
```

這是一個“沒有工作目錄的純儲存庫”，別名為"裸儲存庫" (bare repository)

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

Git 屬於「分散式版本控管」，每個人都有一份完整的儲存庫(Repository)。也就是說，當你想要建立一個「工作目錄」時，必須先取得這個「裸儲存庫」的內容回來，這時你必須使用 git clone [REPO_URI] 指令「複製」(clone)一份回來才行，而透過 git clone 的過程，不但會自動建立工作目錄，還會直接把這個「裸儲存庫」完整的複製回來。這個複製的過程，就如同「完整備份」一樣，是把所有 Git 儲存庫中的所有版本紀錄、所有版本的檔案、...等等

* 在一台多人使用的機器上進行協同開發，可開放大部分人對這個「裸儲存庫的資料夾」僅有唯讀權限，只讓一個人或少許人才有寫入權限。

* 有些人會把裸儲存庫放到 Dropbox 跟自己的多台電腦同步這個裸儲存庫


## 在 GitHub 或其他 Git 平台建立遠端的儲存庫 (remote repository)

"共用儲存庫"與"遠端儲存庫"是相同的概念，"共用儲存庫"是使用檔案直接做儲存；"遠端儲存庫"是用ssh, Git protocol, HTTP等協定遠端儲存Git儲存庫

先在github設定好repo

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


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

並可以

```bash=
git clone [repo_url]
# exp: git clone https://github.com/howard0615/git_demo.git
```

## git 指令

### 新增檔案

`[master +10 ~0 -0 !]`
* +10 代表有 10 個「新增」的檔案
* ~0 代表有 0 個「修改」的檔案
* -0 代表有 0 個「刪除」的檔案

如圖： master ?3 代表有3個新增
```bash=
# 代表將這dir中的檔案全部加入Git 版本控管中
git add .
```

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

輸入
```bash=
# 重設工作目錄的索引狀態
git reset
```
![](https://i.imgur.com/KbYMsxJ.png)


這時逐一加入檔案進入
```bash=
git add [file/dir]
# exp: git add 1.txt
```
![](https://i.imgur.com/JfFtUWh.png)

### 提交變更/建立版本

在 Git 版本控管中，所有的版本都必須擁有「版本紀錄的說明文字」 ( 簡稱 Log )，不像 Subversion 預設可以簽入「沒有版本紀錄說明」的版本。所以當你直接輸入 git commit 的話，預設會開啟 Notepad (記事本) 讓你輸入這個版本的訊息。開啟後的檔案會有很多 # 符號開頭的文字，這些都是註解，不會成為 Log 的一部分。

將前方加入的檔案或資料夾進行 **說明**
![](https://i.imgur.com/R3UT3NO.png)


### 查詢歷史紀錄

想要查詢版本的歷史紀錄
```bash=
git log
```
![](https://i.imgur.com/9TI9wDL.png)

當想要限制輸出版本的數量時，在後方加入 -數量，就可以限定輸出最近幾筆的紀錄

```bash=
git log -10
```

### 刪除檔案

在 Git 指令列工具中也有個 rm 指令，可以用來刪除檔案。例如我們想刪除 3.txt 這個檔案，可以輸入以下指令：

```bash=
git rm '3.txt'
```

這時會同時做兩件事：
1. 刪除工作目錄快取的 '3.txt' (刪除在版本控管中的3.txt)
2. 刪除工作目錄下真實 '3.txt' 



### 檔案更名
在 Git 指令列工具中也有個 mv 指令，可以用來變更檔案或目錄的名稱。例如我們想把 test 目錄更名為 unit-test 名稱，可以輸入以下指令：

```bash=
git mv 2.txt rename2.txt
```

### 顯示工作目錄的索引狀態
可以使用 `git status`顯示工作目錄的狀態，也可以使用 `git status -s`顯示精簡版的

### 重置目前的工作狀態

我們曾經學過如何利用 git reset 重置目前工作目錄的索引狀態，但請注意，這個指令預設只會重置「索引狀態」，那些你用 git rm 刪除的目錄或檔案，還是用 git mv 更名的目錄或檔案，透過 git reset 都無法把「實體檔案」給救回來。
```bash=
git reset --hard
```

### 還原其中一個被改壞的檔案
如果檔案編輯到一半，發現被改壞了，你希望能救回改沒修改前的版本，這時你可以利用以下指令還原檔案：
```bash=
git checkout master 3.txt
```

---

## 了解 Git 的資料結構

在 Git 裡有兩個重要的資料結構，分別是「物件」與「索引」。

「物件」用來保存版本庫中所有檔案與版本紀錄，「索引」則是用來保存當下要進版本庫之前的目錄狀態。

### 關於物件
所謂的「物件」是一個「特別的檔案」，該檔案的產生過程很有趣，是將一個檔案的內容中取出，透過內容產生一組 SHA1 雜湊值，然後依照這個 SHA1 雜湊值命名的一個檔案。

在使用 Git 進行版本控管的過程中，所有要進行控管的目錄與檔案，都會先區分「目錄資訊」與「檔案內容」，我們稱為 tree 物件與 blob 物件。

其中 blob 物件就是把原本的「檔案內容」當成 blob 檔案的內容 (注意: blob 物件其實就是一個實體檔案)，然後再將其內容進行 SHA1 雜湊運算後產生的一個 hash id，再把這個 hash id 當成 blob 檔案的檔名。由此可知，blob 物件是一個「只有內容」的檔案，其檔名又是由內容產生的，所以，任何一的單獨存在的 blob 檔案通常對版本控管沒有任何幫助。

另一個 tree 物件，則是用來儲存特定資料夾下包含哪些檔案，以及該檔案對應的 blob 物件的檔名為何。在 tree 物件中，除了可以包含 blob 物件的檔名與相關資訊，還可以包含其他的 tree 物件。所以 tree 物件其實就是「資料夾」的代名詞。

無論 blob 物件與 tree 物件，這些都算是物件，這些物件都會儲存在一個所謂的「物件儲存區」 (object storage) 之中，而這個「物件儲存區」預設就在「儲存庫」的 objects 目錄下。


### 關於索引
所謂的「索引」是一個經常異動的暫存檔，這個檔案通常位於 .git 目錄下的一位名為 index 的檔案。簡單來說，「索引」的目的主要用來紀錄「有哪些檔案即將要被提交到下一個 commit 版本中」。換句話說，如果你想要提交一個版本到 Git 儲存庫，那麼你一定要先更新索引狀態，變更才會被提交出去。

這個索引檔，通常保存著 Git 儲存庫中特定版本的狀態，這個狀態可以由任意一個 commit 物件，以及 tree 物件所表示。

我們通常不會直接去編輯 .git\index 這個二進位檔，而是透過標準的 git 指令去操作這個索引檔，對於索引檔的操作指令大概有以下幾個：

* git add
* git mv
* git rm
* git status
* git commit
* git ls-files

Git 的「索引」是一個介於「物件儲存區」 (object storage) 與「工作目錄」 (working directory) 之間的媒介。

https://ithelp.ithome.com.tw/articles/10134089

### 資料結構名稱

簡單來說，「索引」的目的主要用來紀錄「有哪些檔案即將要被提交到下一個 commit 版本中」。

換句話說，「如果你想要提交一個版本到 Git 儲存庫，那麼你一定要先更新索引狀態，變更才會被提交出去。」


* Index (索引)
* Cache (快取)
* Directory cache (目錄快取)
* Current directory cache (當前目錄快取)
* Staging area (等待被 commit 的地方)
* Staged files (等待被 commit 的檔案)
* 

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


首先，先介紹四種檔案狀態：

* untracked (未追蹤的，代表尚未被加入 Git 儲存庫的檔案狀態)
* unmodified (未修改的，代表檔案第一次被加入，或是檔案內容與 HEAD 內容一致的狀態)
* modified (已修改的，代表檔案已經被編輯過，或是檔案內容與 HEAD 內容不一致的狀態)
* staged (等待被 commit 的，代表下次執行 git commit 會將這些檔案全部送入版本庫)


### git status

取得working tree下的狀態

以下透過一個例子可較好理解 **目前最新版** 與 **索引黨** 之間的差異

```bash=
git status
```

```bash output=
On branch master


Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: c.txt
# 目遣最新板 並沒有c.txt
# 索引檔 已加入這個c.txt
# 執行 git commit   c.txt 會被存入下一個版本

Changes not staged for commit:
# (a.txt 已經被變更，但尚未標示可提交 not staged)
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: a.txt
# 目前最新板 有 a.txt這個檔案
# 索引檔 沒有 a.txt這個檔案
# 執行 git commit   a.txt 的變更不會被存入下一個版本

Untracked files:
# (這區有 b.txt檔案，但表b.txt 尚未被追蹤 untracked)
(use "git add <file>..." to include in what will be committed)
b.txt
# 目前最新板 沒有 a.txt這個檔案
# 索引檔 沒有 a.txt這個檔案
```


### git add
git add 指令，是為了將目前「工作目錄」的變更寫入到「索引檔」裡。

使用 git add -u 則可以僅將「更新」或「刪除」的檔案變更寫入到「索引檔」中。

### git rm
我們以 git rm 為例，當你直接在檔案系統中刪除一個檔案，這只是從「工作目錄」中刪除而已，並沒有更新到索引檔，你可以利用 git status 看到這層改變，不過若要真正把「刪除」的狀態寫進索引檔的話，則要靠 git rm filename 更新索引檔。

在執行 git rm filename 的時候，除了更新索引檔之外，連工作目錄下的檔案也會一併被刪除。若你只想刪除索引檔中的該檔，又要保留工作目錄下的實體檔案，那麼你可以在指令列加上 --cached 參數，就能做到，例如：

git rm --cached a.txt

### git mv
使用 git mv oldname newname 可以將檔案更名，執行此命令會同時更新索引與變更工作目錄下的實體檔案。

### git commit
這個指令，則是把「索引檔」與「目前最新版」中的資料比對出差異，然後把差異部分提交變更成一個 commit 物件。

### git ls-files
在索引檔之中，預設就包含了 目前最新版 的所有檔案，外加你在工作目錄中新增檔案且透過 git add 更新索引檔後的那些檔案。透過 git ls-files 命令，可以列出所有目前已經儲存在「索引檔」中的那些檔案路徑。

https://ithelp.ithome.com.tw/articles/10134531

## 分支 Branch
詳細內容可觀看 [連結](https://ithelp.ithome.com.tw/articles/10135016)

```bash=
# 觀看現在有的branch
git branch

# 刪除所選的分支
git branch -d [BRANCH]

# 新增新的分支
git branch [NEW_BRANCH]

# 轉換至[BRANCH_NAME]
git checkout [BRANCH_NAME]

# 若[BRANCH_NAME]不存在現有的branch中，會新增出新的
git checkout -b [BRANCH_NAME]

```

可再使用 `git status` 檢查目前所在分支
```bash    
位於分支 newbranch2
沒有要提交的檔案，工作區為乾淨狀態
```

## git diff 基本觀念

diff 應用部分可觀看 [連結](https://ithelp.ithome.com.tw/articles/10135441)


查看版本間的差異，其中commit1用較舊的版本，commit2用較新的版本
```bash=
git diff [commit1] [commit2]
```

#### 四種比較方法

1. `git diff`

    在什麼參數都不加的使用情況，比對的是「工作目錄」與「索引」之間的差異。這是個很常用的指令，因為當你執行 git add . 指令之前，先透過 git diff 查看你自己到底改了哪些東西。

    註：事實上，在使用 Git 版本控管的過程中，在執行 git commit 之前，的確有可能會執行 git add 指令好幾次，用以確認到底哪些檔案要加入到索引之中，最後才會 commit 進版本。
2. `git diff commit`

    如果你只在 git diff 之後加上一個 commit id，比對的是「工作目錄」與「指定 commit 物件裡的那個 tree 物件」。

    最常用的指令是 git diff HEAD，因為這代表你要拿「工作目錄」與「當前分支的最新版」進行比對。這種比對方法，不會去比對「索引」的狀態，所以各位必須區分清楚，你到底比對的是甚麼 tree 物件的來源。
3. `git diff --cached commit`

    在執行 git commit 之前，索引狀態應該已經都準備好了。所以如果你要比對「當前的索引狀態」與「指定 commit 物件裡的那個 tree 物件」，就可以用這個指令完成比對任務。

    最常用的指令一樣是 git diff --cached HEAD，這個語法代表的是「當前的索引狀態」與「當前分支的最新版」進行比對。這種比對方法，不會去比對「工作目錄」的檔案內容，而是直接去比對「索引」與「目前最新版」之間的差異，這有助於你在執行 git commit 之前找出那些變更的內容，也就是你將會有哪些變更被建立版本的意思。

    註1: git diff --cached 與 git diff --staged 是完全一樣的結果，--staged 只是 --cached 的別名，讓你比較好記而已!

    註2: git diff --cached 與 git diff --cached HEAD 執行時也是完全一樣的結果，最後的 HEAD 可以省略。
4. `git diff commit1 commit2`

    最後一種則是透過兩個不同的版本 ( commit id ) 來比對其差異，這個命令可以跳過「索引」與「工作目錄」的任何變更，而是直接比對特定兩個版本。事實上 Git 是比對特定兩個版本 commit 物件內的那個 tree 物件。

    最常用的指令則是 git diff HEAD^ HEAD 命令，這代表你要比較【最新版的前一版】與【最新版】之間的差異。這裡的 HEAD 與 ^ 的意義，我們會在日後的文章中說明。
    

## Commit 物件名稱

從 `git log` 中觀察到
```bash output=
commit e45a23d05b04a761ed5269d8fa905b469f4b7c18 (HEAD -> newbranch1)
Author: ****** <*******@gmail.com>
Date:   Mon Mar 13 13:13:18 2023 +0800

    Add b.txt in newbranch1

commit 95f8e063d748fdbc498971286637059479151fa8
Author: ****** <*******@gmail.com>
Date:   Mon Mar 13 12:54:42 2023 +0800

    a.txt: set 1 as content

commit b07eaa2ef24c39a818ff57be82180398f1afb7f1
Author: ****** <*******@gmail.com>
Date:   Mon Mar 13 12:53:56 2023 +0800

    Initial commit
```

可以利用 `git cat-file -p commitid` 取得，如：

```
tree 3a1fdd64de8fc83e5ebd9cfd037b995bb594afa2
parent 95f8e063d748fdbc498971286637059479151fa8
author ****** <********@gmail.com> 1678684398 +0800
committer ****** <********@gmail.com> 1678684398 +0800

Add b.txt in newbranch1
```

### 物件絕對名稱的簡短語法

在Git標示絕對名稱時，可以用前面幾碼代替，最少不可低於4個字元。也就是說 4 ~ 40 個字元長度的「絕對名稱」都是可以用的。

若覺的 `git log` 的輸出太過於攏長，可使用 `git log --pretty=oneline`。

若僅想要輸出部分的絕對名稱，可以使用

```bash=
git log --pretty=oneline --abbrev-commit
```


### 關於.git/refs/目錄

從上述範例其實已經能看出，所有的「參照名稱」都是個檔案，而且一律放在 git/refs/ 目錄下。而 Git 的參照名稱所放置的目錄位置，主要有三個：

* 本地分支：.git/refs/heads/
* 遠端分支：.git/refs/remotes/
* 標 籤：.git/refs/tags/


### 認識物件的符號參照名稱 (symref)
符號參照名稱 (symref) 其實也是參照名稱 (ref) 的一種，只是內容不同而已。我們從下圖應可看出其內容的差異，「符號參照」會指向另一個「參照名稱」，並且內容以 ref: 開頭：

在 Git 工具中，預設會維護一些特別的符號參照，方便我們快速取得常用的 commit 物件，且這些物件預設都會儲存在 .git/ 目錄下。這些符號參考有以下四個：

**HEAD**
* 永遠會指向「工作目錄」中所設定的「分支」當中的「最新版」。
* 所以當你在這個分支執行 git commit 後，這個 HEAD 符號參照也會更新成該分支最新版的那個 commit 物件。 

**ORIG_HEAD**
* 簡單來說就是 HEAD 這個 commit 物件的「前一版」，經常用來復原上一次的版本變更。

**FETCH_HEAD**
* 使用遠端儲存庫時，可能會使用 git fetch 指令取回所有遠端儲存庫的物件。這個 FETCH_HEAD 符號參考則會記錄遠端儲存庫中每個分支的 HEAD (最新版) 的「絕對名稱」。

**MERGE_HEAD**
* 當你執行合併工作時 (關於合併的議題會在日後的文章中會提到)，「合併來源｣的 commit 物件絕對名稱會被記錄在 MERGE_HEAD 這個符號參照中。

這裡比較複雜，建議觀看 [連結](https://ithelp.ithome.com.tw/articles/10136575)

## 暫存版本

Git裡有個 `git stash` 指令，可以自動將改寫到一半的檔案建立一個 特殊的版本，更這版本為stash版本，暫存版

```bash
git stash -u

git cat-file -p stash
tree 5bd5c169e4ac245d128d71460648c61fca825b6c
parent ee25b185738cd2e31b8814b3de391b55680f9707
parent ee7630115433c0f16de11bfe2a29ebff57b3af12
parent 839897cc171a44a72c6128ab4e2cd72963859b58
```

從上述執行結果你應該可以從「訊息紀錄」的地方清楚看出這三個版本分別代表那些內容：

1. 原本工作目錄的 HEAD 版本
2. 原本工作目錄所包含的索引內容
3. 原本工作目錄裡所有未追蹤的內容

也就是說，他把「原本工作目錄的 HEAD 版本」先建立兩個暫時的分支，這兩個分支分別就是「原本工作目錄所包含的索引內容」與「原本工作目錄裡所有未追蹤的內容」之用，並在個別分支建立了一個版本以產生 commit 物件並且給予預設的 log 內容。最後把這三個分支，合併到一個「參照名稱」為 stash 的版本 （這也是個 commit 物件)。還不僅如此，他還把整個「工作目錄」強迫重置為 HEAD 版本，把這些變更與新增的檔案都給還原，多的檔案也會被移除。

### 取回暫存版本

```bash=
git stash pop
```

執行完畢後，所有當初的工作目錄狀態與索引狀態都會被還原。事實上 Git 骨子裡是透過「合併」的功能把這個名為 stash 的版本給合併回目前分支而已。最後，它還會自動將這個 stash 分支給刪除，所以稱它為【暫存版】非常貼切！

### 建立多重暫存版

Git 的 stash 暫存版可以不只一份，你也可以建立多份暫存檔，以供後續使用。不過，在正常的開發情境下，通常不會有太多暫存版才對，會有這種情況發生，主要有兩種可能：

1. 你的開發習慣太差，導致累積一堆可能用不到的暫存版。
2. 你老闆或客戶「插單」的問題十分嚴重，經常改到一版就被迫插單。(這就是身為 IT 人的 BI 啊~~~XD) (BI = Business Intelligence 或另一層意思... Well, you know


```bash=
# 可以自動暫存版的註解
git stash save -u <message>
```

若有好幾個版本的 stash
```bash=
git stash list
stash@{0} : .....
stash@{1} : .....

# 若直接執行
git stash pop
# 會先取回前一個暫存板，上面的例子會先取回stash@{1}

# 若想取回特定一個暫存板
git stash apply "stash@{1}"

# 若確確定合併時，想刪除 stash@{1}的話，可透過
git stash drop "stash@{1}"

# 若想清除全部的暫存板
git stash clear

```

## Git 合併

這部分可看 [連結](https://ithelp.ithome.com.tw/articles/10138437) 有更詳細說明

當你在 Git 工作目錄下建立分支時，可以讓你的系統依據不同的需求分別進行開發，又不互相影響。例如你原本穩定的系統可以放在 master 分支中進行開發，而當要修正錯誤時則額外建立一個 bugfix 分支來改正軟體錯誤，等 Bugs 修正後，在透過「合併」的方式將 bugfix 分支上的變更重新套用到 master 上面，這就是一種主要的使用情境。

一般來說，大家都是以一個主要或預設分支進行開發(master)，然後再依據需求建立分支(bugfix)，最後則是將兩個分支合併成一個。事實上，執行「合併」動作時，是將另一個分支合併回目前分支，然後再手動將另一個分支給移除，這樣才符合「兩個分支合併成一個」的概念。

實務上，也經常有機會將三個、四個或更多的分支合併到其中一個分支。例如你除了主要分支(master)外，還額外建立了除錯用的分支(bugfix)與新增功能(feature)的分支，當開發到一定程度後，你可以決定要不要將這個兩個分支一起合併回主要分支(master)。

在 Git 使用合併時，有一個重要的觀念是【合併的動作必須發生在同一個儲存庫中】。請回想一下，在任何一個 Git 儲存庫中，都必須存在一個 Initial Commit 物件(初始版本)，而所有其他版本都會跟這個版本有關係，這個關係我們稱為「在分支線上的可追蹤物件」(the tracked object on the branch heads)，所以你不能將一個儲存庫的特定分支合併到另一個毫不相干的儲存庫的某個分支裡。

合併的時候，如果兩個分支當中有修改到相同的檔案，但只要修改的行數不一樣，Git 就會自動幫你套用/合併這兩個變更。但如果就這麼剛好，你在兩個分支裡面改到「同一個檔案」的「同一行」，那麼在合併的時候就會引發衝突事件。當合併衝突發生時，Git 並不會幫你決定任何事情，而是將「解決衝突」的工作交給「你」來負責，且這些發生衝突的檔案也都會被標示為 unmerged 狀態，合併衝突後你可以用 git status 指令看到這些狀態。

## 修正commit歷史紀錄

到目前為止，我還沒提到關於「遠端儲存庫」的細節，所以大部分的 Git 操作都還專注在本地端，也就是在工作目錄下的版本管控，這個儲存庫就位於你的 .git/ 目錄下。然而，之後我們即將提到「遠端儲存庫」的應用，到時就不只一個人擁有儲存庫，所需要注意的細節也就更多。

完全開放每個人都能夠任意的修正 commit 歷史紀錄，這個概念對於熟悉 Subversion 或 TFVC 的人來說或許聽起來非常很奇怪，因為以往大家都集中連接到版本控管的伺服器上，用的是集中式的儲存庫，如果有人可以任意串改歷史紀錄，那版控還叫做版控嗎？

所以，到底甚麼樣的使用情境會需要去修改版本紀錄呢？以下幾點各位可以參考看看。

假設我們現在有 [AA] -> [BB] -> [CC] 三個版本：

* 可能 [CC] 版本你發現 commit 錯了，必須刪除這一版本所有變更
* 你可能 commit 了之後才發現 [CC] 這個版本其實只有測試程式碼，你也想刪除他
* 其中有些版本的紀錄訊息有錯字，你想修改訊息文字，但不影響檔案的變更歷程
* 你可以想把這些版本的的 commit 順序調整為 [AA] -> [CC] -> [BB]，讓版本演進更有邏輯性
* 你發現 [BB] 這個版本忘了加入一個重要的檔案就 commit 了，你想事後補救這次變更
* 在你打算?分享」分支出去時，發現了程式碼有瑕疵，你可以修改完後再分享出去

```bash=
# 將最後的版本刪除
git reset --hard "HEAD^"
# 還原
git reset --hard ORIG_HEAD
# 刪除最近一次的版本，但保留最後一次變更
git reset --soft "HEAD^"

# 重新提交最後一個版本（HEAD版本）
git reset --amend
# 他會自動將已在索引中的檔案自動加入，並可更改最後一個commit內容
```

## 設定GitHub的忽略清單

可透過建立新的repo的時候設定 .gitignore

## 還原 使用git revert

觀看 [連結](https://ithelp.ithome.com.tw/articles/10139129)