Повторно зафиксированные коммиты не помечаются каким-либо особым образом.
Перебазирование работает путем копирования коммитов, как если бы git cherry-pick
, а затем отказывается от оригинальных коммитов в пользу новых и (предположительно?) Улучшенных копий. Оригиналы продолжают существовать, но если нет способа найти их, вы их больше не увидите.
Филиал Имена здесь в значительной степени не имеют значения. Имена ветвей просто позволяют вам находить конкретные коммиты - очень полезные для людей и полезные для Git, но как только коммиты найдены, имена уже не имеют значения для Git. Имя ветки работает так, что оно содержит идентификатор ha sh один конкретный commit; по определению, этот коммит является последним коммитом в ветке.
Для нас очень важно, чтобы был граф коммитов . Каждый коммит имеет свой уникальный идентификатор ha sh, который никогда не изменяется и всегда означает , что commit - фактически, это означает, что конкретный коммит в каждый Git, даже в Git хранилище, в котором еще нет этого коммита. 1 Копирование коммита в новый и улучшенный коммит приводит к новому, другому коммиту с другим идентификатором ha sh. 2
Помните, что наряду со снимком вашего кода каждый коммит содержит некоторые метаданные , включая того, кто их сделал (автор и коммиттер) и когда (дата и время - штампы) и почему (сообщение в журнале). В этих метаданных вы также найдете родительского га sh ID . Родителем коммита является коммит, который приходит до коммита. Таким образом, учитывая некоторый коммит, мы можем оглянуться назад, чтобы найти предыдущий коммит. Если мы позволим заглавным буквам заменять настоящие идентификаторы ha sh, мы можем нарисовать это:
<-H
Коммит, чей идентификатор ha sh равен H
содержит идентификатор ha sh из или указывает на , для удобства - некоторые более ранние коммиты. Давайте все это один G
:
<-G <-H
Конечно, G
указывает на F
, и так далее:
... <-F <-G <-H
A имя ветви просто содержит идентификатор ha sh, т. Е. Указывает на last commit:
... <- F <-G <-H <-- branch
Git использует имя ветви для поиска последнего коммита. Какой бы коммит не имел sh ID, хранится под именем branch
, это последний коммит в ветке. Более ранние коммиты являются неявными: они определяются графом, который определяется различными отношениями «указывает на», хранящимися в коммитах. Так как они находятся в коммитах, их нельзя изменить: ничто в любом коммите не может быть изменено.
Обычная идея перебазирования - взять некоторый набор коммитов:
...--F--G--H--L <-- master
\
I--J--K <-- develop
и «пересадить» их, чтобы они пришли после какого-то другого коммита. Для этого Git необходимо превратить каждый снимок в набор изменений, а затем применить эти изменения к какой-либо другой фиксации. В этом случае мы хотим скопировать коммит I
, что само по себе хорошо, в новый и улучшенный I'
. Разница между I
и I'
будет:
- Родитель
I'
будет L
, а не H
. - Снимок в
I'
будет результатом применения H
-vs- I
к тому, что находится в L
.
Этот процесс принятия H
-vs- I
и применения к L
- это a git cherry-pick
: мы проверяем commit L
и запускаем git cherry-pick <hash-of-I>
, чтобы сделать это.
Теперь, когда мы сделали I'
:
I' <-- HEAD
/
...--F--G--H--L <-- master
\
I--J--K <-- develop
нам нужно скопировать J
в J'
. Это еще один git cherry-pick
: мы хотим найти то, что изменило между I
и J
, и применить эти изменения здесь, чтобы зафиксировать I'
. Когда мы закончим, у нас будет:
I'-J' <-- HEAD
/
...--F--G--H--L <-- master
\
I--J--K <-- develop
Теперь нам нужно скопировать коммит K
с последним выбором вишни-пика, который нам нужно сделать. Когда мы закончим, у нас есть:
I'-J'-K' <-- HEAD
/
...--F--G--H--L <-- master
\
I--J--K <-- develop
, и мы создали наши копии. Последний шаг git rebase
состоит в том, чтобы восстановить name develop
off commit K
и вместо этого указать K'
:
I'-J'-K' <-- develop (HEAD)
/
...--F--G--H--L <-- master
\
I--J--K [abandoned]
Rebase также повторится -прикрепите специальное имя HEAD
, которое запоминает, на какой ветке мы находимся. (Вот почему мы должны нарисовать имя HEAD
, прикрепленное к некоторой ветви, подобной этой, за исключением случаев, когда Git находится в режиме detached HEAD , что имеет место при ребазе, когда мы копируем коммиты , один коммит за раз. В режиме отсоединенного HEAD специальное имя HEAD
содержит необработанный идентификатор * ha sh фиксации, а не имя ветви.)
1 Этот особый волхв c достигается тем, что ID ha sh становится контрольной суммой содержимого коммита. Чтобы обеспечить уникальность и предотвратить подделку, контрольная сумма является криптографической c. Но это означает, что если вы когда-нибудь попытаетесь изменить содержимое любого коммита, даже хотя бы одного бита, результатом будет не измененный коммит, а скорее новый коммит с другим га sh ID. Исходный коммит остается с исходным идентификатором ha sh.
2 Если вы скопируете коммит и сделаете нет изменений вообще, так что новый коммит бит за битом идентичен исходному коммиту, вы снова получаете исходный идентификатор ha sh. Так что в этом случае нет фактической копии: но это нормально, ничего не изменилось . возможно сделать это в некоторых случаях, и многие виды перебазирования сделают это автоматически, когда это возможно. Опция --force
для git rebase
говорит Git: * Даже если вы можете оставить коммит абсолютно идентичным, скопируйте его в любом случае: измените что-то, предоставив новой копии текущую дату и время как его метка времени коммиттера вместо повторного использования метки времени коммитера оригинального коммита. *
Вещи могут и могут go неверны
Во время каждого копирования-коммита (git cherry-pick
) шаг, вы можете получить конфликты слияния. Это происходит потому, что фактическим механизмом выбора вишни является обычное трехстороннее слияние Git.
Давайте рассмотрим более типичное слияние, когда мы на самом деле начинаем с двух ветвей, которые разошлись после некоторого общего начала точка:
I--J <-- branch1 (HEAD)
/
...--G--H
\
K--L <-- branch2
Мы будем запускать git merge branch2
пока на branch1
вот так. Git will:
Найти базу слияния : общий коммит, который находится на обеих ветвях и best такой. Для этого Git начинается с того места, где мы сейчас находимся, - фиксируем J
, на что указывает branch1
, к которому прикреплен HEAD
- и работают в обратном направлении, и, в то же время, начиная с другого коммит мы называем: L
, верхушка branch2
. Git работает в обратном направлении от J
и одновременно с L
в своего рода алгоритме затопления графиков, который следует стрелкам назад.
Вы можете думать об этом как Временно закрасьте все коммиты зеленым цветом, начиная с J
и работая в обратном направлении. Точно так же, краска все фиксирует красный, начиная с L
и работая в обратном направлении. Когда две краски начинают смешиваться, мы достигли базы слияния.
В этом случае, коммит H
явно является базой слияния. (Существуют более сложные графы, в которых база слияния не столь очевидна, но до тех пор, пока две ветви в конечном итоге встретятся, будет некоторая база слияния. В некоторых уродливых случаях их может быть больше, чем одна база слияния, но здесь мы ее проигнорируем!)
Теперь, когда у нас есть три коммитов, выполните процесс слияния: слияние как глагол , как мне нравится. Мы рассмотрим это более подробно через мгновение.
Если все пойдет хорошо, сделайте коммит слияния . Это использует слово merge в качестве прилагательного, модифицируя слово commit . Мы (и Git) можем даже назвать это слово и просто сказать слияние , что означает коммит слияния .
(Если что-то не так go так что Git останавливается, оставляя вас в конфликте слияния. Вы должны исправить это самостоятельно и затем запустить git merge --continue
, или просто git commit
, чтобы сделать окончательный коммит слияния.)
Результатом является коммит, который работает как обычно: у него, как обычно, снимок, и метаданные с сообщением журнала, как обычно. Единственное, что un - необычное в том, что вместо перечисления one parent ha sh ID, в нем перечисляются обоих родителей, поэтому мы рисуем его следующим образом это:
I--J
/ \
...--G--H M <-- branch1 (HEAD)
\ /
K--L <-- branch2
Commit M
- наш коммит слияния; Git заставляет имя branch1
продвигаться вперед, чтобы указывать на новый коммит, как обычно, поэтому теперь имя branch1
идентифицирует коммит M
как вершину ветви.
Конфликты слияния возникают во время процесс слияния , или слияние-как-глагол
Давайте немного подробнее рассмотрим процесс слияния на шаге 2 выше.
Мы уже знаем, что каждый коммит содержит снимок. Мы также знаем, что мы можем Git сравнить любые два снимка:
git diff <hash1> <hash2>
Git будет просматривать все файлы в первом, левостороннем коммите и все файлы во втором, правый коммит. Когда файлы совпадают, Git проверит их содержимое . Если содержимое совпадает, Git ничего не говорит об этих файлах. Если содержимое отличается , Git сравнивает содержимое и вычисляет серию изменений, которые при применении к левому файлу приводят к правому файлу.
Некоторые файлы совсем не обязательно совпадать. Левый файл может быть просто удален, а новый файл может быть добавлен справа. Те, которые отображаются в diff как удаляет и добавляет. Мы также можем попросить Git угадать переименовывает , и он попытается объединить добавление справа с удалением слева: если файлы достаточно похожи, особенно если содержимое 100% идентично - Git может сопоставить их и сказать, что файл левой стороны переименован в , чтобы стать файлом правой стороны, возможно, с некоторыми изменениями содержимого тоже.
Что объединить как глагол означает запустить эти виды различий, но запустить два из них. Мы начинаем с базы слияния, которая находится на обеих ветвях, и дифференцируем ее по каждому из двух кончиков ветвей:
git diff --find-renames <hash-of-base> <hash-of-HEAD> # what we changed
git diff --find-renames <hash-of-base> <hash-of-other> # what they changed
Для любого файла, который не изменилось , оно одинаково во всех трех коммитах, и мы можем просто использовать любой из них. Для файлов, которые изменились , процесс объединения должен объединить два изменения и применить объединенные изменения к тому, что находится в базе слияния . Таким образом, мы сохраняем наши изменения и добавляем их изменения.
Конфликты слияния возникают, когда Git не может выполнить это объединение самостоятельно. Git не может объединить изменения, когда:
- мы и они коснулись одинаковых строк базового файла, и
- мы и они сделали различные изменения к тем же строкам.
Эта вещь "одинаковых линий" немного расширяется: если мы коснулись, скажем, строк с 5 по 9, и они коснулись строк 10 и 11, это конфликт тоже. Если они коснулись строк 11 и 12, так что строка 10 не изменилась, конфликта нет. Для этого нет веской теоретической причины, просто она хорошо работает на практике. Но обратите внимание, что Git не не понимает того, что объединяет, вообще: он просто следует простым правилам объединения строк.
В любом случае, где Git способен объединяя наши изменения и их изменения, Git применяет объединенные изменения к тому, что находится в базе слияния . Это дает правильный результат. Таким образом, объединение как глагол означает:
- diff базу слияния (ее снимок) против каждого коммита (снимок)
- объединение изменений
- применяет объединенные изменения к моментальному снимку *1315* базы слияния
, и все идет хорошо, когда Git считает, что оно правильно скомбинировало изменения. Это верно, даже если Git не на самом деле правильно их комбинирует (в некотором более глубоком смысле слова), что иногда случается. Например, если в базе слияния была объявлена неиспользуемая переменная, а слева мы удалили объявление, а справа - переменную, Git может объединить их. В конечном результате используется необъявленная переменная! (В некоторых языках программирования это менее вероятно, чем в других.)
И, конечно, мы получаем конфликты слияния, когда Git не может объединить изменения.
Как все это относится к git cherry-pick
Ранее мы описывали git cherry-pick
как «копирование» коммита: выясните, что изменилось, и примените эти изменения к другому снимку. Но на самом деле Git использует свой механизм слияния для полного слияния .
База слияния этого полного слияния является просто родителем коммит мы хотим скопировать . Коммит --ours
является текущим (HEAD
). Коммит --theirs
- это коммит , который мы хотим скопировать . Поэтому, если у нас есть:
I' <-- HEAD
/
...--F--G--H--L <-- master
\
I--J--K <-- develop
, и мы пытаемся скопировать J
, чтобы сделать J'
, мы выбираем commit I
в качестве базы слияния и commit J
как --theirs
коммит. Git затем выполняет git diff
из коммита I
, чтобы зафиксировать I'
, чтобы увидеть, что «мы» изменились.
Что «мы» изменили, чтобы получить от I
до I'
, это то, что вошло в I'
из-за H
-vs- L
! Это произошло во время первого выбора вишни, когда мы комбинировали H
-vs- L
(как --ours
, хотя он и их) с H
-vs- I
(как --theirs
, даже хотя мы перебираем наш коммит).
То, что "они" изменили, чтобы получить от I
до J
, это, конечно, изменения, которые мы хотим скопировать - наш отличается от оригинальной develop
ветки. Объединение этих двух наборов изменений будет работать, но может заставить нас разрешать конфликты.
Когда нам действительно нужно разрешать конфликты, мы git add
принимаем наши решения, как обычно, и запускаем git <em>whatever</em> --continue
для возобновления. Если бы мы использовали git cherry-pick
напрямую, мы бы использовали git cherry-pick --continue
, но, поскольку мы используем git rebase
, мы используем git rebase --continue
. Git завершит sh выбор вишни, сделав обычный коммит non -merge обычным способом.
Перебазирование, повторное конфликтование и / или слишком большое копирование
Перебазирование может снова увидеть одни и те же конфликты несколькими способами. Все зависит от , который обязывает вас копировать и , какие конфликты вы получаете и как вы их разрешаете .
Давайте go вернемся к исходной диаграмме перебазирования, и посмотрите на рисунок «после»:
I'-J'-K' <-- develop (HEAD)
/
...--F--G--H--L <-- master
\
I--J--K [abandoned]
Мы могли бы разрешить некоторые конфликты, когда создали цепочку I'-J'-K'
. Это было бы хорошо, если бы I-J-K
действительно действительно исчезло, но что, если рисунок не завершен? Предположим, что исходный чертеж должен был выглядеть так:
...--F--G--H--L <-- master
\
I--J--K <-- develop
\
M--N--O <-- feature
Когда мы копируем I-J-K
в новый I'-J'-K'
и вытягиваем имя develop
до точки последнего скопированного коммита, мы получаем:
I'-J'-K' <-- develop (HEAD)
/
...--F--G--H--L <-- master
\
I--J--K
\
M--N--O <-- feature
Коммиты I-J-K
не заброшены. Они прямо там feature
, где они всегда были! Допустим, теперь мы объединяем feature
в master
с помощью регулярного слияния:
I'-J'-K' <-- develop (HEAD)
/
...--F--G--H--L---------------P <-- master
\ /
I--J--K--M--N--O <-- feature
Мы осуществляем слияние обычным способом, дифференцируя базу слияния H
против L
, чтобы увидеть, что мы изменили на master
, и изменили H
против O
, чтобы увидеть, что они изменили на feature
. Мы объединяем изменения, применяем объединенный результат к H
и получаем коммит P
, в котором записываются оба родителя L
и O
.
Если мы теперь go сделаем ребазу feature
на master
, мы можем увидеть некоторые конфликты. Правильное разрешение, вероятно, просто для отбрасывания фиксаций I'-J'-K'
полностью, поскольку они уже включены в master
: мы должны просто иметь:
...--F--G--H--L---------------P <-- master, develop (HEAD)
\ /
I--J--K--M--N--O <-- feature
в качестве нашего конечного результата.
Есть много способов иметь очень похожие конфликты, и поскольку git rebase
неоднократно использует cherry-pick, в некоторых случаях вам может потребоваться разрешить то же самое конфликтовать несколько раз, даже в одной перезагрузке. Это происходит, когда вы выбираете их изменение или включаете его части, и ваши изменения в ваших последующих коммитах в последовательности cherry-pick влияют на те же строки, в которых вы использовали их разрешение или изменили ваши изменения.
Не видя реального графика фиксации и различных снимков, которые Git видит, все, что мы можем здесь сказать, это то, что это общая проблема. Git не знает, что перебазированный коммит был перебазирован раньше: это просто коммит, как и любой другой коммит. Git собирается выбрать вишню, используя ее родителя в качестве базы слияния и сравнивая эту базу слияния с вашим текущим / HEAD-коммитом и с дочерним коммитом, который вы выбираете с-вишней. Если изменения затрагивают те же строки или примыкают, Git объявит конфликт и заставит вас разрешить его. Вот и все, что нужно сделать: Вы должны выяснить, нужно ли вообще копировать текущий коммит, и если да, то как. Вы должны выяснить, была ли более ранняя версия этого коммита включена в какой-либо коммит, который вы используете как HEAD
в вашей ребазе. Все зависит от вас: Git знает только простые текстовые правила.