Git 和社交編碼:如何無畏地合併

工程 | Dave Syer | 2010年12月21日 | ...

Git 非常適合社交編碼和開源專案的社群貢獻:貢獻者可以輕鬆地試用程式碼,並且可以有大量的人進行分叉和實驗,而不會危及現有使用者。本文提供了一些 Git 命令列示例,可能有助於您對這個過程建立信心:如何獲取、拉取和合並,以及如何撤銷錯誤。如果您對社交編碼過程本身以及如何為 Spring 專案做出貢獻感興趣,請檢視此網站上 Keith Donald 的另一篇部落格

Grails 在 Github 上已經存在一段時間了,並且社群貢獻的體驗非常棒,因此 SpringSource 的其他一些專案也開始遷移到那裡。一些遷移的專案是新的(例如 Spring AMQP),而一些已經成熟並從 SVN 遷移過來(例如 Spring Batch)。在 SpringSource 託管的 Gitorious 例項上也有一些 Spring 專案,例如 Spring Integration。Github 和 Gitorious 的社交編碼流程略有不同,但底層的 Git 操作是相同的,而這正是我們在這裡要介紹的。希望在閱讀完本文並可能透過示例進行實踐後,您會受到啟發,嘗試新的模型併為 Spring 專案做出貢獻。Git 很有趣,並且具有一些很棒的功能,非常適合此類開發。

如果您從未用過 Git,那麼這裡可能不是開始學習的地方。如果您正從 SVN 遷移到 Git,並且在出現問題時不夠自信,或者您想擺脫那些煩人的“Merged branch 'master'...”日誌訊息,並保持歷史記錄的簡潔線性,那麼這裡就是為您準備的。如果您已在社交編碼網站註冊,並希望將您的更改合併到您最喜歡的開源專案中,本文將幫助您對此更有信心,但您仍應閱讀您託管網站關於 fork 和 merge 的文件。希望屆時一切都會變得清晰明瞭。

本文將引導您瞭解 Git 和多個使用者的一些簡單但常見的場景。我們首先介紹兩個使用者共享一個儲存庫,並展示他們可能遇到的某些陷阱以及一些自我挽救的技巧。然後,我們將進入一個社交編碼示例,其中仍有兩個使用者,但現在有兩個遠端儲存庫。這在開源專案中很常見,並且在變更管理方面具有一些優勢,我們將看到這一點。

起源

我們將從設定一個簡單的儲存庫開始,用於進行一些示例。以下是一些您可以在任何 UN*X shell 中自行執行的 Git 命令列操作,然後是一個 Git 索引的草圖,展示提交和分支的佈局。

$ mkdir test; cd test; touch foo.txt bar.txt
$ git init .
$ git add .
$ git status
To be added
$ git commit -m "Initial"
[master (root-commit) 5f1191e] initial
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 bar.txt
 create mode 100644 foo.txt
$ git checkout -b feature
$ echo bar > bar.txt
$ git commit -am "change bar"
$ git checkout master
$ echo foo > foo.txt
$ git commit -am "change foo
A - B (master)
  \
    C (feature)

一個簡單的佈局,但足夠複雜以至於很有趣。有 3 個提交(我們在圖中省略了提交訊息),以及兩個獨立的分支。這兩個分支是故意設計成沒有衝突的——它們包含不同檔案的更改。如果您正在使用命令列示例,並且也想檢視索引樹,請使用 Git UI 工具(我使用的是 gitk --all,我認為它在所有平臺上都可用)。

最後一件事是準備好此儲存庫以供克隆。

$ git checkout HEAD~1 

我們故意使用了一個引用 HEAD1 而不是分支名稱,以便 origin 留下一個分離的 HEAD。如果您習慣了遠端儲存庫工作流,這就會有意義,因為我們正在本地模擬一個遠端儲存庫,而遠端儲存庫通常是“裸”的(沒有簽出的分支)。HEAD1 引用表示“回退一步,但不要將新的 HEAD 分配給任何分支”,這使得以後可以從克隆中推送更改到儲存庫。

Bob 克隆並跟蹤分支

Bob 是我們儲存庫的第一個使用者。這是他的終端和本地儲存庫中的索引布局。

$ git clone test bob
$ cd bob
$ git checkout --track origin/feature
A - B (master,origin/master)
  \
    C (feature,origin/feature)

Bob 知道 feature 分支是實驗性的,但現在已經過測試,所以他想將其合併到 master 中以包含在下一個版本中。但是,如果他從這裡合併,他會得到一個非線性的混亂(儘管實際上沒有衝突)。

$ git merge master
Merge made by recursive.
 foo.txt |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
A - B (master,origin/master) - D (feature) "Merge branch 'master' into feature"
  \                           /
    C (origin/feature) ------

Bob 討厭這樣。歷史是非線性的,因此很難看出所有更改的來源,而且還留下了可怕的自動生成的提交訊息“Merge branch 'master'...”。(無論是將 feature 合併到 master,還是將 master 合併到 feature,結果都是相同的結構,具有相同的祖先和相同的子提交,只是自動生成的提交訊息略有不同。)從這裡進行推送是合法的,但他最終會向所有人展示醜陋的歷史以及不太有用的自動生成的註釋。

Bob 不要驚慌!由於他還沒有推送任何內容,他仍然可以恢復到原始索引。

$ git reset --hard origin/feature
A - B (master,origin/master)
  \
    C (feature,origin/feature)

從那裡,他可以坐下來,等待別人來解決問題。然後 Jane 出現了……

(請注意,並非每個人都同意 Bob 的觀點,即不必要的非線性歷史和沒有新更改的自動生成的提交日誌是不好的。有些人實際上認為有並行開發跡象是“令人欣慰”的。他們通常不使用 rebase,而更喜歡簡單的 pull 和 merge 方法與 Git 進行協作。)

Jane 克隆另一個副本並執行本地 Rebase

Jane 也是一個有權訪問測試儲存庫的開發人員。她比 Bob 更大膽,並決定需要進行 rebase 以保持歷史的線性。

$ git clone test jane
$ cd jane
$ git checkout --track origin/feature
$ git rebase master
A - B (master,origin/master) - D (feature)
  \
    C (origin/feature)

(請注意,Jane 可以透過與 Bob 相同的路線獲得相同的結果——合併 master,然後進行 rebase,因為 rebase 足夠智慧,可以意識到它可以節省一些重複,並且不顯示不包含任何新更改的中間狀態。)

現在一切看起來都還可以(ish),但 git 不允許推回到 origin,因為 feature 已經分叉。

$ git push
To file:///path/to/test
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to 'file:///path/to/test'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes before pushing again.  See the 'Note about
fast-forwards' section of 'git push --help' for details.

如果 Jane 聽從提示並在此處合併,她將真的後悔。rebase 的結果實際上只是一般般——它有重複的提交(CD,在進行 quint 操作時具有相同的日誌訊息和相同的更改),因此合併將不會好看。Git 只會按照她告訴它的那樣做,並且合併是合法的,但效果將是:

  • 非線性的歷史
  • 自動生成的提交訊息
  • 重複的提交訊息(每個祖先分支上一個)

這是結果。

$ git merge origin/feature 
Merge made by recursive.
A - B (master,origin/master) - D "change bar" - E (feature) "Merge branch 'master' into feature"
  \                                            /
    C (origin/feature) "change bar" ----------

她只對原始碼進行了兩次更改,但結果是索引中有 5 個提交。這太糟糕了。要恢復,她可以使用與之前相同的技巧,只是現在在 D 提交處沒有命名的分支。她可以新增一個,或者使用 UI 工具(gitk 在這方面做得很好),或者使用相對引用。

$ git reset --hard HEAD~1
A - B (master,origin/master) - D (feature)
  \
    C (origin/feature)

不友好的做法,也是所有 Git 手冊警告你的做法,就是強制推送。Jane 嘗試了一下。

$ git push --force
A - B (master,origin/master) - D (feature,origin/feature)

這樣就好多了!兩次更改和三個提交(變化的兩側各一個),以及一個漂亮的線性歷史,沒有令人不悅的提交訊息。那麼,為什麼這是一個糟糕的做法呢?讓我們再次看看我們不幸的朋友 Bob。

Bob 現在可能處於混亂之中

如果他沒有修改“feature”分支,他會沒事的。

$ git checkout master
$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From file:///path/to/test
 + 4b223e2...4db65c2 feature       -> origin/feature  (forced update)
Already up-to-date.
A - B (master,origin/master) - D (origin/feature)
  \
    C (feature)

這裡看起來有點醜陋,但 Git 已經將一切都整合在一起了。Bob 可以看到 Jane(或某人)強制更新了他正在跟蹤的遠端分支,所以他的本地分支是由於他的過錯而分叉的。他可能對此有點惱火,但在這種情況下,這是無害的,因為他沒有對他的本地分支進行任何更改,所以他可以簡單地重置他的分支。

$ git checkout feature
Switched to branch 'feature'
Your branch and 'origin/feature' have diverged,
and have 1 and 2 different commit(s) each, respectively.
$ git reset --hard origin/feature
A - B (master,origin/master) - D (feature,origin/feature)

皆大歡喜!因此,在某些情況下,強制推送是可以接受的。特別是,對於在“主”專案的 fork 上工作的人來說,這可能是可以接受的,就像在 Github 這樣的社交編碼網站上經常出現的那樣。讓我們更詳細地研究一下這個用例。

Fork 和社交編碼

Git 的預期有用功能之一是它可以作為分散式儲存庫使用——您不必採用 SVN 和舊系統常見的單一起源方法。當您從公共開源專案 fork 並要求專案所有者將您的某些更改合併到主儲存庫時,分散式功能會被大量使用,但並非廣泛使用。

所以,假設有一個名為 main 的很棒的開源專案,由 Mary 擁有,Bob 從專案主頁 fork 了它。他得到了一個具有 main 儲存庫 Git 索引精確副本的新儲存庫,並且可以隨意命名(他選擇 bob-main 以便我們區分)。這部分的 Git 操作非常簡單——他實際上只是克隆了 main,將 origin 引用移動到他在伺服器上自己的空間的新位置,然後將更改推回。社交編碼應用程式會在後臺處理所有這些,並樂於建議 Bob 克隆他的新遠端 fork。

現在我們有一個 main 儲存庫(對 Mary 來說是 origin,但對 Bob 不是),以及一個 bob-main 儲存庫,它們是相同的。為了簡單起見,讓我們讓它從一個提交開始(因此,採用第一個示例中的 origin 建立配方,在第一個提交後停止)。

A (master)

Mary 的本地副本與 Bob 的副本最初是相同的,它們看起來都像這樣。

A (master,origin/master)

但它們的 origin 引用是不同的。對於 Mary 來說。

$ git remote -v
origin	git@host:/mary/main (fetch)
origin	git@host:/mary/main (push)

對於 Bob 來說。

$ git remote -v
origin	git@host:/bob/bob-main (fetch)
origin	git@host:/bob/bob-main (push)

通常 Mary 無權推送 Bob 的儲存庫,反之亦然。

Bob 添加了一個功能

Bob 對主專案有一個很棒的想法,所以他建立了他的 feature 分支並開始編碼,最終變成了這樣。

$ git checkout -b feature
$ echo foo >> foo.txt
$ git commit -am "change foo"
A(master,origin/master) - C (feature)

他對此很滿意,所以他將它推回他自己的 origin。

$ git push origin feature
A(master,origin/master) - C (feature,origin/feature)

注意 Bob 如何將他所有的更改都保留在一個分支上。這並非強制性,但正如我們稍後將看到的,這使得跟蹤與 main 儲存庫的差異變得非常容易(儘管到目前為止 Bob 沒有與那裡建立明確的連線)。Github 上的使用者文件實際上並不推薦這種方法,但您可能會發現它很有用。

Mary 做出了一些更改

Mary 是專案所有者,她可以在任何時候將更改推送到她的 master 分支。所以她這樣做了。

$ echo bar >> bar.txt
$ git commit -am "change bar"
$ git push
A - B (master,origin/master)

Bob 傳送了一個 Pull Request

現在 Bob 請求 Mary 合併他的更改。Mary 遵循社交編碼網站上的友好說明,並拉取 Bob 的更改進行檢視。

$ git checkout -b bob master
$ git pull https://host/bob/bob-main feature
A - B (master,origin/master) - D (bob) "Merge branch 'feature' of '...bob-main' into bob"
  \                           /
    C  ----------------------

Mary 立即看到 Bob 的分支與她的 master 分叉了。她該怎麼辦?

選項 1:不強制推送

在這種情況下,如果沒有任何衝突,這可能非常直接。她決定花一些時間清理歷史記錄,以防萬一很容易。這與 Bob 在之前的單一起源示例中使用的過程相同。

$ git reset --hard HEAD~1
$ git rebase master
A - B (master,origin/master) - C (bob)

沒有問題,歷史記錄再次線性化。Mary 只需將更改合併到她的主專案中。

$ git checkout master
$ git merge bob
$ git push
$ git branch -D bob
A - B - C (master,origin/master)

她在最後刪除了本地分支 bob,因為它不再標記任何重要的內容,而且它也沒有跟蹤遠端分支,所以她不必處理該引用。

選項 2:在 Fork 中強制推送

如果上面的 rebase 失敗,或者 Mary 簡單地認為,如果 Bob 希望合併他的更改,那麼讓他使歷史記錄線性化的責任就在於他,她可以讓他基於她的 master 進行 rebase。她透過那個精巧的社交編碼網站給他發了一條訊息,然後重置她的本地副本。

$ git checkout master
$ git branch -D bob
$ git prune
A - B (master,origin/master)

現在 Bob 開始工作了。他仍然在他的 feature 分支上,所以。

$ git remote add main https://host/mary/main
$ git fetch main
A (master,origin/master) - B (main/master)
 \
  C (feature,origin/feature)

所以現在他有一個主儲存庫的只讀引用和一個指向它的別名,這樣他就可以快速跟上 Mary 的工作。(別名是可選的,但它將幫助他保持最新,並一目瞭然地看到他的 master 相對於 Mary 的 master 的位置。)首先,他將他的 master 與主儲存庫保持一致。

$ git checkout master
$ git merge main/master
$ git push
A - B (master,origin/master,main/master)
 \
  C (feature,origin/feature)

在這裡,我們看到了在 feature 分支上工作的優勢:如果 master 分支沒有本地更改(master 永遠不會超前於 main/master),那麼將 master 與主儲存庫合併總是很容易的。現在他嘗試了 Mary 所要求的 rebase。

$ git checkout feature
$ git rebase master
A - B (master,origin/master,main/master) - D (feature)
 \
  C (origin/feature)

Bob 看到歷史記錄是他想要的,所以他把它推送到他的遠端儲存庫。

$ git push --force
A - B (master,origin/master,main/master) - D (feature,origin/feature)

Bob 在這裡使用了與 Jane 在上一個示例中相同的技巧——他強制推送了一個本地分支以保持線性歷史。

Bob 和 Mary 是自願的成年人,feature 分支存在於 Bob 的儲存庫中的唯一原因是為了錨定 pull request,因此其他人不太可能跟蹤該分支。如果有人跟蹤該分支,他們可能會感到不便,甚至可能非常不便,如果他們在該分支上標記了公共釋出。這是 Bob 決定承擔的風險——實際上在這個例子中沒有任何風險,因為 Bob 是唯一有權訪問他儲存庫的人,而且他很有信心沒有人使用他的分支進行釋出。

結論

除非更改很瑣碎,否則合併貢獻的過程並不簡單,但 Git 確實消除了其中的很多痛苦,一旦您掌握了它,它就足夠容易了。示例中的關鍵點是 Git 正在以一種特定的風格使用,並且存在一些限制和約定使其更容易:Bob 和 Mary 的儲存庫彼此之間是隻讀的,而且 Bob 實際上是唯一有權訪問他的 fork 的人,所以 Mary 想讓他強制推送他一點也不介意。這遠非 Git 吸引開源開發者的唯一功能,但它在很大程度上解釋了為什麼我們中的一些人正在遷移到 Github 這樣的網站。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有