Возврат только одного файла отправленного коммита
Ниже приведена история изменений.
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 дает больше информации.