簡體   English   中英

重寫 git 歷史合並兩個文件

[英]Rewrite git history to merge two files

問題設置

我正在為一個大型 git 存儲庫做貢獻。 在某些時候,制作了許多文件的副本,然后在原始文件幸存下來的同時對其進行了編輯。 現在我想將復制文件(下面命名為new_file )的修改包括回原始文件(命名為original )。 問題是初始副本是使用普通cp完成的,因此new_file沒有original的歷史記錄。

original -----A----x-------x-- D-----
new_file       B------x-x-x---C

在上面的草圖中, new_file已從original提交A復制(使用普通cp ,創建提交B ),然后多次編輯。 我知道如何使用git rm original && git mv new_file original (commit D ) 將new_file (commit C ) 復制到original中,但這會丟棄originalnew_file的行歷史記錄。

換句話說,我處在一個文件被分叉的情況下,我想將它合並回來,但是這個分叉並不是作為一個正確的 git-fork 完成的,而是作為一個文件副本完成的。

注意修改original的分支不能修改,但是修改new_file的分支可以。

我正在尋找的解決方案

我想重寫original文件的 git 行歷史記錄,使其包含來自originalnew_file的合並歷史記錄,就好像提交D是包含original文件的分支和包含對new_file所做修改的另一個分支之間的合並。

您需要的是git merge-file ,並自己找到正確的合並基礎文件。 合並的結果可能很好,但您可能不喜歡此結果的某個方面。

在 Git 中確實沒有文件歷史記錄。Git 存儲庫中的歷史記錄由提交組成。 每個提交都包含其中的每個文件——或者更確切地說,Git 知道的每個文件,無論誰進行提交,都進行了該提交。

使用正常的git merge ,您可以通過簽出來選擇一個提交:

git checkout somebranch

這讓你得到當前“在”分支somebranch上的最后一次提交,然后你運行:

git merge otherbranch

選擇當前“在”分支otherbranch上的最后一次提交作為要合並的提交。 Git 然后使用作為歷史記錄的提交查找兩個分支上的最佳共享提交,並使用來自該提交的文件作為合並基礎輸入。

注意,[原文件]被修改的分支不能被修改,但是[副本]被編輯的分支可以。

這個說法不太合理。 Git 中的分支名稱只是選擇了一些提交。 根據定義,它選擇的提交是鏈中的最后一個提交。 不能修改任何提交,甚至 Git 本身也不能。 git commit所做的是進行的提交。

也就是說,我們從提交鏈開始,其最后一次提交由分支名稱表示:

... <-F <-G <-H   <-- branch (HEAD)

這里H代表最后一次提交的實際 hash ID,不管它是什么。 我們已經檢查了分支branch ——這是附加(HEAD)的意思——因此當前提交是提交H 提交H導致回到更早的提交G ,這又導致回到F ,依此類推。

任何提交中的文件都不能更改。 它們都以一種特殊的、只讀的、僅 Git 的、凍結的和去重的格式存儲。 重復數據刪除處理了這樣一個事實,即H中的大多數文件副本與G中的大多數文件副本基本相同。 這些都是共享的(並且將來會與任何其他匹配的文件共享,無論您如何將匹配文件放入某個提交中)。 但是因為它們無法更改,甚至無法被非 Git 程序使用,所以 Git 將這些文件復制到您的工作樹中,以便您可以查看和處理它們。

當您進行的提交時,Git 會在新的提交中存儲更新的文件——同樣是完整的副本,但同樣是去重的。 新的提交自動指向上一個鏈中的最后一個提交,Git 將新提交的 hash ID 寫入分支名稱:

... <-F <-G <-H <-I   <-- branch (HEAD)

假設你的意思是你想做一個添加到某個分支的新提交,並且在這個新提交中,你想要一個名為original的文件,其內容是一些三向合並過程的結果,這是你如何做到的:

  1. 找到當前文件的當前版本。 這很容易,就像在new_file中一樣。

  2. 找到他們文件的另一個最新版本的當前內容,該文件名為original 這也很容易,因為它在他們分支的最后一次提交中是original的。

  3. (這是困難的部分,但看起來您已經這樣做了):找到兩個分支中相同的文件的任何版本。 您的描述暗示提交B中名為new_file的文件就是這樣一個文件。 它應該與提交A中名為original的文件相匹配:

     git diff A:original B:new_file

    應該什么都不顯示(用實際的 hash ID 替換AB )。 因此,這兩個文件中的任何一個都足夠了。

  4. 將每個文件的文本提取到一個臨時區域(當前目錄中的三個單獨的文件就可以了,或者您可以將它們放在三個單獨的目錄中,或者您想要做的任何事情)。 當前版本的new_file很簡單,因為您只需使用cp即可:

     cp new_file ours

    當前版本的original分支otherbranch很容易通過git show獲得:

     git show otherbranch:original > theirs

    該文件的基本版本需要知道提交AB的 hash ID,但在其他方面與theirs的相同:

     git show B:new_file > base

    例如。

  5. 運行git merge-file ours base theirs (假設您使用了上面的名稱oursbasetheirs )。 git merge-file命令將對這三個輸入文件執行單個低級合並,就好像您運行git merge並且它選擇了B (或您從中獲取基礎文件的任何地方)作為合並基礎一樣。

git merge-file將把合並的最大努力結果放入名為ours的文件中,給出上述命令。 如果存在合並沖突,您將必須像往常一樣手動解決它們,或者您可以使用--ours--theirs--union等同於-X ours-X theirs或不是的東西實際上可用在git merge 有關--union的注意事項,請參閱git merge-file文檔

現在文件已合並,您只需將其重命名為您喜歡的任何名稱(例如original ),然后運行git add將該文件復制到 Git 的索引中,在此過程中對其進行壓縮和去重,為下一個文件做好准備犯罪。 Git 的索引現在包含此文件的合並版本,因此git commit現在會將此版本的文件保存在新快照中(連同當前在 Git 索引中的所有其他文件——您可以使用git ls-files --stage詳細查看它們,盡管這很少有用,或者git status將索引與HEAD提交和您的工作樹進行比較)。

請注意,這只會添加到提交歷史記錄中

當你提交時,你得到的當然只是一個新的提交,帶有一組新的文件。 如果新提交同時包含originalnew_file ,則它包含兩個文件。 如果您在提交之前刪除new_file (使用git rm new_file ),新提交將缺少new_file但具有original ,其中包含您使用git add寫入 Git 索引的內容。

當你有 Git 遍歷提交歷史時,你可以啟用 Git 的重命名檢測和/或復制檢測 如果 Git 確定前一次提交的new_file與新提交的original足夠相似, 1它可能會將名為new_file的文件(在舊提交中)標識為名為original的文件(在新提交中)的源。 但這里並沒有保證,還有其他絆腳石:見腳注 1。

通常,Git 將新舊文件按名稱配對。 只有當一個文件在左側提交中丟失,並且一個具有不同名稱的新文件突然出現在右側提交中時,Git 才會打擾重命名檢測器。 即使那樣,您也必須將其打開。 它在 2.9 或更高版本的 Git 版本中默認打開,您可以在較早的版本中使用--find-renamesdiff.renames配置設置打開它。

如果右側文件是新的並且您打開復制查找代碼,Git 也會將左側文件視為復制操作的源。 不過,您必須使用--find-copies-harder選項使其將所有左側文件視為可能的來源。

不過,聽起來每個差異對的右側文件都不會是新的,所以這無濟於事,而且git log --follow都不會打開它——所以git log --follow original不會注意到重命名,並且不會嘗試在new_file返回提交時嘗試跟隨它,一次一個,以向您顯示提交歷史記錄,或者提交歷史記錄被編輯為僅顯示“觸及”特定的特定提交文件與其父提交相比。


默認情況下,足夠相似需要 50% 或更高的“相似性指數”。 Git 的相似性計算相當模糊:它使用 packfile delta 壓縮器來識別文件的哪些部分匹配,哪些不匹配。 然后它將匹配大小除以整體大小。

git diff代碼有一個-B選項來破壞配對,但聽起來它對您的情況根本沒有幫助。)

這是一個稍微不同的問題的不同答案。 假設我們有:

...--A--o--o--...--o   <-- theirbranch
      \
       B--C--D--...--H   <-- ourbranch

並且,回到提交B提交B的人選擇將文件original復制到文件new_file ,但我們現在認為這是一個錯誤:提交B不僅應該保留名稱original ,所以應該通過H提交C包括在內.

雖然更改任何現有提交在物理上是不可能的,但我們——人類和我們對 Git 的指導——通過使用分支名稱來查找提交。 因此,如果我們可以將提交B復制到一個新的和改進的提交B' ,而不是在H之后進行新的提交來修復名稱,但給我們留下不滿意的歷史記錄怎么辦? 我們會有:

       B'  <-- improved
      /
...--A--o--o--...--o   <-- theirbranch
      \
       B--C--D--...--H   <-- ourbranch

復制B' (沿途進行了一些更改:具體來說,使用new_file及其新內容,重命名為original )以制作新的和改進B' - 具有不同 hash ID 的不同提交,但具有相同的父A和以前一樣——我們現在將現有提交C復制到新的和改進C' 這一次,我們不僅將new_file重命名為original ,還將新改進C'的父級設置為B' ,這樣我們就有了:

       B'-C'  <-- improved
      /
...--A--o--o--...--o   <-- theirbranch
      \
       B--C--D--...--H   <-- ourbranch

我們對提交D重復此操作,依此類推,一直到我們分支上的最后一次提交,以便我們擁有:

       B'-C'-D'-...--H'  <-- improved
      /
...--A--o--o--...--o   <-- theirbranch
      \
       B--C--D--...--H   <-- ourbranch

現在我們使用偷偷摸摸的技巧:我們將名稱ourbranch從提交H中拉出來,讓它指向提交H' ,這樣我們就有了:

       B'-C'-D'-...--H'  <-- improved, ourbranch
      /
...--A--o--o--...--o   <-- theirbranch
      \
       B--C--D--...--H   [abandoned]

我們現在可以安全地完全刪除名稱improved ,並且,由於我們通過分支名稱找到提交,我們將不會再找到提交H或通過B返回的任何早期提交:

       B'-C'-D'-...--H'  <-- ourbranch
      /
...--A--o--o--...--o   <-- theirbranch

似乎不知何故,我們更改了ourbranch上的每個提交。 我們沒有——我們所做的是更改ourbranch上的整個提交集,而不是在每個提交中進行更改。 原件仍然存在,如果我們密切注意丑陋的 hash ID,我們可以看出這些提交( B'H' )實際上是全新的提交,但誰會注意那些 hash ID ?

這是一個反問句,但它有一個答案: Git關注那些 hash ID。 因此,請確保您的 Git 指的是您的新提交,而不是舊提交,方法是確保每個選擇提交的名稱都會選擇其中一個新提交。 現在您已經在您的存儲庫中完成了此操作,您必須讓存儲庫的所有其他克隆其他 Git)執行相同的操作。 這是重寫歷史的真正痛苦的部分:通常還有您的存儲庫的其他副本,並且它們必須在這里與您一起使用。

我們 go 如何制作副本

Git 為這種一系列提交的大量復制內置了兩個東西:

  • git rebase ,不是為這個工作設計的;
  • git filter-branch ,但很難使用。

它還有一個工具,這是git rebase主要使用的工具: git cherry-pick復制單個提交。 如果您必須對每個副本進行復雜的更改,由坐在鍵盤前的人驅動,使用git cherry-pick是到 go 的方法。(例如,可能必須手動檢查每個提交以查找對new_file 。)

假設您不必在復制每個提交時查看它,那么git filter-branch將作為完成這項工作的工具。 因為它是如此笨拙——緩慢且難以使用——它現在正在積極地被git filter-repo取代,但即使是當前版本的 Git 實際上也沒有附帶git filter-repo 因此,我將說明git filter-branch的用法。

我們通常從復制存儲庫開始:

git clone <url>
git checkout ourbranch   # use checkout's "DWIM mode" to create the branch

或類似的,因為過濾器分支出錯可能會非常混亂。 (一旦你對 Git 非常熟悉,從中恢復並不難,但通過處理副本,你可以避免那種沉沒的感覺:如果你破壞了副本,你只需將其刪除並重新開始。原版還是不錯的。)

現在我們運行git filter-branch -- er... uhm... what goes here? 現在我們遇到了一個問題,因為 filter-branch 有令人眼花繚亂的過濾選項。 使用哪一個取決於您。 每個都有不同的目的和不同的能力。 的很難用; 的比較容易; --tree-filter是最慢的之一,也是最簡單的之一。 在這種情況下--index-filter可能是可用的,這是最快的之一。 要使用--index-filter ,我們只需要 Git 在 Git 的索引中重命名new_file以使其命名為original 但是,如果我們需要對樹的rest做任何事情,那將變得非常困難。

我將在這里說明樹過濾器,因為它在概念上更簡單。 --tree-filter的工作方式是這樣的:

  • 對於要復制的每個提交,Git 將整個提交提取到一個工作區中。 它使用的工作區不是您的標准工作區。 它在某個臨時目錄中關閉。

  • 然后,Git 運行您的過濾器。 您的過濾器可以是任何東西:一個 Python 程序、一個 shell 腳本、一個二進制文件,等等。 您的過濾器從臨時目錄運行,並且可以對該目錄中的所有文件執行任何它喜歡的操作。

  • 當您的程序成功完成(返回零退出狀態)時,Git 檢查臨時目錄。 無論這里有什么文件,無論它們以什么形式存在,這些都是 go 復制到副本中的文件。 因此,您所做的任何更改都會顯示在副本中。 副本獲取原作者和committer姓名和email地址和日期等,並保留日志信息; 此處唯一發生變化的是新提交的 hash ID 和快照以及父 hash ID 由於任何較早的復制而需要。

因此,如果您需要做的只是重命名一個文件,那么您的--tree-filter可以包含單個命令mv new_file original 這將重命名臨時目錄的new_file副本並以成功狀態退出(因為該文件確實存在並且已成功重命名)。 如果您需要做更多的事情,您可以編寫一個程序來搜索特定文件以查找必須更改的new_file的引用,然后更改它們。 您對任何臨時文件所做的所有更改都將 go 放入新提交中。 請注意,如果您的程序在臨時目錄中創建了備份文件,或者不小心刪除了任何文件,那么這些備份文件或刪除操作也會 go 進入新的提交!

現在我們有了合適的--tree-filter程序或腳本或其他任何東西,我們需要選擇filter-branch將復制的提交集,以及filter-branch在完成所有操作后將移動的分支名稱復制。 這部分在過濾器和一個可選的(但你應該總是使用它)之后進行-- ,在我們的例子中,我們只想復制可從ourbranch訪問的提交,並且只復制提交A之后的提交,因此我們將使用:

git filter-branch \
   --tree-filter /tmp/fixup.sh \
   -- \
   A..ourbranch

為了發布目的,我將其分成四行,並假設樹過濾器腳本很復雜並且位於/tmp/fixup.sh中(請注意,這必須是可執行文件和絕對路徑,因為過濾器-分支操作正在某些不可預測的臨時目錄中運行)。 第一行是 filter-branch 本身的調用,第二行是我們選擇的樹過濾器,第三行是我們應該使用的--最后一行是提交A的 hash 作為“停止”或否定引用,和字面名稱ourbranch作為“開始”或正面參考。 因此,過濾器分支將:

  • 復制git rev-list A..ourbranch將列出的提交——事實上,filter-branch使用git rev-list來獲取其 hash ID;
  • 完成后,使用對名稱ourbranch的正引用來了解要調整的名稱。

這意味着我們可以在開始之前運行git log A..ourbranch ,以確保這實際上是要復制的正確提交集。 由於我們在存儲庫的副本中執行所有這些操作,因此從某種意義上說,如果我們弄錯了,它是“安全的”,但是由於 filter-branch 非常慢,弄錯了很煩人。

當 filter-branch 結束時,它會留下refs/original/refs/heads/ourbranch 如果過濾成功並且我們對更新后的克隆感到滿意,我們應該刪除剩余的名稱。

如果您有指向將被復制的提交的標記名稱,請注意這些標記名稱將指向原始提交。 要移動標簽名稱,您必須添加--tag-name-filter

(如果你可以安裝git filter-repo並使用它,它會更快更方便,雖然你通常想知道 Python 用它來做任何花哨的事情。)

暫無
暫無

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

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