[英]git filter-branch - discard the changes to a set of files in a range of commits
假設我有一個分支dev
,我想放棄在dev
分支的提交范圍內對一組文件所做的所有更改,因為它與master
分開。 如果此范圍內的提交僅觸及我喜歡的那些文件,則會將其修剪掉。 我得到的最接近的是:
git checkout dev
git filter-branch --force --tree-filter 'git checkout master -- \
a/b/c.png \
...
' --prune-empty -- master-dev-older-ancestor..HEAD
但這有這些缺點
error: pathspec 'a/b/c.png' did not match any file(s) known to git.
我可能決定git checkout master-dev-older-ancestor
但是接着, dev
從根本上說,我不想告訴git簽出文件的特定版本 - 我想告訴git過濾范圍 master-dev-older-ancestor..HEAD
中的所有提交,以便在任意集合中進行所有更改文件(呈現上的任何地方主與否 ) 丟棄 。
那我怎么告訴git?
從根本上說,filter-branch的作用是什么 - 其他一切都是優化和/或邊緣情況: 1
現在讓我們考慮你想要的行動,但我要強調一個不同的詞:
過濾[a]范圍內的所有提交...以使任意文件集中的所有更改 ...被丟棄
我在此強調“更改”,因為每次提交都是一個完整的,獨立的實體。 承諾沒有 “改變”,他們只是有文件 。 查看更改的唯一方法是將一個特定提交與另一個特定提交進行比較:例如git diff commitA commitB
。
因此,當你說“改變某些文件”時,顯而易見的問題應該是:關於什么的改變?
在大多數情況下,談論“提交中的更改”的人意味着“此提交相對於其直接祖先的更改”:對於簡單(非合並)提交,您使用git show
或git log -p
獲得的補丁git log -p
。 (通常他們沒有考慮如果提交是一個合並它們意味着什么,因此有多個父母。對於這些, git show
通常顯示合並提交與其所有父項的組合差異,但這可能與用戶的意圖不匹配這里;有關詳細信息,請參閱git-show文檔 。)
使用git filter-branch
,您必須自己定義(更改相關內容)。 filter-branch
命令為您提供簽出提交的SHA-1 ID - 即使它僅在步驟1中“虛擬”檢出,而不是實際填充到磁盤樹中 - 在環境變量$GIT_COMMIT
。 因此,如果您對“關於什么”的定義是“關於第一個父母”,您可以使用gitrevisions
語法來引用父級: ${GIT_COMMIT}^
是第一個父級,即使${GIT_COMMIT}
是原始SHA-1。
一個非常粗略和未優化的--tree-filter
只是簡單地提取每個這樣的文件的父版本,如下所示: 2
for path in ...list-of-paths...; do
git checkout -q ${GIT_COMMIT}^ -- $path 2>/dev/null
done
exit 0 # in case the last "git checkout" failed, override its status
它只是要求git檢索父提交的文件版本,丟棄由於該文件在父版本中不存在而發生的任何錯誤消息。 但這可能與您的意圖不符:如果文件不在父文件中,則不清楚是否要刪除該文件。 此外,如果在您的范圍內的提交序列中的某處添加或刪除文件,則僅將每個原始提交與其(單個)原始父提交進行比較可能會錯誤觸發。 例如,如果文件foo
在提交C5中不存在,確實存在於C6中,並且在C7中保持不變,則C7和C6之間的比較表示“文件未更改”,而早期的C5到C6比較表示“文件已添加” 。 如果你的新的(改變的)C6-let叫它C6'告訴他們分開 - 刪除foo
因為它不在C5中,大概你的C7'也應該省略文件foo
。
另一種方法是將每個提交與整個范圍之前的(單個)提交進行比較。 如果您的范圍涵蓋提交C1,C2,C3,...,C9,我們可以調用單個先前的提交C0。 然后,不是將C1與C1 ^,C2與C2 ^進行比較,而是將C1與C0,C2與C0,C3與C0進行比較,依此類推。 根據您的“變化”的定義,這可能正是你想要的,因為“撤消變更”可能是傳遞的:除去foo
在我們的新C6,因此,我們必須消除foo
在我們新的C7為好; 我們在新的C7中添加了背bar
,因此我們必須將它添加回新的C8中,依此類推。
比較腳本的粗略版本就像這樣(這也可以針對--index-filter
進行優化,雖然我會把工作留給其他人,因為這是為了說明):
# Note: I haven't tested this either, not sure how it behaves if
# used inside git filter-branch. As a --tree-filter you would not
# really want to "git rm" anything, just to "rm" it. As an
# --index-filter you would want to "git rm --cached". For
# checkout, as a tree filter you want to extract the file into
# the working tree, and as an index filter you want to extract
# the file into the index.
git diff --name-status --no-renames $WITH_RESPECT_TO $GIT_COMMIT \
-- ...paths... |
while read status path; do
# note: $path may have embedded white space, so we
# quote it below to protect it from breaking into words
case $status in
A) git rm -- "$path";; # file was added, rm it to undo
D|M) git checkout $WITH_RESPECT_TO -- "$path";; # deleted or modified
*) echo "file $path has strange status $status, help!" 1>&2; exit 1;;
esac
done
說明:上面假設您正在過濾一個(可能是線性的,可能是branch-y)系列的提交C1
, C2
,..., Cn
。 對於某些父級的C1
提交,您希望它們“不改變某些路徑的內容甚至存在”。 您必須在$WITH_RESPECT_TO
設置適當的說明$WITH_RESPECT_TO
。 (這可能來自環境,或者只是硬編碼到實際的腳本中。請注意,對於--index-filter
或--tree-filter
,您可以讓shell運行腳本,而不是嘗試執行它一切都好。)
例如,如果您正在過濾X..Y
,這意味着“所有可從標簽Y
到達的提交(不包括從標簽X
可到達的所有提交”), $WITH_RESPECT_TO
的適當值可能只是X
,但更可能是X
和Y
合並基礎。 如果X
和Y
是看起來像這樣的分支:
...-o-o-o-o-o-o <-- master
\
*-o-o <-- X
\
o-o-o-o <-- Y
然后你要過濾底行的提交,並且第一個要過濾的提交可能應該“相對於commit *
某些路徑不變”(我用星號標記的提交)。 這就是git merge-base XY
提出的提交。
如果您正在使用原始SHA-1 ID,則可以使用以下內容:
WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \
git filter-branch ... (filter-branch arguments go here) ... --
676699a0e0cdfd97521f3524c763222f1c30a094..branch
其中原始SHA-1是commit *
的ID,就像它一樣。
至於git diff
本身,讓我們看一下它產生的輸出類型:
$ git diff --name-status --no-renames \
> 2cd861672e1021012f40597b9b68cc3a9af62e10 \
> 7bbc4e8fdb33e0a8e42e77cc05460d4c4f615f4d
M Documentation/RelNotes/1.8.5.4.txt
A Documentation/RelNotes/1.8.5.5.txt
M Documentation/git.txt
M GIT-VERSION-GEN
M RelNotes
(這是git
本身的源代碼樹上git diff
實際輸出)。 在這兩個版本之間,修改了一個發布說明文本文件,添加了一個,修改了Documentation/git.txt
,依此類推。 現在讓我們再次嘗試,但將其限制為一個真正的路徑名和一個假路徑名:
$ git diff --name-status --no-renames \
> 2cd861672e1021012f40597b9b68cc3a9af62e10 \
> 7bbc4e8fdb33e0a8e42e77cc05460d4c4f615f4d \
> -- Documentation/RelNotes/1.8.5.5.txt NoSuchFile
A Documentation/RelNotes/1.8.5.5.txt
現在我們找到一個添加的文件,但沒有關於不存在的文件的抱怨。 所以給“不存在”的路徑是可以的; 它們根本不會出現在輸出中。
如果針對某些后來的提交C
$WITH_RESPECT_TO
提交$WITH_RESPECT_TO
表示路徑p
在提交C
添加,我們知道它在$WITH_RESPECT_TO
中不存在並且在C
,因此我們想要刪除它以使其“未更改”。 (這是狀態字母A
。)
如果差異表示路徑p
在C
被刪除,我們知道它確實存在於第一個中,並且必須恢復以保持“不變”。 (這是狀態字母D
。)
如果diff表示路徑p
存在,但文件內容在C
不同,則必須恢復內容以保持“不變”。 (這是狀態字母M
。)
其他差異狀態字母是C
, R
, T
, U
, X
和B
,但有些不能發生(我們通過指定適當的git diff
選項排除C
, R
和B
; U
僅在不完全合並期間發生;並且X
應該永遠不會發生:看看Git“配對破壞”和“未知”狀態意味着什么,它們何時發生? )。 T
情況可能會導致中止過濾(例如,常規文件更改為符號鏈接,反之亦然;或者替換為子模塊)。
如果在考慮了問題一段時間之后,你決定“關於” 應該使用父提交,你可以使用git diff-tree
,它給定一個提交 - 比較提交樹和那些提交樹它的父母。 (但請再次注意它在合並提交時的行為,並確保這是你想要的。)
1當使用--tree-filter
,它實際上會執行完整的檢查 - 所有內容部分。 使用--index-filter
它將提交寫入索引,但實際上不會寫入文件系統,並允許您在索引中進行所有更改。 使用--env-filter
, - --msg-filter
, --parent-filter
--commit-filter
和--commit-filter
,它允許您更改每個提交的文本,作者和/或父級。 --tag-name-filter
允許您根據需要更改標記名稱,並使新名稱指向新提交而不是舊提交(因此--tag-name-filter cat
名稱不變並使這些名稱保持不變指向舊的提交,現在指向新的提交)。
--prune-empty
覆蓋了一個邊緣情況:如果你有--prune-empty
提交C1 <- C2 <- C3
,你的C2'
(你的C2
副本)與你的C1'
具有相同的底層樹,比較樹木C2'
和C1'
產生空差異。 filter-branch操作通常會保留這些,但如果你使用--prune-empty
則省略它們:你的新鏈將是C1' <- C3'
。 但請注意,原始鏈可能有“空”提交; 在這種情況下,即使副本實際上與原始副本相同, filter-branch
也會修剪它們。
2這些腳本就像在腳本文件中一樣編寫。 如果你將它們變成單行,你需要根據需要添加分號,也可以將exit
轉換為return
,因為你不希望在eval
ed時退出整個東西。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.