簡體   English   中英

git何時會在合並期間丟失更改?

[英]When does git lose changes during a merge?

讓我們說:

  1. 我們有一個主分支,其中一個同事意外地添加了一系列應該屬於新功能的提交(讓我們稱之為ABC )。
  2. 我發現了,並且我告訴他將這些提交移動到一個新的分支,但保留稍后在master中完成的其他無關的提交。 我問他這個問題並告訴他跟着回復: git:如何移動分支的根兩個提交回來
  3. 幾天后,當新功能分支准備就緒時,我將其合並為master。
  4. 解決合並中的所有沖突后,我提交更改...
  5. ......我發現那些第一次提交( ABC的)已經消失了。
  6. 我問我的同事,他說“他認為”他使用鏈接中提到的方法移動了這些更改(基本上:檢查最后一個常見提交然后使用git cherry-pick來選擇我們之后想要的提交),但他不記得確切。
  7. 我檢查了回購的歷史, ABC 在特性分支,在開始。 它們看起來像是從主人那里成功遷移出來的。

鑒於上述情況,任何人都可以解釋為什么git丟失了這些變化? (我的個人理論是git以某種方式“記住”我們已經撤消了提交ABC ,所以當他們來自新功能分支時,git決定不合並它們。編輯:對不起,如果這個解釋聽起來太像“魔法思維”,但是我很茫然。我歡迎任何嘗試用更技術性的術語來解釋這個問題,如果它是正確的話。

很抱歉無法提供更多詳細信息,但我沒有親自在repo中進行這些更改,因此無法詳細說明所執行的操作。

編輯:好的,正如這里建議的那樣,我讓我的同事在他的機器上執行git reflog ,所以我在這里粘貼結果。 要回到我以前的(鏈接)問題,我們有一個這樣的樹:

A - B - C - D - E - F  master
            \ 
             \- G - H  new feature branch

我們想將B和C移動到新功能分支。

所以,他發給我的git reflog就在這里。 提交5acb457將對應於5acb457 “提交A”:

4629c88 HEAD@{59}: commit: blah
f93f3d3 HEAD@{60}: commit: blah
57b0ea7 HEAD@{61}: checkout: moving from master to feature_branch
4b39fbf HEAD@{62}: commit: Added bugfix F again
4fa21f2 HEAD@{63}: commit: undid checkouts that were in the wrong branch
1c8b2f9 HEAD@{64}: reset: moving to origin/master
5acb457 HEAD@{65}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to master
5acb457 HEAD@{66}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{67}: checkout: moving from 1c8b2f9bf54ca1d80472c08f3ce7d9028a757985 to master
1c8b2f9 HEAD@{68}: rebase: checkout master
5acb457 HEAD@{69}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{70}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to master
5acb457 HEAD@{71}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{72}: merge origin/master: Fast-forward
5acb457 HEAD@{73}: checkout: moving from master to master
5acb457 HEAD@{74}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to master
5acb457 HEAD@{75}: checkout: moving from undo_branch to 5acb4576eca4b44e0a7574eea19cca067c039dc5
5acb457 HEAD@{76}: checkout: moving from master to undo_branch
1c8b2f9 HEAD@{77}: checkout: moving from undo_branch to master
525dbce HEAD@{78}: cherry-pick: Bugfix F
a1a5028 HEAD@{79}: cherry-pick: Bugfix E
32f8968 HEAD@{80}: cherry-pick: Feature C
8b003cb HEAD@{81}: cherry-pick: Feature B
5acb457 HEAD@{82}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to undo_branch
5acb457 HEAD@{83}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{84}: checkout: moving from 1c8b2f9bf54ca1d80472c08f3ce7d9028a757985 to master
1c8b2f9 HEAD@{85}: pull origin HEAD:master: Fast-forward
5acb457 HEAD@{86}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
5acb457 HEAD@{87}: reset: moving to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{88}: merge origin/master: Fast-forward
5acb457 HEAD@{89}: reset: moving to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{90}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to master
5acb457 HEAD@{91}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
1c8b2f9 HEAD@{92}: merge origin/master: Merge made by the 'recursive' strategy.
7b912cd HEAD@{93}: checkout: moving from 7b912cdf33843d28dd4a7b28b37b5edbe11cf3b9 to master
7b912cd HEAD@{94}: cherry-pick: Bugfix F
df7a9cd HEAD@{95}: cherry-pick: Bugfix E
d4d0e41 HEAD@{96}: cherry-pick: Feature C
701c8cc HEAD@{97}: cherry-pick: Feature B
5acb457 HEAD@{98}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
22ecc3a HEAD@{99}: checkout: moving from 5acb4576eca4b44e0a7574eea19cca067c039dc5 to master
5acb457 HEAD@{100}: checkout: moving from master to 5acb4576eca4b44e0a7574eea19cca067c039dc5
22ecc3a HEAD@{101}: commit: bugfix E
3b568bc HEAD@{102}: checkout: moving from feature_branch to master
57b0ea7 HEAD@{103}: commit: blah
152c5b9 HEAD@{104}: checkout: moving from master to feature_branch
3b568bc HEAD@{105}: commit: bugfix D
fe3bbce HEAD@{106}: checkout: moving from feature_branch to master
152c5b9 HEAD@{107}: commit: blah
2318ebc HEAD@{108}: commit: blah
cc5ea32 HEAD@{109}: commit: blah
a5c2303 HEAD@{110}: commit: blah
544a99a HEAD@{111}: commit: blah
299f86a HEAD@{112}: commit: Feature G
fe3bbce HEAD@{113}: checkout: moving from master to feature_branch
fe3bbce HEAD@{114}: commit: Feature C
3852e71 HEAD@{115}: commit: Feature B
5acb457 HEAD@{116}: merge origin/master: Fast-forward

任何人都可以連續了解這4個cherry-pick嗎? 我懷疑他並沒有真正做git cherry-pick master~3事情,特別是不是~3部分(當我第一次看到它時,它確實把我扔掉了)。

提交A,B和C的原因是丟失的原因,這是因為這是您與同事共享的鏈接。 讓我們通過下面的圖說明:

1.假設你的同事做的原始提交歷史

...X---A---B---C---D---E  master

2.將ABC移動到feature分支。 因此,您的同事從master(commit E )或任何提交創建了一個新的feature分支。 並通過以下步驟進行改造:

git checkout -b feature
git cherry-pick master~5 master~2

...X---A---B---C---D---E  master
                        \
                         A'---B'---C' feature 

3.修改master分支

git checkout X
git cherry-pick master~2..master
git branch -f master
git checkout master

提交結構如下所示:

...X---D---E  master
     \
       A'---B'---C' feature 

所以直接的原因是命令git cherry-pick master~2..master 它將直接在提交X上重新提交DE ,因此您無法在主分支上找到ABC

更新:

根據git flog ,似乎這些HEAD信息不足以顯示你的同事做了什么。 feature分支似乎從提交C結賬,而不是D

3b568bc HEAD@{105}: commit: bugfix D
fe3bbce HEAD@{106}: checkout: moving from feature_branch to master
152c5b9 HEAD@{107}: commit: blah
2318ebc HEAD@{108}: commit: blah
cc5ea32 HEAD@{109}: commit: blah
a5c2303 HEAD@{110}: commit: blah
544a99a HEAD@{111}: commit: blah
299f86a HEAD@{112}: commit: Feature G
fe3bbce HEAD@{113}: checkout: moving from master to feature_branch
fe3bbce HEAD@{114}: commit: Feature C

所以結構應該是:

A---B---C---D---E  master
         \
          G---H feature

如果您只想更改提交結構,例如:

A ---D---E  master
 \
  B---C---G---H feature

您可以將master分支和feature分支重置為原始分支,然后在master分支上重新提交提交,詳細信息如下:

git checkout master
git reset --hard <original commit id for E>
git checkout feature 
git reset --hard  <original commit id for H>
git checkout master
git checkout <commit id for A>
git cherry-pick master~4..master~2 #To make the commits as A---D---E (drop B and C)
git branch -f master
git checkout master

讓我們專注於合並結果,但首先快速瀏覽一下這部分(我重新繪制了一些圖表):

要回到我以前的(鏈接)問題,我們有一個這樣的樹:

 A--B--C--D--E--F <-- master \\ G--H <-- feature 

我們想將B和C移動到新功能分支。

結果應該看起來像這樣(帶有刻度標記表示您現在擁有的提交是副本 ,而不是原件,因此他們的哈希ID已經更改,因此獲得原件的每個人都必須爭搶以確保他們使用新的也復制)。 但我只是假設它實際上看起來像這樣:

A--D'-E'-F'   <-- master
    \
     B'-C'-G'-H'   <-- feature

(請注意,唯一沒有復制並切換到的提交是A !)。

當你現在運行:

git checkout master
git merge feature

Git會按以下順序執行這些操作:

  1. 獲取當前提交的哈希ID( git rev-parse HEAD )。
  2. 獲取feature提示的哈希ID( git rev-parse feature )。
  3. 找到這兩個提交的(單個,在這種情況下)合並基礎。 合並基礎的技術定義是DAG中的最低共同祖先,但從寬松的角度講,它恰好在兩個分支分歧之前,這就是“提交D”。
  4. 運行相當於git diff D' F' :使用master的tip來合並基礎。 這是“自合並基礎以來我們在master上所做的更改”:文件的大列表(及其哈希ID版本),以及任何計算的重命名信息等。
  5. 運行相當於git diff D' H' :使用feature提示來區分合並基礎。 這是“他們在feature改變”,與步驟4中的相同。我在步驟4中使用“我們”一詞,在步驟5中使用“他們”,因為我們可以使用git checkout --oursgit checkout --theirs在合並沖突期間提取特定文件:-- --ours指提交F'文件,即“我們”更改的內容,以及 - --theirs是指提交H'文件。
  6. 嘗試將差異組合起來以獲得單個變更集。

    如果Git能夠自己完成所有這些組合,它會聲明勝利,將此單個變更集應用於基礎提交D' ,並使用新的commit-let以通常的方式調用此M進行合並(以便master指向M ),除了M有兩個父母:

     A--D'-E'-F'-----M <-- master \\ / B'-C'-G'-H' <-- feature 

    然而,如果自動合並失敗 ,Git會拋出它的隱喻之手並讓你弄得一團糟,你必須自己清理。 我們馬上就會談到這一點。

三個輸入,一個輸出

請注意,此三向合並有三個輸入

  • 合並基礎的樹
  • 當前( --oursHEAD )提示的樹
  • 其他( - --theirs )提示的樹

合並基礎在這里工作,因為它實際上是兩個提交已經分歧的最佳通用起點。 Git能夠直接進行兩個分支提示,因為每個提交都是一個完整的快照: 1它永遠不必查看所有中間提交,除了圖形方面以便找到合並基礎。

我們還故意掩蓋一系列微妙的技術問題,例如配對和重命名(見腳注1),以及合並策略( -s ours意思是我們甚至不他們的)和策略選項( -X ours-X theirs )。 但只要你只是運行git merge feature並且很少或根本沒有重命名擔心,這不是問題。

但是 - 這是關鍵項目之一 - 為了弄清楚Git將要做什么,你必須繪制圖形,或以其他方式識別合並基礎。 一旦你有了合並基礎提交的哈希ID,你就可以(如果你想的話) git diff將合並基礎與兩個提交提交git diff開來,看看Git會做什么。 但是, 如果合並基礎不是您期望的合並基礎,則合並將不會按預期執行。


1與Mercurial比較,其中每個提交或多或少地存儲為其父提交的delta或changeset。 那么,您可能會認為,Mercurial必須從合並基礎開始,並在每個分支鏈的每個提交中前進。 但是這里有兩點需要注意:首先,Mercurial可能必須在合並基礎之前啟動,因為這也可能是早期提交的變更集。 其次,假設沿着鏈條要么提示,要么做出一些改變,然后退出。 當Mercurial將最終的變更集合並實現與Git相同的合並時,提交及其退出的恢復對最終結果沒有影響。 所以從這個意義上講,中間提交都不重要! 我們只需要它們來重建要組合的兩個最終變更集。

事實上,雖然,水銀沒有做任何的這一點,因為在水銀每個文件存儲偶爾會重新,完整無缺,使水銀不必遵循極長的變更鏈重建的文件。 因此,Mercurial所做的實際上與Git的作用相同:它只是提取基本提交,然后提取兩個提示,並完成兩個差異。

這里有一個很大的技術差異,那就是Mercurial不必猜測重命名:中間提交,再次就像Git一樣 - 它必須遍歷以找到合並基礎,每個記錄任何關於其父提交的重命名,所以Mercurial可以確定每個文件的原始名稱是什么,以及它在任何一個提示中的新名稱。 Git不記錄重命名:它只是猜測如果路徑dir/file.txt出現在合並庫中,而不是在一個或兩個提示中,則可能在一個或兩個提示中重命名了dir/file.txt 如果tip commit#1有other/new.txt不在合並庫中,那么這是重命名的候選文件。

在某些情況下,Git無法以這種方式重命名。 還有其他控制旋鈕。 如果文件已經“太多”改變,有一個打破配對,即讓Git說只是因為dir/file.txt在base和tip中,它實際上可能不是同一個文件。 還有另一個設置Git聲明文件匹配的閾值,用於重命名檢測。 最后,有一個最大配對隊列大小,可配置為diff.renameLimitmerge.renameLimit 默認的合並配對隊列大小大於默認的差異配對隊列大小(自Git版本1.7.5起,目前為400對1000)。


如果有沖突你會得到的混亂

當Git聲明“合並沖突”時,它會在步驟6的中間停止。它不會使新的合並提交M 相反,它會讓你一團糟,存放在兩個地方:

  • 工作樹最好地猜測它可以做什么作為自動合並,以及用沖突標記寫出的所有沖突合並。 如果file.txt有沖突--Git無法將“我們做了什么”與“他們做了什么”合並的地方 - 可能有幾行看起來像這樣:

     <<<<<<< HEAD stuff from the HEAD commit ======= stuff from the other commit (H' in our case) >>>>>>> feature 

    如果你將merge.conflictStyle設置為diff3 (我推薦這個設置;另請參見diff3應該是git上的默認沖突風格嗎? ),上面的內容被修改為包含合並基礎中的內容(在我們的例子中是commit D' ),即什么文本在“我們”和“他們”改變它之前就在那里:

     <<<<<<< HEAD stuff from the HEAD commit ||||||| merged common ancestors this is what was there before the two changes in our HEAD commit and our other commit ======= stuff from the other commit (H' in our case) >>>>>>> feature 
  • 同時, 索引 - 您構建下一個提交的位置 - 每個沖突文件每個“插槽”最多有三個條目。 在這種情況下,對於file.txt ,有三個版本的file.txt ,編號為:

    • :1:file.txt :這是它出現在合並庫中的file.txt的副本。
    • :2:file.txt :這是我們(HEAD)提交中出現的file.txt的副本。
    • :3:file.txt :這是副本file.txt ,因為它出現在他們的(的尖端feature )提交。

現在,僅僅因為file.txt中存在沖突並不意味着Git無法自行解決其他一些變化。 例如,假設合並基礎版本為:

this is file.txt.
it has a bunch of lines.
we plan to change some of them on one side of the merge.
we plan to change other lines on the other side.
here is something to change without conflict:
la la la, banana fana fo fana
here is something else
to change with conflict:
this is what was there before the two
changes in our HEAD commit and our other commit
and finally,
here is something to change without conflict:
one potato two potato

HEAD ,讓我們以這種方式讀取文件,使用我們想要達到的許多提交:

this is file.txt.
it has a bunch of lines.
we plan to change some of them on one side of the merge.
we plan to change other lines on the other side.
here is something to change without conflict:
a bit from the Name Game
here is something else
to change with conflict:
stuff from our HEAD commit
and finally,
here is something to change without conflict:
one potato two potato

(請注意,我們創建了兩個不同的更改區域。默認情況下, git diff會將它們組合成一個diff差異塊,因為它們之間只有一個上下文行,但git merge會將它們視為單獨的更改。)

在另一個( feature )分支中,讓我們做一組不同的更改,以便file.txt讀取:

this is file.txt.
it has a bunch of lines.
we plan to change some of them on one side of the merge.
we plan to change other lines on the other side.
here is something to change without conflict:
la la la, banana fana fo fana
here is something else
to change with conflict:
stuff from the other commit (H' in our case)
and finally,
here is something to change without conflict:
cut potato and deep fry to make delicious chips

我們再次進行了兩次更改,但只有一次發生沖突。

合並文件的工作樹版本將采取沖突的每個更改,以便文件將完整讀取:

this is file.txt.
it has a bunch of lines.
we plan to change some of them on one side of the merge.
we plan to change other lines on the other side.
here is something to change without conflict:
a bit from the Name Game
here is something else
to change with conflict:
<<<<<<< HEAD
stuff from the HEAD commit
=======
stuff from the other commit (H' in our case)
>>>>>>> feature
and finally,
here is something to change without conflict:
cut potato and deep fry to make delicious chips

作為合並者,你的工作就是解決沖突。

您可以選擇這樣做:

git checkout --ours file.txt

要么:

git checkout --theirs file.txt

但是這些中的任何一個只是將“我們的”或“他們的” 索引版本(從插槽2或3)復制到工作樹。 無論您選擇哪一個,您都將失去其他分支的更改。

您可以手動編輯文件,刪除沖突標記並保留或修改部分或全部剩余行以解決沖突。

或者,當然,您可以使用任何您喜歡的合並工具來處理沖突。

但是,在所有情況下,工作樹中的任何內容都將是您的最終產品。 然后你應該運行:

git add file.txt

擦除階段1,2和3條目並將文件的工作樹版本復制到正常階段零file.txt 這告訴Git現在為file.txt解析了合並。

您必須對所有剩余的未合並文件重復此操作。 在某些情況下(重命名/重命名沖突,重命名/刪除,刪除/修改等)還有一些工作要做,但這一切都歸結為確保索引只有最后的階段為零的條目,你想要的,沒有更高級的參賽作品。 (你可以使用git ls-files --stage看到他們的各個階段中的所有條目,雖然git status確實總結了有趣的一個不錯的工作,特別是有階段的非零項的所有文件完全匹配HEAD提交非常無聊, git status跳過它們。如果有數百或數千個這樣的文件,這非常有幫助。)

解決索引中的所有文件后,運行git commit 這使得合並提交M 什么是犯是無論是在你的指數,即無論你git add -ed刪除更高的階段索引項和插入階段的非零項。

使用git checkout檢查並同時解決

如上所述, git checkout --oursgit checkout --theirs只需從索引槽2或3獲取副本並將其寫入工作樹。 這不會解析索引條目:所有插槽1,2和3未合並的條目仍然存在。 您必須git add工作樹文件以將其標記為已解決。 正如我們也注意到的,這會丟失其他提示的任何更改。

如果這就是你想要的,那就有一個捷徑。 您可以:

git checkout HEAD file.txt

要么:

git checkout MERGE_HEAD file.txt

這將從HEAD( F' )或MERGE_HEAD( H' )提交中提取file.txt的版本。 這樣做,它將內容寫入 file.txt第0階段,這將消除第1,2和3階段。實際上,它獲取--ours--theirs版本 git add s結果,全部在一旦。

同樣,這會從提示提交中丟失任何更改。

這很容易弄錯

這些解決步驟錯誤很容易。 特別是,使用HEADMERGE_HEAD git checkout --oursgit checkout --theirs以及它們的快捷版本,將對方的更改刪除到文件中。 您將擁有此唯一的指示是合並結果缺少某些更改。 就Git而言,這是正確的結果:希望這些更改被刪除; 這就是你在進行合並提交之前以這種方式設置階段零索引條目的原因。

它也很容易獲得一個驚喜合並基礎,特別是如果你嘗試做很多git rebasegit cherry-pick工作來復制提交並移動分支名稱指向新副本。 總是值得仔細研究提交DAG。 從“狗”獲得幫助: git log --all --decorate --oneline --grapha ll d ecorate o neline g raph; 或者使用gitk或其他圖形查看器來可視化提交圖。 (相反的--all您也可以考慮在問題使用兩個分支名稱,即,狗,而不是任何舊的狗: git log --decorate --oneline --graph master feature產生的圖形可能是。更簡單,更易於閱讀。但是,如果你做了很多基礎重建和挑肥揀瘦的, --all可能會透露更多。你甚至可以用特定的引用日志的名字,如結合這個feature@5 ,雖然這變得有點啰嗦並使圖表非常混亂。)

你已經得到了很長很好的答案。 讓我補充一下:

我的個人理論是git以某種方式“記住”我們已經撤消了提交ABC,所以當他們來自新功能分支時,git決定不合並它們。

Git從來沒有“以某種方式”“記住”任何有關存儲庫內容的信息。 根據你之前所做的事情,它也決定不做或不做任何事情。 在這方面非常干凈。 它的所有命令都只是工具,可以處理其提交的有向非循環圖(以及較低級別,它存儲的所有其他對象)正在構建。 為了使它更容易,它只會添加東西,永遠不會改變或刪除任何東西。

除了提交(即作者,時間戳,父提交等),樹(即目錄),blob(即二進制數據)和一些不那么重要的事情之外,實際上沒有關於文件的數據結構或進一步的管理信息等等在存儲庫中。 合並提交不會留下任何特定於“合並”的信息; 它只是一個多父母的提交。

肯定沒有神奇的,無證件的東西在繼續。 存儲庫是非常開放的,您可以使用git命令逐字查看所有內容,並且所有內容都已完整記錄(如果您感興趣,可以使用google“git data structures”或“git internals”)。 如果您願意,甚至修改內部對象也很容易。

有一點點位保存歷史信息,這就是所謂的“rerere cache”,它存儲以前的沖突解決方案,因此確實可以改變未來合並的行為。 確實非常方便,但默認情況下未啟用,當然與手頭的主題無關。

編輯:對不起,如果這個解釋聽起來像“魔法思維”,但我不知所措。 如果正確的話,我歡迎任何嘗試用更多技術術語來解釋這個問題

相信來源,盧克。 很高興您正試圖讓自己的頭腦周圍的git,並堅信一切都是平淡無奇的應該有所幫助,希望如此。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM