version-control git阮一峰 - Git工作流和rebase vs合併問題




gitlab工作流 flow (9)

使用Git沒有“正確的”工作流程。 使用任何漂浮你的船。 但是,如果您在合併分支時經常發生衝突,您可能應該與您的開發人員協調更好的工作? 聽起來你們兩個都在編輯相同的文件。 此外,請留意空白和顛覆關鍵字(即“$ Id $”等)。

我已經與其他開發人員一起在一個項目上使用Git幾個月。 我有幾年的SVN經驗,所以我想我帶來了很多關係的包袱。

我聽說Git非常適合分支和合併,到目前為止,我只是沒有看到它。 當然,分支是簡單的,但當我嘗試合併時,一切都會變得很糟糕。 現在,我已經習慣了SVN的這一點,但在我看來,我只是將一個子版本系統換成另一個。

我的合作夥伴告訴我,我的問題源自我渴望融合的願望,並且在很多情況下我應該使用rebase而不是merge。 例如,以下是他制定的工作流程:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

實質上,創建一個功能分支,總是從主分支轉移到分支,並從分支合併回主。 重要的是要注意分支始終保持本地。

這是我開始的工作流程

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

有兩個本質區別(我認為):我總是使用合併而不是重新綁定,並將我的功能分支(和我的功能分支提交)推送到遠程存儲庫。

我對遠程分支的推理是,我希望在工作時備份我的工作。 我們的存儲庫會自動備份,如果出現問題,可以恢復。 我的筆記本電腦不是,或者不是很完善。 因此,我討厭在我的筆記本電腦上使用其他地方沒有鏡像的代碼。

我對合併而不是重新合併的推理是合併似乎是標準的,並且重新分配似乎是一個高級功能。 我的直覺是,我試圖做的不是高級設置,所以rebase應該是不必要的。 我甚至仔細閱讀了關於Git的新語用編程書,它們涵蓋了廣泛的合併,幾乎沒有提到rebase。

無論如何,我在最近的一個分支上關注了我的工作流程,當我嘗試將它合併回主人時,這一切都變成了地獄。 與應該沒有關係的事情有很多衝突。 衝突對我來說毫無意義。 我花了一整天的時間把所有東西都整理出來,並最終以強迫推向遠程主人的方式達到高潮,因為我的當地主人已經解決了所有衝突,但是遠程主人仍然不高興。

什麼是這樣的“正確的”工作流程? Git應該使分支和合併變得超級簡單,而我只是沒有看到它。

更新2011-04-15

這似乎是一個非常受歡迎的問題,所以我認為自從我第一次提問以來,我會用兩年的經驗進行更新。

原來的工作流程是正確的,至少在我們的情況下是這樣。 換句話說,這就是我們所做的工作:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

事實上,我們的工作流程有點不同,因為我們傾向於壓縮合併而不是原始合併。 ( 注意:這是有爭議的,見下文。 )這使我們可以將我們的整個功能分支轉換為主服務器上的單個提交。 然後我們刪除我們的功能分支。 這使我們能夠在邏輯上構建主人的承諾,即使他們在我們的分支上有點混亂。 所以,這就是我們所做的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

壁球合併爭議 - 正如幾位評論者指出的那樣,壁球合併將會拋棄你的特徵分支上的所有歷史。 顧名思義,它將所有提交壓縮為一個提交。 對於小功能,這是有道理的,因為它將它凝聚成一個包。 對於更大的功能,這可能不是一個好主意,特別是如果你的個人提交已經是原子性的。 這真的歸結為個人偏好。

Github和Bitbucket(其他?)拉取請求 - 如果您想知道合併/重新分配與合併請求的關係,我建議您按照以上所有步驟操作,直到您準備好合併回主數據庫。 不要用git手動合併,你只需接受PR。 具體來說,它是這樣工作的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

我愛上了Git,從不想回到SVN。 如果你在掙扎,只要堅持下去,最終你會看到隧道盡頭的燈光。


在你的情況下,我認為你的伴侶是正確的。 重新定義的好處在於,對於局外人來說,你的改變看起來像是他們都是以一個乾淨的順序發生的。 意即

  • 您的更改很容易查看
  • 你可以繼續做出很好的小提交,但是你可以將這些提交集合一次全部公開(通過合併成為主)
  • 當您查看公共主分支時,您會看到不同開發人員針對不同功能提交的不同系列提交,但它們不會混合在一起

您仍然可以繼續將您的私人開發分支推送到遠程存儲庫以備份,但其他人不應該將其視為“公共”分支,因為您將重新分配。 順便說一句,這樣做的一個簡單命令是git push --mirror origin

使用Git的文章Packaging軟件在解釋合併與重新分配之間的權衡方面做得相當不錯。 這是一個不同的背景,但校長是相同的 - 它基本上取決於您的分支機構是公共還是私人,以及您計劃如何將它們整合到主線。


在我的工作流程中,我盡可能多地進行重組(我會盡量做到這一點),不要讓這些差異累積起來,以減少分支之間碰撞的數量和嚴重程度。

但是,即使在基本上基於rebase的工作流程中,也有合併的地方。

回想一下,合併實際上創建了一個有兩個父母的節點。 現在考慮以下情況:我有兩個獨立的功能分區A和B,現在想要在功能分支C上開發依賴於A和B的東西,而A和B正在審查中。

我所做的是:

  1. 在A之上創建(並簽出)分支C.
  2. 與B合併

現在,分支C包含來自A和B的變更,並且我可以繼續對其進行開發。 如果我對A做任何改變,那麼我按照以下方式重新構建分支圖:

  1. 在A的新頂部創建分支T.
  2. 將T與B合併
  3. 將C重新轉化為T
  4. 刪除分支T

通過這種方式,我可以實際上維護分支的任意圖形,但是執行比上述情況更複雜的操作已經太複雜了,因為在父級更改時沒有自動工具進行重新綁定。


不要在幾乎任何情況下使用git push origin --mirror。

它不問你是否確實想要這樣做,而且最好確定一下,因為它會清除所有不在本地盒子上的遠程分支。

http://twitter.com/dysinger/status/1273652486


“衝突”意味著“同一內容的平行演變”。 因此,如果在合併過程中它“全部到了底層”,這意味著您對同一組文件進行了大規模的演變。

為什麼rebase然後比合併更好的原因是:

  • 你用主人之一重寫本地提交歷史記錄(然後重新應用你的工作,然後解決任何衝突)
  • 最終的合併肯定會是一個“快進”的合併,因為它將擁有主人的所有提交歷史記錄,再加上您重新申請的更改。

我確認在這種情況下正確的工作流程(通用文件集合的演變) 首先rebase,然後合併

但是,這意味著,如果您推送本地分支(出於備份原因),則不應該由其他任何人拉(或至少使用)該分支(因為提交歷史將由連續的分標改寫)。

在這個話題上(rebase然後合併工作流程), barraponto在評論中提到了兩個有趣的帖子,均來自randyfay.com

使用這種技術,你的工作總是在公共分支之上,就像當前HEAD的最新補丁一樣。

(類似的技術存在集市


在閱讀你的解釋之後,我有一個問題:難道你沒有做過嗎?

git checkout master
git pull origin
git checkout my_new_feature

在功能分支中執行'git rebase / merge master'之前?

因為您的主分支不會自動從您​​朋友的存儲庫中更新。 你必須用git pull origin來完成。 也就是說,你總是會從一個永不改變的本地主分支中變質? 然後來推時間,你正在推動一個倉庫(本地)提交你從未見過,因此推送失敗。


“即使你是一個只有少數分支的開發人員,也應該習慣使用rebase和合併的習慣,基本的工作模式如下所示:

  • 從現有分支A創建新的分支B.

  • 在分支B上添加/提交更改

  • 來自分支A的重新更新更新

  • 將分支B的更改合併到分支A上“

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/


TL; DR

git rebase工作流不能保護您免受衝突解決不好的人員或習慣了SVN工作流程的用戶,就像避免Git災難:血腥故事中的建議一樣。 它只會使解決衝突更加繁瑣,並且使得難以從不良衝突解決中恢復。 相反,請使用diff3,以便首先不會如此困難。

Rebase工作流程不會更好地解決衝突!

我對清理歷史非常親睞。 但是,如果我遇到衝突,我立即放棄rebase並進行合併! 它真的讓我感到害怕,人們推薦使用rebase工作流程作為衝突解決合併工作流程的更好替代方案(這正是這個問題所關注的)。

如果它在合併期間“全部到了地獄中”,在重新綁定期間它將“全部到了地獄”,並且可能還有更多地獄! 原因如下:

原因1:解決衝突一次,而不是每次提交一次

當你重新組合而不是合併時,你必須執行沖突解決的次數達到你承諾重新分配的次數,因為同樣的衝突!

真實場景

我從主人那里分支來重構分支中的複雜方法。 我的重構工作總共包含15次提交,因為我正在重構它並獲取代碼評論。 我的重構的一部分涉及修復之前在master中存在的混合選項卡和空格。 這是必要的,但不幸的是,它將與之後在master中對此方法所做的任何更改相衝突。 果然,在我使用這種方法的同時,有人對主分支中的同一個方法進行了簡單的,合法的更改,這些方法應該與我的更改合併在一起。

當需要將我的分支與主人合併時,我有兩種選擇:

混帳合併:我得到一個衝突。 我看到他們為了掌握和合併(我的分支的最終產品)所做的更改。 完成。

git rebase:我與第一次提交有衝突。 我解決了衝突並繼續進行了重組。 我與第二次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第三次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第四次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第五次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第六次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第七次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第八次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第九次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第十次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與我的第十一個承諾發生衝突。 我解決了衝突並繼續進行了重組。 我與第十二次提交發生衝突。 我解決了衝突並繼續進行了重組。 我與第十三次承諾發生衝突。 我解決了衝突並繼續進行了重組。 我與第十四次承諾發生衝突。 我解決了衝突並繼續進行了重組。 我與第十五次提交發生衝突。 我解決了衝突並繼續進行了重組。

如果是您首選的工作流程,您必須在開玩笑。 它所需要的只是一個空白修補程序,它與主機上發生的一個更改相衝突,並且每個提交都會發生衝突並且必須解決。 這是一個只有空白衝突的簡單方案。 天堂禁止你有一個真正的衝突涉及跨文件的主要代碼更改,必須多次解決。

如果您需要解決所有額外的衝突解決方案,則會增加您犯錯的可能性。 但是,因為你可以撤消,git中的錯誤是好的,對吧? 當然......

原因2:有了rebase,沒有撤銷!

我認為我們都可以同意解決衝突可能很困難,而且有些人對此很不好。 它可能非常容易出錯,這就是為什麼它如此之好以至於git可以很容易地撤銷它!

當你合併一個分支時,git會創建一個合併提交,如果衝突解決效果不佳,可以將其丟棄或修改。 即使您已經將錯誤的合併提交推送到公共/權威倉庫,您也可以使用git revert來撤銷合併引入的更改,並在新的合併提交中正確地重新進行合併。

當你重組一個分支時,如果衝突解決方案做錯了,你可能被搞砸了。 現在每個提交都包含錯誤的合併,並且您不能只重做rebase *。 充其量,你必須返回並修改每個受影響的提交。 不好玩。

重新綁定之後,​​不可能確定提交的原始內容以及由於解決衝突問題導致的內容。

*如果你可以從git的內部日誌中挖掘舊的refs,或者如果你創建第三個分支指向rebasing之前的最後一次提交,那麼可以撤銷rebase。

從衝突解決中解脫出來:使用diff3

以這個衝突為例:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

縱觀這場衝突,我們不可能知道每個分支有什麼變化或者其意圖是什麼。 這是我認為衝突解決困惑和困難的最大原因。

diff3來拯救!

git config --global merge.conflictstyle diff3

當你使用diff3時,每個新的衝突都會有第三部分,即合併的共同祖先。

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

首先檢查合併的共同祖先。 然後比較每一方以確定每個分支的意圖。 您可以看到HEAD將EmailMessage更改為TextMessage。 其目的是將用於TextMessage的類更改為傳遞相同的參數。 您還可以看到,功能分支的意圖是為:include_timestamp選項傳遞false而不是true。 要合併這些更改,請結合兩者的意圖:

TextMessage.send(:include_timestamp => false)

一般來說:

  1. 將共同的祖先與每個分支進行比較,並確定哪個分支具有最簡單的變化
  2. 將該簡單更改應用於其他分支的代碼版本,以便它包含更簡單和更複雜的更改
  3. 刪除除了剛剛合併到一起的更改之外的所有衝突代碼部分

備用:通過手動應用分支的更改來解決

最後,即使使用diff3,一些衝突也很難理解。 這種情況發生時,尤其是當diff找到共同的語義上不通用的行時(例如,兩個分支碰巧在同一個地方有空行!)。 例如,一個分支改變了一個類的縮進或重新排序類似的方法。 在這些情況下,更好的解決策略可以是檢查合併的任一側的更改,並手動將差異應用於其他文件。

讓我們來看看如何在合併origin/feature1其中lib/message.rb發生衝突)的場景中解決衝突。

  1. 確定我們目前簽出的分支( HEAD ,或--ours )還是我們正在合併的分支( origin/feature1 ,或--theirs )是一個更簡單的更改。 使用diff與三重點( git diff a...b )顯示自從b與a最後發散後發生的變化,或者換句話說,比較a和b的共同祖先與b。

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. 查看更複雜的文件版本。 這將刪除所有衝突標記並使用您選擇的一面。

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. 檢出複雜的變更後,拉起簡單變更的差異(參見步驟1)。 將此差異的每個更改應用於衝突文件。


$ git log --diff-filter=D --summary  | grep "delete" | sort






git version-control git-merge git-rebase