Как Мэтт сказал , вы не можете изменить фиксацию. Что делает git commit --amend
, так это делает новую фиксацию, которая не расширяет текущую ветку.
Я думаю, что больше людей получают это, когда они могут видеть визуально, что это значит. Но это означает, что вам сначала нужно научиться рисовать графики фиксации. Поскольку вы используете IDE, возможно, вы уже узнали об этом, но вы не упоминаете, какую IDE (да и я сам обычно не использую IDE). Кроме того, очень сложно ввести графику в сообщение StackOverflow. :-) Итак, рассмотрим эту текстовую версию рисунка графа фиксации:
... <-F <-G <-H
Здесь мы имеем серию коммитов, все в красивой аккуратной строке, с фиксацией, ha sh это H
- H
здесь означает какое-то большое уродливое Git ha sh ID - это последний в цепочке. Некоторые IDE рисуют это вертикально, с фиксацией H
вверху:
H <commit subject line>
|
G <commit subject line>
|
:
, но для целей StackOverflow мне нравятся горизонтальные рисунки.
Обратите внимание, как каждая фиксация «указывает назад» на свой немедленная родительская фиксация. Внутренне это означает, что каждая фиксация хранит полный идентификатор ha sh своего родителя, потому что Git на самом деле находит коммитов по их идентификаторам ha sh.
Чтобы найти фиксацию H
конкретно (и быстро), Git также должен где-то хранить свой ha sh ID. Git может использовать H
, чтобы найти G
ha sh ID, а затем G
, чтобы найти F
ha sh ID, и так далее, но Git требуется на начать с H
га sh ID. Место Git находит H
га sh ID - это имя вашей ветки:
...--F--G--H <-- branch (HEAD)
имя ветки позволяет Git найти H
легко, потому что само имя ветки содержит необработанный sh ID фиксации. Затем коммит H
позволяет Git легко находить G
, что позволяет Git легко находить F
и т. Д. Это наша обратная цепочка коммитов.
Обратите внимание, когда у вас есть несколько веток, они имеют тенденцию воссоединяться где-то в прошлом:
I--J <-- branch1
/
...--G--H <-- master
\
K--L <-- branch2 (HEAD)
Здесь мы сделали branch1
два коммиты, которых еще нет в master
, и сделанные branch2
, имеют две фиксации, которых еще нет в master
. Коммиты до H
находятся на всех трех ветвях . Имя master
конкретно означает commit H
, но также - при необходимости - означает все коммиты до H
включительно. Имя branch1
означает коммиты до J
включительно или просто J
, в зависимости от контекста; а имя branch2
означает фиксацию до H
плюс K-L
или просто L
, в зависимости от контекста.
Я нарисовал имя HEAD
здесь, прикрепленное к branch2
чтобы показать, что у нас есть branch2
и, следовательно, фиксация L
, проверенная в данный момент. Общая идея с Git такова:
- Git разрешит
HEAD
в фиксацию, найдя имя ветки и следуя стрелке имени ветки до фиксации. - Это текущая фиксация , которую вы проверили.
- Каждая фиксация находит предыдущую (родительскую) фиксацию, которая определяет цепочку коммитов, которую мы называем «ветвью» .
- Но слово ветка неоднозначно, потому что иногда оно означает имя ветки , иногда фиксацию на конце ветки, как указано to по имени , а иногда это означает фиксация подсказки плюс некоторые или все предыдущие фиксации .
Когда люди используют слово ветка , иногда они означают серию коммитов, включая коммит, который не идентифицируется именем ветки . Помимо обычных имен веток, Git имеет теги имена и имена удаленного отслеживания , например origin/master
и origin/branch1
и так далее. Все эти имена в конечном итоге просто указывают на одну конкретную c фиксацию - точно так же, как имя ветки, - но только имена ветки имеют возможность присоединять к ним HEAD
.
Теперь рассмотрим, как обычно работает добавление фиксации
Предположим, у нас есть:
...--G--H <-- branch (HEAD)
У нас есть файлы из фиксации H
извлечены, потому что H
- это текущий коммит . Мы вносим некоторые изменения в эти файлы, git add
обновленные файлы, чтобы скопировать изменения поверх копий этих файлов в Git индексе или промежуточной области , и запустить git commit
. Git теперь замораживает файлы, которые находятся в его индексе, добавляет правильные метаданные - наше имя и адрес электронной почты, наше сообщение журнала, родительский ha sh ID H
и т. Д. - и тем самым создает новую фиксацию, который получает новый уникальный большой уродливый ha sh ID, но мы просто назовем его I
:
...--G--H <-- branch (HEAD)
\
I
Поскольку I
теперь последний коммит, Git теперь выполняет один трюк, который делает имена веток отличными от любых других имен: Git сохраняет I
's ha sh ID в name branch
, потому что это имя который HEAD
прилагается. Мы получаем:
...--G--H
\
I <-- branch (HEAD)
, который мы можем исправить сейчас:
...--G--H--I <-- branch (HEAD)
Вот как git commit
работает:
- Он зависает, для все время, все файлы, которые находятся в Git индексе (также известном как промежуточная область ). Тот факт, что Git делает коммиты из своих готовых к замораживанию копий в своей промежуточной области, а не из обычных файлов, которые у вас есть в вашем рабочем дереве, вот почему вам все время приходится
git add
файлов. Эти замороженные файлы становятся новым снимком в новой фиксации. - Он добавляет соответствующие метаданные: ваше имя, ваш адрес электронной почты, текущую дату и время и т. Д.
- Это устанавливает новый родительский ha sh ID на текущий ha sh коммита.
- Он записывает фактический коммит (который получает новый, уникальный ha sh ID).
- Наконец, он записывает новый коммит ha sh ID в текущую ветку имя , чтобы имя продолжало указывать до последней фиксации в цепочке.
После записи новой фиксации изменяется текущая фиксация - идентификатор ha sh, который HEAD
означает, что теперь фиксация I
вместо фиксации H
- но снова текущий моментальный снимок фиксации соответствует файлам в Git индексе , который, если вы git add
-ed все - также сопоставьте файлы в вашем рабочем дереве.
Теперь мы можем увидеть, как git commit --amend
работает
Когда вы используете git commit --amend
, Git идет через все те же шаги, что и для любого коммита, за одним исключением: родительский нового коммита (или родители, во множественном числе, если текущая фиксация является фиксацией слияния) взяты из текущего фиксация вместо , являющейся текущей фиксацией. То есть, вместо того, чтобы делать:
...--G--H--I--J <-- branch (HEAD)
с новой фиксацией J
, указывая на существующую фиксацию I
, Git делает следующее:
I ???
/
...--G--H--J <-- branch (HEAD)
Фактически, Тогда-текущий коммит I
теперь «убран с дороги», чтобы поместить новый коммит в конец цепочки , не увеличивая цепочку .
Существующий коммит I
все еще существует. У него просто больше нет имени .
Когда вы задействуете другой репозиторий Git, вы обмениваетесь коммитами с ними
Git is , по сути, все о совершает . Вы делаете новые коммиты, а затем ваш Git вызывает другой Git и отправляет ему свои коммиты. Или вы вызываете этот другой репозиторий Git - независимо от того, делали ли вы какие-либо новые коммиты или нет - и получаете любые новые коммиты, которые у них есть, а у вас нет.
Одна из этих двух команд - git fetch
. Это тот, который вызывает их Git и находит, какие коммиты у них есть, а у вас нет: он загружает их коммиты в ваш собственный Git репозиторий.
Другая команда - git push
: с git push
ваш Git вызывает их Git и отправляет коммиты. Однако эти двое не совсем симметричны. Давайте сначала посмотрим на git fetch
, потому что именно отсюда имена удаленного отслеживания вроде origin/master
и origin/branch
.
Мы уже видели Git находит коммиты , беря имя - возможно, имя ветки - чтобы найти последний коммит, а затем работая в обратном направлении. Тем временем ваш Git звонит другому Git. Другой Git имеет имена веток B1, B2, B3, ..., каждое из которых указывает последний идентификатор ha sh фиксации для этого Git
Это их ветки, а не ваши ветки. У вас могут быть или не быть ветки с одинаковыми именами, но это их имена, указывающие на их последние коммиты. Ваш git fetch
не касается ваших имен веток.
Предположим, например, что мы начинаем с:
...--G--H <-- master (HEAD)
, но получили фиксацию H
из origin
. Тогда у нас действительно есть:
...--G--H <-- master (HEAD), origin/master
То есть в их Git репозитории, их имя master
также выбирает фиксацию H
. Итак, наш Git записал их Git имя master
как наше origin/master
; затем мы сделали наш master
из их origin/master
, и теперь оба имени указывают на существующий коммит H
.
Если мы теперь сделаем наш собственный новый коммит I
, наш master теперь указывает на фиксацию I
. Наш origin/master
по-прежнему указывает на H
, как и раньше:
...--G--H <-- origin/master
\
I <-- master (HEAD)
Между тем, предположим, что они - кем бы они ни были - делают свою новую фиксацию. Он получит какой-то большой уродливый уникальный ha sh ID; мы просто назовем его J
. Их коммит J
находится в их репозитории:
...--G--H--J <-- master [in their Git]
Мы запускаем git fetch
, и наш Git вызывает их Git и обнаруживает, что у них есть новый коммит чего мы никогда раньше не видели. Наш Git получает его от своего Git и помещает в наш репозиторий. Чтобы запомнить идентификатор J
ha sh, наш Git обновляет наш собственный origin/master
:
I <-- master (HEAD)
/
...--G--H--J <-- origin/master
(я поставил наш наверху только из эстетических соображений - мне нравится, чтобы буквы были больше здесь в алфавитном порядке).
Теперь у нас есть своего рода проблема. Наша фиксация I
и их фиксация J
образуют две ветки, в зависимости от того, что мы подразумеваем под словом ветка:
I <-- master (HEAD)
/
...--G--H
\
J <-- origin/master
Нам нужно как-то их объединить, в какой-то момент. Мы можем сделать это с помощью git merge
или git rebase
, чтобы скопировать существующую фиксацию I
в новую улучшенную фиксацию - назовем ее I'
- которая расширяет их J
:
I ??? [abandoned]
/
...--G--H--J <-- origin/master
\
I' <-- master (HEAD)
Мы отказываемся от нашего I
в пользу нашего нового и улучшенного I'
, который дополняет существующие коммиты. Теперь мы можем git push origin master
. Или мы используем git merge
, чтобы объединить работу в новый коммит, со снимком, сделанным немного сложным процессом, включающим сравнение снимка коммита H
с каждым из двух снимков в I
и J
:
I
/ \
...--G--H M <-- master (HEAD)
\ /
J <-- origin/master
Теперь мы снова можем git push origin master
.
Почему pu sh не является симметричным c с выборкой
Допустим, мы есть только это:
I <-- master (HEAD)
/
...--G--H
\
J <-- origin/master
Другими словами, мы еще не перекомпилировали и не объединились. Наше имя master
указывает на совершение I
; наше имя origin/master
, представляющее master
на origin
, указывает на фиксацию J
. Мы можем попробовать запустить:
git push origin master
, который вызовет их Git, отправит им наш коммит I
- у них его еще нет, потому что мы не дали это им раньше - а затем попросите их установить их master
, чтобы они указывали на фиксацию I
.
Помните, что их master
в настоящее время указывает на (общий, скопированный в оба Gits) коммит J
. Если они сделают то, что мы просим, у них получится:
I <-- master
/
...--G--H
\
J ??? [abandoned]
То есть они потеряют зафиксируют J
полностью. Git находит коммиты, начиная с имени ветки, например master
, и работая в обратном направлении. Их master
использовали, чтобы найти J
; и если они воспользуются нашей просьбой, чтобы вместо этого master
указывало на I
, они больше не смогут найти J
.
Вот почему они просто отклоняют нашу вежливую просьбу , говоря , а не перемотка вперед . Мы исправляем эту проблему, используя git rebase
или git merge
, чтобы сделать I'
или какой-нибудь коммит слияния. Затем мы либо отправляем им I'
и просим их установить master
так, чтобы они указывали на I'
, что нормально, потому что I'
идет после J
и, следовательно, сохраняет фиксацию J
в изображение; или мы отправляем им M
(и снова I
, если они его уронили) и просим их установить их master
так, чтобы они указывали на M
, что нормально, потому что и I
и J
предшествует M
, чтобы они могли найти J
.
Иногда мы действительно хотим, чтобы они выбросили фиксацию
Когда мы используем git commit --amend
, мы берем такую цепочку:
...--H--I <-- branch (HEAD)
и превращаем ее в такую:
I ??? [abandoned]
/
...--H--J <-- branch (HEAD)
, что делает фиксацию I
видимой go. На самом деле он остается на некоторое время - по крайней мере, около месяца - на случай, если мы захотим его вернуть, через механизм, который Git вызывает reflogs . Но он ушел из повседневного представления, так как нет имени, указывающего прямо на него, и нет другого имени, указывающего на некоторую фиксацию, которая в конечном итоге указывает на I
.
Но что, если бы мы отправили фиксацию I
другому Git? Что, если, в частности, мы запустили:
git push origin branch
, так что теперь у нас есть:
I <-- origin/branch
/
...--H--J <-- branch (HEAD)
, где наш origin/branch
представляет origin
s branch
, который теперь указывает на нашу старую фиксацию I
?
Если мы просто запустим:
git push origin branch
, это сообщит их Git: Здесь: иметь новую фиксацию J
. Теперь, пожалуйста, если все в порядке, настройте branch
так, чтобы он запомнил фиксацию J
. Они скажут «нет» по той же причине, по которой они сказали «нет» в нашем другом примере: это потеря фиксация I
, в их репозитории Git.
Но это именно то, что мы хотим. Мы хотим, чтобы они потеряли фиксацию I
в своей ветке branch
. Чтобы это произошло, мы отправляем такую же операцию - еще одну git push
- но мы меняем наш последний вежливый запрос на более сильную команду.
У нас есть два варианта:
Мы можем сказать: Задайте свое имя branch
так, чтобы оно указывало на фиксацию J
! Это просто говорит им удалить все коммиты, которые могут быть отброшены таким образом , даже если это сейчас I
и K
тоже.
Или мы можем сказать: Я думаю, ваш branch
определяет фиксацию . Если да, измените его, чтобы вместо этого идентифицировать фиксацию J
. В любом случае, дайте мне знать, что произошло.
Первый простой git push --force
. Второй - git push --force-with-lease
. Наш Git заполнит часть «Я думаю» commit I
ha sh из нашего origin/branch
и, конечно же, получит идентификатор J
ha sh ID таким же образом, как и всегда.
Опасность любого git push --force
, с частью -with-lease
или без нее, заключается в том, что мы говорим какому-то другому Git выбросить некоторые коммиты . Конечно, это то, что мы хотим, чтобы не было опасным, пока мы знаем, что просим выкинуть коммиты. Но если мы git push
используем репозиторий GitHub, есть ли другие люди, которые используют этот репозиторий GitHub для git fetch
откуда? Возможно, они взяли наш коммит I
и используют его. Они могли вернуть фиксацию I
. Или, может быть, мы делаем для них дополнительную работу, так что им придется переделать свои коммиты, чтобы использовать фиксацию J
вместо фиксации I
.
Мы должны организовать продвигайтесь вместе с другими пользователями этого origin
Git, чтобы они знали, в каких ветвях могут быть удалены вот так.
Ваш собственный случай
В вашем случае , вы выполнили ошибку git push
, а затем git pull
. Команда git pull
означает запустить git fetch
, а затем запустить вторую Git команду . Вторая команда - git merge
по умолчанию.
Итак, допустим, вы начали с:
...--G--H <-- master, origin/master, branch (HEAD)
затем добавили фиксацию I
:
...--G--H <-- master, origin/master
\
I <-- branch (HEAD)
You затем запустил (успешно) git push -u origin branch
, что привело к:
I <-- branch (HEAD), origin/branch
/
...--G--H <-- master, origin/master
(опять же, на этот раз я просто поставил I
сверху для эстетики).
Затем вы использовали git commit --amend
, который сделал новую фиксацию J
, не имеющую I
в качестве родителя:
I <-- origin/branch
/
...--G--H <-- master, origin/master
\
J <-- branch (HEAD)
Вы пробовали обычную git push
, которая не удалась с без перемотки вперед: их Git сказал вашему Git, что этот пу sh потеряет коммиты (в частности, I
).
Затем вы запустили git pull
:
- Это запустило
git fetch
, что ничего не дало, потому что у вас уже есть коммиты H
и I
, и нет никаких изменений, которые нужно вносить ни в одно из ваших origin/*
имен. - Затем он запустил
git merge
, чтобы объединить I
и J
в новую фиксацию слияния.
Я перестану рисовать имена master
и origin/master
по мере того, как они мешают, но это произошло именно так, как мы теперь ожидали:
I <-- origin/branch
/ \
...--G--H M <-- branch (HEAD)
\ /
J
, а затем вы запустили git push
, который отправил им коммиты J
и M
в добавить к своим branch
. Они сказали «ОК», так что ваш Git обновил ваш origin/branch
:
I
/ \
...--G--H M <-- branch (HEAD), origin/branch
\ /
J
, и это то, что вы теперь видите в своем репозитории.
Вы можете, если хотите, заставьте ваше имя branch
указывать на фиксацию J
снова напрямую, затем используйте git push --force-with-lease
, чтобы попросить другой Git отменить обе фиксации M
и I
.
Чтобы заставить ваш текущий (HEAD
), чтобы указать на одну конкретную c фиксацию, используйте git reset
. В этом случае вы можете сначала убедиться, что у вас нет ничего, что может уничтожить git reset --hard
, и использовать git reset --hard HEAD~1
для перехода к первому родительскому элементу M
. См. Примечание к первому родительскому элементу ниже.
(Чтобы переместить ветку, в которой вы не , используйте git branch -f
, для которого требуются два аргумента: имя ветки и фиксация для move-to. Так как операции git reset
в ветке, в которой вы выполняете , git reset
просто принимает спецификатор фиксации.)
Примечание: --first-parent
Есть одна хитрость, которая плохо отображается на моих рисунках с горизонтальным графиком. Каждый раз, когда вы делаете новое слияние коммит, например M
, Git проверяет, что первый из нескольких родителей, выходящих из M
, указывает на фиксацию, которая был верхушкой вашей ветки раньше. В данном случае это означает, что первым родителем M
является J
, а не I
.
У вас могут быть git log
и другие Git команды, только посмотрите на первый родитель каждого слияния при просмотре коммитов. Если вы сделаете это, картинка будет выглядеть так:
...--G--H--J--M <-- branch (HEAD), origin/branch
Фактически, M
по-прежнему указывает на I
как на своего второго родителя.
Это --first-parent
Параметр в основном полезен для просмотра ветки типа master
, когда функции всегда разрабатываются в своих собственных ветвях:
o--o--o <-- feature2
/ \
...--●---------●---------●--... <-- master
\ /
o--o--o <-- feature1
Просмотр master
с --first-parent
отбрасывает все эти входящие боковые подключения, так что можно увидеть только коммиты solid. Но само понятие имеет значение всякий раз, когда вы имеете дело с фиксацией слияния: M^1
означает первый родительский элемент M
, а M^2
означает второй родительский элемент M
. Обозначение суффикса тильды ведет отсчет назад только через ссылки первого родителя, так что M~1
означает шаг назад на одну ссылку первого родителя .