簡體   English   中英

如何在將少量文件從一個GIT存儲庫移動到另一個存儲庫時保留預重命名歷史記錄?

[英]How to retain pre-rename history while moving few files from one GIT repository to another?

問題摘要

我需要將幾個文件從一個存儲庫移動到另一個存儲庫,同時保留其更改歷史記錄。 我已經將它們在源存儲庫中移動到一個帶有git mv的專用文件夾中(根據Greg Bauer廣泛引用的帖子 ,這導致所有文件夾移動歷史記錄在遵循Greg的腳本時不會被復制到目標存儲庫中。

我在每個涉及的存儲庫中只有master分支。

對於第一個源存儲庫,原始文件在移動到專用文件夾之前用於駐留在根文件夾中。

如果是第二個源存儲庫,(其他)原始文件用於駐留在第一級文件夾中,該文件夾還存儲許多其他文件(我不需要移動)。

目標存儲庫已經有一些我需要保留的文件和文件夾,以及它的提交歷史。

最后,如果所有內容都正確地復制到目標存儲庫,我需要一種干凈的方法來從源存儲庫中刪除(隱藏?)原始文件。

更新2019-03-25 12:00 UTC:關於我的情況的更多細節,遵循torek的精彩解釋

  1. 我是並且是所有三個存儲庫的唯一用戶(源存儲庫和一個目標存儲庫); 每個源存儲庫都在單個工作站上使用
  2. 一個源存儲庫在GitHub上托管; 另一個在GitLab(作為一個私人項目)。 目標存儲庫作為私有項目在GitLab上托管。 如果我正確理解“存儲庫”的具體含義,那么此時沒有“多個存儲庫存儲相同的提交”。
  3. 這些存儲庫的本地“.git”文件夾非常小; 最大的是磁盤上只有12 MB,2.5K文件。 因此,性能似乎不是一個大問題。
  4. 我最感興趣的是在目標存儲庫中看到的是:(a)必要的,差異的“之前與之后”的文件; (b)足夠重要,原始變化的時間戳; (c)很高興有原始委員的姓名(總是我自己)
  5. 在將來的情況下,我需要從私有存儲庫(包含其他私有文件)遷移到公共存儲庫,該存儲庫不應該提及這些私有文件或其他內容。 然而,在我今天的具體情況中,這不是一個問題。

我考慮過的東西 - 但沒有使用“現成的”:

我不熟悉GIT存儲庫的結構,所以'git ls-files ... | grep ... INDEX_FILE ... git update-index ... 'git ls-files ... | grep ... INDEX_FILE ... git update-index ...來自第1階段,第5步對我來說聽起來像個魔術。

答案到另一個問題 ,目前還不清楚它是否有助於將已移入專用文件夾的單個文件(和/或在遷移之前是否可以安全地回滾移動)。

另外,我如何決定不使用/使用這些步驟

git reflog expire --expire=now --all
git reset --hard
git gc --aggressive
git prune

我也在努力從這篇帖子中的一組片段中編譯一個單獨的腳本,這似乎也有些相關。

在每種情況下,每個人的答案都不會令人滿意。 那是因為你從字面上不能從一個Git倉庫復制文件的歷史到另一個,對於Git不會文件歷史記錄,原因很簡單。 您無法從(現有)歷史記錄中刪除文件,以獲取不同但相關的原因。 但你得到的東西可能已經足夠了。

Git歷史提交,提交是不可變的

正如我之前多次說過的那樣,Git的存在理由就是提交。 Git所做的是存儲提交,還有一些額外的功能可以使它們更有用。 額外的部分意味着有時候 ,你可以做一些足以滿足想要的東西 - 雖然這當然取決於你想要的東西 - 或者,或許,你會滿足於什么。 讓我們仔細看看提交,看看它們如何成為歷史的。

每次提交都是一個大多數獨立的實體。 提交保存所有文件的完整快照 - 該提交的所有文件,即一些元數據 每個唯一提交由其哈希ID唯一標識。 這是來自Git自身的Git存儲庫的實際提交 (可能會將 @更改為空格,或許可以減少垃圾郵件):

$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800

Fourth batch after 2.20

Signed-off-by: Junio C Hamano <gitster pobox.com>

當然,這不是GitHub如何顯示它,但這是存儲提交的內部Git對象。 保存的快照通過tree線獲得。 parent行列出了此提交之前的提交 ,它本身是一個合並提交,因此它有兩個parent行。

這里重要的是:

  • 提交由其哈希ID標識,例如, b5101f929789889c2e536d915698f58d5c5c6b7a 這就是宇宙中任何一個Git都知道它是否有這個提交的方式:你是否擁有這個哈希ID,所以你有這個提交,或者你沒有,所以你沒有。

  • 提交列出了一tree ,即保存的快照。

  • 提交列出其父級或父級的哈希ID。

意味着Git只需要上次提交的哈希ID。 假設我們用一個字母表示這個丑陋的哈希ID,例如H (用於hash )。 我們說提交H存儲其父級的哈希ID,我們將其表示為G而不是另一個大的丑陋字符串。 然后提交H 提交G

          G <-H

G 一個提交。 這意味着它存儲其父級的哈希ID,我們可以將稱為F

... <-F <-G <-H

當然, F在一個向后看的鏈中存儲E的哈希ID,依此類推。 鏈可以分叉和重新組合,如果我們前進而不是后退,當我們建立分支時會發生分叉,並且當我們合並分支時會發生重新組合。 但是由於Git實際上是倒退的,所以在合並中發生了分叉; 當我們用完合並的東西時重新組合:

             I--J
            /    \
...--F--G--H      M--N--...--T   <-- master
            \    /
             K--L

無論如何,這個鏈 Git的歷史。 如上圖所示,提供鏈中最后一次提交的哈希ID的項目是分支名稱,例如master

這就是Git所擁有的。 沒有文件歷史記錄,只有提交。 我們通過提示開始查找提交,比如T ,我們通過名稱找到它的哈希ID,就像master一樣。 我們通過創建父級T的新提交U ,然后名稱master 更改為指向新提交U來向存儲庫添加新的歷史記錄 - 新提交。

提交是不可變的, 因為它們的真實名稱 - 它們的哈希ID - 是通過對提交的所有內容運行加密校驗和來計算的。 如果我們接受上面的提交並更改它的任何內容 - 例如authorcommitter行上存儲的日期戳,或日志消息,或快照tree我們必須計算新的校驗和數據。 那個校驗和會有所不同,而不是改變 現有的提交H ,我們只需要一個新的提交H'

...--F--G--H--I--J   <-- master
         \
          H'  <-- need-a-name-here

這個新的提交H'G作為其父級,因此H'只是一個分支。 我們現在必須發明一個分支名稱,以便存儲新提交H'的哈希ID,它是H的副本,但有一些更改。 我們沒有更改任何提交,我們只是添加了一個新的提交。

但是我可以運行git log --follow somefile.ext ,是不是那個文件歷史記錄?

也許是! 但它沒有存儲在Git中 存儲在Git中的是提交。 git log做的是從某個分支名稱開始,比如master ,並在那里找到提交 - 分支的提示 該提交具有哈希ID,日志消息和快照。 當然,Git能夠找到提交的提交,保存在提示中。

現在是棘手的部分。 這一切都發生在一個大循環中,每次提交工作,一次一個提交。 Git選擇是否顯示正在進行的提交 ,以及git log somefile.ext

  • Git將父提交快照提取到臨時區域。

  • Git將提交的快照提取到臨時區域。

    (它並沒有真正提取提交,但如果你這樣想,它可能更有意義。實際上它只是比較樹內哈希ID,這就足夠了。后來,如果你已經要求git log顯示差異,它確實做了一些部分提取。但這只是一個優化,真的。)

  • 現在git log比較兩個快照。 somefile.ext變化嗎? 如果是,請顯示此提交。

  • 顯示或未顯示此提交后,移動到提交的父級。

沒有--follow ,這就是git log somefile.ext所做的所有 git log somefile.ext 您會看到一個合成的“文件歷史記錄”,其中包含文件從父級更改為子級的提交歷史記錄的子集。 而已! 您看到的是選定的提交歷史記錄 如果您願意,可以調用 “文件歷史記錄”,但它是從Git實際保留的提交歷史中動態計算的。

添加--follow告訴git log做更多的事情:在比較兩個提交時,檢查比較是否表明在父提交中, somefile.ext不同的路徑名 例如,如果父提交調用文件oldname.dat ,則git log --follow會在提交歷史記錄中向后移動一步時切換名稱

這里有一些障礙,特別是在合並提交時。 合並提交是具有兩個父級而不是一個父級的提交。 Git實際上不能同時顯示兩條路徑 - 它通過提交歷史回溯,一次提交一條。 因此,當它碰到這些合並時 - 由於Git向后工作,這是歷史分歧的地方 - 它通常只選擇歷史的一條腿

(這里的細節變得相當復雜。請參閱git log文檔的歷史簡化部分,但它很重要。當沒有特定文件名運行時,為了顯示所有提交, git log默認情況下會合並到合並的兩個部分,以一種有點難以正確描述的方式:我們必須在這里引入優先級隊列的概念。線性歷史,沒有合並,避免了所有這些混亂,並且更容易思考。)

現在回到手頭的問題

讓我們回到所需結果的原始,簡潔,總結:

我需要將幾個文件從一個存儲庫移動到另一個存儲庫,同時保留其更改歷史記錄。

也就是說,我們希望從RepoA提交的文件以某種方式出現在RepoB中的提交中。

我們可以立即看到問題:這些文件的歷史記錄實際上是RepoA中的所有提交 ,或者充其量是來自RepoA的一些提交子集 每個提交都是其所有文件的完整快照

此外,如果我們將這些快照 - 作為整體或以某種簡化形式 - 並將它們放入RepoB中, 那些快照與RepoB中的任何現有快照都不同。 讓我們舉一個簡單的具體例子,其中RepoA在一個漂亮的線性鏈中有四個快照ABCD ,而RepoB同樣有另外四個EFGH

RepoA:

A--B--C--D   <-- master

RepoB:

E--F--G--H   <-- master

如果我們只是將所有提交從RepoA復制到RepoB未經修改,我們在RepoB中得到這個:

E--F--G--H   <-- master

A--B--C--D   <-- invent-a-name-here

這顯然不是我們想要的。 我們可以做一些事情,這就是你一直在關注的所有答案。

我們在這做什么

如果我們想要repoA中的somefile.ext ,並且它首先在提交B創建然后在提交D修改,我們可以做的是創建兩個只有一個文件的新提交IJ 我們可以在任何地方制作它們 - 所有Gits都是相同的 - 所以讓我們通過克隆RepoA制作RepoC,然后在RepoC制作它們,主要是為了說明:

$ git clone <url-of-RepoA> repo-c
$ cd repo-c
$ git checkout --orphan for-transplanting
$ git rm -rf .                              # empty the index and work-tree
$ git checkout <hash-of-B> -- somefile.ext  # get the first copy of the file
$ git commit -m 'initial commit of somefile.ext'  # and commit it
$ git checkout master -- somefile.ext       # get the 2nd and last copy
$ git commit -m 'update somefile.ext'       # and commit that one

現在RepoC包含:

A--B--C--D   <-- master, origin/master

I--J   <-- for-transplanting

我們現在可以將提交IJ復制到RepoB:

$ cd <path-to-repo-B>
$ git fetch <path-to-repo-C> for-transplanting:for-transplanting

在RepoB中給我們這個:

E--F--G--H   <-- master

I--J   <-- for-transplanting

提交IJ有我們想要的文件。

該文件位於J -then- I -then-stop歷史記錄中 ,該歷史記錄包含這兩個提交。 (該git checkout --orphan招確信,當我們做了犯I ,它沒有父-這是一個根犯,就像最初的承諾,我們將建立一個新的,空的版本庫。記住,所有的承諾,以它們的唯一哈希ID在每個 Git存儲庫中是通用的:你要么擁有它的哈希ID,要么你沒有.RepoB沒有它們,現在,在git fetch ,RepoB擁有它們。)

這些歷史顯然是無關的:沒有辦法從J跳到H和背鏈,反之亦然。 但我們現在可以告訴Git“嫁給”提交HJ ,進行新的提交K

$ git checkout master
$ git merge --allow-unrelated-histories for-transplant

這使用(不存在,通過空樹偽造) 真正空提交作為合並基礎,因此H中的所有文件都是新創建的,並且J所有文件(只是一個文件)是新創建的。 它結合了這些更改 - 將所有文件添加到任何內容 並將somefile.ext添加到任何內容 - 這很容易做到,將這些更改應用於沒有文件的空樹,並將結果提交為新提交K

E--F--G--H--K   <-- master
           /
I---------J   <-- for-transplanting

現在通過查看K找到新文件somefile.ext的合成“文件歷史記錄”,看到該文件存在於J但不存在於H ,並且向后跟隨該行。 該文件存在於IJ並且不同,因此將顯示提交J 然后Git去了I I之前,該文件在不存在的提交中不存在,所以它在I的明顯不同並提交I的顯示。 然后沒有更多的提交回去,所以git log停止。

請注意,我們可以直接在RepoA創建IJ 或者,我們可以將所有RepoA的提交( ABCD )復制到RepoB ,然后在RepoB中創建IJ ,然后刪除導致提交ABCD所有名稱痕跡。 現在未使用/未引用的提交最終會真正消失(通常是30天后的某個時間),同時你不會看到它們,也不會打擾你; 他們只會占用一小塊磁盤空間。 使用RepoC的真正好處是我們可以在那里進行實驗,如果出現問題,只需將整個事情吹走並重新開始。

現在你有一個更難的問題

最后,如果所有內容都正確地復制到目標存儲庫,我需要一種干凈的方法來從源存儲庫中刪除(隱藏?)原始文件。

沒有一個。 只有骯臟的方式。 污垢有多臟或有多臟,取決於您的需求。

同樣,原始存儲庫具有所有提交。 他們都有的所有文件。 在我們的例子中,我們做了簡化的假設,即有四個提交:

A--B--C--D   <-- master

somefile.ext第一中出現B ,在保持不變D ,然后被存儲在不同的內容D

由於該文件不在A ,因此您可以保留提交A 但是你必須構建一個像B一樣的替換B' ,它具有相同的元數據,包括父A ,和以前一樣 - 但是有一個省略文件的保存快照:

A--B--C--D   <-- master
 \
  B'  <-- ??? (we'll get to this)

B B'后,你現在需要創建一個類似於C的新提交C' ,除了件事:

  • 它的父母是B'而不是B ,和
  • 它省略了somefile.ext

一旦你已經做出復制C'C ,您有:

A--B--C--D   <-- master
 \
  B'-C'  <-- ??? (we'll get to this)

現在你必須以同樣的方式將D復制到D'

A--B--C--D   <-- master
 \
  B'-C'-D'  <-- ??? (we'll get to this)

現在是時候了解問號問題中的分支名稱

顯而易見的事情是將分支名稱master從剝離D剝離並使其指向D'

A--B--C--D   [abandoned]
 \
  B'-C'-D'  <-- master

任何現在出現並查看此存儲庫的人都將從名稱 master開始,以獲取D'的哈希ID。 他們甚至不會注意到D'散列ID與D完全不同。 他們將看D'並回到C' ,然后從那里回到B'然后回到A

好吧, 幾乎任何人。 如果另一個Git出現怎么辦? 如果其他 Git已經有ABCD怎么辦? Git有他們以及他們的ID哈希他們知道 哈希ID是Git交換的通用貨幣。

可能出現的其他Gits是您從原始存儲庫中創建的任何克隆。 RepoA的所有克隆都具有原始哈希ID,列在他們自己的名稱master 你現在必須說服所有克隆人將他們的 masterD切換到你的新替代D'

如果你願意這樣做 - 而且他們也是如此 - 那么你就得到了答案:對RepoA這樣做並讓每個人都切換。 這只留下了必要的機制: 如何對RepoA這樣做,如果不手動操作, 如何獲得正確的RepoC提交?

git filter-branch

Git有一個內置命令可以做到這一點: git filter-branch filter-branch命令通過復制提交來工作。 邏輯上(雖然不是物理上除了最慢的過濾器, - --tree-filter ),filter-branch的作用是:

  • 檢查每個提交;
  • 應用你的過濾器;
  • 根據map-so-far 映射原始提交的父哈希;
  • 從過濾結果中構建新提交,並在地圖中輸入<oldhash,newhash>。

如果新提交是100%,與原始提交一點一點,則它最終成為原始提交。 映射條目表示提交A仍為提交A 提交B的過濾器進行更改 - 它會刪除文件。 因此,下一次提交的父級是A (因為A映射到A ),但新提交獲得一個新的哈希ID, B' ,現在地圖顯示A = AB = B' 現在發生C的過濾器,刪除文件並使新提交的父項為B' ,這樣結果就是新的提交C'並進入映射。 最后, D的過濾器發生,使用父C'進行新的提交D' C'

現在所有提交都已過濾, git filter-branch使用構建映射來替換master下存儲的哈希ID。 地圖上說D變為D'所以filter-branch將D' '的散列存儲在名字master ,我們得到了我們想要的東西。

RepoC中可以使用相同的技術。 請記住,RepoC是一個臨時的,我們可以在這里發生任何我們想要的破壞。 而不是刪除的somefile.ext ,我們想做的事,在我們的過濾器,是刪除一切somefile.ext 我們幾乎肯定也想要--prune-empty參數。

什么--prune-empty確實足以描述。 讓我們從沒有 --prune-empty事情開始。 在復制過程中,每個原始提交都會復制到一個新的提交。 即使新的提交在應用過濾器之后沒有做出任何更改,也是如此 如果我們有像C這樣的提交沒有觸及somefile.ext ,它可能會觸及其他文件。 (Git通常不會讓你連續兩次提交具有相同內容的git commit --allow-empty - 你必須使用git commit --allow-empty來實現這一點。)但是如果我們刪除所有其他文件......好吧,那么我們實際上有BC相同的 ,所以在我們將B復制到B' 只有 somefile.ext ,我們將C復制到C' 只有 somefile.ext 這兩份副本將匹配。 默認情況下,filter-branch無論如何都會生成C' ,因此C有一些要映射的內容。

添加--prune-empty告訴Git: 不要制作C' ,而只是將C映射到B' 當我們這樣做時,我們得到了我們想要的東西:Git根本沒有制作A' ,使B' - 我們正在調用I而不是來自BI 沒有父母, 不會制作C' ,並且D' - 我們用B'調用J -from D ,呃, I作為它的父母:

RepoC:

A--B--C--D   [abandoned]

   I-----J   <-- master

還剩下什么

剩下的就是弄清楚如何為git filter-branch編寫過濾git filter-branch 這就是你正在閱讀的現有答案。

使用的簡單過濾器是--tree-filter 當您使用此過濾器時,Git會在臨時目錄中運行您的shell腳本片段。 該臨時目錄已過濾提交中的所有文件(但沒有.git目錄,並且不是您的工作樹!)。 您的過濾器只需要修改文件,或刪除一些文件或添加一些文件。 Git將根據您的過濾器在該臨時目錄中留下的內容進行新的提交。

到目前為止,這也是最慢的過濾器。 在大型存儲庫中使用它時,請准備好等待數小時或數天。 (它有助於使用-d參數將git filter-branch指向基於內存的“文件系統”,在其中完成所有工作,但它仍然非常慢。)因此,大多數答案都集中在弄清楚如何jigger其中一個,更快的過濾器來完成這項工作。

您可以選擇使用這些,或使用非常慢的--tree-filter 無論哪種方式,如果你使用filter-branch,你現在知道你在做什么以及為什么。

暫無
暫無

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

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