git乾貨系列:(五)多人協同工作之分支管理 | 嘟嘟獨立博客
- 1. 前言
- 2. 正文
- 3. 分支簡介
- 4. 分支創建
- 5. 分支切換
- 6. 合併分支(快速合併)
- 7. 刪除分支
- 8. 分支合併衝突
- 9. 合併分支(普通合併)
- 10. 分支管理策略
- 11. 團隊多人開發協作
- 11.1. 推送分支
- 11.2. 抓取分支
前言
分支就是科幻電影裡面的平行宇宙,當你正在電腦前努力學習Git的時候,另一個你正在另一個平行宇宙里努力學習SVN。如果兩個平行宇宙互不干擾,那對現在的你也沒啥影響。不過,在某個時間點,兩個平行宇宙合併了,結果,你既學會了Git又學會了SVN!
正文分支簡介
為了真正理解 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 命令:
1 |
$ git branch testing |
這會在當前所在的提交對象上創建一個指針。
兩個指向相同提交歷史的分支。
那麼,Git又是怎麼知道當前在哪一個分支上呢? 也很簡單,它有一個名為 HEAD 的特殊指針。 請注意它和許多其它版本控制系統(如 Subversion 或 CVS)里的 HEAD 概念完全不同。 在 Git中,它是一個指針,指向當前所在的本地分支(譯註:將 HEAD 想像為當前分支的別名)。 在本例中,你仍然在master 分支上。 因為 git branch 命令僅僅 創建 一個新分支,並不會自動切換到新分支中去。
HEAD 指向當前所在的分支.
你可以簡單地使用 git log 命令查看各個分支當前所指的對象。 提供這一功能的參數是 --decorate。
1234 |
$ git log --oneline --decoratef30ab (HEAD, master, testing) add feature #32 - ability to add new34ac2 fixed bug #1328 - stack overflow under certain conditions98ca9 initial commit of my project |
正如你所見,當前 「master」 和 「testing」 分支均指向校驗和以 f30ab 開頭的提交對象。
要切換到一個已存在的分支,你需要使用git checkout命令。 我們現在切換到新創建的 testing 分支去:
1 |
$ git checkout testing |
這樣 HEAD 就指向 testing 分支了。
HEAD 指向當前所在的分支.
上面的創建分支和切換分支命令可以合起來用下面這個命令來替代。
1 |
$ git checkout -b testing |
那麼,這樣的實現方式會給我們帶來什麼好處呢? 現在不妨再提交一次:
12 |
$ vim test.rb$ git commit -a -m "made a change" |
HEAD 分支隨著提交操作自動向前移動.如圖所示,你的 testing 分支向前移動了,但是 master 分支卻沒有,它仍然指向運行 git checkout 時所指的對象。 這就有意思了,現在我們切換回 master 分支看看:
1 |
$ git checkout master |
檢出時 HEAD 隨之移動.這條命令做了兩件事。 一是使 HEAD 指回 master 分支,二是將工作目錄恢復成 master 分支所指向的快照內容。 也就是說,你現在做修改的話,項目將始於一個較舊的版本。 本質上來講,這就是忽略testing 分支所做的修改,以便於向另一個方向進行開發。可以使用 git branch命令查看當前分支,注意前面帶*的表示當前分支

Note分支切換會改變你工作目錄中的文件在切換分支時,一定要注意你工作目錄里的文件會被改變。 如果是切換到一個較舊的分支,你的工作目> 錄會恢復到該分支最後一次提交時的樣子。 如果
Git不能幹凈利落地完成這個任務,它將禁止切換分支。
合併分支(快速合併)
假如我們在testing上的工作完成了,就可以把testing合併到master上。Git怎麼合併呢?最簡單的方法,就是直接把master指向testing的當前提交,就完成了合併,這裡你需要使用git merge命令
123456 |
$ git merge testingUpdating 64ba18a..760118bFast-forward hello.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 hello.txt |
git merge命令用於合併指定分支到當前分支。合併後,再查看內容,就可以看到,和testing分支的最新提交是完全一樣的。注意到上面的Fast-forward信息,Git告訴我們,這次合併是「快進模式」,也就是直接把master指向testing的當前提交,所以合併速度非常快。當然,也不是每次合併都能Fast-forward,我們後面會講其他方式的合併。
刪除分支
合併完分支後,甚至可以刪除dev分支。刪除dev分支就是把dev指針給刪掉,刪掉後,我們就剩下了一條master分支,這裡需要使用git branch -d命令來刪除分支
12 |
$ git branch -d testingDeleted branch testing (was 760118b). |
分支合併衝突
人生不如意之事十之八九,合併分支往往也不是一帆風順的。準備新的dev分支,繼續我們的新分支開發:
12 |
$ git checkout -b devSwitched to a new branch "dev" |
修改README.md內容,添加一樣內容」day day up~」,在dev分支上提交:
123 |
$ git commit -am "one commit"[dev 6a6a08e] one commit 1 file changed, 1 insertion(+) |
切換到master分支:
123 |
$ git checkout masterSwitched to branch "master"Your branch is up-to-date with "origin/master". |
Git還會自動提示我們當前master分支比遠程的master分支要超前1個提交。在master分支上把README.md文件的最後改為 good good study,然後提價
123 |
$ git commit -am "two commit"[master 75d6f25] two commit 1 file changed, 1 insertion(+) |
現在,master分支和dev分支各自都分別有新的提交,變成了這樣:
這種情況下,Git無法執行「快速合併」,只能試圖把各自的修改合併起來,但這種合併就可能會有衝突,我們試試看:
1234 |
$ git merge devAuto-merging README.mdCONFLICT (content): Merge conflict in README.mdAutomatic merge failed; fix conflicts and then commit the result. |
果然衝突了!Git告訴我們, README.md文件存在衝突,必須手動解決衝突後再提交。git status也可以告訴我們衝突的文件:
1234567891011121314151617181920212223 |
$ git statusOn branch masterYour 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.mdno changes added to commit (use "git add" and/or "git commit -a")``` 我們可以直接查看`README.md`的內容: ``` bash$ cat README.md#gitLearn<<<<<<< HEADgood good study=======day day up>>>>>>> dev |
Git用<<<<<<<,=======,>>>>>>>標記出不同分支的內容,我們修改如下後保存:
1 |
#gitLearngood good studyday day up |
再提交:
12 |
$ git commit -am "merge commit"[master 9a4d00b] merge commit |
現在,master分支和dev分支變成了下圖所示:
用帶參數的git log也可以看到分支的合併情況:
123456789101112131415161718192021222324 |
$ 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`分支: ``` bash$ git branch -d devDeleted branch dev (was 6a6a08e). |
合併分支(普通合併)
通常,合併分支時,如果可能,Git會用Fast forward模式,但這種模式下,刪除分支後,會丟掉分支信息。如果要強制禁用Fast forward模式,Git就會在merge時生成一個新的commit,這樣,從分支歷史上就可以看出分支信息。下面我們實戰一下--no-ff方式的git merge:首先,仍然創建並切換dev分支:
12 |
$ git checkout -b devSwitched to a new branch "dev" |
修改README.md文件,並提交一個新的commit:
123 |
$ git commit -am "submit"[dev fee6025] submit 1 file changed, 1 insertion(+) |
現在,我們切換回master:
12 |
$ git checkout masterSwitched to branch "master" |
目前來說流程圖是這樣:
準備合併dev分支,請注意--no-ff參數,表示禁用Fast forward:
1234 |
$ git merge --no-ff -m "merge with no-ff" devMerge made by the "recursive" strategy. README.md | 1 + 1 file changed, 1 insertion(+) |
因為本次合併要創建一個新的commit,所以加上-m參數,把commit描述寫進去。
合併後,我們用git log看看分支歷史:
1234567 |
$ git log --graph --pretty=oneline --abbrev-commit* b98f802 merge with no-ff|| * fee6025 submit|/* 9a4d00b merge commit... |
可以看到,不使用Fast forward模式,merge後就像這樣:
分支管理策略
實際公司開發的時候一般3個分支就可以了:
- mster 主分支用來發布
- dev 日常開發用的分支
- bug 修改bug用的分支
首先,master分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面幹活;幹活都在dev分支上,也就是說,dev分支是不穩定的,到某個時候,比如1.0版本發布時,再把dev分支合併到master上,在master分支發布1.0版本,你和你的小夥伴們每個人都在dev分支上幹活,每個人都有自己的分支,時不時地往dev分支上合併就可以了;bug分支用來處理日常bug,搞定後合到dev分支即可;
假設遠程公共倉庫,有一個master和一個dev分支,進行多人協作開發時候(每個人的公鑰必須加入到遠程賬號下,否則無法push), 每個人都應該clone一份到本地。 但是clone的只是master,如果遠程的master和dev一樣,沒關係;如果不一致,則需要clone出dev分支 git checkout -b dev origin/dev 之後每個人在本地的dev分支上獨自開發(最好不要在mast上開發), 開發完成之後push到遠程dev, git push origin dev。 之後審核人再確定是否合併dev到master。
當你從遠程倉庫克隆時,實際上Git自動把本地的master分支和遠程的master分支對應起來了,並且,遠程倉庫的默認名稱是origin。要查看遠程庫的信息,用git remote:
12 |
$ git remoteorigin |
或者,用git remote -v顯示更詳細的信息:
123 |
$ git remote -vorigin [email protected]:tengj/gitLearn.git (fetch)origin [email protected]:tengj/gitLearn.git (push) |
上面顯示了可以抓取和推送的origin的地址。如果沒有推送許可權,就看不到push的地址。
推送分支,就是把該分支上的所有本地提交推送到遠程庫。推送時,要指定本地分支,這樣,Git就會把該分支推送到遠程庫對應的遠程分支上:
1 |
$ git push origin master |
如果要推送其他分支,比如dev,就改成:
1 |
$ git push origin dev |
多人協作時,大家都會往master和dev分支上推送各自的修改。現在,模擬一個你的小夥伴,可以在另一台電腦(注意要把SSH Key添加到GitHub)或者同一台電腦的另一個目錄下克隆:
1234567891011 |
$ git clone [email protected]:tengj/gitStudy.gitCloning 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`命令看看:``` bash$ git branch* master |
現在,你的小夥伴要在dev分支上開發,就必須創建遠程origin的dev分支到本地,於是他用這個命令創建本地dev分支(程分支dev要先創建)。
12 |
$ git checkout -b devgit |
創建dev分之後,先同步遠程伺服器上的數據到本地
123 |
$ git fetch originFrom git.coding.net:tengj/gitStudy * [new branch] dev -> origin/dev |
現在,他就可以在dev上繼續修改,然後,時不時地把dev分支push到遠程:
1 |
$ git commit -am "test"[dev c120ad6] test 1 file changed, 1 insertion(+)$ git push origin devCounting 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 [email protected]:tengj/gitStudy.git 65c05aa..c120ad6 dev -> dev |
你的小夥伴已經向origin/dev分支推送了他的提交,而碰巧你也對同樣的文件作了修改,並試圖推送:
123456789 |
$ git push origin devTo [email protected]:tengj/gitStudy.git ! [rejected] dev -> dev (fetch first)error: failed to push some refs to "[email protected]:tengj/gitStudy.git"hint: Updates were rejected because the remote contains work that you dohint: not have locally. This is usually caused by another repository pushinghint: to the same ref. You may want to first integrate the remote changeshint: (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抓下來,然後,在本地合併,解決衝突,再推送:
1234567891011 |
$ git pull origin devremote: 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/devAuto-merging a.txtCONFLICT (content): Merge conflict in a.txtAutomatic merge failed; fix conflicts and then commit the result. |
因此,多人協作的工作模式通常是這樣:
- 首先,可以試圖用
git push origin branch-name推送自己的修改; - 如果推送失敗,則因為遠程分支比你的本地更新,需要先用
git pull試圖合併; - 如果合併有衝突,則解決衝突,並在本地提交;
- 沒有衝突或者解決掉衝突後,再用
git push origin branch-name推送就能成功!
如果git pull提示「no tracking information」,則說明本地分支和遠程分支的鏈接關係沒有創建,用命令git branch --set-upstream-to branch-name origin/branch-name。這就是多人協作的工作模式,一旦熟悉了,就非常簡單。
到此,Git分支管理就學完了,整理一下所學的命令,大體如下:
1234567891011 |
git branch 查看當前分支git branch -v 查看每一個分支的最後一次提交git branch -a 查看本地和遠程分支的情況git branch --merged 查看已經與當前分支合併的分支git branch --no-merged 查看已經與當前分支未合併的分支git branch -r 查看遠程分支git branch dev 創建分支 devgit checkout dev 切換到分支devgit checkout -b dev 創建並切換分支devgit merge dev 名稱為dev的分支與當前分支合併git branch -d dev 刪除分支dev |
最近擼了個java的公眾號,學習資源超級多,視頻,電子書,最新開發工具一個都不能少,已全部分享到百度雲盤,求資源共享,打造一個學習方便,工作方便的java公眾號,開源開源,有需求的可以關注~撒花
推薦閱讀:
※禮包|極市分享資源大整理
※有了Ta,你的圖片也可以下起雨來
※視頻轉場技巧大分享【PR學習乾貨】
※藝術乾貨:畫個可怕、不懷好意的吻,是種什麼體驗?|張小玉
※【乾貨碼頭】| 「易」神探:家中養魚的12條禁忌!
