Когда вы говорите "ветвь А имеет пять коммитов", вы, вероятно, не учитываете все коммиты, которые содержит ветвь А.То же самое относится к вашим семи коммитам в ветви B. Чтобы действительно понять это, важно понимать, что в Git ветви - или, точнее, ветви names - на самом деле не имеют никакого значения.Только коммиты имеют значение.
Чтобы увидеть, как это работает, давайте начнем с очень маленького репозитория, содержащего всего три коммита.Три коммита имеют большие длинные уродливые имена Git-hash-ID, но давайте просто назовем их коммитами A
, B
и C
, как если бы в коммитах были настоящие имена из одной заглавной буквы.(Мы закончим довольно быстро, и это одна из причин, по которой Git использует эти большие уродливые хеш-идентификаторы.)
Первый большой важный секрет Git заключается в том, что каждый коммит хранит свой предыдущий хеш коммита.ID внутри него.Всякий раз, когда у вас есть хеш-идентификатор коммита в ваших руках, мы говорим, что вы указываете на этого коммита.Таким образом, наши три коммита выглядят так:
A <-B <-C
Коммит C
хранит хэш-идентификатор B
, поэтому C
указывает на B
.B
хранит хэш-идентификатор A
, поэтому B
указывает на A
.A
это, конечно, самый первый коммит, который мы когда-либо сделали: он не может указывать дальше.Это особый случай - коммит root , из которого всегда есть хотя бы один, если хранилище не пустое.Обычно существует ровно один корневой коммит, причем этот является первым коммитом.
Имена ветвей
Следующий большой важный секрет - это простое продолжение первого, и этоветвь name наподобие master
или develop
просто указывает на один коммит .Единственный коммит, на который указывает master
, в этом случае будет коммитом C
:
A--B--C <-- master
Мне всегда лень рисовать внутренние стрелки между коммитами по разным причинам.Во-первых, когда мы делаем коммит, ничто и никто - даже сам Git - не может изменить коммит.Фиксация C
заморожена во времени навсегда, всегда указывая на B
, который заморожен и указывает на A
, и так далее.Поэтому внутренние стрелки неизменно указывают назад .Git называет их родителей коммита: родительский элемент C
равен B
, а родительский B
равен A
.
Указатели имени ветви отличаются,В отличие от замороженного содержимого каждого коммита, указатель имени ветви может и действительно меняет .
Let's git checkout master
, который извлекает коммит C
в наше рабочее дерево , давая нам файлы, которые мы можем видеть и работать с / с.Затем мы внесем некоторые изменения, git add
обновленные файлы и git commit
, чтобы сделать новый коммит, который мы назовем D
.Git упакует наши новые файлы 1 и сделает этот новый коммит D
, указывая на тот коммит, который у нас был, т. Е. C
, так что теперь у нас есть:
A--B--C--D
, а затем в качестве заключительного акта git commit
записывает хэш-идентификатор D
в имя master
, так что master
теперь указывает не на C
, а на D
:
A--B--C--D <-- master
Вот как растут ветви при добавлении новых коммитов: каждый новый коммит указывает на тот, который был последним в ветке, а затем Git обновляет имя ветки так, чтобы имяТеперь идентифицирует новый совет.Всякий раз, когда Git ищет историю - то, что произошло со временем - он работает, начиная с последнего коммита, на который указывает имя, и работая в обратном направлении, по одному коммиту за раз.
Создание новых веток
Чтобы создать новую ветку , Git просто добавляет новое имя , указывающее на некоторый существующий коммит.Давайте теперь сделаем ветку branch-a
в нашем хранилище с четырьмя коммитами:
A--B--C--D <-- master, branch-a (HEAD)
Помимо добавления имени branch-a
, указывающего на D
, я прикрепил специальное имя HEAD
- во всехпрописные, хотя вы можете использовать @
, если вам нравится более короткое имя - к одному из двух названий ветвей.Вот как Git запоминает текущую ветку .
Прежде чем мы сделаемНовые коммиты, ответьте сами: сколько коммитов в master
и сколько в branch-a
?Если вы не ответили «четыре» каждый раз, почему бы и нет?Если вы спросите Git, ответ будет четыре: есть четыре коммита: D
, C
, B
, A
, , оба ответвления.
Давайте добавим пятьфиксирует нашу новую branch-a
сейчас, изменяя вещи и используя git add
и git commit
обычным способом.Git создаст пять новых уникальных больших уродливых хеш-идентификаторов, но мы назовем новые коммиты E-F-G-H-I
и нарисуем их:
E--F--G--H--I <-- branch-a (HEAD)
/
A--B--C--D <-- master
Когда мы сделали E
, Git сделал это с parentD
, а затем изменили имя branch-a
, указав E
.Когда мы сделали F
, его родитель был E
, а Git обновил branch-a
, указав F
.Мы повторили это пять раз, и у нас есть 11 коммитов на branch-a
, которые не на главном, плюс четыре коммита на обе ветви .Так что branch-a
имеет не пять, а девять коммитов.Просто пять из них только на branch-a
.
Теперь давайте сделаем branch-b
, сначала переключившись обратно на master
, а затем создав новое имя branch-b
,указывая на фиксацию D
:
E--F--G--H--I <-- branch-a
/
A--B--C--D <-- master, branch-b (HEAD)
Обратите внимание, что здесь ничего не изменилось внутри самого хранилища.Наше рабочее дерево (и индекс) изменилось - они вернулись к фиксации D
- и мы добавили новое имя branch-b
, которое, подобно master
, идентифицирует фиксацию D
, но все коммиты не потревожены.
Теперь давайте добавим семь коммитов, уникальных для branch-b
:
E--F--G--H--I <-- branch-a
/
A--B--C--D
\
J--K--L--M--N--O--P <-- branch-b (HEAD)
На самом деле branch-b
есть 11 коммитов, но четыреони делятся (с master
, который я перестал рисовать из-за лени, и с branch-a
).
Слияние
Теперь вы хотите объединить branch-b
в branch-a
.Таким образом, команды, которые вы запускаете, будут:
git checkout branch-a
git merge branch-b
Первый шаг выбирает коммит I
в качестве текущего коммита и branch-a
в качестве имени, к которому должен быть присоединен HEAD
.Он копирует содержимое commit I
в рабочее дерево (и index / staging-area).В самом графике нет никаких изменений, но теперь HEAD
обозначает branch-a
и, следовательно, фиксирует I
:
E---F----G---H----I <-- branch-a (HEAD)
/
A--B--C--D
\
J--K--L--M--N--O--P <-- branch-b
(я также немного растянул верхнюю строку из-за чего-то, чтонамеревается нарисовать через мгновение. Положение коммитов на графике растягивается, потому что Git не заботится о фактическом времени коммита, только о форме коммитов и их соединительных дуг, иВы можете изгибать и скручивать график так, как вам нравится, если вы не разрываете ни одно из соединений или создаете новые, которых там нет.)
Команда git merge
затем что-то делаетнемного сложно.Сначала он находит базу слияния между текущим коммитом I
и другим коммитом P
.Основой слияния является, грубо говоря, точка, в которой две ветви разошлись.В данном случае это очень очевидно из графика: это commit D
.
Git теперь выясняет, что "мы" изменили на branch-a
, выполнив:
git diff --find-renames <hash-of-D> <hash-of-I> # what we changed
Получаетвторой анализ, чтобы выяснить, что они изменили на branch-b
:
git diff --find-renames <hash-of-D> <hash-of-P> # what they changed
Затем Git объединяет два набора изменений, применяя объединенные изменения к тому, что находится в снимке в коммите.D
.
Этот процесс «создайте две разницы, объедините их и примените их к базе слияния» - это форма действия слияния.Мне нравится называть это глаголом , чтобы объединить , то есть объединить изменения.Поскольку коммиты - это снимки, а не наборы изменений, Git должен выполнить два сравнения.Чтобы иметь разумную отправную точку, Git должен найти базу слияния.Вот почему у нас есть вся эта работа, которая происходит как часть глагола для объединения , когда мы объединяем коммиты I
и P
.
Слияние коммитов
Теперь, когда Git выполнил всю эту работу по слиянию, Git сделает коммит слияния . Ну, он будет часто или обычно делать один - мы увидим исключения через мгновение. Обратите внимание, что здесь используется слово merge в качестве прилагательного, хотя оно модифицирует слово commit . Мы также можем ссылаться на этот новый коммит слияния как слияние , используя слово merge как существительное. Мне нравится называть это «слияние как существительное» или «слияние как прилагательное», чтобы отличить его от process , от глагола to merge . Для команды git merge
мы сначала выполняем процесс, а затем делаем коммит слияния в конце. Но давайте нарисуем это:
E---F----G---H----I
/ \
A--B--C--D Q <-- branch-a (HEAD)
\ /
J--K--L--M--N--O--P <-- branch-b
Этот новый коммит, коммит слияния Q
, особенный точно в одном отношении: у него два родителя вместо одного. Сначала он указывает на коммит I
, например, коммит I
был на кончике branch-a
минуту назад и является родителем коммита Q
, но затем он также указывает на коммит P
, говоря коммит P
также является родителем коммита Q
.
Если мы теперь спросим Git, сколько и какие коммиты находятся на branch-a
, Git начинается с Q
, затем работает в обратном направлении через и I
и P
, в конечном итоге достигнув D
(на который master
все еще указывает), а затем полностью вернувшись к A
. Таким образом, число коммитов теперь составляет 17: A
до D
плюс E
до I
плюс J
до P
плюс Q
. Если мы спросим, сколько коммитов на branch-a
, а не на master
, мы получим 13: пять для E
до I
, семь для J
до P
, и один для Q
.
Есть много способов нарисовать это
Вот еще один способ нарисовать, что произошло:
...--D--E--F--G--H--I------Q <-- branch-a (HEAD)
\ /
J--K--L--M--N--O--P <-- branch-b
число достижимых коммитов остается неизменным, хотя: Git начинается с Q
, возвращается к обоим I
и P
, возвращается к обоим H
и O
, и так далее до достижения D
, когда он возвращается к тому, что предшествует общему коммиту D
.
Если вы git log
рисуете график, используя git log --graph
или git log --graph --oneline
, Git будет рисовать его вертикально, с коммитом Q
вверху и структурой ветвления, представленной в виде отдельных линий:
* hashofQ (HEAD -> branch-a) Merge ..
|\
| * hashofP commit message for P
* | hashofI commit message for I
...
или аналогичный - точная позиция каждой *
и строки зависит от дополнительных параметров сортировки, которые вы можете передать git log
, например --author-date-order
, хотя --graph
всегда обеспечивает как минимум параметр --topo-order
. Средства просмотра графики, такие как gitk
и различные графические интерфейсы, могут имитировать git log --graph --oneline
, но делают все это красивее (хотя, как всегда, красота в глазах смотрящего).
Слияние сквоша: git merge
не всегда сливается
Команда git merge
может сделать больше, чем построить слияние (существительное), используя для слияния (глагол) процесса. Аркус упомянул git merge --squash
, который выполняет для слияния часть процесса, но затем просто останавливается, не делая фиксацию и не записывая тот факт, что следующий коммит должен быть слиянием , В этом конкретном случае мы бы сами запустили git commit
, чтобы сделать коммит Q
. Новый коммит Q
будет обычным коммитом , а не коммитом слияния, и мы можем нарисовать его следующим образом:
...--D--E--F--G--H--I--Q <-- branch-a (HEAD)
\
J--K--L--M--N--O--P <-- branch-b
Поскольку нет никакой связи между Q
и P
, кто-то, кто придет позже - включая вас или Git - и, глядя на этот график, может и не догадываться, что фиксация Q
является результатом слияния. Семь коммитов, которые являются эксклюзивными для branch-b
, все еще являются эксклюзивными для branch-b
. В общем, если вы сделали это, вы должны немедленно удалить имя branch-b
из этого хранилища и из каждого клона этого хранилища , чтобы полностью забыть, что коммиты J-K-L-M-N-O-P
когда-либо существовали.
Это яИногда, но не всегда, жизнеспособный, полезный и хороший рабочий процесс.Это особенно полезно, когда отдельные коммиты на branch-b
никогда не были замечены где-либо еще, так что вы знаете , никто их не имеет, и вы сделали их только как временные коммиты с намерениемзаменить их всех одним коммитом «добавить функцию», то есть, коммитом Q
, в конце.После слияния в сквош вы заставляете Git удалить ваше имя branch-b
и забыть, что когда-либо делали какие-либо отдельные коммиты.У вас есть один последний хороший коммит, и вы притворяетесь, что знаете, как сделать этот коммит одновременно.
Иногда, хотя, даже если вы представляете какую-либо функцию, хорошо бы сохранить ее каксерия отдельных коммитов.В частности, что если вы тоже ввели ошибку?В этом случае, если вы сократите свою функцию до серии простых, но четких коммитов - скажем, трех из них - и затем объедините их с реальным слиянием, вы получите график, подобный этому:
...--D--E--F--G--H--I--Q <-- branch-a (HEAD)
\ /
R-------S-----T <-- branch-b
Если теперь выяснится, что у вас есть введенная ошибка, возможно, возможно проверить коммиты R
и S
и T
и посмотреть , какой из этих коммитов представил ошибку .Затем вы можете сравнить R
против D
, S
против R
или T
против S
, чтобы помочь вам выяснить, как появилась ошибка, и выяснить, что нужно сделать, чтобы ее исправить.
То, что сводится к тому, что сквош слияния не плохие, они просто инструмент.Используйте свои инструменты, чтобы делать вещи таким образом, чтобы в будущем вам было легче. Если это означает раздавливание, продолжайте и давите.Если нет, не надо.
Перемотка вперед: git merge
не всегда объединяет
Мы также должны охватить операции перемотки вперед .Рассмотрим ситуацию, в которой вы создаете ветку:
...--C--D <-- master, feature (HEAD)
Затем вы делаете некоторые коммиты на этой ветке:
...--C--D <-- master
\
E--F--G--H <-- feature (HEAD)
Все кажется великолепным, и выхотел бы представить функцию сейчас, сохраняя все четыре из этих коммитов нетронутыми.Если вы сейчас выполните:
git checkout master
git merge feature
Git скажет что-то о fast-forward , и у вас останется следующий график:
...--C--D--E--F--G--H <-- master (HEAD), feature
Имя feature
не переместился - он по-прежнему указывает на фиксацию H
- но имя master
переместило , и теперь также указывает на фиксацию H
.Нет нового коммита слияния!
Что Git сделал здесь, так это то, что он выполнил поиск базы слияния так же, как и для реального слияния, и обнаружил, что лучший общий коммит между master
и feature
былсовершить D
.Имя master
указывало на коммит D
, поэтому, если бы Git должен был сделать обычный , чтобы объединить глагол , он бы запустил:
git diff --find-renames *hash-of-D* *hash-of-D* # what we changed
и ответ был бы,конечно, если бы мы ничего не изменили! Тогда Git нужно было бы проанализировать D
против H
, чтобы узнать, что они изменили, что, конечно, будет тем, что они изменили.Git применил бы эти изменения к D
и снова получил бы ... commit H
.
Если бы Git сделал из этого реальное слияние, это выглядело бы как:
...--C--D------------I <-- master (HEAD)
\ /
E--F--G--H <-- feature
Снимок для фиксации I
будет соответствовать снимку для фиксации H
.
Вы можете force Git сделать этот коммит слияния:
git checkout master; git merge --no-ff feature
Таким образомвы получаете то же самое истинное слияние, которое вы получили бы, если бы master
имел некоторый коммит после D
.Вы можете сделать это, если хотите подчеркнуть для будущего зрителя - который может стать самим собой через год или два - что коммиты E-F-G-H
были сделаны группой, и вместе они реализуют какую-то функцию.Или вам может быть все равно: вы и будущий год спустя предпочитаете рассматривать коммиты E-F-G-H
как логическое расширение master
, при этом вам не нужно помнить, что эти четыре были сделаны специально для какой-то конкретной функции..
Сноваэто действительно сводится к тому, что ускоренное слияние против реального слияния - это инструмент, который вы можете использовать для передачи информации будущим пользователям этого хранилища.Используйте свои инструменты, чтобы упорядочить вещи, чтобы сделать жизнь в будущем проще для вас.
Если вы думаете, что предпочтете увидеть слияние в git log --graph
или графическом средстве просмотра, форсируйтепрямое слияние с git merge --no-ff
.Если вы думаете, что вы предпочтете , а не , чтобы увидеть слияние, вы даже можете использовать git merge --ff-only
, чтобы убедиться, что Git просто не удастся , если требуется истинное слияние (после чего вынужно будет сделать что-то другое, и это выходит за рамки этого уже слишком длинного ответа).