GIT團隊合作探討之一-保持工作同步的概念和實踐

世有因果知因求果發表於2016-03-25

感謝英文原文作者,這是我看到的關於git協同工作寫的最清晰簡潔的文章了:

https://www.atlassian.com/git/tutorials/syncing/git-push  

SVN使用一個單一的中央庫,作為所有開發人員的通訊中樞,而合作是通過在開發人員的拷貝和中央庫之間傳遞變更集來實現的。這和GIT的合作模型大不相同。GIT下面,每一位開發人員都有一個repo的完整拷貝,本地工作完成後,開發人員就有了自己的本地歷史和分支結構。開發人員通常需要分享一些列commits(而不是單一一個變更集)。GIT不像SVN那樣從本地copy向中央庫遞交單個變更集,GIT實際上是讓你在不同的庫之間分享分支。

      下面的米in高齡允許你管理和其他庫之間的connections,通過pushing branches到其他的repo來發布Local history,而通過pulling branches來獲取其他人員的貢獻到local repo.

git remote

git remote 命令允許你create,view,delete和其他庫的connection. Remote connection更像一個bookmark書籤,而不是一個直接連結到其他的庫中。這些remote connection並不會提供對其他庫的實時訪問機制,他們僅僅作為一種能夠用以引用其他repo的並不友好的url而方便使用的名字而已。

例如,下圖中顯示了從你的repo分別指向“中央庫”和其他開發人員庫。我們並不用使用全名稱的URL來引用其他庫,你只需要傳入origin和john短connection名稱給其他的git命令即可。

用法:

git remote
git remote -v     //列出所有指向其他repo的connections詳細資訊
git remote add <name> <url> //建立一個指向一個remote repo的新的connection.在上述命令執行後,你就可以通過使用<name>來作為引用<url>的便利手段了,比如:
git remote rm <name> //通過這個命令來刪除和remote repo的一個<name>連線
git remote rename <old-name> <new-name> //重新命名從old到new

 

探討:

Git被設計成給每一個開發人員一個完整獨立的開發環境。這意味著資訊並不會自動地在不同repo之間流動。相反,開發人員需要手工地pull upstream commits到本地repo,或者手動地push local commits到中央repo。git remote命令實際上僅僅是座位一種方便傳遞repo url到這些"sharing"命令的一種手段。

origin remote

當你clone一個repo時(git clone命令),git將自動建立一個被命名為origin的remote connection,這個origin connection將自動指向被clone的庫。這對開發人員建立中央庫的本地copy並且實現本地開發非常有用,因為通過origin,開發人員可以非常方便地pull upstream或者publish local commits.這種行為也是為什麼大多數git-based 專案就稱呼他們的中央庫為origin的原因吧。

Repository URLs

Git支援非常多的方式來引用一個remote repo。兩種最簡單的方法是:HTTP或者SSH。HTTP是一種允許匿名訪問的read-only的協議模式,例如:

http://host/path/to/repo.git

但是通常不允許push commit到一個HTTP地址。為了寫操作,你需要使用SSH協議(加上一個帳號密碼):

ssh://user@host/path/to/repo.git

你需要在host主機上有一個有效的SSH帳號。

例子:

除了origin,為了方便合作,通常你可能需要指向你的同事的repo的一個remote connection。

例如,如果你的同僚John,他可能維護了一個可以公開訪問的repo:dev.example.com/john.git.你可以這樣來增加一個remote connection:

git remote add john http://dev.example.com/john.git

 通過這種方式來訪問個別開發人員的repo的模式使得git能夠在“中央庫”之外來達到溝通合作。這種模式對於小團隊大專案非常有用。也正因為這種模式使得在git中“中央庫”的概念越來越淡化,甚至可以沒有central repo,因為每個人都可以稱為中央庫(如果你願意的話)

git fetch

git fetch命令從remote repo中import commits到本地repo.這個命令的結果是remote repo的commits被作為本地repo的一個remote branch的形式來儲存在本地repo中。也就是說remote branch代表了我們的remote repo的真實內容。這種方式給你在真正整合遠端變更之前來review確認他們的變更的一個機會

用法

git fetch <remote>  //從遠端repo中fetch所有的分支。
git fetch <remote> <branch> //僅fetch遠端repo的指定的branch分支

探討:

Fetching是當你希望看到別人的工作成果時你需要做的。既然獲取到的commits以一個remote branch來代表,也就意味著fetch方式對你本地開發工作沒有任何影響。這種模式給你整合應用別人的改動之前來review提供了方便。這一點和svn update命令類似,它允許你看看中央庫有哪些變更,但是並不強迫你實際merge這些變更到你的repo中。

remote branches

remote branches和local branch是完全一樣的,不同的是:remote branch代表著來自其他repo的commits(這裡其他repo:有可能是其他人的,也可能是你自己的)。你也可以check out一個remote branch,但是這時你將會進入detached HEAD狀態(就像checkout一個老的commit一樣)。你可以把remote branch視為只讀的branch. Remote branch會由一個remote connection名稱來做字首,這樣你永遠也不會將remote branch和local branch混淆起來。例如:

git branch -r
# origin/master           
# origin/develop
# origin/some-feature
//上面的命令就會列出remote名為origin的repo中存在著master,develop,some-feature分支

 

再次指出,你可以通過checkout那些remote branch並且通過git log命令來檢查這些branch的變更。如果你批准了一個remote branch的變更,你可以通過git merge命令來merge這個分支的內容到你的本地分支上去。從這裡看出,要獲取別人的變更,需要兩個過程:fetch和merge。但是git pull命令也提供了一種合二為一的方式。

下面給出一個實際的典型的場景:同步您的本地repo和遠端repo的master分支:

$git fetch origin
a1e8fb5..45e66a4 master -> origin/master
a1e8fb5..9e8ab1c develop -> origin/develop
* [new branch] some-feature -> origin/some-feature
//上面的命令說明origin這個remote我們有master,develop,some-feature三個分支都已經拉回來了,分別放在origin/master。。。分支上。

在下面的圖中,所有來自remote branch的commits都以方框表示。正如你看到的,git fetch給了你另一個repo整個分支結構的能力!

為了檢查到底有了哪些改動在upstream master上,你可以通過給git log命令傳入一個origin/master作為filter引數來實現:

git log --oneline master..origin/master

為了批准並且merge這些remote branch上的他人的變更(到你的local branch),你需要這麼做:

git checkout master //切換到本地分支環境
git log origin/master //檢查remote變更集
git merge origin/master //merge到本地

通過上面的命令完成後origin/master和master就同時指向了相同的commit,你就和upstream developments完全同步了。

git pull

在git-based協作工作流模型中,merge upstream變更到local repo是一個非常普通的任務,雖然通過git fetch+merge兩步可以完成這個任務,但是git也提供了一種合二為一的命令git pull

git pull <remote> //獲取remote的current branch並且立即merge到local分支,相當於以下命令:
git fetch <remote> & git merge origin/<current-branch>
git pull --rebase <remote>//和上面命令的區別是:使用git rebase而不是git merge來合入remote branch的變更

探討:

你可以將git pull看作svn update相同功能的版本。它是一種便利地同步本地repo和upstream change的手段。下面幾張圖描繪了這個pull的過程。

你可能想當然地認為你的repo和remote是同步的,但是git fetch卻發現了remote origin的master分支在你最後一次pull/fetch後已經向前走了幾步:

pulling via rebase

上面命令例子中提到--rebase引數,這個引數可以通過阻止非必要的merge commit(這是預設的git pull merge模型中必然產生的無意義commit)產生,從而保證一個線性地歷史記錄。很多開發人員更喜歡rebase,而不是merge。這就像說這麼句話:”我想把我的改動放在別人已經做過的工作之上“。

事實上,正是由於--rebase是一個如此common的工作流,以至於已經有了一個特定的配置選項:

git config --global branch.autosetuprebase always //執行這條命令後,所有的git pull命令將整合git rebase,而不是git merge!!!

例子:

下面的例子演示如何和中央庫上的master branch來保持同步

git checkout master
git pull --rebase origin

上面的命令將你的local change直接放置於任何其他人的工作之上,從而你也就有了別人的貢獻

git push

push是你如何將本地commits傳到remote repo上去的方法,這和git fetch是相反的操作,然而fetch/pull是匯入commits到local branch上去(也需要通過remote origin/branch做中轉),push則是輸出你的本地commits到remote repo的local branch(通過本地的remote orgin/branch做中轉)中去。這有可能會覆蓋變更,所以你需要小心使用,下面就談談這些潛在可能存在的問題:

用法:

git push <remote> <branch>

上述命令將本地的<branch> 上的所有commit和內部objects push到<remote>庫中,並且這條命令的結果會在<remote> repo中建立一個local branch。為了阻止你可能覆蓋remote repo中<branch>分支上的commits,GIT在得知push操作並不會在remote repo中產生一個fast-forward merge時,git會禁止你做push操作。也就是說,預設情況下,你的local branch相比remote origin的local branch(以origin/<branch>來代替)更新的情況下,才允許你push.(換句話說就是remote repo的branch沒有做過變更!)

當然,git也為高階使用者提供了--force命令來強制允許push操作:

git push <remote> --force//注意一定要小心使用,除非你知道你在幹什麼,否則別用
git push <remote> --all //push所有的本地branch到指定的remote上去
git push <remote> --tags//預設情況下tags並不會自動push上,使用該標誌則push所有本地建立的tag

探討:

git push最常見的使用場景是釋出你的本地修改到中央庫中。在你積累了多個本地commit後,並且希望將這些變更分享給團隊其他成員,你(optionally)可以通過rebase interactive  來將這些commit梳理一下,然後push到中央庫中。

 

上面這張圖展示了當你local master相對於中央庫的master分支已經向前走了幾步後,你通過執行git push origin master命令來發布你的本地commit到底發生了什麼。注意一點:git push基本上就像是在remote repo中執行git merge localmaster命令一樣,仔細體會一下

關於Force Pushing:

我們知道當git push會產生一個non-fast-forward merge時,git會拒絕執行這個git push命令,因為一旦執行這條命令,你就會覆蓋中央庫的歷史。

正因為此,如果remote history已經和你的local history分叉了(diverged),你需要首先git pull(git fetch/git merge)到你的本地,並且做好可能的衝突解決,隨後再次push。這和SVN如何確保你通過在commit一個change set之前,必須做svn update來和中央庫保持同步是類似的概念。

--force標誌將取消上面這種預設行為,產生的後果是:remote repo的branch將完全反映你的local history,其他自從你上次git pull操作後upstream分叉出來的所有變更歷史都將會丟失。一個可能使用這個--force標誌的場景是:當你發現你剛剛分享釋出的commits是不對的,而你通過git commit --amend或者一個interactive rebase操作解決了這個問題從而再次釋出。然而,你必須確認:沒有團隊成員pull過你在--force之前push的哪些commits.

Only push to Bare Repositories

而且,你應該只向那些通過--bare標誌建立的repo去做push操作。既然push會將remote repo的local branch的結構弄的混亂(原因是push時會在remote repo中建立local branch),因此很重要的一點是:不要向另外一個開發人員的本地repo做push操作(當然也不是很絕對哦!!),由於bare庫沒有working directory,所以不可能打斷任何人的開發工作。

例子:

下面的例子描述了將本地變更向中央庫釋出的標準方法。首先,通過git fetch/git rebase操作來確保你的本地master分支是up-to-date的(已經包含了所有的remote變更,run local changes on top of them). interactive rebase在這時也是一個在分享釋出你的變更前清理他們的很好的機會。然後,git push命令則將所有你的local master變更釋出到central repo中去。

git checkout master
git fetch origin master
git rebase -i origin/master
#squash commits, fix up commit messages etc.
git push origin master

既然我們確保我們的local master 是uptodate的,這將會產生一個fast-forward merge,git push再也不會抱怨non-fast-forward問題了。

 

相關文章