Возврат только одного файла отправленного коммита

Ниже приведена история изменений.

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 коммита 3. Но я хочу, чтобы изменения произошли с этим файлом в коммите 4 и 5.

2 ответа

Решение

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

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

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

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

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

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

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

Название филиала как 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 добавил к этому. Если мы пойдем дальше и добавим изменения от G к этому, это, вероятно, не то же самое, что в G; и если мы добавим изменения от H после этого, это, вероятно, не то же самое, что в H,

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

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

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

Вот довольно хороший способ думать о git revert Он превращает коммит - снимок - в набор изменений, как и любая другая команда Git, которая просматривает коммит, сравнивая коммит с его родителем. Так что для фиксации F было бы сравнить его, чтобы совершить 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 commit составляет git log (только для этого одного коммита) с последующим git diff (от родителя этого коммита до этого коммита).

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

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

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

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

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

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

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

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

git apply < /tmp/instructions

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

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

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

git show -R hash -- b | git apply

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

git show hash -- b | git apply -R

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

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

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

git revert hash-of-F

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

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

Теперь это тривиально git checkout два файла a а также c от коммита H:

git checkout hash-of-H -- 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 из пути, делая коммит J использовать коммит H как J родитель. совершить I все еще существует, он просто заброшен. В конце концов (примерно через месяц) он истекает и действительно уходит.

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

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

    git revert -n hash-of-F
    git checkout HEAD -- a c
    git commit

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

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

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

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

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

Другие вопросы по тегам