Git поставляется с командой git filter-branch
, которая является инструментом, который помогает с этим типом задачи.Обратите внимание, что git filter-branch
само по себе не выполняет работу: это просто инструмент, который вы можете использовать, чтобы вы могли выполнять работу.Вы все еще должны написать свои собственные команды.В конце вы, вероятно, будете использовать:
git filter-branch --tree-filter '<some command here>' --tag-name-filter cat -- --all
Что делает ветвь фильтра
Здесь есть основная проблема: ни один коммит, после его создания, никогда не может быть изменен в любомпуть. Ничего о коммите не может измениться: ни имя человека, который его сделал, ни метки даты и времени, ни снимок, ни необработанный хэш-идентификатор родительского коммита.Так что git filter-branch
этого не делает.
Вместо этого он извлекает каждый коммит (из некоторого набора коммитов - в вашем случае вы хотите, чтобы этот набор был все коммитов),по одному, затем выполните произвольную пользовательскую команду (команды) * извлеченный коммит.Что бы это ни делало, фильтр-ветвь затем делает новый коммит из результата.
Если новый коммит точно, полностью, на 100% бит за битом идентичен оригинальному коммиту,это фактически повторно использует оригинальный коммит.В противном случае он создает новый коммит с новым и другим хеш-идентификатором.
После того, как вы сделали новый и другой коммит, каждый последующий коммит, как правило, будет, по крайней мере, немного отличаться: у него будет другой родительский элемент.Инструмент ответвления фильтра позаботится об этом процессе воспитания.Таким образом, выполняются две сложные задачи:
- извлечение фиксации, запуск фильтров и повторная фиксация
- , обновляющая родительское связывание соответствующим образом
оставшаяся тяжелая работа - это, конечно, написание и запуск фильтров.Эта ветвь фильтра уходит к вам.
--tree-filter
, вероятно, самый простой фильтр для использования и, следовательно, тот, который вам нужен.Стоит отметить, что --index-filter
намного быстрее, но с ним гораздо сложнее работать, если ваша задача - каким-то образом изменить моментальный снимок в каждом коммите.Фильтр-ветвь имеет много параметров фильтрации , потому что --tree-filter
является самым медленным фильтром и потому что он хорош только для изменения снимков .Например, --msg-filter
может редактировать или заменять текст сообщения в каждом коммите.Пока вы хотите запустить clang-format
для всех файлов в каждом снимке, придерживайтесь --tree-filter
.
Как работает часть командной строки, более подробно
Давайте рассмотримкратко рассмотрим, как это работает на практике, начиная с примера, в котором всего три коммита.Эти три коммита имеют большие уродливые хэш-идентификаторы, но для простоты мы назовем их A
, B
и C
.Вы начинаете с:
A <-B <-C <-- master
Имя ветви master
содержит идентификатор хеша коммита C
, так что мы (и Git) можем видеть, какой из них последний коммит.Commit C
сам содержит хэш-идентификатор commit B
, а commit B
содержит хэш-идентификатор commit A
, так что Git может работать в обратном направлении от последнего коммита до первого.У коммита A
нет родителя , потому что это первое, поэтому это позволяет остановить действие «все за всем».
Для запуска git filter-branch
вы можете использовать:
git filter-branch --tree-filter '<command to run>' -- master
В самом конце - master
- это имя ветки, которое вы хотите filter-branch
использовать, когда в нем перечислены все коммиты, с которыми он должен работать.То есть он начнется с master
и будет работать в обратном направлении, пока не переместится назад.Затем он будет копировать каждый из этих коммитов, применяя фильтр, и повторно фиксировать.Когда это будет сделано, имя одной ветви, которое он будет обновлять, будет master
.
Использование --all
говорит ему начинать с каждой ветви (и тега, и другой ссылки - это может некорректно работать на stash
ref ииногда --branches --tags
может быть лучше, но --all
традиционно, по крайней мере).Мы вернемся к опции --tag-name-filter
позже.А сейчас давайте просто перейдем к master
.
--
до master
состоит в том, чтобы отделить часть, в которую вы помещаете имена ветвей, от остальных опций, некоторые из которых, возможно, могут напоминать действительные имена ветвей.Это все, что нужно: просто шаблон, чтобы отметить «конец фильтра, начало имен веток».
Последнее, давайте посмотрим на --tree-filter
, не глядя на как написать дерево.фильтр.Это просто означает: запустить фильтр дерева .Таким образом, filter-branch будет извлекать каждый коммит во временный каталог, который содержит только зафиксированные файлы.Этот временный каталог не имеет подкаталога .git
, и не является вашим рабочим деревом .(На самом деле это подкаталог директории -d
, которую вы передаете, или по умолчанию, подкаталог временной директории, которую создает фильтр-ветвь.) Ваш фильтр дерева должен:
- применить любое изменениеВы хотите
- для каждого файла в его текущем рабочем каталоге
- и рекурсивно, для каждого файла в каждом подкаталоге текущего каталога
Если вы хотитеНапример, вставьте строку заголовка в каждый файл, который вы можете использовать:
find . -type f -print | xargs <command to insert header line in every file>
Вы можете поместить эту команду в скрипт, чтобы облегчить тестирование перед использованием.Если у clang-format
есть правильные параметры (что, вероятно, и есть), вам может вообще не понадобиться скрипт, и вы можете просто указать:
--tree-filter 'clang-format <options>'
, но в любом случае, то, что сделает ветвь фильтра, это использоватьоболочка встроена в exec
для запуска фильтра дерева.Поэтому вы должны убедиться, что ваша команда состоит из допустимых команд оболочки и не имеет в себе команды оболочки return
или exit
(по крайней мере, без предварительного появления подоболочки).Если команда, которую вы собираетесь запустить , является написанным вами сценарием, убедитесь, что этот сценарий можно найти с помощью $PATH
, или укажите полный путь к сценарию:
*Например, 1125 *
.
Давайте посмотрим, как работает простой фильтр
Предположим, что в коммите A
есть один файл, README.md
.Давайте предположим, что commit B
добавляет новый файл foo.cc
, который будет переформатирован, и этот коммит C
изменяет README.md
без изменения foo.cc
.Ваш фильтр изменяет только любые файлы .cc
и .h
, но не README.md
.Итак, во-первых, сама ветвь фильтра перечисляет все коммиты, располагая их в соответствующем порядке: A
, затем B
, затем C
, в данном случае.
Операция фильтра дерева теперь:
- извлекает commit
A
; - запускает ваш фильтр / скрипт / команду во временном каталоге, содержащем один файл
README.md
; - делает новыйКоммит из того, что ваша команда оставляет во временном каталоге.
Поскольку ваша команда не касается README.md
, новый коммит точно, 100%, бит для бита идентичен оригинальному A
,Поэтому ветвь фильтра повторно использует исходный коммит A
.
Теперь ветвь фильтра перемещается для фиксации B
.Он извлекает два файла B
во временный каталог (теперь пустой) и запускает вашу команду.На этот раз ваша команда изменяет foo.cc
, хотя все еще оставляет README.md
в покое.Итак, теперь filter-branch делает новый коммит с измененным foo.cc
.Повторное использование имени автора оригинального коммита, адреса электронной почты и т. Д. Сохраняет исходные метаданные, но теперь снимок изменяется, поэтому теперь мы получаем новый и другой хэш-идентификатор, который мы назовем B'
:
A--B--C <-- [original master]
\
B' [in progress]
Filter-branch теперь переходит к фиксации C
.Он извлекает все свои файлы во временный каталог (освобожденный), поэтому у вас есть те же два файла.Теперь ваш фильтр изменяет foo.cc
так же, как и при работе с содержимым commit B
.Фильтр-ветвь делает новый коммит.Снимок нового коммита имеет измененный foo.cc
и тот же README.md
, что и в C
- новый foo.cc
соответствует тому, что вместо B'
- и у него новый родительский элемент, B'
вместо B
: эта последняя часть - то, что обрабатывает ветвь фильтра для вас.Итак, теперь у нас есть:
A--B--C <-- [original master]
\
B'-C' [in progress]
На этом этапе у нас закончились коммиты для копирования, поэтому filter-branch делает последние пару трюков:
Если есть теги, которые указывают на существующие коммиты, и вы указали --tag-name-filter
, Git создает новые теги, которые указывают на копии этих существующих коммитов.Любой тег, который указывает на A
, может быть оставлен в покое, но если тег указывает на B
, фильтр-ветвь копирует его в новый тег, который указывает на B'
;если тег указывает на C
, ветвь фильтра копирует его в новый, указывающий на C'
.Имена этих новых тегов взяты из --tag-name-filter
: старое имя входит в фильтр, а выходит новое имя тега.
Если у вас нет тегов, все это не имеет значения.
Затем для каждой ветви, которую вы назвали в разделе ветви командной строки, фильтр-ветвь хранит хэш-идентификатор последнего скопированного коммита в эту ветку.Так что здесь, filter-branch устанавливает имя master
, чтобы указывать на C'
.
В случае каких-либо проблем, filter-branch копирует все исходные ветку и тегимена refs/original/
: старый мастер становится refs/original/refs/heads/master
.Если все прошло хорошо, вы в конечном итоге захотите выбросить refs/original/
имен.
Окончательный рисунок вышеупомянутого будет:
A--B--C <-- refs/original/refs/heads/master
\
B'-C' <-- master
Как и в ответе Шверна, вы можетехочу быть в состоянии восстановиться, если все идет ужасно неправильно.Способ сделать это - запустить фильтр-ветвь в копии (например, клон) репозитория, а не в оригинале.Еще один способ сделать это - заметить, что вы всегда можете принудительно вернуть все обновленные ссылки так, как они были сохранены в refs/original/
(но это часто требует некоторого программирования).