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
, но тогда, - этот файл может не существовать в мастер-Дев-старший-предок, и был слит с мастером обратно в
dev
в более поздний момент - в конце концов, я могу отказаться от изменений в некоторых файлах, которые нигде не видны в master
master-dev-older-ancestor..HEAD
иметь все изменения в произвольном наборе файлов (присутствует в любом месте на master или нет ) отброшено.
Так как же мне сказать git ?
1 ответ:
В принципе, то, что делает ветвь фильтра, - это все остальное-оптимизация и / или граничные случаи:1
Теперь давайте рассмотрим ваше желаемое действие, но я подчеркну другое слово:
- для каждого коммита в перечисленных ревизиях):
- проверьте эту фиксацию;
- применить фильтр (ы);
- создайте новый коммит, который может быть или не быть таким же, как старый коммит, в зависимости от шага 2 (т. е. эта новая копия является модифицированной версией старого, если она не идентична биту за бит, и в этом случае "созданный новый" коммит является на самом деле просто старый коммит в конце концов).
- для каждого "положительного" ref в командной строке перепишите его так, чтобы он указывал на новый коммит, сделанный на Шаге 3, где бы он не указывал на старый коммит, извлеченный на шаге 1.
Фильтровать все коммиты в диапазоне [a]... иметь все изменения в произвольном наборе файлов ... отброшено
I подчеркните здесь "изменения", потому что каждая фиксация является полной, автономной сущностью. Коммиты не имеют "изменения", они просто имеют файлы. Единственный способ увидеть изменения-сравнить один конкретный коммит с другим конкретным коммитом:
Таким образом, когда вы говорите "изменения в каком-то файле(файлах)", непосредственным очевидным вопросом должен быть: изменения относительно чего?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
, который просто извлекает родительские версии каждого такого файла, выглядит следующим образом:2for 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 получить версию родительского коммита файла, отбрасывая любое сообщение об ошибке, возникающее из-за того, что файл не существует в родительской версии. Но это также может не соответствовать вашим намерениям: неясно, хотите ли вы удалить файл, если его нет в Родительском файле. Более того, если файл добавляется или удаляется где-то в последовательности коммиты в вашем диапазоне, сравнение каждого исходного коммита только с его (единственным) исходным родительским коммитом может привести к ошибкам. Например, если файл
Другой альтернативой является сравнение каждого коммита с (единственным) коммитом непосредственно перед всего диапазона. Если ваш диапазон охватывает коммиты C1, C2, C3,..., C9, мы можем назвать единственный предыдущий коммит C0. Тогда вместо сравнения C1 с C1^, C2 с C2^ и так далее, мы можем сравнить C1 с C0, C2 с C0, C3 с C0 и так далее. В зависимости от вашего определения "изменений", это может быть именно то, что вы хотите, потому что "отмена изменений" может быть транзитивной: мы удаляемfoo
не существует в commit C5, существует в C6 и остается неизменным в C7, сравнение между C7 и C6 говорит "файл без изменений", в то время как более раннее сравнение C5-C6 говорит "файл добавлен". Если ваш новый (измененный) C6 - назовем его C6', чтобы отличить их друг от друга-удаляетfoo
, потому что его не было в C5, вероятно, ваш C7' также должен опустить файлfoo
.foo
в нашем новом С6, следовательно, мы должны удалитьfoo
и в нашем новом С7; мы добавляем обратноbar
в новом С7, следовательно, мы должны добавить его обратно в новом С8, и так далее.Менее грубая версия сценария сравнения выглядит следующим образом (это может быть оптимизировано и для
--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
, что означает " все коммиты reachable from labelY
исключая все коммиты, достижимые из labelX
", возможно, что соответствующее значение для$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 X Y
придумал бы.Если вы работаете с raw SHA-1 id, вы можете использовать что-то вроде:
WITH_RESPECT_TO=676699a0e0cdfd97521f3524c763222f1c30a094 \ git filter-branch ... (filter-branch arguments go here) ... -- 676699a0e0cdfd97521f3524c763222f1c30a094..branch
Где raw 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
). Между этими двумя редакциями был изменен один текстовый файл release-notes, один был добавлен,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
Если diffing commit
$WITH_RESPECT_TO
против некоторого более позднего commitC
говорит, что путьp
добавляется в commitC
, мы знаем, что это не так. существует в$WITH_RESPECT_TO
и существует вC
, поэтому мы хотим удалить его, чтобы он был "неизменным". (Это относится к статусной буквеA
.)Если разница говорит, что путь
p
удаляется вC
, мы знаем, что он существует в первом и должен быть восстановлен, чтобы остаться "неизменным". (Это относится к статусной буквеD
.)Если разница говорит, что путь
p
существует в обоих, но содержимое файла отличаются друг от другаC
, содержание должно быть восстановлено, чтобы остаться "неизменным". (Это относится к статусной буквеM
.)Другие буквы статуса diff:
C
,R
,T
,U
,X
, иB
, но некоторые не могут произойти (мы исключаемC
,R
, иB
путем указания соответствующихgit diff
параметров;U
происходит только при неполных слияниях; иX
не должно происходить: см. что означают статусы Git "разбитое сопряжение" и "неизвестный" и когда они происходят?). СлучайT
может привести к прерыванию фильтрации (например, обычный файл заменяется символьной ссылкой или наоборот; или что-то заменяется подмодулем).
Если, поразмыслив некоторое время над этим вопросом, вы решите, что "по отношению к" следует использовать родительский коммит(ы), вы можете использовать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'
приводит к пустому различию. Операция ветвления фильтра обычно сохраняет их, но пропускает их, если вы используете--prune-empty
: Ваша новая цепочка будетC1' <- C3'
. Но обратите внимание, что исходная цепочка может иметь "пустые" коммиты; в этом случаеfilter-branch
будет обрезать их, даже если копии фактически совпадают с оригиналами.2 Эти сценарии написаны как бы в файлах сценариев. Если вы превратите их в однострочные, вам нужно будет добавить точки с запятой по мере необходимости, а также, возможно, превратить
exit
вreturn
, так как вы не хотите, чтобы все это завершилось, когдаeval
ed.