Как я могу отформатировать мою ВЕСЬ историю? - PullRequest
0 голосов
/ 21 сентября 2019

Я закончил свою маленькую библиотеку.Когда я начал использовать его, я не знал о clang-формате.Теперь я хотел бы отформатировать весь репозиторий с ним.Я знаю, что это ломает репозитории других народов, поскольку хеш коммитов меняется.Однако, поскольку никто еще не использует мою библиотеку, со мной все в порядке.

Итак, что мне нужно сделать, чтобы запустить формат clang для каждого коммита в моей истории?

Ответы [ 2 ]

2 голосов
/ 21 сентября 2019

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/ (но это часто требует некоторого программирования).

1 голос
/ 21 сентября 2019

Прежде чем вы начнете переписывать историю, я бы рекомендовал пометить ваш текущий коммит.Это позволит вам вернуться к исходной версии, если что-то пойдет не так.Или скопируйте весь репо, на всякий случай.

Мы переписываем историю оптом с git-filter-branch.Это что-то вроде ядерной армейской бензопилы.Мы будем использовать --tree-filter для перезаписи каталогов ("дерева") и файлов.--all говорит, что делать все указанные коммиты (т. Е. Все ветви и теги), а не только те, которые доступны из вашей текущей проверки.

git filter-branch --tree-filter your_rewrite_command --all

Это проверяет каждый коммит, запускает your_rewrite_command и переписываетфиксация с результатом.

Я бы порекомендовал написать небольшой скрипт оболочки, чтобы выполнить переписывание и проверить его перед запуском git-filter-branch.Используйте git ls-files, чтобы получить список всех файлов в коммите, и запустите clang-format для каждого.

...