學習 Git (7) - 分支


Posted by Calon on 2022-05-19

在專案規模比較小時,可以一路往前提交 commit 到底也沒關係,但當今天專案越來越大,或是越來越多人一起開發同一個專案時,這時為了專案的穩定就不能隨意提交 commit 上去,而是會另外開分支來提交 commit,例如:新增新功能、修正 Bug、實驗新作法等,都可以額外開分支進行開發,等到確認沒問題後再合併回來。

開始使用分支

首先我們可以先輸入 git branch

$ git branch
* master

分支的操作都會使用到 git branch,而 git branch 在後面不加任何參數時會印出目前工作目錄有哪些分支,Git 預設會幫我們設定一個 master 的分支,master 前面的 * 則表示現在在 master 分支上。

新增分支

新增分支的方式很簡單,一樣使用 git branch,這次在後面加上想要開出去的分支名稱:

$ git branch test

這樣一個名為 test 的分支就開好了:

$ git branch
* master
  test

變更分支名稱

我們剛才新增了一個 test 分支,但後來覺得不是很喜歡想要改掉,這時我們可以加上參數 -m,後面先帶上要修改的分支名稱,最後在帶上想變成的分支名稱:

# git branch -m 想修改的分支 修改後的分支名稱
$ git branch -m test develop

執行完後來檢視一下:

$ git branch
  develop
* master

有成功改掉了,預設的 master 也可以改成想要的名稱:

$ git branch -m master main
$ git branch
  develop
* main

切換分支

接下來我們要切換到 develop 去做開發,首先先確認現在在哪個分支上:

$ git branch
  develop
* main

main 前面有 *,所以現在是在 main 分支上,現在我們要切換到 develop 分支,使用的指令是在學習 Git - 不小心刪掉的檔案救的回來嗎?有提到的 git checkoutgit checkout 後面加上分支名稱就可以切換到指定的分支上:

$ git checkout develop
Switched to branch 'develop'

執行完後我們來確認一下:

$ git branch
* develop
  main

星號 * 跑到 develop 前面了,所以現在我們已經將分支切換到 develop。

切換到不存在的分支

如果我們使用 git checkout 切換到不存在的分支時會出現下面錯誤:

$ git checkout nothing
error:  pathspec 'nothing' did not match any file(s) known to git.

但每次都要先用 git branch 新增分支後再用 git checkout 切換過去好麻煩。
沒關係,在使用 git checkout 時可以加上 -b 參數來解決:

git checkout -b nothing
Switched to a new branch 'nothing'

切換分支的時候好像少了一些檔案?

在剛才新增的 develop 新增 work1.txt 和 work2.txt,接下來我們用 ls 指令來看一下在 develop 分支的工作目錄下有哪些檔案:

$ ls al
total 16
drwxrwxr-x 3 calon calon 4096  5月 18 20:00 .
drwxrwxr-x 3 calon calon 4096  5月 16 13:49 ..
drwxrwxr-x 8 calon calon 4096  5月 18 20:00 .git
-rw-rw-r-- 1 calon calon    0  5月 16 17:56 rule2.txt
-rw-rw-r-- 1 calon calon    0  5月 16 18:24 rule3.txt
-rw-rw-r-- 1 calon calon    1  5月 16 17:56 rules.txt
-rw-rw-r-- 1 calon calon    0  5月 18 20:02 work1.txt
-rw-rw-r-- 1 calon calon    0  5月 18 20:02 work2.txt

現在我們切到 main 分支在檢視一次目錄底下的內容:

$ git checkout main
Switched to branch 'main'
$ ls -al
total 16
drwxrwxr-x 3 calon calon 4096  5月 18 20:00 .
drwxrwxr-x 3 calon calon 4096  5月 16 13:49 ..
drwxrwxr-x 8 calon calon 4096  5月 18 20:00 .git
-rw-rw-r-- 1 calon calon    0  5月 16 17:56 rule2.txt
-rw-rw-r-- 1 calon calon    0  5月 16 18:24 rule3.txt
-rw-rw-r-- 1 calon calon    1  5月 16 17:56 rules.txt

剛才在 develop 分支底下新增的 work1.txt 和 work2.txt 都不見了?
不用擔心,檔案沒有消失,只要切回 develop 那些檔案就會出現了。

合併分支

我們先來看一下 main 裡面的 commit:

ccf65d1 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

我們在 ccf65d1 後開一個新的分支 develop,並且切換到 develop 後檢視一下 commit:

$ git branch develop
$ git checkout develop
Switched to brach develop
$ git log --oneline
ccf65d1 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

跟 main 的一樣,接下來就可以開始在此分支上進行開發,現在我們要來新增 2 個檔案並在 develop 分支分開提交 commit,也就是會有 2 個新 commit:

$ touch work1.txt work2.txt
$ git add work1.txt
$ git commit -m '新增 work1.txt'
[develop 6f5eefa] 新增 work1.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 work1.txt

剛新增的 work2.txt 也是一樣執行 git addgit commit 提交上去,現在我們的 develop 分支上多了 2 個 commit:

$ git log --oneline
4444f10 (HEAD -> develop) 新增 work2.txt
6f5eefa 新增 work1.txt
ccf65d1 (main) 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

這時我們切回去 main 分支看檢視一下 commit 紀錄:

$ git checkout main
Switched to branch 'main'
$ git log --oneline
ccf65d1 (HEAD -> main) 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

的確比 develop 分支少了 2 個 commit,接下來想要將開發用的 develop 合併回 main,首先我們要切回 main 分支,剛才檢視 main 的 commit 時已經有切換到 main 了,以防萬一還是再檢查一下:

$ git branch
  develop
* main

現在已經有切換到 main 分支了,接下來準備將 develop 合併回 main 分支,我們可以使用 git merge,後面加上想要合併回來的分支名稱,在這邊是 develop:

$ git merge develop
Updating ccf65d1..4444f10
Fast-forward
 work1.txt | 0
 work2.txt | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 work1.txt
 create mode 100644 work2.txt

完成後用 git log 來確認一下:

$ git log --oneline
4444f10 (HEAD -> main, develop) 新增 work2.txt
6f5eefa 新增 work1.txt
ccf65d1 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

快轉模式(Fast-forward)

我們在上面將 develop 合併回 main 的時候有出現 Fast-forward 這個訊息,也就是快轉模式,因為 develop 是從 main 分出去的分支,而在 develop 要合併回 main 的時候,develop 除了多了 2 個新 commit 之外 main 有的 develop 都有,所以合併時並不會產生新的 commit。
我們可以使用 git log 並加上 --graph 參數來使用 ASCII 圖形檢視 commit 紀錄,首先是合併前的 main 分支:

$ git log --oneline --graph
* ccf65d1 (HEAD -> main) 新增 rule 3
* c8699fa 新增 rule2.txt
* 7cf2baa 新增 rules.txt 內容
* deb6d18 init

在每個 commit 前面多了 * 的符號之外好像就沒有什麼不同,接下來我們將 develop 合併回來後再檢視一次:

$ git merge develop
Updating ccf65d1..4444f10
Fast-forward
 work1.txt | 0
 work2.txt | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 work1.txt
 create mode 100644 work2.txt
$ git log --oneline --graph
* 4444f10 (HEAD -> main, develop) 新增 work2.txt
* 6f5eefa 新增 work1.txt
* ccf65d1 新增 rule 3
* c8699fa 新增 rule2.txt
* 7cf2baa 新增 rules.txt 內容
* deb6d18 init

好像和上面介紹分支合併後的結果沒有差別,這是因為 Git 發現只要將 main 分支 commit 往前進到 develop 最新的 commit 即可完成合併,所以沒有額外產生合併的 commit 的關係。
這次我們在將 develop 合併回 main 前先在 main 分支新增 test.txt 檔案並提交一個新 commit:

$ touch test.txt
$ git add test.txt
$ git commit -m '新增 test.txt'
[main 595b530] 新增 test.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test.txt

這樣子 main 分支就會有 develop 上沒有的 commit 紀錄,接下來我們在將 develop 合併回 main:

$ git merge develop

執行之後因為不是使用快轉模式,所以會新增一個合併的 commit,這時會跳出 Vim 的視窗用來編輯這次合併的 commit,編輯完 commit 內容後一樣儲存離開:

$ git merge develop
Merge made by the 'recursive' strategy.
 work1.txt | 0
 work2.txt | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 work1.txt
 create mode 100644 work2.txt

有發現訊息不一樣了嗎?這次沒有出現 Fast-forward 了,接著使用 git log --graph 來檢視 commit 紀錄:

*   485e562 (HEAD -> main) Merge branch 'develop' into main
|\
| * 4444f10 (develop) 新增 work2.txt
| * 6f5eefa 新增 work1.txt
* | 595b530 新增 test.txt
|/
* ccf65d1 新增 rule 3
* c8699fa 新增 rule2.txt
* 7cf2baa 新增 rules.txt 內容
* deb6d18 init

有發現有一個像耳朵的圖案了嗎?每個 * 代表一個 commit,/ 表示分出去的分支所新增的 commit,而 \ 則是合併回來的意思,在這邊合併時產生了新的 commit 所以 \ 會指向 485e562。

不使用快轉模式合併

在上面有介紹過,當今天要合併回來的分支上 Git 發現它有的你也有,只是多出幾個 commit 時就會用快轉模式,而這時使用 git log --graph 來檢視 commit 紀錄就不會有分出去的小耳朵,如果希望每次合併回來都會有小耳朵的圖案,我們可以加上 --no-ff 讓 Git 不使用快轉模式進行合併:

$ git merge develop --no-ff

執行之後會跳出 Vim 的視窗,編輯完 commit 內容後一樣儲存離開:

$ git merge develop --no-ff
Merge made by the 'recursive' strategy.
 work1.txt | 0
 work2.txt | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 work1.txt
 create mode 100644 work2.txt

之後一樣用 git log --oneline --graph 來檢視一下紀錄:

$ git log --oneline --graph
*   acfe38f (HEAD -> main) Merge branch 'develop' into main
|\
| * 4444f10 (develop) 新增 work2.txt
| * 6f5eefa 新增 work1.txt
|/
* ccf65d1 新增 rule 3
* c8699fa 新增 rule2.txt
* 7cf2baa 新增 rules.txt 內容
* deb6d18 init

刪除分支

我們已經成功地將 develop 合併回 main 上了,雖然 develop 分支留著也沒關係,但如果不想要我們也可以把它刪掉,記得在刪除之前要切到不是要刪除的分支上面在進行刪除,否則會出錯,而要刪除分支一樣是使用 git branch 指令,這次我們要加上 -d 這個參數:

$ git branch -d develop
Delete branch develop (was 4444f10)

這樣子 Git 就會刪除 develop,如果是在合併之前使用則會出現以下提示:

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

這是在提示我們 develop 的內容還沒有合併,如果你真的不想要這個分支的話上面訊息也有提到怎麼刪除,只要把 d 改成大寫 D 就可以了:

$ git branch -D develop
Delete branch develop (was 4444f10)

能救回還沒合併卻被刪掉的分支嗎?

當然可以,還是的剛剛在刪除分支的時候的訊息嗎?

$ git branch -D develop
Delete branch develop (was 4444f10)

後面的 4444f10 就是救回分支的關鍵,在刪除分支的時候不是將在分支上所建立的 commit 刪掉,可以將分支想像成一張便利貼,這張便利貼會指向該分支所新增的 commit,而當我們將分支刪除時只是把貼著分支名稱的便利貼拿掉而已,所以我們只要在重新新增一個分支並指向 4444f10 這個 commit 即可:

git branch new_develop 4444f10

接下來我們將分支切到 new_develop 上,並檢視 new_develop 的 commit:

$ git checkout new_develop
Switched to branch 'new_develop'
$ git log --oneline
4444f10 (HEAD -> new_develop) 新增 work2.txt
6f5eefa 新增 work1.txt
ccf65d1 (main) 新增 rule 3
c8699fa 新增 rule2.txt
7cf2baa 新增 rules.txt 內容
deb6d18 init

當工作到一半需要切換到另一個分支時

有時候工作到一半時,會有緊急的突發狀況需要我們去處理,像是要修一些 bug 等,這時我們就需要切換分支去處理,為了避免將現有的工作內容不小心提交 commit 在其他分支,我們可以先將手邊的工作提交 commit 上去,等到處理完其他分支之後在切回原本的分支並使用 git reset 將做到一半的工作的 commit 給拆掉繼續接著做下去。

而除了上述方法,我們在這邊介紹新的指令 git stash

Stash

首先我們先來看一下目前 git 的狀態:

$ git status
On branch main
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:   test.txt

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

我們目前正在 main 分支上,並正在修改 test.txt 這個檔案,這時突然需要切到別的分支去處理其他事情時我們可以使用 git stash 將這些檔案先暫時存起來:

$ git stash
Saved working directory and index status WIP on main: 485e562 Merge branch 'new_develop' into main

如果是還處於 Untracked 狀態的檔案需要加上參數 -u

$ git stash -u

執行完之後我們可以使用 git stash list 來看一下:

$ git stash list
stash@{0}: WIP on main: 485e562 Merge branch 'new_develop' into main

stash@{0} 是 stash ID,WIP 是指 Working In Progress,也就是工作進行中的意思。而 stash 可以儲存很多份,如果我們在別的分支再新增一筆暫存的話:

$ git stash
stash@{0}: WIP on develop: 4444f10 新增 work2.txt
stash@{1}: WIP on main: 485e562 Merge branch 'new_develop' into main

我們可以發現剛才的 WIP on main 這筆的 stash ID 變成 stash@{1} 了,而上面多出的 stash@{0} 這筆就是我們剛新增的暫存,也就是 ID 數字越小時間越新。

接下來,當我們處理完其他分支的事情後想要拿回剛才的佔存檔要怎麼做?這邊我們可以使用 git stash popgit stash apply 這 2 個指令來拿回我們剛才的工作進度,而這 2 個指令的差別在於:

  1. git stash pop 會將指定的 stash 暫存拿出來套用在現在的分支上,並且刪掉該 stash 暫存。
  2. git stash apply 將指定的 statsh 暫存套用在現在分支上後不刪除該 stash。

所以如果確定要拿回的 stash 之後不需要的話可以使用 git stash pop,而以後還會用到則可以使用 git stash apply,我們這邊先使用 git stash apply,如果後面沒有指定 stash ID 的話預設是最新的一筆,而我們要撿回的工作進度的 stash ID 是 stash@{1} ,所以在這邊要加上 stash ID:

$ git stash apply stash@{1}
On branch main
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:   test.txt

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

這樣我們的工作進度救回來了。

我們剛才是使用 git stash apply,也就是說將暫存撿回來後放在 stash 裡的存檔不會消失,如果有些暫存真的不需要的時候可以使用 git stash drop

git stash drop stash@{1}
Dropped stash@{1}(107dda387eaf519a860c4979180d2dd390c2756f)

最後我們來使用 git stash pop 來撿回工作進度的暫存:

$ git stash pop stash@{1}
On branch main
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:   test.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{1}(107dda387eaf519a860c4979180d2dd390c2756f)

執行後出現的訊息是執行完 git stash applygit stash drop 的結果,所以我們也可以將 git stash pop 看成是 applydrop 的組合。


參考資料
  • 高見龍,《為你自己學 Git》

#Git







Related Posts

[雜談] 第一篇先來聊聊準備轉職的想法

[雜談] 第一篇先來聊聊準備轉職的想法

eslint 是什麼?

eslint 是什麼?

Day00 Elements of Clojure 書摘&心得

Day00 Elements of Clojure 書摘&心得


Comments