[英]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的精彩解釋 :
我考慮過的東西 - 但沒有使用“現成的”:
我不熟悉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所做的是存儲提交,還有一些額外的功能可以使它們更有用。 額外的部分意味着有時候 ,你可以做一些足以滿足你想要的東西 - 雖然這當然取決於你想要的東西 - 或者,或許,你會滿足於什么。 讓我們仔細看看提交,看看它們是如何成為歷史的。
每次提交都是一個大多數獨立的實體。 提交保存所有文件的完整快照 - 該提交的所有文件,即一些元數據 。 每個唯一提交由其哈希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 - 是通過對提交的所有內容運行加密校驗和來計算的。 如果我們接受上面的提交並更改它的任何內容 - 例如author
或committer
行上存儲的日期戳,或日志消息,或快照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
修改,我們可以做的是創建兩個只有一個文件的新提交I
和J
我們可以在任何地方制作它們 - 所有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
我們現在可以將提交I
和J
復制到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
提交I
和J
有我們想要的文件。
該文件位於J
-then- I
-then-stop歷史記錄中 ,該歷史記錄包含這兩個提交。 (該git checkout --orphan
招確信,當我們做了犯I
,它沒有父-這是一個根犯,就像最初的承諾,我們將建立一個新的,空的版本庫。記住,所有的承諾,以它們的唯一哈希ID在每個 Git存儲庫中是通用的:你要么擁有它的哈希ID,要么你沒有.RepoB沒有它們,現在,在git fetch
,RepoB擁有它們。)
這些歷史顯然是無關的:沒有辦法從J
跳到H
和背鏈,反之亦然。 但我們現在可以告訴Git“嫁給”提交H
和J
,進行新的提交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
,並且向后跟隨該行。 該文件存在於I
和J
並且不同,因此將顯示提交J
然后Git去了I
。 在I
之前,該文件在不存在的提交中不存在,所以它在I
的明顯不同並提交I
的顯示。 然后沒有更多的提交回去,所以git log
停止。
請注意,我們可以直接在RepoA
創建I
和J
或者,我們可以將所有RepoA
的提交( ABCD
)復制到RepoB
,然后在RepoB中創建I
和J
,然后刪除導致提交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
。 你現在必須說服所有克隆人將他們的 master
從D
切換到你的新替代D'
。
如果你願意這樣做 - 而且他們也是如此 - 那么你就得到了答案:對RepoA這樣做並讓每個人都切換。 這只留下了必要的機制: 你將如何對RepoA這樣做,如果不手動操作, 你將如何獲得正確的RepoC提交?
git filter-branch
Git有一個內置命令可以做到這一點: git filter-branch
。 filter-branch命令通過復制提交來工作。 邏輯上(雖然不是物理上除了最慢的過濾器, - --tree-filter
),filter-branch的作用是:
如果新提交是100%,與原始提交一點一點,則它最終成為原始提交。 映射條目表示提交A
仍為提交A
提交B
的過濾器進行更改 - 它會刪除文件。 因此,下一次提交的父級是A
(因為A
映射到A
),但新提交獲得一個新的哈希ID, B'
,現在地圖顯示A
= A
但B
= 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
來實現這一點。)但是如果我們刪除所有其他文件......好吧,那么我們實際上有B
和C
是相同的 ,所以在我們將B
復制到B'
只有 somefile.ext
,我們將C
復制到C'
只有 somefile.ext
。 這兩份副本將匹配。 默認情況下,filter-branch無論如何都會生成C'
,因此C
有一些要映射的內容。
添加--prune-empty
告訴Git: 不要制作C'
,而只是將C
映射到B'
。 當我們這樣做時,我們得到了我們想要的東西:Git根本沒有制作A'
,使B'
- 我們正在調用I
而不是來自B
而I
沒有父母, 不會制作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.