Подтвердить что ты не робот

Как отменить git commit -amend

Я случайно набрал команду git commit --amend. Это ошибка, потому что я понял, что фиксация на самом деле совершенно новая, и она должна быть совершена с новым сообщением. Я хочу сделать новую фиксацию. Как мне отменить это?

4b9b3361

Ответ 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). Если вы запутались, ну, так и я, и, видимо, я не один: см. комментарии.)