簡體   English   中英

git filter-branch - 放棄對一系列提交中的一組文件的更改

[英]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

但這有這些缺點

  1. 如果文件在master中被刪除,它將失敗並顯示error: pathspec 'a/b/c.png' did not match any file(s) known to git. 我可能決定git checkout master-dev-older-ancestor但是接着,
  2. 這個文件可能不存在於master-dev-older-ancestor中,並且在以后的某個時候從master返回到dev
  3. 畢竟我可能想要放棄一些在master中無處可見的文件的更改

從根本上說,我不想告訴git簽出文件的特定版本 - 我想告訴git過濾范圍 master-dev-older-ancestor..HEAD 中的所有提交以便在任意集合中進行所有更改文件(呈現上的任何地方主與否丟棄

那我怎么告訴git?

從根本上說,filter-branch的作用是什么 - 其他一切都是優化和/或邊緣情況: 1

  • 對於列出的修訂中的每個提交:
    1. 看看那個提交;
    2. 應用過濾器;
    3. 根據步驟2創建一個新提交,它可能與舊提交相同或不同(即,這個新副本是舊副本的修改版本,除非它是逐位相同的,在這種情況下,“創建新的“提交實際上只是舊提交”。
  • 對於命令行中的每個“正”引用,重寫它以指向在步驟3中進行的新提交,無論它指向在步驟1中檢出的舊提交。

現在讓我們考慮你想要的行動,但我要強調一個不同的詞:

過濾[a]范圍內的所有提交...以使任意文件集中的所有更改 ...被丟棄

我在此強調“更改”,因為每次提交都是一個完整的,獨立的實體。 承諾沒有 “改變”,他們只是有文件 查看更改的唯一方法是將一個特定提交與另一個特定提交進行比較:例如git diff commitA commitB

因此,當你說“改變某些文件”時,顯而易見的問題應該是:關於什么的改變?

在大多數情況下,談論“提交中的更改”的人意味着“此提交相對於其直接祖先的更改”:對於簡單(非合並)提交,您使用git showgit 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)系列的提交C1C2 ,..., Cn 對於某些父級的C1提交,您希望它們“不改變某些路徑的內容甚至存在”。 您必須在$WITH_RESPECT_TO設置適當的說明$WITH_RESPECT_TO (這可能來自環境,或者只是硬編碼到實際的腳本中。請注意,對於--index-filter--tree-filter ,您可以讓shell運行腳本,而不是嘗試執行它一切都好。)

例如,如果您正在過濾X..Y ,這意味着“所有可從標簽Y到達的提交(不包括從標簽X可到達的所有提交”), $WITH_RESPECT_TO的適當值可能只是X ,但更可能是XY合並基礎。 如果XY是看起來像這樣的分支:

...-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 。)

如果差異表示路徑pC被刪除,我們知道它確實存在於第一個中,並且必須恢復以保持“不變”。 (這是狀態字母D 。)

如果diff表示路徑p存在,但文件內容在C不同,則必須恢復內容以保持“不變”。 (這是狀態字母M 。)

其他差異狀態字母是CRTUXB ,但有些不能發生(我們通過指定適當的git diff選項排除CRB ; 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.

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