iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 9
0

分支合併

分久必合,合久必分,即然有分支的功能,當然也可以將不同的分支合併起來。如果今天開了一個 bugfix 的分支用來修改 bug,在 bug 修好後,希望修好的程式碼也可以拿到 master 來用,這個時候就要來合併了。有的時候合併不一定是多個分支只留下一個,有可能只是將彼此的工作成果同步,但分支還是各自保留。合併基本上有兩種不同的作法,分別為 git mergegit rebase。在進行分支時,要考慮是那個分支要合併那個分支,請參考以下的情境。

第一種狀況是,要合併的兩個分支在提交記錄中位於同一條線,也就是說提交記錄是沒有岔開的。例如在 master 分支所在的提交開了新分支 bugfix,然後切換到 bugfix 分支進行新的提交,然而 master 分支並沒有新的提交。接續昨天 simplegit-progit 的範例,現在位於 bugfix 分支上,並進行了一次提交,結果如下:

* a4df2f4 (HEAD -> bugfix) add fix.txt
* ca82a6d (origin/master, origin/HEAD, master) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

可以看到現在位於 bugfix 分支(HEAD 指向 bugfix)的 a4df2f4 提交,而 master 分支位於它的前一個提交,ca82a6d。接下來要把 bugfix 這個分支合併到 master 分支。

首先切換到 master 分支,使用 git checkout master 這個指令。可以看到 HEAD 現在指向 master,表示已經切換到 master 分支。

* a4df2f4 (bugfix) add fix.txt
* ca82a6d (HEAD -> master, origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

接下來要合併 bugfix 分支,指令為 git merge bugfix,過程會產生類似下面的輸出。

Updating ca82a6d..a4df2f4
Fast-forward
 fix.txt | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 fix.txt

這裡出現了 fast-forward 快進。這個合併其實很單純,只是將 master 分支的位置向前推進一個提交到 bugfix 分支所在的位置,所以叫作快進。看一下結果,確認 masterbugfix 位於同一個指標,這樣就合併完成了。

* a4df2f4 (HEAD -> master, bugfix) add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

第二種狀況,是提交記錄叉開的狀況。接續剛才的情境,在 master 分支上先進行一次提交。

* 1248772 (HEAD -> master) add new.txt
* a4df2f4 (bugfix) add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

接下來切換到 bugfix 分支,也進行一次提交。這裡刻意和剛才在 master 分支進行的提交,新增一個相同名稱的檔案,但內容不同。

* 822f997 (HEAD -> bugfix) add new.txt
| * 1248772 (master) add new.txt
|/
* a4df2f4 add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

可以看到提交記錄岔開了,現在來進行合併,一樣先切換到 master 分支,再將 bugfix 分支合併進來。git merge 的過程會產生如下輸出:

Auto-merging new.txt
CONFLICT (add/add): Merge conflict in new.txt
Automatic merge failed; fix conflicts and then commit the result.

這表示衝突 (conflict) 發生了,所謂的衝突是 Git 無法決定某個檔案的內容應該是什麼,例如這裡的狀況,兩個分支都新增了同一個檔案,但它們的內容卻不同,這時就必須手動決定該檔案的內容,再將這次的合併完成。使用 git status 看一下目前的狀態,會給予解決衝突的提示:

You have unmerged paths.
  (fix conflicts and run "git commit")

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

    both added:      new.txt

把這個衝突的檔案加入 index,它會被視為衝突已解決,接下來就可以用 git commit 產生新的提交並完成這次的合併。在將衝突的檔案加到 index 前,當然要先決定一下它的內容。基本上它應該會是某種 diff 的格式,可以直接編輯。如果確定這個檔案的內容,要全部採用其中一個分支的提交,可以使用 git checkout --ours <file>git checkout --theirs <file> 的指令,來更新該衝突檔案在工作目錄中的內容。這裡的 ours 和 theirs 是指誰呢?因為我們是在 master 分支要去合併 bugfix 分支,所以 ours 是 master (HEAD),theirs 是 bugfix。如果原本的衝突檔案改爛掉了,想回到合併後一開始衝突的狀態,可以用 git checkout --conflict merge <file> 這個指令。把檔案修好,加到 index ,再進行一個新的提交來結束這次的合併,結果如下。

*   efa2733 (HEAD -> master) Merge branch 'bugfix'
|\
| * 822f997 (bugfix) add new.txt
* | 1248772 add new.txt
|/
* a4df2f4 add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

現在看到提交記錄多了 efa2733 這個提交,它是由 822f9971248772 這兩個提交合併而成,而 HEADmaster 都移到 efa2733 這個提交了。現在可以把 bugfix 分支刪除,指令是 git branch -d bugifx,或者是將它保留。但是可能希望 bugfix 分支也有 master 分支的新成果,然後可以繼續在 bugfix 分支修復其他的 bug。此時可以拿 bugfix 分支來合併 master 分支。看一下提交記錄,會發現這個合併是 fast-forward 快進合併。

git merge 介紹完了,現在來介紹 git rebase。rebase 一詞在 Pro Git 中文版翻成衍合,這裡還是採用原文。rebase 的概念是這樣的:把在我這個分支做的事情,拿到你的分支再做一次。再使用一次上面的範例,不過我想先回到之前 master 分支和 bugfix 分支各有一次提交,尚未合併的狀態。想想看該怎麼做呢?

我剛才沒有拿 bugfix 分支來合併 master 分支,所以現在還是在 master 分支的 efa2733 提交。要回到 1248772 這個提交,可以用 git reset --hard 1248772 這個指令。它會將 HEADmaster 同時移到 1248772 提交,而且 HEAD 還是指向 master--hard 選項表示同時要更新工作目錄與 index。結果如下:

* 822f997 (bugfix) add new.txt
| * 1248772 (HEAD -> master) add new.txt
|/
* a4df2f4 add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit 

接下來進行 rebase。這裡的操作是:我在 bugfix 分支,要把在 bugfix 分支做的事,在 master 分支再作一次。所以要先切到 bugfix 分支,然後在 master 進行 rebase。指令分別為 git checkout bugfixgit rebase master。過程輸出如下:

First, rewinding head to replay your work on top of it...
Applying: add new.txt
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging new.txt
CONFLICT (add/add): Merge conflict in new.txt
error: Failed to merge in the changes.
Patch failed at 0001 add new.txt
The copy of the patch that failed is found in: .git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

不意外地又產生衝突了。這裡的提示說明當衝突解決時,可以用 git rebase --continue 來繼續。一樣執行 git status 來看一下解決衝突的方法,和前面提過的方式是相同的,也可以使用 git checkout --ours <file>git checkout --theirs <file> 來決定採用那一方的版本。這裡的 ours 是 bugfix 分支,theirs 是 master 分支。

rebase in progress; onto 1248772
You are currently rebasing branch 'bugfix' on '1248772'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

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

    both added:      new.txt

接下來將衝突的檔案加入 index,並執行 git rebase --continue,結果如下:

* 844ff29 (HEAD -> bugfix) add new.txt
* 1248772 (master) add new.txt
* a4df2f4 add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

現在發現原來岔開的提交記錄變成一直線了,bugfix 分支跑到 master 分支之後,並且多出一個提交。對 bugfix 分支而言,它的歷史記錄改變了,原本是 a4df2f4 -> 822f997,變成 a4df2f4 -> 1248772 -> 844ff29822f997 這個提交從歷史中消失了。實際上這個提交並沒有被刪除,只是在 bugfix 分支中參考不到它。詳細的過程可以參考 Pro Git 的說明,這裡我們只要記得 rebase 是將 bugfix 分支做的事在 master 上再做一次,而對 bugfix 分支而言,它會改變歷史。到這裡還沒有全部完成,因為 master 分支並沒有移動,表示它還沒有納入 bugfix 分支的成果,所以接下來必須切回 master 分支,然後進行一次快進合併,指令分別為 git checkout mastergit merge bugfix,最後結果如下:

* 844ff29 (HEAD -> master, bugfix) add new.txt
* 1248772 add new.txt
* a4df2f4 add fix.txt
* ca82a6d (origin/master, origin/HEAD) changed the verison number
* 085bb3b removed unnecessary test code
* a11bef0 first commit

git rebase 另外有互動模式,可以挑選想要保留的提交,以上面的例子,就是可以在 bugfix分支中選擇我們要的提交,並根據這些挑選出來的提交,在 master 分支上重做。有一個類似可以挑選提交的指令是揀櫻桃 cherry-pick。這部分請再參考文件的說明。

存取遠端儲存庫及與他人合作

終於把合併講完,現在要進入第三個維度,也就是如何和別人合作。當進行團隊合作時,會需要一個中間者,來讓大家協調同步彼此的狀態,而新加入的成員也可以從中間者取得目前的程式碼或文件,這個可以讓團隊成員取得程式碼的中間者,對其他人而言就是一個遠端儲存庫 (remote repository)。遠端儲存庫通常會放在某台伺服器上讓大家可以存取,遠端儲存庫不會拿來開發,所以它不會有工作目錄存在。它除了管理同步大家的開發狀態外,還可以跟其他工具整合,例如 Docker、Jenkins,作為持續整合/交付的一環。

Git 本身就提供了架設 Git 遠端儲存庫的功能,也可使用 GitLab 這類的工具,它提供了方便友善的網頁介面,讓我們可以快速地建立新的專案儲存庫,或與其他工具流程進行整合。如果不想自行架設,目前市面上有一些基於 Git 的源碼託管平台可以利用,例如 GitHub、GitLab、Bitbucket 等等。

接下來我們看一下如何和遠端儲存庫進行互動。以我個人的經驗,大概有以下三類型的場景:

一、知道遠端儲存庫的位置,然後要拿一份到本機進行開發

一個專案在遠端儲存庫通常是透過 HTTP 或者 SSH 的方式取回至本機。遠端儲存庫的位置大概是 http://test.com/project.git 這樣的形式,由協定、位址、專案名稱組成。初次取回的動作叫克隆 (clone),指令為 git clone,這個已經使用過了。如果不指定本機目錄,會在當下目錄建立一個和專案名稱同名的目錄,然後將取回的內容放在這個目錄裡。取回儲存庫後,Git 會使用最新一次提交的內容來更新工作目錄。

二、建立一個新的遠端儲存庫,然後把本機已經存在的檔案放上去讓他人存取

如果在架設 Git 伺服器時就選擇 GitLab 這類的工具,那麼要建立一個新的遠端儲存庫只要在網頁介面上輸入一些資訊,再點幾個按鈕就可以完成。若使用 Git 指令來新建儲存庫倒也不難,只是必須另外進行登入方式、帳號等等的設定,這部分就請大家自行查閱,這裡只介紹建立儲存庫的指令。假設要在家目錄下建立一個遠端儲存庫 my-project.git,在家目錄下執行下述指令:

$ git init --bare my-project.git

會產生 my-project.git 目錄。接著要把本機的檔案丟上去。為了示範,在本機上同時建立遠端儲存庫,以及開發用的專案目錄。假設開發專案目錄是 my-project,放在家目錄下,請自行在專案目錄中放置幾個檔案。接下要讓這個目錄被 Git 納管。複習一下步驟,進入 my-project 目錄後,執行 git initgit addgit commit。然後要把 my-project 的內容丟到 my-project.git 這個儲存庫,這裡有幾個步驟。

  1. 告訴 Git 遠端儲存庫的位置。遠端儲存庫可以給它一個名稱,通常會叫它 origin,指令為 git remote add <name> <url>。在上面的例子,先進入 my-project 資料夾,此時來指定遠端儲存庫,名稱為 origin,位置為 ~/my-project.git,指令是 git remote add origin ~/my-project.git

  2. 把本地的 master 分支推 (push) 到遠端儲存庫,這個動作會在遠端儲存庫建立一個相對應的分支(這裡為 master 分支),並且將本地端的資料丟上去,指令為 git push origin master。如此一來本地端的資料就同步到遠端儲存庫中了。

三、在本地作了一些修改後,同步到遠端儲存庫

有可能要將本地修改同步到遠端儲存庫時,也有他人作了修改,這個時候若要 push 回去會失敗,git 會希望使用者 自行處理和其他人工作成果 merge 的操作,所以第一步會先把遠端儲存庫的資料先拉下來。遠端儲存庫一樣是 origin,指令為 git fetch origin。假設有他人作了修改,所以 fetch 後 origin/master 分支的位置會改變,可以用 git log origin/master 來查看此遠端分支的提交狀況。而在本地端,我們目前還在 master 分支,首先先合併剛才拉下來的他人工作成果,可以使用 git merge origin/master 指令。合併完成後應該會更改 master 分支的位置,此時 master 分支已包含其他人的工作成果。接下來我們就可以用 git push origin master,把在 master 分支的工作成果推回 origin 遠端儲存庫。

Submodule

Submodule 大概可以翻成「子模組」或「次模組」,之前沒有聽過這個工具,但是在應試目標能力有提到,所以就來研究一下。Submodule 的基本精神就是一個 Git 專案目錄中包含另一個 Git 專案目錄,但希望這兩個專案在操作時可以分開,例如有分開的提交記錄等等,在實務上可能是需要使用另一個程式庫,有可能在開發時會對這個程式庫進行更改,或者希望這個程式庫的遠端儲存庫有更新時也能在本端拉回更新。

假設有一個主專案,它的儲存庫位置是 https://github.com/chaconinc/MainProject,而另一個要作為 submodule 的儲存庫是 https://github.com/chaconinc/DbConnector。首先是在本地的 MainProject 專案目錄中加入 DbConnector 作為 submodule,指令是

$ git submodule add https://github.com/chaconinc/DbConnector

如同 git clone,它會建立一個 DbConnector 的目錄,然後把專案複製進來。這裡沒有特別說明 DbConnector 目錄應該直接置於 MainProject 之下,或者 MainProject 中任何一個子目錄都可以,假設它直接位於 MainProject 之下的第一層。

接下來用 git status 看一下,除了 DbConnector 外,還會多一個 .gitmodules 檔案,它會記住 submodule 的遠端儲存庫位置以及本地的路徑。這個檔案也是要被 Git 納管的。然後是 git addgit commit,都是在 DbConnector 目錄之外進行的操作,所以提交是屬於 MainProject 的記錄。最後用 git push origin master 將變動推回遠端儲存庫。

要克隆一個有 submodule 的專案,一開始的步驟和克隆一般的專案相同:

$ git clone https://github.com/chaconinc/MainProject

克隆下來的本地工作目錄會包含 DbConnector 這個 submodule 的目錄,但是是空的,必須自己手動將內容拉回來,指令是 git submodule initgit submodule update。這些步驟可以合併成下面的指令:

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject

因為個人懶惰的關係,submodule 就先介紹到這裡,我覺得這個功能好像比較少用到,所以大概知道 submodule 是什麼就可以了,關於後續的操作請再參考文件說明。

結語

關於 Git 就先介紹到這裡,我們先從在本地端工作目錄新增 git 儲存庫,將檔案加入暫存區並提交開始,接下來提到一些檔案或狀態回復的方式,再介紹分支及合併的概念,最後介紹遠端儲存庫和與他人合作開發的流程以及 submodule。Git 本身是個功能很強大的工具,這幾天也覺得好像還有很多東西沒有提到,但總之希望還沒在開發過程中使用源碼管理工具的朋友,可以試著使用看看 Git,相信它不會讓你失望的。

對於 Git 有興趣的朋友,接下來可以去瞭解 Git 的內部對於檔案、提交這些物件的表示方法。最後附上一些學習資源,首先是官網的 Pro Git,雖然內容有點多,但謮完後會對 Git 有很清楚的概念。然後是高見龍大大的《為你自己學 Git》,以問答的方式說明 Git 在特定情境下的操作方法,尤其是對 Git 內部物件的說明,看完後會功力大增,最後是保哥前幾年的鐵人賽系列文,請參考 https://github.com/doggy8088/Learn-Git-in-30-days。那麼 Git 就到這裡了。


上一篇
[Day 08] Git (4)
下一篇
[Day 10] Ansible (1)
系列文
30 天準備 LPI DevOps Tools Engineer 證照30

尚未有邦友留言

立即登入留言