Если более старый коммит содержит нежелательное изменение одного файла, вы можете отменить изменение этого файла относительно текущей ветки и сохранить результат в новом коммите. Это сохраняет последующие коммиты на своих местах и оставляет внесенные ими правки нетронутыми, если обратный патч по-прежнему ложится без конфликтов. На macOS используйте Терминал из репозитория; сначала проверьте патч, особенно если более поздняя работа затрагивала те же строки.

Выберите правильный способ изменения истории

Эта процедура добавляет коммит, который отменяет изменения целевого коммита по одному пути. Она не стирает этот файл из старого коммита, поэтому старая версия сохраняется в истории. Такой подход позволяет избежать перезаписи истории ветки и обычно подходит в тех случаях, когда коммит уже был отправлен в общий доступ или от него зависят последующие коммиты. Git описывает команду git revert как создание нового коммита, отменяющего более ранний, но в ней нет встроенной опции для отката только одного пути из существующего коммита. Вместо этого описанные ниже шаги генерируют патч для этого пути и применяют его в обратном порядке. Git: документация по git-revert

Если изменение файла необходимо удалить непосредственно из самого исторического коммита, это уже задача по перезаписи истории, которая обычно решается с помощью интерактивного rebase. Перезапись меняет идентификаторы коммитов и может помешать коллегам, чья работа основана на старых коммитах. В этом руководстве рассматривается более безопасный, аддитивный подход.

Отмена изменений целевого коммита для одного файла

Откройте Терминал, перейдите в репозиторий и проверьте рабочее дерево перед внесением изменений. Замените TARGET на ID коммита, а path/to/file — на путь относительно корня репозитория. В примере используется временный файл патча в каталоге /tmp macOS; выберите другое временное имя, если такой файл уже существует.

git status --short
git show --stat --oneline TARGET
git diff TARGET^ TARGET -- "path/to/file" > /tmp/remove-file-change.patch

Команда diff сравнивает целевой коммит с его первым родителем и ограничивает результат указанным путем. Для обычного коммита (не коммита слияния) это и есть изменения файла, внесенные этим коммитом. Убедитесь, что сводка указывает на нужный коммит, а патч не пуст. Вы можете просмотреть его с помощью less /tmp/remove-file-change.patch; нажмите q, чтобы выйти. Git поддерживает сравнение между коммитами с опциональным ограничением по пути. Git: документация по git-diff

Перед его применением убедитесь, что у вас нет посторонних индексированных или неиндексированных правок в целевом файле. Обратный патч изменяет рабочую копию, поэтому чистое рабочее дерево — самая простая отправная точка. Если у вас есть полезная локальная работа, сначала закоммитьте ее или надежно отложите (stash); не перезаписывайте ее командой восстановления.

Проверьте, подходит ли обратный патч, а затем примените его:

git apply --reverse --check /tmp/remove-file-change.patch
git apply --reverse /tmp/remove-file-change.patch

Параметр --reverse применяет diff в обратном порядке, а --check проверяет, применяется ли он, без фактического изменения файлов. По умолчанию git apply отклоняет весь патч целиком, если некоторые фрагменты (hunks) не подходят, а не применяет только подходящие части. Если проверка не прошла, остановитесь и проверьте файл и патч. Последующие правки могли пересечься с целевым изменением, поэтому Git не может безопасно определить, что именно следует удалить. Скорректируйте нужное содержимое вручную или используйте рабочий процесс с перезаписью истории, если крайне важно сохранить точно отредактированную историческую последовательность. Не добавляйте флаг --reject без необходимости: он может применить только совпадающие части и оставить отклоненные фрагменты в файлах-отчетах. Git: документация по git-apply

Проверка и фиксация результата

Внимательно проверьте, что именно изменилось, перед добавлением в индекс. Команда git diff показывает правки в рабочем дереве; укажите путь еще раз, чтобы сфокусировать проверку. Если результат верный, добавьте файл в индекс и проверьте подготовленный diff перед созданием нового коммита.

git diff -- "path/to/file"
git add -- "path/to/file"
git diff --cached -- "path/to/file"
git status --short
git commit -m "Remove file change from earlier commit"

Кавычки защищают путь от интерпретации оболочкой (shell), а -- обозначает конец списка опций. Они особенно полезны для имен, содержащих пробелы или метасимволы оболочки. Коммит фиксирует подготовленный индекс; проверка с помощью git diff --cached позволяет убедиться в точности содержимого, которое будет закоммичено. Git: документация по git-commit

После фиксации выполните git status --short и изучите новый коммит с помощью git show --stat --oneline HEAD. Если исходный коммит добавлял файл, отмена его патча может удалить этот файл — это ожидаемо, если более позднего содержимого оставаться не должно. Если же последующие коммиты добавляли в этот файл полезный контент, эти дополнения должны сохраниться при чистом применении обратного патча; просмотрите итоговый файл и diff для подтверждения.

Случаи, требующие особого внимания

Коммит слияния (merge commit) имеет несколько родителей, и его изменения зависят от того, с каким родителем вы его сравниваете. TARGET^ обозначает первого родителя; это может быть не то изменение, которое вы хотели удалить. Определите правильного родителя и явно выполните сравнение относительно него перед применением чего-либо. Переименования, бинарные файлы и файлы, которые с тех пор были перемещены, также могут сделать патч с ограничением по пути неприменимым; проверьте diff и остановитесь, если он не описывает нужное изменение файла. Что касается отмены коммитов слияния, документация по Git revert подробно объясняет выбор родительской ветки (mainline) и связанные с этим последствия. Git: документация по git-revert

Надежная контрольная точка — это проверенный патч: он должен содержать только изменения целевого коммита по нужному пути. Если он не может примениться к текущему файлу без конфликтов, не форсируйте процесс. Аккуратно восстановите желаемый текущий файл вручную, а затем индексируйте и делайте коммит только после проверки полного результата.

Источники