Git原理與高階使用(3)

Desmonddai583發表於2018-04-21

遠端版本庫

相信大家對遠端版本庫都有所瞭解,並且也都有在使用類似github,gitlab或者bitbucket之類的服務,那我們這裡就主要來說一下本地版本庫與遠端版本庫互動時的一些注意點,通常在我們建立好了遠端倉庫並且打算將本地同步上去時會先執行git remote add origin https://github.com/test/test.git,這裡其實就可以當做我們給遠端版本庫取了一個別名origin,其實origin這個名字是可以隨便定的,當然服從規範的話我們通常不會去用其他的名字。接著我們就會執行git push -u origin master來將我們本地版本庫推送到遠端,這裡注意我們加-u其實是建立了一個origin和master之間的關聯,之後我們就只要簡單執行git push,git就會自動去選擇將master分支推送給遠端,同樣git pull也是。這時候我們可以通過執行git remote show來檢視現有的遠端地址,

Git原理與高階使用(3)

所以其實本地的一個版本庫是可以同時有多個遠端地址的,例如origin2,origin3。如果想要只檢視某個遠端地址的資訊和通本地的關聯可以使用git remote show origin

Git原理與高階使用(3)

這裡還有一點要注意的是,有時候在提交時會出現這樣一段警告

Git原理與高階使用(3)

這裡就涉及了git push的一個預設行為模式,在git 2.0之前預設是使用matching的,也就是它會將現在本地關聯的這條分支推送到遠端存在並且同名的分支,例如現在我們將master與origin關聯,那麼當我們使用matching模式去提交時,就會去找遠端是否有一個也叫做master的分支,如果有然後就將我們本地分支推送到遠端這個master分支,而在git 2.0之後就採取了一個相對保守的策略,也就是simple模式,它在push時用的遠端分支會是我們執行git pull時的那個遠端分支,例如說我git pull是預設是一個叫做dev的分支,那麼git push時就會推送去這個dev分支,為什麼說simple是更保守呢,就是因為matching其實是一種git大膽的猜測我們預設推送和拉取用的是同名的遠端分支和本地分支。我們可以像圖中所說使用git config去設定或改變預設行為模式。

在我們將本地版本庫與遠端版本庫做了關聯之後,每次我們呼叫git status就會看到這樣一條資訊

Git原理與高階使用(3)

這裡背後其實在本地我們會多了一個叫做origin/master的分支,它被用來追蹤遠端的master分支,每次當我們執行git pull時,其實背後是跑了兩條指令git fetch與git merge,git fetch會先同步遠端的master分支與本地的origin/master分支,然後再將origin/master分支merge到本地的master分支,而在我們執行git push時,也是先將本地分支與origin/master分支同步,再將本地master分支推送到遠端分支

要檢視遠端分支時執行git branch -a即可

Git原理與高階使用(3)

分支實踐

分支的實踐個人覺得沒有所謂的最佳,其實都是要根據不同專案的情況來決定,這裡就介紹一個自己平常比較多使用的方式,分為3個主要分支加一種型別的分支:

  1. develop分支 (頻繁變化的一個分支,主要用於開發人員每日開發所用)
  2. test分支 (供測試和產品等人員使用的一個分支,變化不是特別頻繁)
  3. master分支 (生產釋出分支,變化非常不頻繁的一個分支,通常會加予許可權來管理)
  4. bugfix(hotfix)分支 (這個代表的是一個型別的分支,也就是當生產系統中出現了緊急的bug,用於緊 急修復的分支)

裸庫

git裸庫實際上就是一個沒有工作區的git倉庫,那它的作用是什麼呢,其實就是用來做一個存放於中轉的倉庫,通常我們會把它放到自己的伺服器上,因為我們不需要在伺服器上去做檔案的操作,所以其實我們只需要有.git那個資料夾的內容即可,根據前面幾篇的內容我們知道其實所有版本資訊都在這個.git資料夾下。建立裸庫的話只要我們執行git init --bare即可

Submodule

在開發中,有時候我們可能有多個專案會依賴於一個庫,那如果我們把專案放在不同的版本控制系統,那麼每當我們的庫有更新時我們就需要重新把最新的庫拷貝過來重新部署升級到各個專案中,試想如果這個庫一天有多次的改動,那專案也要跟著去修改多次就顯得很沒效率,於是就有了git的submodule來專門解決這種問題,git submodule可以讓我們在專案中引用另外一個版本庫的專案,使它在我們當前的專案中可見,並且只要另外那個版本庫一旦有更新,我們要做的只是使用一條git指令然後就會自動更新最新的程式碼了。我們通過在專案中執行git submodule add git@github.com:desmond/git_child.git mymodule就可以將git_child這個專案以submodule的形式加入到我們當前專案的mymodule資料夾下,注意的是這個目錄事先不可以存在,如果已存在的話git會報錯

Git原理與高階使用(3)
這時候可以看到child就成功加入到了當前專案中,並且git還會建立一個.gitmodules的檔案,裡面記錄了submodule的資訊

Git原理與高階使用(3)

當child發生了更新時,我們只要在mysubmodule中執行git pull就可以獲取到最新的提交資訊了,那假設我們專案中有多個submodule的話那更新不就很麻煩了嗎,git其實也提供了一個git submodule foreach git pull,當我們在專案根目錄下執行後,git就會迴圈的給每個submodule都執行一次pull操作

當然在pull完之後,我們還要記得對submodule執行一次git push才會真正把本地專案中submodule的改動推送到遠端版本庫中

另外要注意的是,如果我們是第一次從遠端版本庫clone下來時,git是不會幫我們把submodule中的內容也一起獲取的,這時候我們就要先執行git submodule init,再執行git submodule update --recursive,此時本地就會獲取到submodule中的內容。不過git clone也提供了一個選項幫我們簡化上面的操作,我們只需執行git clone git地址 --recursive就能做到上面的效果了

如果我們clone完之後進入submodule的資料夾,就會看到現在的分支會變成一個遊離的狀態指向submodule的最新commit,不過本質上它就是我們submodule上master的最新commit,所以我們可以直接執行git checkout master切換回去

Git原理與高階使用(3)

Subtree

subtree和submodule其實解決的都是同一類問題,但當我們在使用submodule時通常是通過更新被依賴的module然後再在使用這個module的專案中去更新,但是如果反過來想做到修改專案中的module也可以更新module本身的話submodule就會出現些bug,所以我們就引入了subtree。也就是說subtree可以用來完成雙向修改。所以在我們掌握了subtree之後,其實就完全可以代替原本的submodule了,git官方也是這麼推薦的。

首先我們執行git remote add subtree-origin git地址在本地專案中新增module的遠端地址,接著執行git subtree add --predix=subtree subtree-origin master --squash就可以在本支的subtree資料夾下加入module的master分支內容,這裡squash的意思是在加入module內容時,會將module的所有commit合併成一個commit合併進本地的內容,這樣在本地的提交記錄中我們就只會看到一條關於module庫的commit。而如果不加squash的話,就可以看到module的每一條commit都會被合併到本地

下圖可以看到如果使用squash的情況下產生的commit,其實就兩條,一條就是把所有的commit合併成一個,另一條就是將這條commit合併成一個subtree

Git原理與高階使用(3)

當我們建立submodule時,其實那個資料夾代表的是指向module地址的一個引用,而subtree是真正的在專案中放入了module的內容,這是它與submodule不同的地方

那麼此時如果module本身更新了,我們可以在專案中執行git subtree pull --prefix=subtree subtree-origin master --squash來更新專案

那如果我們要通過更新專案自身來後將修改apply到module時要怎麼做呢,我們只需要執行git subtree push --prefix=subtree subtree-origin master即可,但是這裡如果我們使用squash選項的話其實是會失敗的,這裡就牽扯到了squash這個選項的問題,當我們使用squash時其實我們等於是產生了一個新的commit,即便我們知道這個commit其實就是把三個commit合併在一起,但是在commit物件鏈中通過該commit是無法與module裡面的commit聯結上的,這就會導致我們在git pull和git push時由於沒有共同的父commit物件而出現conflict的情況,需要我們手動去處理,那如果不加squash呢,那這個問題就可以解決了,因為兩邊其實都是擁有一樣的commit物件鏈,但是不用squash的話,我們就會在專案本身中看到module的commit,這部分commit其實我們大多數時候是不關心的,同時如果專案中有除了subtree修改提交外還是其他檔案的修改,那當我們同步推送回module本身時,也會把這些不屬於module的commit給同步過去,這樣就會造成兩邊都被汙染。所以大家可以根據實際情況來選擇使用哪一種,但是一個宗旨就是如果一開始就使用squash選項,那麼務必要確保之後所有subtree操作都要使用squash,反之亦然。

指令

  1. 檢視所有分支(包括在本地的遠端關聯分支)

    git branch -a,如果想顯示每條分支的最後一條commit,可以執行git branch -av

  2. clone遠端專案並自定義資料夾名

    git clone 遠端地址 資料夾名

  3. 為分支建立一個與遠端的關聯

    git push --set-upstream origin 分支名,這時候關聯就會建立並且在本地多了一個'origin/分支名'的branch,其實這條命令與git push -u origin master是達到一樣的效果,不過git新版本推薦是使用--set-upstream的做法

  4. git拉取之後怎麼建立遠端的分支

    通常在我們拉取了遠端倉庫之後會在本地建立一個關聯分支

    Git原理與高階使用(3)

    Git原理與高階使用(3)
    那在我們獲取到關聯分支後如何建立一個本地的與之對應的分支呢,我們可以執行git checkout -b test origin/test來建立,這樣就會多出一個本地的test分支並且commit物件鏈與origin/test一樣,另外,我們也可以使用git checkout --track origin/test來建立一個本地分支,它與前者的區別就在於它會自動用origin/後面的這個遠端名字來作為本地建立的分支名

  5. 如何刪除遠端的一個分支

    第一種方法我們可以通過執行git push origin :test,這裡其實代表的意思是我們將一個空分支推送到遠端的test分支,就變相做了一個刪除的動作,第二種方法比較直接,就是執行git push origin --delete test

  6. 建立一個與本地分支名不同的遠端分支

    git push --set-upstream origin develop:develop2這條指令就代表在遠端建立一個develop2分支並且與本地develop分支關聯,但是如果這個時候我們在本地develop分支呼叫git push會受到這樣一個錯誤警告

    Git原理與高階使用(3)
    也就是說在遠端與本地分支不同名時,我們只能通過git push origin HEAD:develop2或者git push origin develop:develop2的方式來推送,當然其實這兩條命令背後是一樣的,因為當前HEAD指向的就是develop分支

  7. 修改遠端倉庫的名字

    git remote rename origin origin2,這樣就把遠端倉庫名從origin重新命名為origin2了

  8. 刪除遠端倉庫名

    git remote rm origin

場景

  1. 如何刪除submodule呢

    git並沒有提供直接的指令幫助我們刪除submodule,所以我們就需要先執行git rm --cached mymodule將submodule從暫存區中移除,再之心rm -rf mymodule將資料夾徹底刪除,接著提交我們的修改,同時.gitmodule這個檔案也沒有用了,所以也可以通過上述方法將其刪除,這樣我們就成功的刪除了submodule

  2. 怎樣跟上當初fork的專案記錄

    我們可以通過在本地fork的版本庫中加入一個原作的遠端分支,然後執行git fetch將原作的最新程式碼拉取下來,通常我們會把這個分支名叫做upstream,接著我們可以執行merge操作,將原作的upstream分支merge到我們當前分支,然後提交推送,就跟上最新記錄了。

Q&A

  1. 使用subtree時,有什麼方法可以既不會汙染專案本身又可以不會經常出現需要解決衝突的情況

    這裡要先說的一個概念是squash其實並不是subtree這個指令專有的,git merge同樣也可以指定這個選項並且作用是相同的

    我們可以通過另外建立一個類似叫no-squash的分支然後不加squash選項更新subtree,這樣就保留了module的歷史記錄,沒有煩人的反覆衝突問題,然後再將no-squash分支合併到主分支,但是這裡合併時用的是squash的方式git merge --squash, 這樣專案的主分支上只會體現一個commit,比直接git subtree add/pull --squash還要簡潔(原本是兩個commit)。

    當然這種做法也有缺點,首先新開的分支歷史記錄就會稍顯混亂,另外就是每次新分支做subtree的操作就要記得merge一次回master,但我自己覺得還是在能夠接受的範圍內得。

  2. 在使用git pull時經常會出現多了一個merge commit的情況,這是為什麼呢

    其實經常會出現別人推送到遠端後我們又做了一些修改,這時候當我們執行git pull時,會先執行git fetch,接著執行git merge,這時候其實很多時候會因為合併產生額外的commit,這時候我們就可以通過執行git pull --rebase來解決這個問題

  3. 假設沒有github,要怎麼在兩個host之間傳修改呢

    這個算是比較冷門一點的問題,不過git也是有方法幫我們來做的,我們可以通過執行git format-patch -n -o path這裡n只的是最近的n條commit,然後我們使用-o後面接一個路徑,接著git就會在指定路徑中建立每個commit對應的一個patch檔案,接著我們可以把這些patch檔案通過email等方式傳給另外一個host,然後在那個host的專案中執行git am path,這裡的path就是放傳過來的這些檔案,這樣git就會幫我們更新了

相關文章