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, так как вы не хотите, чтобы все это завершилось, когдаevaled.