Я случайно набрал команду git commit --amend. Это ошибка, потому что я понял, что фиксация на самом деле совершенно новая, и она должна быть совершена с новым сообщением. Я хочу сделать новую фиксацию. Как мне отменить это?
Как отменить git commit -amend
Ответ 1
Комментарий PetSerAl - это ключ. Здесь две последовательности команд делают именно то, что вы хотите:
git reset --soft @{1}
git commit -C @{1}
и объяснение того, как это работает.
Описание
Когда вы делаете новый коммит, Git обычно 1 использует эту последовательность событий:
- Прочитайте идентификатор (хэш SHA-1, например
a123456...
) текущего фиксации (черезHEAD
, который дает нам текущую ветку). Позвольте называть этот идентификатор C (для текущего). Обратите внимание, что этот текущий фиксатор имеет родительский фиксатор; позвольте назвать его идентификатор P (для родителя). - Поверните указатель (aka staging-area) в дерево. Это создает другой идентификатор; позвольте этому идентификатору T (для дерева).
- Напишите новый фиксатор с родительским = C и tree = T. Этот новый коммит получает другой идентификатор. Позвольте называть это N (для нового).
- Обновите ветвь с новым идентификатором фиксации N.
При использовании --amend
Git процесс немного изменится. Он по-прежнему записывает новый коммит как прежде, но на шаге 3 вместо записи нового commit с parent = C он записывает его с parent = P.
Фото
Изобразительно, что мы можем сделать то, что произошло таким образом. Начнем с графика фиксации, заканчивающегося на P--C
, на который указывает branch
:
...--P--C <-- branch
Когда мы создаем новый commit N
, получаем:
...--P--C--N <-- branch
Когда мы используем --amend
, вместо этого получаем это:
C
/
...--P--N <-- branch
Обратите внимание, что commit C
все еще находится в репозитории; его просто отодвинули в сторону, откинув назад, так что новый commit N
может указывать на старый родительский P
.
Цель
То, что вы поняли, что хотите, после git commit --amend
, должно выглядеть так:
...--P--C--N <-- branch
Мы не можем этого сделать - мы не можем изменить N
; Git никогда не может изменять какой-либо фиксатор (или любой другой объект) после его сохранения в репо, но обратите внимание, что цепочка ...--P--C
все еще находится там, полностью неповрежденная. Вы можете найти commit C
через reflogs, и это то, что делает синтаксис @{1}
. (В частности, это сокращение для [email protected]{1}
, 2 что означает "где currentbranch
указал на один шаг назад", что было "commit C
".)
Итак, теперь мы запускаем git reset --soft @{1}
, который делает это:
C <-- branch
/
...--P--N
Теперь branch
указывает на C
, который указывает на P
.
Что происходит с N
? То же самое, что произошло с C
до: он некоторое время сохранялся через reflog.
Нам это действительно не нужно (хотя это может пригодиться), потому что флаг --soft
для git reset
не содержит индексацию/промежуточную область (вместе с деревом). Это означает, что мы можем сделать новую фиксацию снова, просто запустив еще один git commit
. Он выполнит те же четыре шага (прочитайте ID из HEAD
, создайте дерево, создайте новый фиксатор и обновите ветвь):
C--N2 <-- branch
/
...--P--N
где N2
будет нашим новым новым (вторым новым?) фиксацией.
Мы даже можем сделать git commit
повторно использовать сообщение commit из commit N
. Команда git commit
имеет аргумент --reuse-message
, также пишется -C
; все, что нам нужно сделать, это дать ему что-то, что позволяет найти исходный новый commit N
, из которого нужно скопировать сообщение, чтобы сделать N2
с. Как мы это делаем? Ответ: он в рефлоге, так же, как C
был, когда нам нужно было сделать git reset
.
На самом деле это тот же @{1}
!
Помните, @{1}
означает "где это было всего лишь минуту назад", а git reset
только что обновил его, переместив его с C
на N
. Мы еще не сделали новую фиксацию N2
. (Как только мы это сделаем, N
будет @{2}
, но мы еще этого не сделали.)
Итак, положив все это вместе, получим:
git reset --soft @{1}
git commit -C @{1}
1 Места, в которых это описание разбивается, включают, когда вы изменяете слияние, когда вы находитесь на отдельной HEAD, и когда вы используете альтернативный индекс. Даже тогда, хотя, довольно очевидно, как изменить описание.
2 Если HEAD
отделяется, поэтому нет текущей ветки, значение становится [email protected]{1}
. Обратите внимание, что @
сам по себе является коротким для HEAD
, поэтому тот факт, что @{n}
ссылается на текущую ветвь, а не на HEAD
сам, немного несогласован.
Чтобы увидеть, как они отличаются, рассмотрим git checkout develop
, за которым следует git checkout master
(при условии существования обоих ветвей). Первый checkout
изменяет HEAD
на develop
, а второй изменяет HEAD
на master
. Это означает, что [email protected]{1}
указывает на то, что зафиксировано на master
, перед последним обновлением до master
; но [email protected]{1}
- это фиксация develop
точек на данный момент - возможно, некоторые другие фиксации.
(Recap: после этих двух команд git checkout
@{1}
теперь означает [email protected]{1}
, [email protected]{1}
означает ту же фиксацию, что и develop
, а @
означает HEAD
). Если вы запутались, ну, так и я, и, видимо, я не один: см. комментарии.)