Git應用詳解第七講:Git refspec與遠端分支的重要操作

AhuntSun發表於2020-04-17

前言

前情提要:Git應用詳解第六講:Git協作與Git pull常見問題

這一節來介紹本地倉庫與遠端倉庫的分支對映關係:git refspec。徹底弄清楚本地倉庫到底是如何與遠端倉庫進行聯絡的。

一、Git refspec

refspecReference Specification的縮寫,字面意思就是具體的引用。它其實是一種格式git通過這種格式來表示本地分支遠端分支的對映關係;

在本地倉庫建立master分支外的其他兩個分支developtest:

image-20200329114836186

develop分支上執行git push命令,出現如下錯誤:

image-20200329114910354

這是由於本地分支develop沒有與任何的遠端分支建立聯絡導致的。通過git branch -vv檢視本地與遠端分支的關聯情況,可見並沒有建立任何聯絡:

image-20200416172209683

二、本地遠端分支

在講解如何建立與本地分支關聯的遠端分支之前,首先我們來介紹期待已久的本地遠端分支:

  • git中其實有三種分支:本地分支、本地遠端分支、遠端分支;
  • 可以這樣理解:本地遠端分支是遠端分支的一個映象,並且在本地倉庫與遠端倉庫之間起到一個橋樑的作用;
  • 在沒有辦法直接檢視遠端倉庫的時候,可以通過本地遠端分支觀察遠端分支的變化情況。比如本地遠端分支origin/develop就對應著遠端分支develop

1.三分支關係

當本地master分支建立了與之關聯的遠端分支master後,檢視當前分支狀態:

image-20200416172925457

圖中的origin/master為本地遠端分支,代表的是遠端倉庫的master分支,而這個分支是在本地的;也就是說加上遠端倉庫的master分支,一共有三個master分支:

image-20200407133027775

並且,當本地倉庫中的每一個分支都有與之關聯的遠端分支之後,本地倉庫都會建立對應的本地遠端分支,它們所處的位置和關係如下圖所示:

image-20200407134533388

可以這樣理解:本地遠端分支origin/master為遠端分支master的本地化形式;

假設遠端倉庫和本地倉庫檔案內容是一樣的,都只有兩次提交,此時三個分支的狀態如下圖所示:

image-20200407133442447

然後,在本地的master分支中新增了提交3rd,本地倉庫的分支情況變為:

image-20200416174501257

上圖中的git dog為指令:git log --all --decorate --oneline --graph的別名,有關內容將在下一節講解。

分支的示意圖如下:

image-20200407133629409

可見本地master分支比本地遠端分支origin/master多了一次提交。這是因為本地遠端分支是為了追蹤遠端分支而存在的,只有在執行pullpush操作時它的指向才會更新。比如在執行推送(push)指令時:

  • 首先,本地master分支對應的本地遠端分支(origin/master)會指向本地master分支最新的提交(向前走了幾步);

  • 然後,本地master分支再將檔案推送到遠端master分支中。完成推送後,三分支的狀態為:

    image-20200416175059690

回到終端,我們將剛才新增的提交3rd推送到遠端分支,成功後檢視本地分支以及本地遠端分支的提交歷史:

image-20200416180507680

可見,本地遠端分支的指向得到了更新,指向了最新的提交3rd,由此驗證了上述說法。

檢視分支關聯

可以通過以下指令檢視本地分支與本地遠端分支的關聯情況:

git branch -vv

image-20200410120936906

可以看到:本地的master分支有本地遠端分支origin/master關聯,說明本地master分支已經和遠端master分支建立了關聯;

其餘兩個本地分支popdevelop並沒有與之關聯的本地遠端分支,所以它們並沒有與遠端分支建立聯絡。

簡單點說:只要本地分支有與之對應的本地遠端分支,就有與之對應的遠端分支

總結:origin/master作用:追蹤遠端分支。當執行git push/pull操作時,該分支的指向都會相應地發生變化,用於與遠端倉庫保持同步;比如:本地倉庫在執行git push操作的時候,不僅會把本地的修改推送到遠端;還會同時修改origin/master分支的指向;

2.實戰演示

可通過該指令檢視本地的所有分支及其最新的提交資訊

git branch -av

image-20200328164526656

首先,在master分支上進行三次提交,並將它推送到與之關聯的遠端master分支,此時各分支的提交歷史為:

image-20200416181958738

三個分支的狀態為:

image-20200416182211614

在此基礎上,在master分支上進行一次提交4th,然後檢視狀態git status

image-20200416182358491

圖中提示資訊表明,當前分支(master)已經領先於origin/master分支一次提交。為了看得更清楚,我們檢視本地各分支的提交歷史:

image-20200416182650885

從圖中可看出,origin/master分支確實落後了一次提交,表示遠端master分支落後了一次提交。此時可以使用git push將新增的提交推送到遠端master分支,在這個過程中會將本地遠端分支origin/master指向最新的提交4th。成功推送之後,再次檢視本地各分支的提交歷史:

image-20200416182921683

可見,通過git push操作本地遠端分支確實發生了更新,指向了最新提交4th。這就驗證了執行git push時進行了兩步操作:

  • 將本地master分支的新提交推送到與之關聯的遠端master分支;
  • 將本地遠端分支origin/master指向本地master分支的最新提交;

git pull操作同理,也會更新本地遠端分支的指向;

也就是說:每次執行pushpull操作後,本地分支、本地遠端分支、遠端分支三個分支的指向都會達到同步。

當切換到origin/master分支上時,如下圖所示:

image-20200416183352637

git並不會直接將分支切換到origin/master上,而是切換到最新的一次提交上,即一個遊離的提交。這從側面說明了:git禁止我們直接修改origin/master分支的,只允許我們切換到最新的提交上;

也就是說本地遠端分支(如origin/master)是隻讀的,只能由git來改變,這就解釋了為何使用git branch無法檢視本地遠端分支。

image-20200416183719001

三、設定遠端分支

弄清楚了什麼是本地遠端分支,就能更好地理解接下來所要介紹的,如何建立本地分支與遠端分支的聯絡了。

1.設定同名遠端分支

上圖提示資訊中的:upstream branch表示上游分支,即遠端倉庫的分支。當前的本地分支develop並沒有一個遠端倉庫的develop分支與之對應;要想推送develop分支到遠端倉庫的同名分支,首先要建立對應的遠端分支,有以下兩種型別四種方法:

  • 型別一:建立本地與遠端分支追蹤關係的。

    git push --set-upstream origin <branch>
    git push -u origin <branch>
    

    使用該型別方法,只需設定一次,之後就可以使用簡寫形式git push進行推送。

  • 型別二:不建立本地與遠端分支追蹤關係的。

    git push origin HEAD
    git push origin <branch_name>
    

    使用該型別方法,每次推送都需要採用上述的完整寫法。

下面就來詳細介紹這四種方法:

git push --set-upstream origin <branch>

方法一:採用下述指令為本地倉庫mygitdevelop分支建立遠端分支:

git push --set-upstream origin develop

該命令的作用為:在遠端倉庫創鍵一個與本地分支develop關聯的同名分支develop,並將本地分支develop的檔案推送到該遠端分支上。

也就是將本地分支develop的上游分支設定為遠端倉庫的develop分支,並進行檔案同步。

執行完上述命令後會有這樣的提示:

image-20200416172401646

表示本地的develop分支已與遠端的develop分支建立聯絡;此時檢視本地分支,會發現多了一個本地遠端分支origin/develop,並且已與本地develop分支建立了聯絡:

image-20200416235458838

隨後再次執行git push就不會出現問題了:

image-20200329120642796

此時在github上檢視對應的遠端倉庫,就能檢視到新增的遠端分支develop了:

image-20200329122141124

上圖中的master分支是遠端倉庫建立時預設建立的,並沒有與本地master分支建立聯絡。

隨後點開branch可以看到:

image-20200329122249532

當前一共有兩個分支,master分支是default(預設)分支,是不能夠被刪除的;活躍的分支為deavelop

git push -u origin <branch>

方法二:先切換到test分支,再執行以下命令,為本地倉庫mygittest分支建立對應的遠端分支:

git push -u origin test

image-20200417000257150

-u--set-upstream作用是類似的,都是在遠端倉庫新建一個新的分支,並與本地分支建立聯絡。

執行完上述指令後,再次檢視本地分支的詳細情況,以及分支對應關係,可以發現test分支已與遠端test分支建立聯絡:

image-20200417000515217

git push origin HEAD

方法三:

如下圖所示,通過該指令成功設定了本地develop分支對應的遠端develop分支。但沒有顯示追蹤資訊,之後不能使用git push推送。

image-20200417121129943

git push origin <branch>

方法四:

如下圖所示,該方法實質上與方法三相同,因為HEAD指向的就是當前分支。同樣沒有顯示追蹤資訊,之後也不能使用git push推送。

image-20200417121419529

總結:當本地分支與遠端分支同名時,一旦手動建立了它們之間的聯絡。之後推送本地分支的檔案到對應的遠端分支時可以採用簡寫形式:git push

這是因為在已經建立三個分支的對應關係並後,再執行git pushgit會自動地將同名的本地分支與遠端分支進行匹配;

而其他情況則要採用完整寫法進行推送。關於這些結論,將在第三大點-u引數的作用中詳細介紹。

2.設定不同名遠端分支

主要有以下四種方法,注意:使用每種方法前都需要先切換到對應分支上。

git push --set-upstream origin <branch1>:<branch2>

方法一: 比如當前位於develop分支,如果採用的是以下簡寫命令:

 git push --set-upstream origin develop

則會建立一個同名的遠端分支develop。而如果採用該命令的完整寫法,就可以自定義遠端分支的名字了,比如設為develop2

git push --set-upstream origin develop:develop2

image-20200417001750391

執行上述指令後,成功建立了對應的,不同名的本地遠端分支origin/develop2。表示本地develop分支已與遠端develop2分支建立聯絡(因為遠端分支與本地遠端分支是一一對應的關係):

image-20200417001824384

github上檢視本地倉庫關聯的遠端倉庫MY,可以看到順利建立了develop2分支:

image-20200417002057662

可以發現這麼一個規律:在建立遠端分支的同時會建立同名的本地遠端分支。

git push -u origin <branch1>:<branch2>

方法二:

如下圖所示,使用-u引數也能將本地develop分支的遠端分支自定義為develop2

image-20200417103419756

git push origin HEAD:<branch>

方法三:

通過該方法也能成功設定與本地分支關聯的,不同名的遠端分支develop2

image-20200417003727161

git push origin <branch1>:<branch2>

方法四:該方法與方法二實質上是一樣的,因為方法二中的HEAD指標指向的就是當前所在的分支,也就是develop分支。過程與方法二類似:

image-20200417004155159

上面這四種設定不同名遠端分支的方法,都有一個共同特點:不能使用git push進行推送

若使用git push都會出現找不到對應遠端分支的錯誤:

image-20200417004455754

原因在下面第三點的-u引數作用中會詳細講解。

既然是-u引數追蹤問題,那我加上-u引數不就行了麼?其實這樣也行不通:

image-20200417005033407

解決方案:每次推送的時候,指明本地分支與遠端分支的對應關係,即採用上述命令的完整寫法,比如:

git push --set-upstream origin develop:develop2
git push -u origin develop:develop2
git push origin develop:develop2
git push origin HEAD:develop2

採用了完整寫法後,成功地進行了推送,如下圖所示:

image-20200407215703682

注意:雖然可以自定義遠端分支與本地遠端分支的名字,但是十分不推薦,因為容易出錯。所以,建議本地遠端分支和遠端分支都使用預設的,與本地分支相同的名字。

3.總結

以本地分支develop為例,不難發現:

  • 使用下列簡寫命令時,遠端分支和本地遠端分支都會採用預設的,與本地分支相同的名字:

    git push --set-upstream origin develop
    git push -u origin develop
    
  • 而使用下列命令的完整寫法時,就可以自定義遠端分支與本地遠端分支的名字:

git push --set-upstream origin develop:develop2
git push -u origin develop:develop2
git push origin develop:develop2
git push origin HEAD:develop2

四、git push origin mastergit push -u origin master的區別

第一次將本地倉庫的master分支推送到遠端倉庫的master分支上時,使用前者和後者都可以順利推送,區別在於是否使用了-u引數:

  • 推送時不使用-u引數:

    image-20200407204915066

  • 推送時使用-u引數:

    image-20200407205035250

注意到推送時使用-u引數會列印下列提示資訊:

Branch 'master' set up to track remote branch 'master' from 'origin'.

表示本地的master分支被設定去追蹤遠端的master分支,在第2~n次推送中,只需要使用git push這樣的簡寫命令(當然,完整寫法效果等同)。git就會自動將本地的master分支與遠端的master分支進行匹配,完成推送:

image-20200407205848977

而不使用-u引數時,沒有上述的分支追蹤資訊。此時使用簡寫git push進行推送會出現錯誤:

image-20200407210028495

錯誤資訊顯示:當前分支沒有與之對應的遠端分支。這個時候想要成功推送,必須採用指明對應關係的完整寫法,比如:

git push origin master

image-20200407210350445

這就是推送時使不使用-u引數的區別。並且,根據上面的介紹,使用如下指令進行推送也能達到-u引數的效果:

git push --set-upstream origin develop

image-20200407210704627

之後也可以使用簡寫的git pull指令進行推送:

image-20200407210730668

細心的你一定發現了,以上都只是本地分支與遠端分支同名的情況。不同名的情況下,上面的兩個方法還好使嗎?

首先驗證方法一:-u引數:

image-20200407211150483

設定不同名的遠端分支時要注意寫成完整形式:pop:pop2

可以看到,即使建立不同名的遠端分支,-u引數也一樣能夠設定追蹤關係;但是,奇怪的是git push卻不好使了:

image-20200407211424929

還是和沒使用-u引數時一樣,找不到對應的遠端分支,需要採用指明對應關係的完整寫法,比如:

git push origin pop:pop2

image-20200407211604378

其次驗證方法二:--set-upstream

image-20200407212552925

同樣設定分支對應關係時要使用完整寫法。可以看到,該方法也設定了追蹤關係。奇怪的是git push同樣不管用:

image-20200407212657862

同樣找不到對應的遠端分支,需要採用指明對應關係的完整寫法,比如:

git push origin bob:bob2

image-20200407212901263

所以可以得出結論:

  • 本地/遠端分支同名時:
    • -u引數的作用是設定本地分支與遠端分支的追蹤關係,設定了追蹤關係後,之後的推送可使用簡寫git pushgit內部會自動進行匹配;
    • --set-upstream引數與-u引數效果等同;
  • 本地/遠端分支不同名時:
    • --set-upstream引數與-u引數依然可以設定分支的追蹤關係,但是,之後的推送不能使用簡寫git push,只能使用指定分支對應關係的完整寫法;

總結:十分建議將所有的本地分支與對應的遠端分支設為同名,並且第一次推送使用--set-upstream-u引數建立分支追蹤關係,之後就可以使用簡寫git push進行推送了!

五、git push -f

該命令的完整寫法為:

git push -f origin master

意思為強制推送:直接跳過與遠端倉庫的master分支合併的環節,強制覆蓋遠端倉庫上master分支的內容,即以本地的master分支內容為準。應慎用該命令,否則將覆蓋遠端倉庫中master分支上其他人推送的檔案(一星期的成果沒了)。

1.應用場合

  • 當遠端倉庫的歷史提交記錄太亂了,想要重新整理時。注意:一定要與其他人協商好再用本地分支強制覆蓋遠端分支。
  • 只有一個人開發時,程式碼以本地為準。為了避免推送時繁瑣的合併,可以使用-f強制推送,直接覆蓋遠端分支上的內容;

分兩種寫法:

  • 第一種:已經通過-u引數等方式,設定了本地分支與遠端分支的追蹤關係時,採用:

    git push -f
    

    image-20200417113501672

  • 第二種:還未設定追蹤關係,採用:

    git push -u origin master -f
    

    image-20200417114605971

2.預防措施

Github提供了相應的分支保護機制,可以在Settings選項中進行設定:

image-20200409092021721

可以看到Github預設是保護分支的:

image-20200409092311908

3.補救措施

讓有進度的人,再次對被強制覆蓋的遠端分支執行一次 git push -f 指令,把正確的內容強制推送上去,覆蓋前一次 git push -f 所造成的災難。

六、設定遠端分支對應的本地分支

假如遠端倉庫M3Y中有masterdevelop兩個分支,此時新建一個空的本地倉庫mygit,通過以下指令將它的遠端倉庫地址origin設定為M3Y的地址:

git remote add origin git@github.com:AhuntSun/M3Y.git

此時兩倉庫的狀態為:

image-20200417140610948

由於mygit是空倉庫與遠端倉庫M3Y沒有任何公共提交歷史,所以在執行git pull時會出現下圖所示的不同源衝突(上一節中詳細介紹過該衝突):

image-20200417141801581

雖然git pull操作失敗了,但是也成功地將遠端倉庫M3Y的分支拉取了下來。但是,通過git branch -vv檢視分支追蹤關係,發現並沒有本地分支與這兩個遠端分支建立了聯絡:

image-20200417142011442

如何建立這兩個本地遠端分支對應的本地分支?可以通過以下兩種方法:

1.git checkout -b <branch> origin/<branch>

比如可以通過以下命令,設定本地遠端分支origin/master與本地master分支的追蹤關係:

git checkout -b master origin/master

image-20200417142353425

以上為本地master分支已存在的情況,如果本地分支develop未建立,可以採用下述命令建立並切換到develop分支,並且設定origin/developdevelop的追蹤關係:

git checkout -b develop origin/develop

image-20200417142901803

設定了本地分支與遠端分支的追蹤關係,接下來就可以在本地倉庫執行git push進行推送了:

image-20200417143211765

2.git checkout --track origin/<branch>

重置條件,新建立一個空的本地倉庫mygit2,同樣將其遠端地址origin設定為遠端倉庫M3Y的地址。隨後在本地倉庫mygit2中執行git pull操作,將遠端倉庫M3Y中的兩個分支拉取到本地:

image-20200417144521310

與上次一樣,拉取到本地的兩個本地遠端分支沒有與任何本地分支建立追蹤關係。這次可以採用另外一種方法:

git checkout --track origin/test

建立並切換到develop分支,並且設定該分支與origin/develop分支的追蹤關係:

image-20200417144827348

可以說該方法是方法一的特殊情況,因為該方法沒有指明建立的本地分支的名字,所以預設採用與遠端分支一樣的名字develop來命名;

如果想在本地建立一個develop2(不同名)的分支與本地遠端分支origin/test建立追蹤關係,則應採用第一種方法。

七、遠端分支資訊

可以進入.git目錄,檢視儲存遠端分支資訊的檔案:

image-20200417123200144

1.檢視config檔案

使用vim編輯器開啟該檔案,可以檢視到關於遠端分支的資訊:

image-20200417123001405

可以看到remote這一欄中有兩個資訊,第一個是遠端倉庫的url,第二個是fetch資訊,這兩個資訊尤為重要:

  • refs/heads/*表示遠端倉庫的refs/heads目錄下的所有引用都會寫入到本地的refs/remotes/origin目錄中;
  • 其中的+號是可選的,加了表示無論是否能夠自動合併,即是否為Fast Forward方式,都將遠端倉庫所有檔案拉取到本地。
  • 而不加+則表示如果不是Fast Forward方式就不拉取。一般情況下都是加上+號的,先把檔案拉取到本地,不是Fast Forward方式就手動合併;

2.檢視refs檔案

refs資料夾儲存著refspec的檔案,裡面維護著三個目錄:

image-20200417123302707

  • 第一個目錄heads:儲存的是本地倉庫的分支資訊:

    image-20200417123344501

    可以檢視其中一個分支:

    image-20200417123406330

    是一個SHA1值,表示分支就是一個指標,指向當前提交。

  • 第二個目錄remotes:裡面存放著遠端分支資訊,遠端倉庫中也存在這樣的目錄與檔案;

    image-20200417123559607

    從上圖可以看到,遠端分支只有master,沒有develop(因為之前被刪除了)。並且它們本質上也是一個代表提交的SHA1值:

    image-20200417125705684

    建立refspec對映(即本地分支、本地遠端分支、遠端分支三者間對應關係)後,git會獲取遠端上refs/heads下的所有引用,並將它們寫入本地的refs/remotes/origin目錄下。所以,可以通過檢視本地遠端分支(如origin/master)的方式檢視本地倉庫最後一次訪問遠端倉庫時,遠端倉庫master分支上的歷史提交記錄:

    //完整寫法
    git log refs/remotes/origin/master
    //進一步簡寫
    git log remotes/origin/master
    //繼續簡寫
    git log origin/master
    

    上述兩種省略的寫法最終都會轉換為完整的寫法:

    image-20200417125810727

  • 第三個目錄tags:存放標籤資訊,也是一個SHA1值:

    image-20200417130027257

    詳細內容將在下一節介紹。

八、刪除遠端分支

如下圖所示,遠端倉庫有三個分支masterdeveloptest

image-20200329125145378

通過前面的學習,我們知道通過下述指令可以刪除本地develop分支:

git branch -d develop

image-20200329125322296

那麼如何刪除遠端分支呢?

首先我們來看看git push的完整寫法:

git push origin srcBranch:destBranch
  • srcBranch表示本地的分支,destBranch表示對應的遠端分支;

  • 表示將本地的分支推送到遠端分支,這兩個分支可以不同;

  • 之所以可以直接使用git push是因為我們設定的本地分支和遠端分支的名字是相同的,並且手動建立了聯絡,所以git能夠自動識別;

明白了這點後,就不難理解下列刪除遠端分支的兩種做法了:

1.git push origin :destBranch

將空的分支推送到遠端分支,這樣就能將該遠端分支刪除;比如刪除本地分支develop的遠端分支:

git push origin :develop

image-20200417120547162

可以看到成功刪除了遠端分支develop以及它所對應的本地遠端分支origin/develop

注意:並不需要切換到需要刪除遠端分支的本地分支develop上,再執行上述指令。也就是說,可以在任意本地分支上刪除任意本地分支對應的遠端分支。

2.git push origin --delete destBranch

還可以採用更加直觀的--delete引數,比如刪除遠端分支develop

 git push origin --delete develop

image-20200417120853849

這兩種方式是等價的,可根據需求選擇。

3.git remote prune origin

該方法用於刪除無效的遠端分支對應的本地遠端分支,具體場合如下:

如圖所示mygitmygit2共享一個有三個分支的遠端倉庫:

image-20200409230534065

首先在mygit2中刪除遠端倉庫的develop分支,可以看到mygit2中遠端分支develop對應的本地遠端分支origin/develop被刪除了:

image-20200329161409609

然後在mygit中檢視遠端分支詳細資訊:

image-20200329161520419

可以看到提示資訊中顯示遠端分支develop對應的本地遠端分支origin/develop處於stale(腐爛,遊離)狀態,即該分支對於mygit來說已經失效,可以使用:

git remote prune origin

prune:裁剪)刪除mygit上這個無效的本地遠端分支:

image-20200329161857475

再次檢視分支資訊,可發現mygit中的本地遠端分支origin/develop已經被刪除了:

image-20200329161939271

注意:一般本地遠端分支設定了保護措施,不能隨意刪除;

九、重新命名分支

1.本地分支

可以通過以下命令,將本地分支dev重新命名為develop

git branch -m dev develop

image-20200407193850382

2.遠端分支

無法直接重新命名遠端分支,只能通過先刪除原來的遠端分支,再建立重新命名後develop分支對應的遠端分支,過程為:

//刪除遠端分支dev
git push origin :dev
//建立重新命名後develop分支對應的遠端分支
git push -u origin develop

由此間接地完成了遠端分支的重新命名。

以上就是本節的全部內容,相信看到這裡的你已經十分熟悉git refspec了。下一節將介紹git標籤與別名。

相關文章