Возврат только одного файла отправленного коммита - PullRequest
0 голосов
/ 29 ноября 2018

Ниже приведена история push-коммитов.

Commit      Changed Files
Commit 1|  (File a, File b, File c)
Commit 2|  (File a, File b, File c)
Commit 3|  (File a, File b, File c)
Commit 4|  (File a, File b, File c)
Commit 5|  (File a, File b, File c)

Я хочу отменить изменения, произошедшие с Файл b из Commit 3 . Но я хочу, чтобы изменения произошли с этим файлом в коммите 4 и 5.

Ответы [ 2 ]

0 голосов
/ 29 ноября 2018

В Git каждый коммит сохраняет снимок , то есть состояние каждого файла, а не набор изменений.

Однако каждый коммит- ну, почти каждый коммит - также имеет родительский (предыдущий) коммит.Если вы спросите Git, например, , что произошло в коммите a123456? , Git найдет parent из a123456, извлеките этот снимок,затем извлеките саму a123456, а затем сравните их.Что бы ни отличалось отличается от a123456, это то, что Git скажет вам.

Поскольку каждый коммит представляет собой полный моментальный снимок, легко преобразовать в конкретную версиюконкретный файл в конкретном коммите.Вы просто говорите Git: Получите мне файл b.ext из коммита a123456, например, и теперь у вас есть версия файла b.ext из коммита a123456.Это то, что вы, казалось, спрашивали, и, следовательно, что дает связанный вопрос и текущий ответ (на момент, когда я набираю это).Однако вы отредактировали свой вопрос, чтобы попросить что-то совсем другое.

Немного больше фона

Теперь я должен угадать фактические хэш-идентификаторы для каждого из ваших пяти коммитов.(Каждый коммит имеет уникальный хеш-идентификатор. Хеш-идентификатор - большая уродливая строка букв и цифр - это «истинное имя» коммита. Этот хеш-идентификатор никогда не меняется; его использование всегда дает вам , что Коммит, пока существует этот коммит.) Но это большие уродливые цепочки букв и цифр, поэтому вместо предположения, скажем, 8858448bb49332d353febc078ce4a3abcc962efe, я просто назову ваш «коммит 1» D, ваш «коммит»2 "E и т. Д.

Поскольку большинство коммитов имеют одного родителя, что позволяет Git переходить назад от новых коммитов к старым, давайте расположим их в одну линию с этими стрелками назад:

... <-D <-E <-F <-G <-H   <--master

A имя ветви подобно master действительно содержит хэш-идентификатор последнего коммита в этой ветви.Мы говорим, что имя указывает на коммит, потому что у него есть хэш-идентификатор, который позволяет Git получить коммит.Так master указывает на H.Но H имеет хеш-идентификатор G, поэтому H указывает на его родителя G;G имеет хэш-идентификатор F;и так далее.Вот как Git в первую очередь показывает, что вы делаете коммит H: вы спрашиваете Git , какой коммит master? , и он говорит H.Вы просите Git показать вам H и Git выдержки и G и H, сравнить их и рассказать, что изменилось в H.

Что выпопросил

Я хочу отменить изменения, произошедшие с файлом b коммита 3 [F].Но я хочу, чтобы изменения [которые] произошли с этим файлом в коммите 4 и 5 [G и H].

Обратите внимание, что эта версия файла , вероятно, не появляетсяв любом коммите .Если мы возьмем файл в том виде, в котором он отображается в коммите E (ваш коммит 2), мы получим файл без изменений из F, но к нему не будут добавлены изменения из G и H.Если мы продолжим и добавим do изменения из G, это, вероятно, не то же самое, что в G;и если мы добавим изменения от H к нему после этого, это, вероятно, не то же самое, что в H.

Очевидно, тогда это будет немногосложнее.

Git предоставляет git revert для этого, но он делает слишком много

Команда git revert предназначена для таких вещей, но она делает это на основа для фиксации .В действительности git revert делает, чтобы выяснить, что изменилось в некотором коммите, а затем (попытаться) отменить просто эти изменения .

Здесь яДовольно хороший способ думать о git revert: он превращает коммит - снимок - в набор изменений, как и любая другая команда Git, которая просматривает коммит, сравнивая коммит с его родителем.Поэтому для commit F он сравнил бы его с commit E, найдя изменения в файлах a, b и c.Затем - вот первый хитрый бит - он применяет эти изменения к вашему текущему коммиту.То есть, поскольку вы на самом деле делаете коммит H, git revert может взять все, что есть во всех трех файлах - a, b и c - и (попытаться) отменить именно то, что было сделано для их в коммите E.

(Это на самом деле немного сложнее, потому что эта идея "отменить изменения" также учитывает другие изменения, сделанные после коммита F, с использованием механизма трехстороннего слияния Git.)

После обратного применения всех изменений из некоторого конкретного коммита, git revert теперь создает новый совершать.Поэтому, если вы сделали git revert <hash of F>, вы получите новый коммит, который мы можем назвать I:

...--D--E--F--G--H--I   <-- master

, в котором F изменяет все трифайлы удаляются, создавая три версии, которые, вероятно, отсутствуют в любых предыдущих коммитов.Но это слишком много: вам нужно было только F внести изменения в b отменено.

Таким образом, решение состоит в том, чтобы сделать немного меньше или сделать слишком много, а затем исправить это

Мы уже описали действие git revert следующим образом: найдите изменения, затем примените те же изменения в обратном порядке .Мы можем сделать это вручную, самостоятельно, используя несколько команд Git.Давайте начнем с git diff или сокращенной версии, git show: оба эти превращают снимки в изменения.

  • С git diff мы указываемGit для родителя E и ребенка F и спросите Git: В чем разница между этими двумя? Git извлекает файлы, сравнивает их и показывает нам, что изменилось.

  • С git show мы указываем Git совершить F;Git находит родителя E самостоятельно, извлекает файлы, сравнивает их и показывает, что изменилось (с префиксом сообщения журнала).То есть git show <em>commit</em> равняется git log (только для этого одного коммита), за которым следует git diff (от родителя этого коммита до этого коммита).

Изменения, которые GitПо сути, это будут инструкции: они говорят нам, что если мы начнем с файлов, которые находятся в E, удалим некоторые строки и вставим некоторые другие, мы получим файлы, которыев F.Так что нам просто нужно обратить разность, что достаточно просто.Фактически, у нас есть два способа сделать это: мы можем поменять хэш-идентификаторы на git diff, или мы можем использовать флаг -R либо на git diff, либо git show.Затем мы получим инструкции, которые по сути говорят: Если вы начнете с файлов с F и примените эти инструкции, вы получите файлы с E.

Конечно, эти инструкции скажут нам внести изменения в все три файла, a, b и c.Но теперь мы можем убрать инструкции для двух из трех файлов, оставив только инструкции, которые мы хотим .

Опять же, есть несколько способов сделать это,Очевидным является сохранение всех инструкций в файле, а затем редактирование файла:

git show -R <em>hash-of-F</em> > /tmp/instructions

(а затем редактирование /tmp/instructions).Однако есть еще более простой способ, который заключается в том, чтобы сказать Git: только показывать инструкции для определенных файлов .Файл, который нас интересует, это b, поэтому:

git show -R <em>hash-of-F</em> -- b > /tmp/instructions

Если вы проверите файл инструкций, он должен теперь описать, как взять то, что в F, и не изменятьb чтобы было похоже на то, что вместо E.

Теперь мыпросто нужно, чтобы Git применил эти инструкции, за исключением того, что вместо файла из commit F мы будем использовать файл из current commit H, который уже находится в нашем рабочем деревеготов к исправлению.Команда Git, которая применяет патч - набор инструкций о том, как изменить некоторый набор файлов, - это git apply, поэтому:

git apply < /tmp/instructions

должно сработать.(Тем не менее, обратите внимание, что это завершится с ошибкой , если в инструкциях говорится об изменении строк в b, которые впоследствии были изменены коммитами G или H. Именно здесь git revert умнее, потому чтоон может выполнять всю эту функцию «слияния».)

После успешного применения инструкций мы можем просмотреть файл, убедиться, что он выглядит правильно, и использовать git add и git commit как обычно.

(Примечание: вы можете сделать все это за один проход, используя:

git show -R <em>hash</em> -- b | git apply

И, git apply имеет свой собственный флаг -R или --reverseтакже, так что вы можете записать это по буквам:

git show <em>hash</em> -- b | git apply -R

, что делает то же самое. Есть дополнительные git apply флаги, в том числе -3 / --3way, которые позволят емуделать причудливые вещи, очень похоже на git revert.)

Подход "сделай слишком много, а потом откажись от этого"

Другой относительно простой способ справиться с этим - позволитьgit revert делать всю свою работу.Это, конечно, отменит изменения в других файлах, которые вы не хотели отменять.Но мы показали, как легко получить любой файл из любой коммит.Предположим тогда, что мы позволяем Git отменить все изменения в F:

git revert <em>hash-of-F</em>

, который делает новый коммит I, который отступает все в F:

...--D--E--F--G--H--I   <-- master

Теперь тривиально git checkout два файла a и c из commit H:

git checkout <em>hash-of-H</em> -- a c

и затем сделать новый коммит J:

...--D--E--F--G--H--I--J   <-- master

Файл b в I и J - это способ, которым мыхотите, и файлы a и c в J - это то, что нам нужно - они соответствуют файлам a и c в H - так что мы уже почти закончили, кромедля раздражающего дополнительного коммита I.

Мы можем избавиться от I несколькими способами:

  • Использовать git commit --amend при создании J: этовыталкивает I в сторону, заставляя commit J использовать commit H в качестве родителя J.Коммит I все еще существует, он просто заброшен.В конце концов (примерно через месяц) он истекает и действительно исчезает.

    График коммитов, если мы делаем это, выглядит так:

                       I   [abandoned]
                      /
    ...--D--E--F--G--H--J   <-- master
    
  • Или,git revert имеет флаг -n, который сообщает Git: Делайте возврат, но не фиксируйте результат. (Это также позволяет выполнить возврат с грязным индексом и / или рабочим деревом, хотяесли вы начнете с чистой проверки фиксации H, вам не нужно беспокоиться о том, что это значит.) Здесь мы начнем с H, вернем F, затем скажите Git getфайлы a и c возвращены из коммита H:

    git revert -n <em>hash-of-F</em>
    git checkout HEAD -- a c
    git commit

    Поскольку мы находимся насовершать H когда мы делаем это, мы можем использовать имя HEAD для ссылки на копии a и c, которые находятся в коммите H.

(Будучи Git, есть еще полдюжины способов сделать это; я просто использую те, которые мы здесь иллюстрируем в общем.)

0 голосов
/ 29 ноября 2018

Предполагая, что хеш коммита, который вам нужен, это c77s87:

git checkout c77s87-- file1 / to / restore file2 / to / restore

Главная страница git checkout дает больше информации.

...