git乾貨系列:(五)多人協同工作之分支管理

嘟嘟MD發表於2017-06-01

原本地址:git乾貨系列:(五)多人協同工作之分支管理
部落格地址:tengj.top/

前言


分支就是科幻電影裡面的平行宇宙,當你正在電腦前努力學習Git的時候,另一個你正在另一個平行宇宙裡努力學習SVN。如果兩個平行宇宙互不干擾,那對現在的你也沒啥影響。不過,在某個時間點,兩個平行宇宙合併了,結果,你既學會了Git又學會了SVN

git乾貨系列:(五)多人協同工作之分支管理

正文


分支簡介


為了真正理解 Git 處理分支的方式,我們需要回顧一下Git是如何儲存資料的。
Git 儲存的不是檔案的變化或者差異,而是一系列不同時刻的檔案快照。在進行提交操作時,Git會儲存一個提交物件(commit object)。知道了Git儲存資料的方式,我們可以很自然的想到——該提交物件會包含一個指向暫存內容快照的指標。 但不僅僅是這樣,該提交物件還包含了作者的姓名和郵箱、提交時輸入的資訊以及指向它的父物件的指標。首次提交產生的提交物件沒有父物件,普通提交操作產生的提交物件有一個父物件,而由多個分支合併產生的提交物件有多個父物件。

Git的分支,其實本質上僅僅是指向提交物件的可變指標。 Git的預設分支名字是 master。 在多次提交操作之後,你其實已經有一個指向最後那個提交物件的 master 分支。 它會在每次的提交操作中自動向前移動。

Git 的 “master” 分支並不是一個特殊分支。它就跟其它分支完全沒有區別。 之所以幾乎每一個倉庫> 都有 master 分支,是因為 git init 命令預設建立它,並且大多數人都懶得去改動它。

分支在實際中有什麼用呢?假設你準備開發一個新功能,但是需要兩週才能完成,第一週你寫了50%的程式碼,如果立刻提交,由於程式碼還沒寫完,不完整的程式碼庫會導致別人不能幹活了。如果等程式碼全部寫完再一次提交,又存在丟失每天進度的巨大風險。
現在有了分支,就不用怕了。你建立了一個屬於你自己的分支,別人看不到,還繼續在原來的分支上正常工作,而你在自己的分支上幹活,想提交就提交,直到開發完畢後,再一次性合併到原來的分支上,這樣,既安全,又不影響別人工作。
其他版本控制系統如SVN等都有分支管理,但是用過之後你會發現,這些版本控制系統建立和切換分支比蝸牛還慢,簡直讓人無法忍受,結果分支功能成了擺設,大家都不去用。
Git的分支是與眾不同的,無論建立、切換和刪除分支,Git在1秒鐘之內就能完成!無論你的版本庫是1個檔案還是1萬個檔案。

分支建立


Git是怎麼建立新分支的呢? 很簡單,它只是為你建立了一個可以移動的新的指標。 比如,建立一個 testing分支, 你需要使用 git branch 命令:

$ git branch testing複製程式碼

這會在當前所在的提交物件上建立一個指標。

git乾貨系列:(五)多人協同工作之分支管理
兩個指向相同提交歷史的分支。

那麼,Git又是怎麼知道當前在哪一個分支上呢? 也很簡單,它有一個名為 HEAD 的特殊指標。 請注意它和許多其它版本控制系統(如 Subversion 或 CVS)裡的 HEAD 概念完全不同。 在 Git中,它是一個指標,指向當前所在的本地分支(譯註:將 HEAD 想象為當前分支的別名)。 在本例中,你仍然在master 分支上。 因為 git branch 命令僅僅 建立 一個新分支,並不會自動切換到新分支中去。

git乾貨系列:(五)多人協同工作之分支管理
HEAD 指向當前所在的分支.

你可以簡單地使用 git log 命令檢視各個分支當前所指的物件。 提供這一功能的引數是 --decorate

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project複製程式碼

正如你所見,當前 “master” 和 “testing” 分支均指向校驗和以 f30ab 開頭的提交物件。

分支切換


要切換到一個已存在的分支,你需要使用git checkout命令。 我們現在切換到新建立的 testing 分支去:

$ git checkout testing複製程式碼

這樣 HEAD 就指向 testing 分支了。

git乾貨系列:(五)多人協同工作之分支管理
HEAD 指向當前所在的分支.

上面的建立分支和切換分支命令可以合起來用下面這個命令來替代。

$ git checkout -b testing複製程式碼

那麼,這樣的實現方式會給我們帶來什麼好處呢? 現在不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'複製程式碼

git乾貨系列:(五)多人協同工作之分支管理
HEAD 分支隨著提交操作自動向前移動.

如圖所示,你的 testing 分支向前移動了,但是 master 分支卻沒有,它仍然指向執行 git checkout 時所指的物件。 這就有意思了,現在我們切換回 master 分支看看:

$ git checkout master複製程式碼

git乾貨系列:(五)多人協同工作之分支管理
檢出時 HEAD 隨之移動.

這條命令做了兩件事。 一是使 HEAD 指回 master 分支,二是將工作目錄恢復成 master 分支所指向的快照內容。 也就是說,你現在做修改的話,專案將始於一個較舊的版本。 本質上來講,這就是忽略testing 分支所做的修改,以便於向另一個方向進行開發。
可以使用 git branch命令檢視當前分支,注意前面帶*的表示當前分支
git乾貨系列:(五)多人協同工作之分支管理


Note
分支切換會改變你工作目錄中的檔案
在切換分支時,一定要注意你工作目錄裡的檔案會被改變。 如果是切換到一個較舊的分支,你的工作目> 錄會恢復到該分支最後一次提交時的樣子。 如果Git不能幹淨利落地完成這個任務,它將禁止切換分支。

合併分支(快速合併)


假如我們在testing上的工作完成了,就可以把testing合併到master上。Git怎麼合併呢?最簡單的方法,就是直接把master指向testing的當前提交,就完成了合併,這裡你需要使用git merge命令

$ git merge testing
Updating 64ba18a..760118b
Fast-forward
 hello.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt複製程式碼

git merge命令用於合併指定分支到當前分支。合併後,再檢視內容,就可以看到,和testing分支的最新提交是完全一樣的。
注意到上面的Fast-forward資訊,Git告訴我們,這次合併是“快進模式”,也就是直接把master指向testing的當前提交,所以合併速度非常快。
當然,也不是每次合併都能Fast-forward,我們後面會講其他方式的合併。

git乾貨系列:(五)多人協同工作之分支管理

刪除分支


合併完分支後,甚至可以刪除dev分支。刪除dev分支就是把dev指標給刪掉,刪掉後,我們就剩下了一條master分支,這裡需要使用git branch -d命令來刪除分支

$ git branch -d testing
Deleted branch testing (was 760118b).複製程式碼

git乾貨系列:(五)多人協同工作之分支管理

分支合併衝突


人生不如意之事十之八九,合併分支往往也不是一帆風順的。
準備新的dev分支,繼續我們的新分支開發:

$ git checkout -b dev
Switched to a new branch 'dev'複製程式碼

修改README.md內容,新增一樣內容"day day up~",在dev分支上提交:

$ git commit -am "one commit"
[dev 6a6a08e] one commit
 1 file changed, 1 insertion(+)複製程式碼

切換到master分支:

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.複製程式碼

Git還會自動提示我們當前master分支比遠端的master分支要超前1個提交。
master分支上把README.md檔案的最後改為 good good study,然後提價

$ git commit -am "two commit"
[master 75d6f25] two commit
 1 file changed, 1 insertion(+)複製程式碼

現在,master分支和dev分支各自都分別有新的提交,變成了這樣:

git乾貨系列:(五)多人協同工作之分支管理

這種情況下,Git無法執行“快速合併”,只能試圖把各自的修改合併起來,但這種合併就可能會有衝突,我們試試看:

$ git merge dev
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.複製程式碼

果然衝突了!Git告訴我們, README.md檔案存在衝突,必須手動解決衝突後再提交。git status也可以告訴我們衝突的檔案:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
You have unmerged paths.
  (fix conflicts and run "git commit")

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

        both modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")複製程式碼

我們可以直接檢視README.md的內容:

$ cat README.md
#gitLearn
<<<<<<< HEAD
good good study
=======
day day up
>>>>>>> dev複製程式碼

Git用<<<<<<<=======>>>>>>>標記出不同分支的內容,我們修改如下後儲存:

#gitLearn
good good study
day day up複製程式碼

再提交:

$ git commit -am 'merge commit'
[master 9a4d00b] merge commit複製程式碼

現在,master分支和dev分支變成了下圖所示:

git乾貨系列:(五)多人協同工作之分支管理

用帶引數的git log也可以看到分支的合併情況:

$ git log --graph --pretty=oneline --abbrev-commit
*   9a4d00b merge commit
|\
| * 6a6a08e one commit
* | 75d6f25 two commit
|/
* ae06dcf 123
* 760118b test
*   64ba18a test
|\
| *   4392848 Accept Merge Request #1 test : (dev -> master)
| |\
| | * a430c4b update README.md
| |/
| * 88ec6d7 Initial commit
* 32d11c8 update README.md
* 8d5acc1 new file README
* e02f115 Initial commit複製程式碼

最後,刪除feature1分支:

$ git branch -d dev
Deleted branch dev (was 6a6a08e).複製程式碼

合併分支(普通合併)


通常,合併分支時,如果可能,Git會用Fast forward模式,但這種模式下,刪除分支後,會丟掉分支資訊。
如果要強制禁用Fast forward模式,Git就會在merge時生成一個新的commit,這樣,從分支歷史上就可以看出分支資訊。
下面我們實戰一下--no-ff方式的git merge
首先,仍然建立並切換dev分支:

$ git checkout -b dev
Switched to a new branch 'dev'複製程式碼

修改README.md檔案,並提交一個新的commit:

$ git commit -am 'submit'
[dev fee6025] submit
 1 file changed, 1 insertion(+)複製程式碼

現在,我們切換回master

$ git checkout master
Switched to branch 'master'複製程式碼

目前來說流程圖是這樣:

git乾貨系列:(五)多人協同工作之分支管理

準備合併dev分支,請注意--no-ff引數,表示禁用Fast forward

$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)複製程式碼

因為本次合併要建立一個新的commit,所以加上-m引數,把commit描述寫進去。

合併後,我們用git log看看分支歷史:

$ git log --graph --pretty=oneline --abbrev-commit
*   b98f802 merge with no-ff
|\
| * fee6025 submit
|/
*   9a4d00b merge commit
...複製程式碼

可以看到,不使用Fast forward模式,merge後就像這樣:

git乾貨系列:(五)多人協同工作之分支管理

分支管理策略


實際公司開發的時候一般3個分支就可以了:

  1. mster 主分支用來發布
  2. dev 日常開發用的分支
  3. bug 修改bug用的分支

首先,master分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面幹活;
幹活都在dev分支上,也就是說,dev分支是不穩定的,到某個時候,比如1.0版本釋出時,再把dev分支合併到master上,在master分支釋出1.0版本,你和你的小夥伴們每個人都在dev分支上幹活,每個人都有自己的分支,時不時地往dev分支上合併就可以了;
bug分支用來處理日常bug,搞定後合到dev分支即可;

假設遠端公共倉庫,有一個master和一個dev分支,進行多人協作開發時候(每個人的公鑰必須加入到遠端賬號下,否則無法push), 每個人都應該clone一份到本地。 但是clone的只是master,如果遠端的masterdev一樣,沒關係;如果不一致,則需要clonedev分支 git checkout -b dev origin/dev 之後每個人在本地的dev分支上獨自開發(最好不要在mast上開發), 開發完成之後push到遠端dev, git push origin dev。 之後稽核人再確定是否合併devmaster

團隊多人開發協作


當你從遠端倉庫克隆時,實際上Git自動把本地的master分支和遠端的master分支對應起來了,並且,遠端倉庫的預設名稱是origin
要檢視遠端庫的資訊,用git remote

$ git remote
origin複製程式碼

或者,用git remote -v顯示更詳細的資訊:

$ git remote -v
origin  git@git.coding.net:tengj/gitLearn.git (fetch)
origin  git@git.coding.net:tengj/gitLearn.git (push)複製程式碼

上面顯示了可以抓取和推送的origin的地址。如果沒有推送許可權,就看不到push的地址。

推送分支


推送分支,就是把該分支上的所有本地提交推送到遠端庫。推送時,要指定本地分支,這樣,Git就會把該分支推送到遠端庫對應的遠端分支上:

$ git push origin master複製程式碼

如果要推送其他分支,比如dev,就改成:

$ git push origin dev複製程式碼

抓取分支


多人協作時,大家都會往masterdev分支上推送各自的修改。
現在,模擬一個你的小夥伴,可以在另一臺電腦(注意要把SSH Key新增到GitHub)或者同一臺電腦的另一個目錄下克隆:

$ git clone git@git.coding.net:tengj/gitStudy.git
Cloning into 'gitStudy'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
Checking connectivity... done.複製程式碼

當你的小夥伴從遠端庫clone時,預設情況下,你的小夥伴只能看到本地的master分支。不信可以用git branch命令看看:

$ git branch
* master複製程式碼

現在,你的小夥伴要在dev分支上開發,就必須建立遠端origindev分支到本地,於是他用這個命令建立本地dev分支(程分支dev要先建立)。

$ git checkout -b dev
git複製程式碼

建立dev分之後,先同步遠端伺服器上的資料到本地

$ git fetch origin
From git.coding.net:tengj/gitStudy
 * [new branch]      dev        -> origin/dev複製程式碼

現在,他就可以在dev上繼續修改,然後,時不時地把dev分支push到遠端:

$ git commit -am 'test'
[dev c120ad6] test
 1 file changed, 1 insertion(+)
$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 262 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@git.coding.net:tengj/gitStudy.git
   65c05aa..c120ad6  dev -> dev複製程式碼

你的小夥伴已經向origin/dev分支推送了他的提交,而碰巧你也對同樣的檔案作了修改,並試圖推送:

$ git push origin dev
To git@git.coding.net:tengj/gitStudy.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'git@git.coding.net:tengj/gitStudy.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.複製程式碼

推送失敗,因為你的小夥伴的最新提交和你試圖推送的提交有衝突,解決辦法也很簡單,Git已經提示我們,先用git pull把最新的提交從origin/dev抓下來,然後,在本地合併,解決衝突,再推送:

$ git pull origin dev
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From git.coding.net:tengj/gitStudy
 * branch            dev        -> FETCH_HEAD
   b7b87f4..f636337  dev        -> origin/dev
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.複製程式碼

因此,多人協作的工作模式通常是這樣:

  1. 首先,可以試圖用git push origin branch-name推送自己的修改;
  2. 如果推送失敗,則因為遠端分支比你的本地更新,需要先用git pull試圖合併;
  3. 如果合併有衝突,則解決衝突,並在本地提交;
  4. 沒有衝突或者解決掉衝突後,再用git push origin branch-name推送就能成功!

如果git pull提示“no tracking information”,則說明本地分支和遠端分支的連結關係沒有建立,用命令git branch --set-upstream-to branch-name origin/branch-name
這就是多人協作的工作模式,一旦熟悉了,就非常簡單。

總結


到此,Git分支管理就學完了,整理一下所學的命令,大體如下:

git branch           檢視當前分支
git branch -v        檢視每一個分支的最後一次提交
git branch -a        檢視本地和遠端分支的情況
git branch --merged  檢視已經與當前分支合併的分支
git branch --no-merged 檢視已經與當前分支未合併的分支
git branch -r        檢視遠端分支
git branch dev       建立分支 dev
git checkout dev     切換到分支dev
git checkout -b dev  建立並切換分支dev
git merge dev        名稱為dev的分支與當前分支合併
git branch -d dev    刪除分支dev複製程式碼

一直覺得自己寫的不是技術,而是情懷,一篇篇文章是自己這一路走來的痕跡。靠專業技能的成功是最具可複製性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡,希望未來技術之巔上有你也有我。

更多幹貨內容,盡在嘟爺java超神學堂(javaLearn),您不掃一下麼

git乾貨系列:(五)多人協同工作之分支管理

相關文章