[檔案管理] git 教學:管理分支--增加產線不是問題 - 精華區

[檔案管理] git 教學:管理分支--增加產線不是問題

阿慶

阿慶圖像

2020-03-25 00:20:55

From:122.116.71.150

什麼是分支

分支就像是主要道路分出來的其他小路。

像是孫悟空想要他的一個分身去執行某件事情,於是拔了一根毛就化成他的樣子去執行任務。

任務執行完了就不見了,或者化身回去本體裡面。

為什麼要開分支

以 git 的專案來說,目前有個捐贈系統,我想要新增一個功能叫做批次匯入功能。

於是就開了一個分支開始撰寫這個功能,寫完了測試沒問題再把這個功能合併回主分支,這樣在開發過程中就不會影響到原本系統的運作。

實務上常遇到的狀況就是,在開發過程不是一兩天就能寫好,如果把還沒寫好的功能提交上去,這樣使用者去使用就會發生問題,甚至把系統資料弄亂。

但也不能因為還沒寫完就不提交,如果中間都不提交,那麼開發時發生問題想回到上個版本就沒辦法了。

我們在分支裡開發,反正改來改去都是在這個分支裡,改壞了也沒關係,正常提交也不會影響原本的系統。

還有個狀況,就是新功能開發到一半,結果使用者回報原本的系統有個問題需要馬上修改並上線,此時就麻煩了,

這時修完 bug 但新功能又還沒開發好,為了這個 bug 就把專案推上去又會發生之前敘述的未完成功能產生的操作風險,

但不推上去就不能立刻讓使用者使用已經修好 bug 的系統,等你新功能開發好再一起推上去都不知是何時了。

這時候分支就很好用了,開個 A 分支修 bug 再開個 B 分支寫新功能,這樣大家都不衝突,看哪個版本想合併回去主幹道就選擇哪個版本。

如果有多人開發不同功能又全都在主幹道的話,系統肯定會被弄得不穩定,這樣也無法發揮多人開發的優勢。

列出分支

使用 branch 可以列出分支:

$ git branch
 * master

這表示現在只有一個 master 也就是主分支,前面有個星號表示目前我們位在這個分支中,

從 git bash 的提示符號也可以看到目前所在分支的名稱。

建立分支

我們建立也就是新增一個分支,名為 dog 好了:

# 建立 dog 分支
$ git branch dog

# 再次查看目前的分支狀況
$ git branch
   dog
 * master

看到 dog 分支出現了,但我們沒有切換到 dog 分支,所以還在 master 主分支(主幹道) 中。

分支更名

可以隨時修改分支的名稱,不會影響到任何檔案內容:

# 把 dog 分支改成 cat
$ git branch -m dog cat

# 再次查看目前的分支狀況
$ git branch
   cat
 * master

分支的確成功被我們更名了。

想要更改主分支的名稱也可以,方法同上。

切換分支

使用 checkout 可以切換分支:

# 切換到 cat 分支
$ git checkout cat
 Switched to branch 'cat'

# 再次查看目前的分支狀況
$ git branch
 * cat
   master

切換成功了,如果在該分支下作業,不管是 add 或 commit 都是只有針對該分支而已,不會影響到其他分支。

這樣我們就不怕把系統改亂了,大不了就不要這個分支就好了。

另外,切換到不存在的分支當然會報錯:

# 切換到 dog 分支
$ git checkout dog
 error: pathspec 'dog' did not match any file(s) known to git

我們沒有 dog 分支,他已經被我們改成 cat 了。

如果在 checkout 後面加上 -b 參數,這樣不存在的分支就會被自動建立,不管存不存在都會切換過去。

刪除分支

先切回 master 主分支,再使用 -d 參數把 cat 分支刪掉,假設該分支還沒有被合併到主分支就無法刪除:

# 切換回 master 主分支
$ git checkout master
 Switched to branch 'master'

# 刪除 cat 分支
$ git branch -d cat
error: The branch 'cat' is not fully merged.
If you are sure you want to delete it, run 'git branch -D cat'.

這算是一種保護措施,避免資料遺失。

從提示訊息也可看出,硬要刪掉的話就把小寫 d 改成大寫 d 就可以刪掉了

主分支也可以刪,但現在所處的分支刪不了,切換到其他分支就可以刪了。

# 強制刪除 cat 分支
$ git branch -D cat
 Deleted branch cat (was e8eeffd).

$ git branch
 * master

刪除成功,我們又回到這章還沒開始的原點囉。

分支的概念

上篇我們操作了一下分支,對於分支的基本運作大概有個認識,讓我們再花點時間了解他的運作。

分支間的關係

其實開分支並不是把原本的所有檔案複製一份過去,如果是這樣的話,一個專案開了幾十個分支,

這樣幾個大專案下來硬碟就吃不消了。

他的概念還是在 commit 上,開分支的時候就是從上個 commit 開始各分東西,

從此以後兩個分支各有各的人生,當然人生有時也會相會。

還記得我們提過的工作目錄、暫存區與儲存庫吧?其實分支的運作就是靠 commit 利用儲存庫配製好你分支的工作目錄而已。

其實儲存庫與暫存區都是同一個,大家共用。

共用暫存區

因為暫存區是共用的,所以在 A 分支的暫存區,切換到 B 分支去還是看得到,

我們做個實驗,就是在 master 分支做某事後切到 test 分支把結果提交到 test 分支去。

現在這兩個分支的暫存區都是空的,都有 abc.txt 檔案且內容為空。

# 確定自己在 master 分支
$ git branch
 * master
   test

# 在 abc.txt 加一行字到 abc.txt 當中
$ echo '想你想你' >> abc.txt

# 檢查暫存區狀態
$ git status
 On branch master
 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:   abc.txt

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

# 記錄到暫存區
$ git add .
# 檢查暫存區狀態
$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   abc.txt

# 切到 test 分支
$ git checkout test
 Switched to branch 'test'
 M       abc.txt

# 他會提醒你,有個 abc.txt 被變更了還在暫存區
$ git branch
   master
 * test

$ git status
 On branch test
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   abc.txt

# 看到了吧,明明是在 master 做的,但因為都是同一個暫存區,所以切到 test 也是一樣
$ git commit -m '修改 abc.txt'
 [test a6a0896] 修改 abc.txt
  1 file changed, 1 insertion(+)

# 看一下 test 的 abc.txt
$ cat abc.txt
 想你想你

# 再去 master 看看 abc.txt
$ git checkout master
 Switched to branch 'master'

# 因為暫存區沒東西,所以也沒有什麼提示
$ cat abc.txt

# 空空沒東西,因為我們是在 test commit 的

不能切分支

那時麼時候不能切分支了,就是當相同的檔案但兩個分支的內容不同時。

其中一個分支又改了這個檔案放在暫存區,此時想切到其他分支就會發生錯誤,要你先處理一下暫存區再說。

就像我們現在的 master 跟 test 的 abc.txt 檔案內容已經不同了,

讓我們試看看:

# 這一次一樣在 master 的 abc.txt 加一行字,但內容跟之前不同
$ echo '你好你好' >> abc.txt

# 紀錄到暫存區
$ git add .

# 確認一下狀態
$ git status
 On branch master
 Changes to be committed:
   (use "git restore --staged <file>..." to unstage)
         modified:   abc.txt

# 切到 test 分支
$ git checkout test
 error: Your local changes to the following files would be overwritten by checkout:
         abc.txt
 Please commit your changes or stash them before you switch branches.
 Aborting

這次就沒辦法切到 test 了,為什麼?因為暫存區紀錄的來源是工作目錄,

兩個分支的工作目錄的 abc.txt 內容是不同的,當然就沒辦法這樣囉。

但如果是不衝突的修改就可以切換分支共享暫存區了。

那如果是提交上去的怎麼辦?那就要來合併分支囉。

補充

我們想把 abc.txt 恢復原狀,但我們剛剛已經 git add . 到暫存區了怎麼辦?

# 先把 abc.txt 從暫存區拔掉
$ git restore --stage abc.txt

# 看一下狀態有沒有回到修改檔案但尚位 git add . 的狀況
$ git status
 On branch master
 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:   abc.txt

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

# 把 abc.txt 還原到還沒修改的狀態
$ git restore abc.txt

# 看一下狀態應該像沒動過檔案那樣就可以收工囉
$ git status
 On branch master
 nothing to commit, working tree clean

其實 git status 都有把這些指令寫在畫面上,大家可以自己玩玩看。

合併分支

當分支的任務告一段落時我們會想把分支合併到主幹道上。

或者反過來說,如果每個月有例行更新的日子,此時會由主幹道來合併想更新的分支項目,然後一起推上正式系統。

舉個實例,最近正在 A 分支新增一個功能,還沒寫完,但收到使用者緊急回報正式系統某功能有重大 bug 必須立刻修,

那我就會開個 B 分支來修這個 bug 修好後由主幹道合併 B 分支,然後將主幹道推上系統。

接著繼續開發我的新增功能,也就是回到 A 分支去寫程式。

等寫好測好都沒問題時再利用主幹道將 A 分支合併推上正式系統。

直接看操作:

# 確認現在有哪些分支與處在哪個分支
$ git branch
 * master
   test

# 切到 test 分支
$ git checkout test
 Switched to branch 'test'

# 新增一個 ooqq.txt 檔案
$ touch ooqq.txt

# 在 ooqq.txt 加入一行字
$ echo '第一行' > ooqq.txt

# 看一下狀態
$ git status
 On branch test
 Untracked files:
   (use "git add <file>..." to include in what will be committed)
         ooqq.txt

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

# 將工作目錄結果存到暫存區
$ git add .

# 從暫存區存到儲存庫
$ git commit -m '新增 ooqq.txt 並寫了一行字'
 [test 40ff865] 新增 ooqq.txt 並寫了一行字
  1 file changed, 1 insertion(+)
  create mode 100644 ooqq.txt

# 查看檔案列表
$ ls
 abc.txt  hello.txt  log.txt  ooqq.txt  test.py

# 查看 ooqq.txt 內容
$ cat ooqq.txt
 第一行

# 假設任務完成,切到 master 合併分支
$ git checkout master
 Switched to branch 'master'

# 還沒合併分支,所以沒有 ooqq.txt 檔案
$ ls
 abc.txt  hello.txt  log.txt  test.py

# 使用 merge 合併 test 分支
$ git merge test
 Updating aa5f7ec..40ff865
 Fast-forward
  abc.txt   | 1 +
  hello.txt | 4 ----
  ooqq.txt  | 1 +
  3 files changed, 2 insertions(+), 4 deletions(-)
  create mode 100644 ooqq.txt

# 查看檔案列表
$ ls
 abc.txt  hello.txt  log.txt  ooqq.txt  test.py

# 查看 ooqq.txt 內容
$ cat ooqq.txt
 第一行

# 查看紀錄
$ git log -1
 commit 40ff865ee973cbd18111bf45ab5d6fc3dc73ccd5 (HEAD -> master, test)
 Author: Logo Kuo <logo@forblind.org.tw>
 Date:   Sun Mar 29 23:10:55 2020 +0800

     新增 ooqq.txt 並寫了一行字

很順利完成了,因為之前在 test 分支還有做其他事情還沒合併,所以上面合併訊息可能大家會覺得奇怪,

明明增加一個檔案,為什麼是變成 3 個檔案有更動了。

那如果是改到相同的檔案再合併會怎樣,這次我們快一點,

我先在 master 主幹道裡的 ooqq.txt 新增第二行與第三行,提交後,

切到 test 分支把 ooqq.txt 第一行字改成「我是第一行」,提交後回到 master 準備合併,

我們只顯示合併的部分:

$ git merge test
 Auto-merging ooqq.txt
 CONFLICT (content): Merge conflict in ooqq.txt
 Automatic merge failed; fix conflicts and then commit the result.

如果是沒有衝突的修改,就會自動合併,但當改到相同檔案且有衝突時就要開啟檔案來手動合併了。

上面的資訊跟你說, ooqq.txt 合併失敗,要用編輯器打開來手動合併一下。

# 顯示一下檔案內容
$ cat ooqq.txt
 <<<<<<< HEAD
 第一行
 第二行
 第三行
 =======
 我是第一行
 >>>>>>> test

小於到等號那段是 master 目前 ooqq.txt 的內容

等號到大於是 test 分支的 ooqq.txt 內容

應該有看到大於後面寫的 test 分支吧。

這時候就看我們想要把檔案變怎樣,使用編輯器打開來改一改並存檔,

不要的符號記得都刪掉喔。

# 我們修改完後顯示一下 ooqq.txt 的檔案內容
$ cat ooqq.txt
 我是第一行
 第二行
 第三行

# 改好後就提交囉
$ 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:   ooqq.txt

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

# 有看到 both modified 關鍵字吧,就是我們合併了兩個分支的 ooqq.txt
$ git add .

$ git commit -m '將 ooqq.txt 改成我想要的'
 [master f0230ed] 將 ooqq.txt 改成我想要的

# 看一下最近三筆的紀錄
$ git log -3
 commit f0230ed91c295b0a34875cc037a094a566261543 (HEAD -> master)
 Merge: 71188bf 6c5b3f7
 Author: Logo Kuo <logo@forblind.org.tw>
 Date:   Sun Mar 29 23:41:43 2020 +0800

     將 ooqq.txt 改成我想要的

 commit 6c5b3f7f060c09a8ad5a07f1a3639524b8466512 (test)
 Author: Logo Kuo <logo@forblind.org.tw>
 Date:   Sun Mar 29 23:27:06 2020 +0800

     把 ooqq.txt 的第一行改成我是第一行

 commit 71188bf29387da3a928d1747ed067cd042ca00fa
 Author: Logo Kuo <logo@forblind.org.tw>
 Date:   Sun Mar 29 23:25:34 2020 +0800

     在 ooqq.txt 新增第二與三行

沒錯,原本在 test 的提交也都來了,目前 ooqq.txt 的內容兩邊是不同的,

master 是我們合併好的,而分支 test 是原本 test 的內容,log 也是一樣。

那合併好了後,test 分支需不需要刪掉,就看個人囉,我有時會繼續在上面做另外的開發,

有時就會把已被合併的分支砍掉再另外開一個新的出來。

另外,誰合併誰的過程不一樣,但結果是相同的。

如果 B 與 C 分支都是由 A 分出來的,

那從 A 去合併 B 或從 A 去合併 C 像我們剛剛那樣,會啟用快轉模式,也就是 Fast Forward

剛剛在合併時應該有看到這個關鍵字,這種合併會比較順利,

反之,想用 B 合併 C 或 C 合併 B 就會很有可能發生衝突,

畢竟他們兩個已經各自有不同的天空了,合併起來比較費力點,但方法是一樣的。

此時會產生一個合併的 commit 來處理這個合併,通常會跳出 git 預設編輯器的視窗讓你輸入 commit 內容,

通常可以總結一下這次合併增加了哪些功能,因為光看一個個 commit 可能會太細節,

如果不寫也沒事,他會自己有個合併的系統 commit 資訊。

合併好了,對分支來說,合併別人的 HEAD 會往前,被合併的還停留在原地,就像上述 master 與 test 那樣,

master 的 HEAD 往前走了,test 還在原地。

最後,在 merge 時加入 --no-ff 參數就一定會強制產生 commit 來紀錄合併。

也就是不使用快轉模式,結果上沒什麼差別,就看自己的著眼點在哪裡了。

救回分支

在之前的章節我們介紹過使用 reset 來回到之前的版本,也就是所謂回到過去的美好時光。

對分支而言也是一樣的道理,其實這些都是在不同的 commit 貼標籤的概念而已,

因此很容易切換且速度快又不占多大空間,可以說 git 是很靈活又小巧好用的版控工具。

刪除分支並不會影響到其他分支的相同檔案,因為開分支並不是把檔案複製或搬移一份到新分支去,

我們已經知道修改衝突的檔案想切換分支時會發生錯誤,當分支還沒有被合併時想刪除也會有提示訊息,

這算是一種保護機制,現在我在 test 分支修改了 abc.txt 也提交了,但還沒有跟 master 主幹道合併就想刪除 test 分支

# 確認現在處在哪個分支
$ git branch
   master
 * test

# 刪除 test 分支
$ git branch -d test
 error: Cannot delete branch 'test' checked out at 'D:/practice'

# 我們犯了一個根本上的錯誤,就是無法刪除現在所在的分支,必須切換到其他分支才行
$ git checkout  master
 Switched to branch 'master'

# 刪除 test 分支
$ git branch -d test
 error: The branch 'test' is not fully merged.
 If you are sure you want to delete it, run 'git branch -D test'.

# 因為還沒 merge 所以不給刪,但使用大寫 D 還是可以硬刪
$ git branch -D test
 Deleted branch test (was fa03700).

# 記一下上面的版號
# 確認一下現在的分支狀況
$ git branch
 * master

# test 被刪了

刪掉分支後,其實那些分支的 commit 還在,只是暫時隱藏起來而已。

如果當時沒有記下那個版號的話,別忘了之前介紹的 reflog 可以查,他會保留 30 天的紀錄。

讓我們救回分支:

# 因為 reflog 內容有點多,我們列出最近的 3 筆就好
$ git reflog -3
 f0230ed (HEAD -> master) HEAD@{0}: checkout: moving from test to master
 fa03700 HEAD@{1}: checkout: moving from master to test
 f0230ed (HEAD -> master) HEAD@{2}: checkout: moving from test to master

# 我們要找的是最後切換到 test 的身影,那就是 fa03700
$ git branch new_test fa03700

# 確認一下分支狀況
$ git branch
 * master
   new_test

# 新分支出現了,但其實是原本 test 分支的內容,想要用原本的名字也可以
$ git checkout new_test
 Switched to branch 'new_test'

# 切換成功,看一下紀錄
$ git log --oneline -3
 fa03700 (HEAD -> new_test) 在 abc.txt 加了一行
 6c5b3f7 把 ooqq.txt 的第一行改成我是第一行
 40ff865 新增 ooqq.txt 並寫了一行字

# 切換到 master
$ git checkout master
 Switched to branch 'master'

# 看一下 master 的紀錄
$ git log --oneline -3
 f0230ed (HEAD -> master) 將 ooqq.txt 改成我想要的
 6c5b3f7 把 ooqq.txt 的第一行改成我是第一行
 71188bf 在 ooqq.txt 新增第二與三行

# 他們的第二個 commit 是一樣的,從這個版號以後是還沒 merge 的部分

發現了吧,分支的名字其實不重要,我們實際上是給這個版號一個新名字而已。

最後補充一個合併 commit 的例子:

$ git merge fa03700
 Merge made by the 'recursive' strategy.
  abc.txt | 1 +
  1 file changed, 1 insertion(+)

其實分支本身不能被合併,我們合併的都是分支的 commit 因此直接指定 commit 版號來合併也沒問題。

特別是不想合併整個分支的情況就可以自己指定想合併的 commit

合併分支衝突

其實我們在 合併分支 已經介紹過發生衝突的處理方式,

不過這次不一樣,因為當時發生衝突的是文字檔,還可以使用純文字編輯器開啟做修改,

那如果是圖檔、執行檔等無法直接編輯的二進位檔該怎麼處理衝突呢?

# 先切到 new_test 分支
$ git checkout new_test
 Switched to branch 'new_test'

# 複製一個 word 檔過來
$ cp /d/生活班232小時課表--0610.doc .

# 看一下檔案列表
$ ls
 abc.txt  hello.txt  log.txt  ooqq.txt  test.py  生活班232小時課表--0610.doc

# 將工作目錄存到暫存區
$ git add .

# 將工作目錄存到儲存庫
$ git commit -m '增加一個 word 檔'
 [new_test 0ceca24] 增加一個 word 檔
  1 file changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 "\347\224\237\346\264\273\347\217\255232\345\260\217\346\231
 \202\350\252\262\350\241\250--0610.doc"

# 切換到 master 主幹道
$ git checkout master
 Switched to branch 'master'

# 我開啟了 D 槽 根目錄的 word 檔刪了一些文字存檔後複製到 master
$ cp /d/生活班232小時課表--0610.doc .

# 看一下檔案列表
$ ls
 abc.txt  hello.txt  log.txt  ooqq.txt  test.py  生活班232小時課表--0610.doc

# 將工作目錄存到暫存區
$ git add .

# 將工作目錄存到儲存庫
$ git commit -m '我也加了相同的 word 檔檔名進來'
 [master 2a621b5] 我也加了相同的 word 檔檔名進來
  1 file changed, 0 insertions(+), 0 deletions(-)
  create mode 100644 "\347\224\237\346\264\273\347\217\255232\345\260\217\346\231
 \202\350\252\262\350\241\250--0610.doc"

# 接下來我們在 master 合併 new_test
$ git merge new_test
 warning: Cannot merge binary files: 生活班232小時課表--0610.doc (HEAD vs. new_te
 st)
 CONFLICT (add/add): Merge conflict in 生活班232小時課表--0610.doc
 Auto-merging 生活班232小時課表--0610.doc
 Automatic merge failed; fix conflicts and then commit the result.

# 查看狀態
$ 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 added:      "\347\224\237\346\264\273\347\217\255232\345\260\217\34
 6\231\202\350\252\262\350\241\250--0610.doc"

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

兩邊擁有同一個檔案且無法直接使用純文字編輯器來處理,經過協商,會有兩種可能:

  1. 保留 master 的檔案 git checkout --ours 檔名
  2. 保留 new_test 的檔案 git checkout --theirs 檔名
# 我們假設保留 new_test 的版本
$ git checkout --theirs 生活班232小時課表--0610.doc
 Updated 1 path from the index

# 接下來你應該知道要做什麼才對
$ git add .

$ git commit -m '保留 new_test 的版本'
 [master 8436216] 保留 new_test 的版本

開啟這個 word 檔應該會跟 new_test 版本的內容一樣。

被打斷的工作

之前在介紹分支時有提過,因為可能有多個任務或臨時任務在進行中,

因此可以自由的切換分支來完成工作,但工作到一半的分支如果不存到暫存區就必須跟其他分支共用工作目錄的成果,

甚至修改到互相衝突的檔案,那就連分支都沒辦法切換過去了。

可是如果存到暫存區或乾脆提交又覺得不適當,因為工作還沒完成,commit 該怎麼寫呀?

其實像這樣實際的狀況,我們應該是有個變通方案,將目前的成果存下來提交是沒錯,

但臨時跑去完成別的工作後回來應該把剛剛的成果接回來繼續完成會比較好,這樣就兩全其美囉。

reset

最簡單的方式就是使用 reset 切版。

現在我們在 new_test 分支編輯了某個檔案到一半,但臨時被老闆叫去修系統的 bug 那就先把目前的成果存下來。

$ git add .

$ git commit -m '做到一半'
 [new_test 4d51034] 做到一半
  1 file changed, 2 insertions(+)

然後切換到修 bug 的分支進行工作,工作完成後切回來 new_test 分支,

使用 reset 把剛剛的工作拆回來繼續做:

$ git reset HEAD~
 Unstaged changes after reset:
 M       abc.txt

# 拆回來了,看一下狀態
$ git status
 On branch new_test
 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:   abc.txt

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

沒錯, abc.txt 呈現剛剛編輯的狀態且還沒從工作目錄存到暫存區,那就是我們剛剛的工作狀態了,

這時候就可以繼續原本的工作囉。

stash

除了 reset 還有另外一個方式,那就是 stash 指令。

剛剛拆回來的狀況假設又被抓去做別的分支了,那就使用 stash 先把這裡的工作存起來:

$ git stash
 Saved working directory and index state WIP on new_test: 0ceca24 增加一個 word
 檔

還沒被追蹤的檔案 Untracked 要使用參數 -u 才會被 stash 記錄喔。

$ git status
 On branch new_test
 nothing to commit, working tree clean

很有趣,看起來的狀態就跟剛提交完一樣。

那剛剛做到一半的成果存去哪了?

$ git stash list
 stash@{0}: WIP on new_test: 0ceca24 增加一個 word 檔

stash@{0} 是目前被暫存下來的工作代號,WIP 是工作進行中字樣,

所以同時可以有很多筆暫存工作,代號就是 stash 0 1 2 等。

號碼數字越少表示越新,所以 0 是最新。

那臨時工作完成了要怎麼把 stash 抓回來繼續做呢?

答案是使用 pop 把暫存紀錄接回來做:

$ git stash pop stash@{0}
 On branch new_test
 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:   abc.txt

 no changes added to commit (use "git add" and/or "git commit -a")
 Dropped stash@{0} (54320c87cabef3ec7bd118c1861bff34aed1cdcc)

有看到 Dropped 這個字吧,這表示 stash 接回來後該紀錄會從 stash 中消失,

如果 pop 後面不接號碼的話就預設 0 也就是最新的暫存紀錄。

除了使用 pop 也可以使用 apply (其他參數語法一樣) 把暫存資料接回來,

差別是 apply 接回來後該暫存紀錄還留在 stash 當中,可以使用 drop 參數刪掉他。

因此,有人說 pop 是 apply + drop

以上兩種方式,你喜歡哪一種呢?

來撿 commit

之前跟大家分享過使用 merge 來合併分支。

但有時我們並不想合併整個分支,而是選擇我們想要的 commit 來合併,

例如 a 分支做了一個更新使用者密碼的功能,

b 分支把新增帳號會錯誤的 bug 修掉,

那在 a 分支與 b 分支可能還有做其他的事,但我目前的專案只想要這幾個功能就好,

其他的功能可能現在不需要,或者還沒測試所以先不要納入正式系統。

那這樣我們就需要分別撿這些 commit 過來合併,而不是合併整個分支。

使用 cherry-pick 接著 commit 的版號就可以解決,並且可以一次輸入多個版號來合併。

加上 --no-commit 參數是會先把該 commit 內容放在暫存區讓我們用 git status 查看一下確認,

真的需要再合併提交,是更保險一點的做法。

我們現在在 master 撿 new_test 分支的 7a1d292 這個 commit 來合併看看:

# 先查出該 commit 的版號
$ git checkout new_test
 Switched to branch 'new_test'

$ git log --oneline -4
 21d3781 (HEAD -> new_test) 修改 abc.txt
 10cf74c 增加一個 word 檔
 0d9c34e 在 abc.txt 加了一行
 7a1d292 把 ooqq.txt 的第一行改成我是第一行

# 切回 master 合併
$ git checkout master
 Switched to branch 'master'

$ git cherry-pick 7a1d292
 CONFLICT (modify/delete): ooqq.txt deleted in HEAD and modified in 7a1d292... 把
  ooqq.txt 的第一行改成我是第一行. Version 7a1d292... 把 ooqq.txt 的第一行改成我
 是第一行 of ooqq.txt left in tree.
 error: could not apply 7a1d292... 把 ooqq.txt 的第一行改成我是第一行
 hint: after resolving the conflicts, mark the corrected paths
 hint: with 'git add <paths>' or 'git rm <paths>'
 hint: and commit the result with 'git commit'

看來不太對勁,合併失敗了,仔細看一下訊息會發現,

在第一行 git 告訴我們 ooqq.txt 這個檔案已經被刪了要怎麼修改?

讓我們檢查一下:

# 先看一下有沒有刪除 ooqq.txt 的 commit
$ git log --oneline -3
 762ece1 (HEAD -> master) 複製一個 test.py 蓋掉原本的同名檔案
 580aaf0 (tag: V2.0) 新增 qqoo.txt
 5bbbe37 刪除 ooqq.txt

# 原來在 5bbbe37 我們已經把 ooqq.txt 刪掉了
# 再看一下現行資料夾有什麼檔案
$ ls
 hello.txt  log.txt  ooqq.txt  qqoo.txt  test.py

# 怪了,不是 ooqq.txt 刪了怎麼還在,如果還在的話又怎麼會合併失敗?
# 檢查一下狀態
$ git status
 On branch master
 You are currently cherry-picking commit 7a1d292.
   (fix conflicts and run "git cherry-pick --continue")
   (use "git cherry-pick --skip" to skip this patch)
   (use "git cherry-pick --abort" to cancel the cherry-pick operation)

 Unmerged paths:
   (use "git add/rm <file>..." as appropriate to mark resolution)
         deleted by us:   ooqq.txt

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

他告訴我們,現在是處在要合併某個 commit 出了狀況,

上面提供了一些指令讓我們可以處理這個狀況,

我們先選擇繼續合併:

$ git cherry-pick --continue
 error: Committing is not possible because you have unmerged files.
 hint: Fix them up in the work tree, and then use 'git add/rm <file>'
 hint: as appropriate to mark resolution and make a commit.
 fatal: Exiting because of an unresolved conflict.
 U       ooqq.txt

當然不行啊,他跟你說你還沒處理,現在還在暫存區要怎麼合併。

有看到 ooqq.txt 前面有個 U 吧。

所以我們可以放棄 --abort 這次的 cherry-pick 合併,看後續想做什麼,

既然那麼好心,工作目錄都有了,那我們選擇把 ooqq.txt 加進來就好:

$ git add ooqq.txt

# 然後看一下狀態
$ git status
 On branch master
 You are currently cherry-picking commit 7a1d292.
   (all conflicts fixed: run "git cherry-pick --continue")
   (use "git cherry-pick --skip" to skip this patch)
   (use "git cherry-pick --abort" to cancel the cherry-pick operation)

 Changes to be committed:
         new file:   ooqq.txt

看來已經修復這個問題了,ooqq.txt 已經變成 new file 了。

因為我們還在 cherry-pick 當中,所以我們不做新的 commit 而是讓 cherry-pick 可以繼續:

$ git cherry-pick --continue
# 跳出編輯器要我輸入 commit 訊息,想用原本的就好,所以直接存檔離開
 [master 05693fe] 把 ooqq.txt 的第一行改成我是第一行
  Date: Sun Mar 29 23:27:06 2020 +0800
  1 file changed, 1 insertion(+)
  create mode 100644 ooqq.txt

# 注意看最後兩行,有新增檔案也有修改資訊
# 檢查狀態
$ git status
 On branch master
 nothing to commit, working tree clean

# 確認紀錄
$ git log --oneline -4
 05693fe (HEAD -> master) 把 ooqq.txt 的第一行改成我是第一行
 762ece1 複製一個 test.py 蓋掉原本的同名檔案
 580aaf0 (tag: V2.0) 新增 qqoo.txt
 5bbbe37 刪除 ooqq.txt

# 看一下 ooqq.txt 的內容
$ cat ooqq.txt
 我是第一行

# 切到 new_test 分支,看一下 ooqq.txt 的內容
$ git checkout new_test
 Switched to branch 'new_test'

$ cat ooqq.txt
 我是第一行

嗯,兩邊一樣,合併成功。

因為合併發生問題,所以我們更需要仔仔細細的檢查,遇到問題不要逃閉,必須設法解決,

書上的 cherry-pick 例子是很順利的合併,但我覺得在實務上一次撿多個 commit 來合併就有可能發生衝突或不合邏輯的狀況,

這時候就需要一步步利用之前學習的知識及 git 提供給我們的做法和資訊來處理,

剛剛我們只撿了一個 commit 來合併,實務上的狀況可能是更複雜的,總之遇到問題難免,

但解法也是很多,不要慌,靜下心來一定可以解決。


來源文章


最後更新:2020-04-29 23:30:15

From: 122.116.71.150

By: 阿慶