《Pro Git》筆記3:分支基本操作


《Pro Git》筆記3:Git分支基本操作

分支使多線開發和合並非常容易。Git的分支就是一個指向提交對象的可變指針,極其輕量。Git的默認分支為master。

1.Git數據存儲結構和分支

 git提交時會將暫存文件內容暫存的目錄結構提交對象含附注標簽對象都以包含信息頭的二進制文件形式存儲到版本庫中(.git/objects目錄),存儲的對象以其自身SHA1值作為唯一標識,SHA1前兩位為存儲對象所在目錄名,SHA1后38位為存儲對象的文件名。存儲的數據對象類型有:

  • blob文件內容
  • tree目錄對象或者說是目錄快照對象,其內容為目錄下所有文件名和文件內容blob對象SHA1的映射,以及子目錄名和子目錄tree SHA1的映射)
  • commit提交對象,內容包含父提交對象SHA1,頂層tree對象SHA1,作者提交者信息,時間戳,空行,提交說明) 
  • tag含附注標簽對象

分支是指向提交對象的指針(refs,分支本質上是存放在.git/refs目錄的文件,其中保存了指向的提交對象的SHA1值)。可見git分支代價微乎其微,可以隨意使用。本地分支是可變指針,遠程分支和輕量級標簽是不可變指針無法直接改變指向。刪除分支也就是刪除指針而已。

HEAD指針:記錄git當前正在哪個分支上工作,是指向分支的指針切換分支時變更HEAD指向。該文件位置為.git/HEAD。其內容是當前所在的分支的全名,如ref: refs/heads/master。

 

2.使用和切換分支

管理分支使用git branch命令。

git branch                    #不帶任何參數時,列出當前所有的分支

git branch -v                #列出所有分支時還,顯示每個分支的最后一次提交SHA1和提交說明

git branch --mered | --no--merged       #顯示已經合並了的分支或還未合並的分支。

 

git branch <分支名>      #從當前所在分支創建一個新分支

git checkout <分支名>   #切換到指定分支(變更HEAD指針並切換工作目錄內容),切換分支前應保持工作目錄干凈,以免檢出時產生沖突。

git checkout -b <分支名>  #從當前分支創建新分支並切換到新分支,等效於前兩條命令的組合。

git checkout -b <分支名> [分支|標簽|SHA1]   #從特定版本創建並切換到新分支

 

a.git默認存在一個叫master的分支指向最后一次提交,默認分支上多次提交后如圖3.3。 

b.在當前提交上創建新分支后(調用git branch testing),實際上只是增加了一個指向當前提交的指針(圖3.4)。

c.切換分支(調用git checkout testing),git做了兩件事:一是修改HEAD指針使其指新的向當前分支(圖3.5,3.6),二是將新當前分支指向提交的快照換進工作目錄。

 

d.在新分支上繼續開發,提交新的版本時(git commit -am "made a change"   ),當前分支(testing)會自動移動指向新的提交。非當前分支(master)不移動,如圖3.7。

e.切換到master分支,HEAD指針指向切換到的分支,並且工作目錄換入切換到的分支的版本(f30ab)(圖3.8)。

f.在master分支上繼續提交其他修改,項目並行向不同方向開發(圖3.9),隨時可以在不同線路上切換,互不影響。

 

3.分支合並(merge)

合並的本質就是在分支間復制變更。要將分支A合並到分支B,就是將A中有而B中沒有的差異復制到B中。合並時以當前所在分支B為合並的基礎,接收差異並被更新。合並命令中指定了差異來源分支A,合並結果就是來源分支A中獨有的修改復制到了當前分支B,如果B中的內容在納入A的修改后發生了變化,還會創建一個新的合並提交(變更來自兩個父提交)將合並結果記錄到版本庫。

基本合並主要分為幾種情況:  

  • 快進合並(一個分支指向的提交是另一個分支指向提交的祖先)。
  • 基於共同祖先的三方合並(兩個分支在一個共同祖先之后走向了分叉的兩邊)。
  • 兩方合並(兩個分支指向的提交無共同祖先,是孤立的,也無法參考共同祖先自動合並)

(1)快進合並

示例項目中,master為主干分支用於發布穩定版本。首先在master分支基礎上創建了iss53分支來開發新功能,提交了C3,開發過程中老版本出現重要bug,於是又重在master分支的基礎上創建了hotfix分支來修復bug,提交了C4,就形成多線並行開發的兩個分支。如下圖。

解決bug后提交C4並測試無誤,就可以將包含bug修復內容的hotfix合並到master中了。master指向的提交C2是hotfix指向提交C4的祖先,那么直接移動master指針使其指向hotfix所在的提交,masert分支自然也就包含了hotfix的全部變更。只需要移動指針,不需要再創建新的快照和合並提交,這就是快進合並。合並結果如下圖:

git merge  <變更來源分支名>   #將指定分支合並到當前分支,也就是將指定分支中特有的變更都並入到當前分支里,不同情況下有不同的合並策略,如快進,三方合並。

 

hotfix並入master后,master分支就可以再次發布了。而hotfix也沒有什么作用了,可以直接刪除(分支就是指針,刪除分支僅僅是刪除這個指針)。調用git branch -d hotfix刪除hotfix分支后如下圖。

git branch -d <分支名>      #刪除已經合並了的分支,未合並過的分支會失敗

git branch -D <分支名>         #不管分支有沒有被合並,都刪除


解決了bug后就可以繼續在iss53上開發新功能了並提交了C5。bug是在C4中解決的,iss53分支的版本C3和C5中不包含修改bug的變更,因此iss53分支上bug仍然存在。可以立即將master(包含bug修復內容)並入iss53,也可以等iss53完成后並入master。

 

(2)三方合並

iss53完成后,並入master分支。同樣是先檢出master分支(git checkout mater),再調用git merge iss53。但是master指向的C4和iss53指向的C5沒有直接的祖先后代關系,而是在共同的祖先C2之后分叉了,合並時先要找到iss53對於master來說特有的變更(即C3和C5的變更),並將這些變更並入master指向的C4,C4的內容被更新了,然后還必須要為C4內容更新后的新狀態作快照並記錄到版本庫中(即新的提交C6)。在這個合並的過程中,iss53對於master來說特有的變更又是如何被找到的呢?Git會自動找到C5和C4的合並起來最佳的共同祖先C2,那么C5相對於C2的變更內容就是C4中沒有的。差異的計算過程中涉及C4,C5和共同祖先C2三個提交,所以這個合並是一次三方合並。三方合並中自動創建的提交C6內容來自兩個分支,其祖先不止一個,這類提交為合並提交

 

合並后master分支中就包含iss53中開發的新功能了,master可以發布,iss53使命也已經完成,可以刪除掉了。

 

回顧下三方合並的特點:直接在兩個分支最后一次提交快照(分支發展的最終結果)的基礎上操作,不考慮分支發展的中間過程,多個分支發展的中間過程以及所有的提交全部保留下來了。

 

(3)解決合並沖突

如果兩個分支都修改了同一個文件的同一部分,合並(快進合並不會沖突)這兩個分支時,Git就無法確定到底以哪個分支中的改動為准了(這種情況只能由人來做決定),這時Git就會將這個文件中的這些區域插入沖突標記(<<<<<< (當前分支版本) ==== (修改的 來源分支版本)>>>>>>)等着事后人工處理,然后繼續合並其他文件,合並出現沖突時也不會自動創建新的合並提交了,而是停留在沖突狀態,等待人工解決沖突。

合並中的沖突其實把合並過程中斷了,需要手動做兩件事才能完成整個合並操作手動解決沖突提交解決沖突后的合並結果

(A)查看沖突情況。沖突時,會給出如下提示。使用git status命令,也會列出沖突的文件(unmerged狀態)。

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

[master*]$ git status
index.html: needs merge
# On branch master
# Changes not staged for commit: (已修改還未暫存的項目)
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#           unmerged: index.html

存在沖突的文件中插入的沖突標記示例。

<<<<<<< HEAD:index.html   (當前分支版本)
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html  (修改的來源分支版本)

(B)手動解決沖突。也是分兩步:第一,就是編輯這些沖突部分的內容,去掉沖突標記,最終應該是什么樣子就編輯成什么樣子保存。第二,運行 git add <PATH> 暫存文件,表明沖突已經解決。解決沖突也可以使用其他文件合並工具(git  mergetool)。

再總結下git add的三個功能(實質上都是將對象加入暫存區):添加新追蹤對象(暫存還未追蹤的對象)暫存已修改的追蹤對象標記對象沖突已解決(還是暫存已修改的追蹤對象)

 

(C)提交。解決沖突后,當前工作目錄同時包含了兩個分支的內容,但只是臨時的,一切換分支或版本就都丟失了,還必須創建一個合並提交,將合並結果記錄到版本庫里。

 

4.分支衍合(rebase)

衍合(rebase)也是一種在分支間復制變更的方式,效果也是把指定分支的變更並入當前分支。

衍合這個詞不太好理解這種操作的作用,直接按英文rebase(更新分支起點)更容易理解。因為這個操作的本質就是提取一個分支中每個提交的變更,以rebase命令中指定的新分支為起點,將這些變更逐個重做一次。從最近的共同祖先開始經過了多少個提交,重做后就會生成多少個新的提交,當前分支原來指向的那些提交被丟棄,然后重新指向最新生成的提交。

它的原理是回到兩個分支(你所在的分支和你想要衍合進去的分支)的共同祖先,提取你所在分支每次提交時產生的差異(diff),把這些差異分別保存到臨時文件里,然后從當前分支轉換到你需要衍合入的分支,依序施用每一個差異補丁文件。

 

git rebase <新分支起點>   #將當前分支的變更,在新分支起點上重做

git rebase <新分支起點>  <特性分支>   #檢出特性分支,將特性分支的變更,在新分支起點上重做

git rebase   --onto <新分支起點>  <特性分支1>  <特性分支2>   #檢出特性分支2,找出特性分支2和特性分支1的共同祖先之后的變化,然后把它們在新分支起點上重演

 

舉個栗子,將分支experiment以master為新的分支起點進行衍合(將experiment中的變更以master指向的C4為起點重做一次)。衍合前的狀態如下圖

調用下面的衍合命令后。提取出experiment分支中C3的變更,以C4為基礎重演,產生了新提交C3',原來的C3被丟棄,experiment分支指向新的提交C3'。命令的效果就是experiiment的起點從C2移動到了C4,experiment分支中已經包含了master的變更。

$ git checkout experiment
$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: C3 commit comment

比較下合並和衍合的結果,衍合歷史更為清晰,但是會變動已有的提交(如丟棄了c3)。

 

另一個衍合的例子:原來分支A中所有修改都是以共同祖先(C2)為起點的。衍合(更新起點)以后,新的分支A中的所有提交就都以分支B為起點了,不同分支也在向一條主線並攏,分支A已經包含了分支B中的所有變更。

           

 

衍合的好處是:衍合操作會將不同分支上並行的提交慢慢並到一條主開發線上(並入的不是原提交本身,但提交中的變更是一樣的)。衍合不會產生一個額外的合並提交。衍合后的提交歷史非常清晰。

衍合的缺陷:衍合以后最初起點之后的那些提交就沒有用了,會被丟棄掉,如果這些提交已經被同步到遠程庫中,並且其他人引用了這些提交,最后就會陷入混亂。所以衍合僅僅應該用在不會推送到遠端的本地分支上來獲得清晰的歷史線。

 

5.衍合沖突解決

 只要是復制變更,就會出現潛在的沖突,衍合也不例外,而且情況更為復雜。衍合的過程是以一個新的提交為起點,一個提交一個提交的重新應用變更,那么每個提交的變更都有可能出現沖突。因為衍合是一個跨越多個提交的過程,應用每個提交時發生了沖突,就解決當前這些沖突,然后繼續(git rebase --continue)應用下一個提交,發生沖突再解決,直到所有提交都重新應用了。衍合中某個提交出現沖突,也可以跳過而不應用這個提交(git rebase --skip),也可以撤銷衍合過程(git rebase --abort),恢復到衍合命令之前的狀態。

 

假如test分支和master分支共同祖先提交為C1,test有C2,C3, master有C4,C5

$git checkout test

$git rebase master 

First, rewinding head to replay your work on top of it...  (當前分支HEAD切換到master)
Applying: commit2 in test branch       (應用test分支中C2的變更)
Using index info to reconstruct a base tree...
M README
Falling back to patching base and 3-way merge...    (采用三方合並,三個提交為C1,C2,C5)
Auto-merging README
CONFLICT (content): Merge conflict in README    (合並產生沖突)
Failed to merge in the changes.
Patch failed at 0001 commit2 in test branch            (test分支中的第一個提交C2在C5上應用失敗)
The copy of the patch that failed is found in:     
c:/Users/Administrator/git-test/.git/rebase-apply/patch   (C2的變更內容保存該文件中)

When you have resolved this problem, run "git rebase --continue".   (手動解決了沖突后,運行git rebase --continue完成應用第一個提交中的變更,繼續應用下一個提交中的變更)
If you prefer to skip this patch, run "git rebase --skip" instead.         (跳過這個提交,這個提交中的變更不重新應用到C5上,衍合后的test分支中不再包含這個提交中的變更)
To check out the original branch and stop rebasing, run "git rebase --abort".  (取消衍合,之前成功應用的變更都撤銷)

 

發生了沖突。此時調用git status可以看到

rebase in progress; onto 4fc0159                               (衍合進行中)
You are currently rebasing branch 'test' on '4fc0159'.
(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)

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: ccc.txt

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

both modified: README

 

手動編輯沖突的文件README,改為正確內容后后運行git add 移除沖突狀態,再運行git rebase --continue繼續衍合流程。

$git rebase --continue

Applying: commit1 in test branch
Applying: commit2 in test branch

 

衍合結束后,test分支丟棄了原來的提交C2,C3,包含了最新的提交C2',C3'

 

 

6.遠程分支和同步

git遠程版本庫和本地版本庫是版本數據互相備份的不同存放位置。從遠端克隆一個項目時,兩者完全相同。當本地不斷添加變更內容並提交版本時,慢慢的本地庫和遠端庫不再同步了。分支是指向提交對象的指針,本地提交新版本時,本地分支會不斷移動到最新提交。git還提供了一些額外的指針標記了最后一次同步時遠端庫中各個分支指向的位置。這些指針就是遠程分支,它們固定的標記遠端庫的分支狀態,所有只有同遠端同步的命令才會更新遠程分支。本地庫可以同時和很多個遠程庫協作,可以同時包含多個遠程庫的多個遠程分支。同步命令有以下幾條:

git clone

git pull     #將遠程庫數據同步到本地庫,並按照分支對象關系嘗試自動合並到本地分支

git push

git fetch <遠程庫名>   #將遠程庫數據同步到本地庫,不會合並

(1)本地分支直接用<分支名>表示。遠程分支用<遠程倉庫名>/<分支名>表示。克隆遠程倉庫(git clone)時會自動獲取遠程分支。

 

(2)同步分支

如果有人先在遠程庫中推送了提交,我們將這些提交更新(git pull或git fetch)到本地后會與本地的提交形成不同的歷史線路。使用git fetch獲取到新的遠程分支數據后並不會自動創建對應的本地分支,可以使用(git checkout -b [本地分支名] [遠程庫名]/[遠程分支名])或git branch命令手動創建。

 

本地庫可以同時與多個遠程庫的多個分支協同工作。

 

(3).推送分支

本地分支默認都是私有的,只在本地庫可見。只有那些確實需要與他人協作的分支才有必要推送到遠程庫中。

git push <遠程庫名> <分支名>

git push <遠程庫名> <本地分支名>:<遠程分支名>

 

(4)追蹤遠程分支

從某個遠程分支檢出一個新的本地分支后,這個本地分支就是跟蹤分支。跟蹤分支記錄了與那個遠程分支關聯,git pull,git push就知道當前分支默認與哪個服務的那個分支同步了,可以不用額外的參數。手動指定追蹤關系命令為:

 

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"

 

 

(5)刪除遠程分支

刪除遠程分支命令沒有單獨的命令,而是在普通推送命令中將本地分支參數設為空即可。

git push <遠程庫名>     :<遠程分支名>   #刪除遠程分支,可以理解為在這里提取空白然后把它變成[遠程分支]

 

 

參考:

《Pro Git》

 http://gitbook.liuhui998.com/index.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM