В 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, есть еще полдюжины способов сделать это; я просто использую те, которые мы здесь иллюстрируем в общем.)