git filter-branch - отменить изменения в наборе файлов в диапазоне коммитов
Скажи у меня есть ветка 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
но это имеет эти недостатки
- если файл был удален в master, он потерпит неудачу с
error: pathspec 'a/b/c.png' did not match any file(s) known to git.
Я мог бы решитьgit checkout master-dev-older-ancestor
но потом, - этот файл может не существовать в master-dev-old-ancestor и был объединен с master обратно в
dev
на более позднем этапе - В конце концов, я могу отменить изменения в некоторых файлах, которые не отображаются в мастере.
Суть в том, что я не хочу, чтобы git извлекал конкретную версию файла - я хочу сказать, что git фильтрует все коммиты в диапазоне. master-dev-older-ancestor..HEAD
чтобы все изменения в произвольном наборе файлов (присутствующих где-либо на главном или нет) были отброшены.
Так как мне сказать, мерзавец?
1 ответ
По сути, то, что делает ветвь фильтра, это - все остальное - оптимизация и / или крайние случаи: 1
- Для каждого коммита в перечисленных ревизиях:
- проверить этот коммит;
- применить фильтр (ы);
- создать новый коммит, который может совпадать или не совпадать со старым коммитом в зависимости от шага 2 (т. е. эта новая копия является модифицированной версией старой, если только она не идентична по битам, в этом случае " создал новый "коммит" на самом деле просто старый коммит в конце концов).
- Для каждого "положительного" ref в командной строке перепишите его так, чтобы он указывал на новый коммит, сделанный на шаге 3, где бы он ни указывал на старый коммит, извлеченный на шаге 1.
Теперь давайте рассмотрим желаемое действие, но я собираюсь подчеркнуть другое слово:
отфильтровать все коммиты в диапазоне [a]... чтобы все изменения в произвольном наборе файлов... были отброшены
Я подчеркиваю "изменения" здесь, потому что каждый коммит является законченной, автономной сущностью. У коммитов нет "изменений", у них просто есть файлы. Единственный способ увидеть изменения - сравнить один конкретный коммит с другим конкретным коммитом: git diff commitA commitB
например.
Таким образом, когда вы говорите "изменения в некоторых файлах", сразу возникает очевидный вопрос: изменения относительно чего?
В большинстве случаев люди, которые говорят об "изменениях в коммите", имеют в виду "изменения в этом коммите по отношению к его непосредственному предку": для простых коммитов (без слияния) патч, который вы получите с git show
или же git log -p
, (Обычно они не думают о том, что они имеют в виду, если коммит является слиянием, и, следовательно, имеют несколько родителей. git show
обычно показывает комбинированную разницу в коммите слияния против всех его родителей, но это может не совпадать с намерением пользователя здесь; подробности смотрите в документации по git-show.)
Когда используешь git filter-branch
Вы должны будете определить это (изменения относительно чего) сами. filter-branch
Команда дает вам идентификатор SHA-1 извлеченного коммита - даже если он был "фактически" извлечен на шаге 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 - назовем его C6, чтобы отделить их - удаляет foo
потому что это не было в C5, вероятно, ваш C7'должен также пропустить файл foo
,
Другой альтернативой является сравнение каждого коммита с (одиночным) коммитом непосредственно перед всем диапазоном. Если ваш диапазон охватывает коммиты C1, C2, C3, ..., C9, мы можем вызвать один предыдущий коммит C0. Затем вместо сравнения C1 с C1^, C2 с C2^ и т. Д. Мы можем сравнить C1 с C0, C2 с C0, C3 с C0 и так далее. В зависимости от вашего определения "изменений", это может быть именно то, что вы хотите, потому что "отмена изменений" может быть переходным: мы удаляем foo
в нашем новом C6, поэтому мы должны удалить foo
и в нашем новом C7; мы добавляем обратно bar
в новом C7, поэтому мы должны добавить его обратно в новый 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
Объяснение: приведенное выше предполагает, что вы фильтруете (возможно, линейный, возможно, ветвь-у) серию коммитов C1
, C2
,..., Cn
, Вы хотите, чтобы они "не изменяли содержимое или даже существование" некоторого набора путей относительно некоторого родителя C1
совершить. Вы должны установить соответствующий спецификатор в $WITH_RESPECT_TO
, (Это может происходить из среды или просто быть жестко закодировано в реальный сценарий. Обратите внимание, что для вашего --index-filter
или же --tree-filter
, вы можете заставить оболочку запускать скрипт, вместо того, чтобы пытаться делать все подряд.)
Например, если вы фильтруете 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
затем вы фильтруете коммиты в нижней строке, и первый коммит, который должен быть отфильтрован, вероятно, должен быть "неизменным по отношению к некоторым путям, как видно из коммита". *
" (коммит, который я пометил звездочкой). Это коммит, который git merge-base X Y
придумал бы.
Если вы работаете с необработанными идентификаторами SHA-1, вы можете использовать что-то вроде:
WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \
git filter-branch ... (filter-branch arguments go here) ... --
676699a0e0cdfd97521f3524c763222f1c30a094..branch
где необработанный SHA-1 является идентификатором коммита *
, как было.
Для 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 diff
на исходном дереве для git
сам). Между этими двумя ревизиями был изменен один текстовый файл с примечаниями к выпуску, один был добавлен, 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
Теперь мы узнаем об одном добавленном файле, но на несуществующий файл нет претензий. Так что можно указывать "несуществующие" пути; они просто не появятся на выходе.
Если дифференцировать совершить $WITH_RESPECT_TO
против некоторых позже совершить C
говорит, что путь p
добавлено в коммит C
мы знаем, что он не существует в $WITH_RESPECT_TO
и делает в C
поэтому мы хотим удалить его, чтобы он "не изменился". (Это касается статуса-письма A
.)
Если дифференциал говорит, что путь p
удаляется в C
Мы знаем, что оно существует в первом и должно быть восстановлено, чтобы оставаться "неизменным". (Это касается статуса-письма D
.)
Если разница говорит, что путь p
существует в обоих, но содержимое файла отличается C
, содержимое должно быть восстановлено, чтобы остаться "без изменений". (Это касается статуса-письма M
.)
Другие разностные буквы C
, R
, T
, U
, X
, а также B
, но некоторые не могут произойти (мы исключаем C
, R
, а также B
указав соответствующие git diff
опции; U
происходит только при неполных слияниях; а также X
никогда не должно происходить: посмотрите, что означают состояния Git "сломано соединение" и "неизвестно", и когда они возникают?). T
case может привести к прерыванию фильтрации (например, обычный файл изменен на символическую ссылку или наоборот; или что-то заменено подмодулем).
Если, подумав над проблемой некоторое время, вы решили, что "по отношению к" следует использовать родительские коммиты, вы можете использовать git diff-tree
, который - с учетом одного коммита - сравнивает дерево коммита с деревьями его родителей. (Но, опять же, обратите внимание на его поведение при фиксации слияний и убедитесь, что это именно то, что вам нужно.)
1 При использовании --tree-filter
, это на самом деле делает полноценную часть проверки все. С --index-filter
он записывает коммит в индекс, но не фактически в файловую систему, и позволяет вам вносить все изменения в индекс. С --env-filter
, --msg-filter
, --parent-filter
, а также --commit-filter
Позволяет изменить текст, автора и / или родителей каждого коммита. --tag-name-filter
позволяет изменять имена тегов, если это необходимо, и заставляет новые имена указывать на новые коммиты вместо старых (следовательно, --tag-name-filter cat
оставляет имена без изменений и делает те, которые указывали на старые коммиты, теперь указывают на новые).
--prune-empty
охватывает крайний случай: если у вас есть цепочка коммитов C1 <- C2 <- C3
, и ваш C2'
(ваша копия C2
) имеет то же дерево, что и ваше C1'
сравнивая деревья C2'
а также C1'
производит пустой дифференциал Операция filter-branch обычно сохраняет их, но пропускает их, если вы используете --prune-empty
: ваша новая цепь будет C1' <- C3'
, Но обратите внимание, что исходная цепочка может иметь "пустые" коммиты; в этом случае, filter-branch
удалит их, даже если копии на самом деле совпадают с оригиналами.
2 Эти сценарии написаны как будто в файлах сценариев. Если вы превратите их в однострочники, вам нужно будет добавлять точки с запятой по мере необходимости, и, возможно, также повернуть exit
в return
, так как вы не хотите, чтобы все это вышло, когда eval
редактор