Вы действительно находитесь в середине перебазирования, как говорит git status
. (Если ваша версия Git 2.0 или выше git status
довольно хорошая и надежная; в выпусках 1.7 и 1.8 было несколько существенных улучшений, так что если у вас действительно древний git status
, вы все равно должны его использовать, но это не так хорош как современный Git.)
Причина, по которой вы застряли в слиянии, заключается в том, что git commit --amend
является своего рода белой ложью: на самом деле не изменяет существующий коммит. Вместо этого он делает новый и улучшенный коммит, а затем говорит: Давайте использовать этот вместо старого. К сожалению, любая ветвь, которая уже имеет и зависит от старой, все еще имеет старую.
Давайте рассмотрим это подробно:
- Я скачал с пульта, у которого была только одна ветка, master.
Я предполагаю, что вы имеете в виду, что бежали:
git clone <url>
cd <new-clone>
здесь.
- Я сделал коммит мастеру
Таким образом:
<edit some file>
git add <that file>
git commit
Давайте теперь немного отступим и опишем, что такое коммит и что он делает, с кратким погружением в как делает это также. (Здесь будет много, но вы можете просто помнить, что было много, и вернуться к нему позже, когда это будет уместно.)
Коммиты и имена ветвей
Каждый сделанный вами коммит имеет полный снимок всех ваших файлов. Эти файлы заморожены навсегда: их нельзя изменить. В коммите также есть немного метаданных , таких как ваше имя и адрес электронной почты, отметка даты и времени , когда вы сделали коммит, и почему вы сделали коммит - ваше лог-сообщение. Git практически не использует сообщение журнала, поэтому вы можете добавить все, что захотите, но смысл сообщения журнала состоит в том, чтобы напомнить себе (или кому-то еще) не столько , что вы сделали - Git может покажите это вам самостоятельно - а точнее почему вы это сделали, то есть то, что просто само изменение не может показать.
Каждый коммит имеет свой уникальный хэш-идентификатор. Идентификатор хэша - это большая уродливая строка типа 83232e38648b51abbcbdb56c94632b6906cc85a6
; они показывают много мест, включая вывод git log
, иногда в сокращенной форме (83232e3
и т.п.). Идентификатор хеша - это настоящее «истинное имя» коммита, и именно так Git находит коммит: Git должен иметь хеш-идентификатор в своих хитрых маленьких лапах, чтобы извлечь коммит. Так что, если бы не имена филиалов, вы бы записывали все это на бумаге все время или что-то в этом роде - но это глупо, у нас есть компьютер, у нас может быть просто it , записываем их в файл или что-то.
Вот что такое имя ветки и что оно делает: это файл, или строка в файле, или что-то, что запоминает хэш-идентификатор. Фактически, он запоминает только один идентификатор хеша: идентификатор хеша последнего коммита в ветви. Но это нормально, потому что каждый коммит также запоминает хэш-идентификатор. В частности, каждый коммит запоминает хэш-идентификатор коммита, который следует перед ним, который Git называет parent . Таким образом, мы в конечном итоге:
... <-F <-G <-H <--branch
с заглавными буквами, обозначающими настоящие хэш-идентификаторы. name , branch
, содержит идентификатор хэша последнего коммита H
. H
сам содержит идентификатор хеша более раннего коммита G
, который содержит идентификатор хеша F
и т. Д. Это позволяет Git идти назад через серию коммитов.
Чтобы создать новую ветку, вы просто выбираете существующий коммит и присоединяете к нему имя:
...--F--G <-- newbranch
\
H <-- master
или
...--F--G--H <-- newbranch, master
Теперь, когда у вас есть более одного имени, Git нужен способ узнать какое имя вы используете. Вот где HEAD
приходит:
...--F--G--H <-- newbranch (HEAD), master
означает, что текущее имя newbranch
(и текущий коммит H
).
ReУчастник, который мы отметили, что файлы в коммитах заморожены на все времена. Они все только для чтения. Они также сжаты, чтобы занять меньше места. Мне нравится называть их сублимированными . Будучи замороженным таким образом, если у вас есть много коммитов, которые продолжают использовать большинство одних и тех же файлов, Git может и на самом деле просто повторно использует уже замороженные файлы. Это один из нескольких приемов, которые Git использует для предотвращения быстрого роста хранилища, даже если каждый коммит сохраняет каждый файл. Но, хотя это отлично подходит для архивирования, очевидно, что оно совершенно бесполезно для выполнения любой новой работы. Вам нужны файлы, которые вы можете читать и писать. Таким образом, git checkout
извлекает коммит, который становится вашим текущим коммитом, и получает все его высушенные сублимацией файлы, повторно скопированные в ваше рабочее дерево . Здесь ваши файлы имеют свой обычный повседневный формат. Вы можете использовать их и работать с ними.
Git может остановиться здесь - с замороженными зафиксированными файлами, один особенно интересный набор замороженных файлов - это файлы текущего коммита и пригодный для использования набор в рабочем дереве - и другие системы контроля версий на этом останавливаются. Но по разным причинам Git скрывает третью копию ваших файлов, как бы между замороженным набором и используемым набором. Эти копии находятся в Git index или staging area - это два названия одной и той же вещи. То, что в индексе, находится в лиофилизированном формате. Но в отличие от фактической фиксации, эти файлы могут быть заменены в любое время, или могут быть добавлены новые файлы или удалены файлы.
Каждый раз, когда вы делаете новый коммит, Git на самом деле упаковывает предварительно зафиксированные index копии в коммит. Это происходит очень быстро и не требует и не использует то, что находится в вашем рабочем дереве! Итак, сначала вы должны заставить Git обновить свой индекс, о чем git add
. Команда git add
freeze сушит файл рабочего дерева и перезаписывает копию в индексе, или, если файл не был в индексе ранее, создает его там.
Упаковав файлы, добавив ваше имя, сообщение в журнал и т. Д., Git устанавливает родителя нового коммита как текущий коммит. Затем он записывает новый коммит в базу данных all-commits:
...--F--G--H <-- newbranch (HEAD), master
\
I
Теперь, когда есть коммит, последний шаг для git commit
- сделать имя , в данном случае newbranch
, указать коммит I
вместо коммита H
:
...--F--G--H <-- master
\
I <-- newbranch (HEAD)
Хотя коммиты являются снимками, Git может показывать изменения
Если вы попросите Git показать, что вы делаете коммит I
, или H
, то Git может просто показать вам все файлы, которые в нем находятся. Но обычно это не то, чего мы хотим знать. Для коммита H
нам хотелось бы знать: Что отличается в H
от G
? Так что когда git log -p
или git show
показывает коммит H
, на самом деле это выглядит как G
и H
. Независимо от того, какие файлы отличаются, Git сравнивает эти два файла и сообщает, что изменилось. При просмотре commit I
Git сравнивает снимки в H
и I
и сообщает вам, что изменилось.
Это общая тема: для любого коммита с одним родителем Git может легко извлечь оба родительского и коммита и сравнить их. Это превращает коммит в набор изменений. Важно помнить, что для этого необходимо выбрать предыдущий коммит. Затем Git сравнит два снимка. Просто для большинства коммитов выбор настолько очевиден, что нам вообще не нужно его называть.
Вернуться к вашим шагам
Итак, это возвращает нас к шагу 2: вы добавили новый коммит в master
. Давайте нарисуем это так:
I <-- master (HEAD)
/
...--F--G--H <-- origin/master
(origin/master
- это метод запоминания вашим Git, где master
находится или находился в репозитории Git, который вы клонировали ранее).
- Iтолкнул изменение после перебазирования мастера на вновь извлеченный источник / мастер (хотя это изменение еще не было объединено с удаленным)
То есть вы запустили:
git fetch origin # or just `git fetch`, which does the same thing
git rebase origin/master # or `git pull --rebase origin master`
git push origin master
Предполагая, что никто не добавил новые коммиты в репозиторий Git на origin
, шаг перебазирования ничего не сделал.Последний шаг, однако, git push origin master
, делает что-то важное:
- Ваш Git вызывает Git на
origin
- Ваш Git узнает, какие коммиты они имеют (по хешу)ID).
- Ваш Git предлагает любые новые коммиты, которые у вас есть, что они не делают, то есть коммит
I
, также по хеш-идентификатору. - Ваш Git заканчивается
git push
сеанс с запросом к ним: Эй, другой Git, пожалуйста, установите master
, чтобы он указывал на фиксацию I
.
Если они подчиняются этому запросу- и здесь нет причин, по которым они не должны этого делать, - они получат:
...--F--G--H--I <-- master
в их хранилище.Ваш Git увидит, что они приняли запрос, и обновит ваш origin/master
, чтобы он соответствовал, поэтому мы можем выправить излом на рисунке вашего содержимого репозитория и перечислить их как:
...--F--G--H--I <-- master (HEAD), origin/master
Вы добавили:
(изменение еще не было объединено с удаленным, хотя)
, но это не имеет значения: это ни истина, ни ложь, поскольку нет ничего для объединить; но у них есть есть ваш новый коммит I
и их master
указывает на I
.
Я сделал новую ветку изmaster вызвал bug1 и внес некоторые изменения
Итак, вы запустили, например:
git checkout -b bug1
, который добавил новую метку ветки bug1
, указывающую на коммит I
, и сделал это HEAD
:
...--F--G--H--I <-- bug1 (HEAD), master, origin/master
и затем вы изменили некоторые файлы, запустили git add
и git commit
, чтобы сделать новый коммит J
:
...--F--G--H--I <-- master, origin/master
\
J <-- bug1 (HEAD)
Я выдвинул bug1 после перехода в origin / master после выборки (это изменение еще не объединено с удаленным)
Опять же, "еще не объединено" в значительной степени бессмысленно:ваш Git вызвал их Git, предложил им ваш коммит J
и попросил их создать имя bug1
, указывающее на коммит J
.Нет никакого вопроса о слиянии во время любого git push
: все зависит только от того, принимают ли они ваши запросы на изменение или создают какие-либо имена филиалов.Так что теперь у них есть только два названия веток: master
указывает на I
и bug1
указывает на J
.Затем ваш собственный Git говорит itseif: Ах, они приняли запрос на создание bug1
, поэтому я сейчас вспомню, что у них есть bug1
, указывающий на коммит J
. Это означает, что ваш собственный репозиторий можеттеперь можно нарисовать как:
...--F--G--H--I <-- master, origin/master
\
J <-- bug1 (HEAD), origin/bug1
Я сделал ветку от мастера, названную bug2.
То есть:
git checkout -b bug2 master
или эквивалентный.Это выбирает коммит I
в качестве текущего коммита и создает для него новое имя, извлекает содержимое I
в ваш индекс и рабочее дерево и присоединяет HEAD
к новому имени:
...--F--G--H--I <-- bug2 (HEAD), master, origin/master
\
J <-- bug1, origin/bug1
(Если вы использовали git branch
, это не отрегулировало HEAD
, и вы все еще могли бы быть на коммите J
, но в остальном это работает почти так же.)
Я исправил свой последний коммит на master (с необходимой информацией, которая была бы полезна для bug2)
Итак:
git checkout master
На этом шаге выбирается коммит I
в качестве текущего коммита и присоединяет HEAD
к имени master
.При необходимости ваш индекс и дерево работы корректируются в соответствии с I
, но если вы уже используете I
, здесь нет работы;конечный результат:
...--F--G--H--I <-- bug2, master (HEAD), origin/master
\
J <-- bug1, origin/bug1
Итак, единственное реальное изменение состояло в том, чтобы изменить, к какому имени HEAD
было присоединено, если вы не были на коммите J
, и в этом случае он также проверял коммитI
.Тогда вы сделали:
<make some changes and run git add>
git commit --amend
Опция --amend
фактически не изменяет коммит I
вообще. Вместо этого он делает новый, другой коммит. Мы могли бы назвать это I'
(чтобы отразить, что это копия I
) или просто дать ему совершенно новое письмо K
. Я сделаю последнее здесь. Сложность git commit --amend
заключается в том, что вместо указания на I
новый коммит указывает на родитель *1340* H
:
K <-- master (HEAD)
/
...--F--G--H--I <-- bug2, origin/master
\
J <-- bug1, origin/bug1
Обратите внимание, что фиксация I
все еще существует и все еще запоминается через bug2
и origin/master
. В репозитории Git на origin
все еще есть коммит I
; он все еще помнит коммит I
через master
. Ваш собственный bug1
запоминает коммит J
, который запоминает коммит I
, а Git over на источнике также имеет J
(который обязательно помнит I
- коммиты идентичны в каждом репозитории Git что у них есть).
Вы можете, локально, отследить, кто что имеет, посмотрев на все ваши собственные имена ветвей - bug1
, bug2
и master
- и пройдя назад, по одному коммиту за раз, чтобы увидеть, какие коммиты вы можете достичь. Сделайте то же самое со своими именами удаленного отслеживания origin/bug1
и origin/master
, чтобы увидеть, какие коммиты у вас есть, которые они могут достичь. 1
Теперь перейдем к проблемному шагу:
- Я проверил bug2 и сделал
git rebase master
, надеясь, что все изменения, которые были сделаны в master, теперь будут отражены в bug2.
Теперь мы должны посмотреть, что делает rebase - но вкратце, он копирует коммитов, как будто git cherry-pick
. Копии коммитов основаны на аргументах, которые вы ему даете.
1 По крайней мере, это то, что у них есть , насколько вам известно вашему Гиту . Он не вызывал другого Git более чем за три секунды , и кто знает, как сильно он мог измениться за все это время! Здесь может быть важно знать, кто меняет исходный Git-репозиторий. Как быстро он обновляется?
Который совершает ребазу копий
Вернемся к графику, скорректированному на git checkout bug2
:
K <-- master
/
...--F--G--H--I <-- bug2 (HEAD), origin/master
\
J <-- bug1, origin/bug1
и команде:
git rebase master
Здесь вы использовали простейшую форму git rebase
. Вы можете использовать git rebase --onto
, чтобы отделить цель перебазирования - , куда поместить копии , более или менее - от ограничителя , то есть , что не скопировать . С простейшей формой это одно и то же: master
- это и , куда помещать копии и , что не нужно копировать .
Git теперь начинается с bug2
и идет назад по графику, перечисляя коммиты. Это I
, H
, G
и т. Д. Он также начинается с master
и идет назад, перечисляя коммиты: K
, затем H
, затем G
и так далее. Все, что находится в втором списке, получает исключено . 2 Так что остается коммит I
.
Результирующий список - это список коммитов для копирования (в неправильном порядке, поэтому Git перевернет список, но здесь есть только одна запись). Rebase теперь будет пытаться скопировать коммит I
после коммита K
. Точный механизм копирования немного сложен - некоторые git rebase
операции на самом деле запускают git cherry-pick
, а некоторые нет - но в любом случае он должен иметь одинаковый результат и работать как git cherry-pick
, По сути это означает, что объединяет изменения, которые кто-то сделал в пути H
-vs- I
, с изменениями, которые кто-то сделал в пути H
-vs- K
.
Конечно,этот "кто-то" был вы .Эти изменения противоречили друг другу.Git не смог решить конфликт самостоятельно.Если бы Git смог разрешить конфликты самостоятельно, Git попытался бы сделать новый коммит - копию I
, за исключением того, что родитель нового коммита был бы K
- и тогда это было бы сделано и установило быимя bug
для указания нового коммита.Поскольку это копия I
(но на самом деле этого не произошло), давайте назовем ее I'
, просто чтобы показать, как все это выглядело бы, если бы произошло :
I' <-- bug2 (HEAD)
/
K <-- master
/
...--F--G--H--I <-- origin/master
\
J <-- bug1, origin/bug1
Вы можете разрешить конфликты, выбрав все файлы из коммита K
, т. Е. Отбросив все изменения из H
-vs- I
, которых еще нет в H
-vs- K
, и просто сохранитечто в K
.Но тогда нет никакого смысла вообще беспокоиться о новом коммите: просто сделайте так, чтобы имя bug2
указывало прямо на коммит K
, например:
K <-- bug2 (HEAD), master
/
...--F--G--H--I <-- origin/master
\
J <-- bug1, origin/bug1
Вы можете сделать это с git rebase
, вам просто нужна более длинная форма:
git rebase --onto master <anything that specifies commit I>
--onto master
говорит Git: ставить копии после коммита, идентифицируемого master
, т.е.после K
, а остальное говорит Git: не копируйте commit I
и ничего более раннего .Так что это сделало бы работу: скопировало бы no commit, затем переместило бы имя bug2
.
В данном случае проще просто переместить имя bug2
самостоятельно.Это особенно легко , потому что вы никогда не использовали git push
, чтобы сказать любому другому Git, чтобы имя bug2
запомнило что-нибудь.Так что больше никому не нужно говорить: О, эй, bug2
Я просил тебя сделать?Забудьте об этом, используйте это вместо этого.
Однако у вас do есть проблема с master
и bug1
.Вы сказали другому Git - тот, что в origin
- запомнить * commit 1515 * как их master
, и J
как их bug1
.Они, вероятно, до сих пор помнят это.Вы можете сказать, что другой Git: Вот коммит K
.И, о, эй, тот коммит I
, который вы помните как свой master
... запомните новый коммит K
как свой master
. , который требует принудительного толчка ,git push -f
или git push --force-with-lease
.Вы также должны сделать что-то со своим собственным bug1
, например, использовать git rebase --onto
, чтобы скопировать коммит J
в новый и улучшенный коммит, который идет после K
, а затем вызвать Git на origin
,отправьте им новый и улучшенный коммит и заставьте их передвинуть их bug1
.
Что теперь делать
Во-первых, вам нужно выйтиперебазировать ад.:-) Самый простой способ сделать это - использовать:
git rebase --abort
, который возвращает все так, как было до того, как вы начали.То есть теперь вы вернулись к:
K <-- master
/
...--F--G--H--I <-- bug2 (HEAD), origin/master
\
J <-- bug1, origin/bug1
Теперь вы можете делать одно (или несколько) из многих вещей.Вот два простых варианта:
git checkout -B bug2 master
или git checkout master; git branch -D bug2; git checkout -b bug2
В результате:
K <-- bug2 (HEAD), master
/
...--F--G--H--I <-- origin/master
\
J <-- bug1, origin/bug1
Теперь вы можете продолжать работать с bug2
.
В какой-то момент - в любое время или позже - вы можете запустить:
git push --force-with-lease origin master
, который заставляет ваш Git вызывать их Gitпредложите им свой новый коммит K
и скажите им: я думаю, что ваши master
очков за коммит I
.Если это так, укажите вместо этого K
. 2 Это очень похоже на --force
/ -f
, за исключением того, что обычная сила просто посылает им команду: Установите master
! Если кто-то еще, предположительно, не вы, добавил больше , совершивший мимо I
, вы вызовете Git на origin
до потерять эти коммиты.В этом случае вы хотите их master
"потерять" коммит I
: его заменили на новый и улучшенный K
.
Тогда, в конце концов, вы будетеВероятно, также хотите:
git checkout bug1
git rebase --onto master bug1~1
Это сспециальный бит синтаксиса - bug1~1
- сообщает Git: Начиная с bug1
, работает в обратном направлении через один коммит. То есть он находит bug1
(коммит J
) и затем отступает на один шаг назадсовершить I
.Это коммиты , а не для копирования - I
и все, что раньше - поэтому git rebase
теперь скопирует J
, выполнив сравнение I
-vs- J
и добавив эти измененияследовать за любым коммитом на вершине master
.Если вы сделали несколько bug2
коммитов, но не новые master
, и заставили origin/master
запомнить K
, это может выглядеть так:
L--M <-- bug2
/
K <-- master, origin/master
/ \
...--F--G--H J' <-- bug1 (HEAD)
\
I
\
J <-- origin/bug1
Теперь, когда вы 'Заменив J
новым и улучшенным J'
, вы можете сказать Git на origin
, что все в порядке, забывать все о I
и J
: они должны установить свои bug1
запомнить J'
вместо:
git push --force-with-lease origin bug1
или, конечно, более простой git push -f origin bug1
, который просто предполагает их bug1
указывает на J
и заставляет их забытьI-J
часть и просто запомните J'
.
2 Опция --force-with-lease
должна сообщить другому Git: Я думаю, что ваше имя указывает на фиксацию .Если это так, установите его на . Место, где он получает эту информацию, остается неизменным: ваш Git имеет ваш origin/master
, помня, где ваш Git видел их Git's master
в последний раз, когда вы запускали git fetch
или удалось с git push origin master
.Поэтому, если имя для обновления - master
, старый хеш принадлежит вашему собственному origin/master
;если имя для обновления bug1
, старый хеш принадлежит вашему собственному origin/bug1
.