Нет истории коммитов .Есть только коммитов; коммитов являются историей.
Каждый коммит уникально идентифицируется по хеш-идентификатору.Этот хэш-идентификатор является истинным именем коммита.Если у вас есть этот коммит, у вас есть этот хэш-идентификатор.Если у вас есть этот хэш-идентификатор, у вас есть этот коммит.Считайте большой некрасивый хеш-идентификатор и посмотрите, есть ли в вашей базе данных «все коммиты, которые у меня есть в этом хранилище»: т.е. посмотрите, знает ли Git это.Если это так, у вас есть этот коммит.Например, b5101f929789889c2e536d915698f58d5c5c6b7a
является допустимым идентификатором хеша: это коммит в Git-репозитории для Git.Если у вас есть этот хэш-идентификатор в вашем Git-хранилище, у вас есть , который commit.
Люди обычно не вводят или не используют эти хэш-идентификаторы вообще. Git использует их, но Git - это компьютерная программа, а не человек.Людям не очень хорошо с этими вещами - я должен вырезать и вставить вышеупомянутый хэш-идентификатор, или я ошибаюсь - поэтому люди используют другой способ, чтобы начать.Люди используют названия ветвей .Но у многих различных Git-репозиториев есть master
, и это master
не всегда (или никогда!) Означает тот большой уродливый хеш-идентификатор, который я напечатал выше.Таким образом, name , как master
, относится к одному конкретному репозиторию Git, а хеш-идентификаторы - нет.
Теперь каждый коммит хранит некоторые вещи.То, что хранит коммит, включает в себя снимок всех файлов, которые идут с , которые фиксируют коммит, чтобы вы могли получить его позже.Он также включает в себя имя и адрес электронной почты человека, который сделал , который совершает, чтобы вы могли сказать, кого похвалить или обвинить.? Оно включает в себя сообщение в журнале: почему человек, который сделал коммит, говорит, что он сделал этот коммитНо - вот первая сложная часть - почти каждый коммит также включает по крайней мере один хэш-идентификатор , который является коммитом, который приходит до этого конкретного коммита.
Итак,если у вас есть b5101f929789889c2e536d915698f58d5c5c6b7a
, то у вас есть это:
$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800
Fourth batch after 2.20
Signed-off-by: Junio C Hamano <gitster pobox.com>
(строка tree
представляет сохраненный снимок, который идет с этим коммитом. Вы можете проигнорировать это здесь.) parent
строка дает идентификатор хеша коммита, который приходит до b5101f929789889c2e536d915698f58d5c5c6b7a
.
Если у вас есть b5101f929789889c2e536d915698f58d5c5c6b7a
, то почти наверняка у вас также есть a562a119833b7202d5c9b9069d1abb40c1f9b59a
.История для последующего коммита равна раннего коммита.
Если мы заменим каждый из этих больших уродливых хеш-идентификаторов одной заглавной буквой, 1 , мы можем нарисовать этоТакая история намного проще:
... <-F <-G <-H
, где H
- это последний коммит в длинной цепочке коммитов.Поскольку H
содержит хэш-идентификатор G
, нам не нужно записывать большой некрасивый хэш-идентификатор G
, мы можем просто записать хэш-код H
.Мы используем это, чтобы Git нашел идентификатор G
внутри H
.Если мы хотим F
, мы используем H
, чтобы найти G
, чтобы найти ID F
, что позволяет Git получить F
.
Но мы все равно должны записать это последний идентификатор хеша.Именно здесь приходят имена ветвей. Имена ветвей, такие как master
, служат нашим способом сохранения хэш-идентификатора last commit.
Для создания new commit, у нас есть Git, сохраняющий хеш-код H
в нашего нового коммита.У нас есть Git, сохраняющий снимок и наше имя, адрес электронной почты и все остальное, а также все остальное - «остальное» включает метку времени, точную секунду , когда у нас был Git, делающий все это.Теперь Git вычисляет фактический хэш-идентификатор всех этих данных, включая отметку времени.Коммит теперь сохраняется в нашей базе данных всех коммитов, и Git дал нам новый хэш-идентификатор I
:
...--F--G--H <-- master
\
I
У нас Git автоматически запись I
ID хэша в наше имя master
:
...--F--G--H--I <-- master
и мы добавили новую историю, которая сохраняет всю существующую историю.
1 Конечно, если бы мы использовали только одну заглавную букву, подобную этой, мы бы исчерпали способность создавать коммиты в любой точке мира после создания всего 26 коммитов.Вот почему Git хеш-идентификаторы такие большие.Они содержат 160 битов, поэтому число возможных коммитов или других объектов составляет 2 160 или 1 461 501 637 330 090 918 203 684 832 716 283 019 655 942 542 976.Как оказалось, этого на самом деле недостаточно, и Git, вероятно, перейдет к большему хешу, который может содержать в 79,228,162,514,264,337,593,543,950,336 раз больше объектов.Хотя первое число достаточно велико, чтобы перечислить все атомы во вселенной, существуют определенные неприятные атаки, поэтому 256-битный хэш - хорошая идея.См. Как недавно обнаруженное столкновение SHA-1 влияет на Git?
Здесь рассказывается, как иметь такую же историю
История равна совершает.Чтобы иметь одинаковую историю в двух ветвях, вам нужно, чтобы оба имени ветви указывали на один и тот же коммит:
...--F--G--H--I <-- master, dev
Теперь история в master
: Начиная сI
, покажите I
, затем вернитесь к H
и покажите H
, затем вернитесь к G
... Аналогично, история в dev
: Начиная сI
, покажите I
, затем вернитесь к H
...
Конечно, это не совсем то, что вы хотите.Вам нужно иметь историю, которая расходится , затем снова сходится .Вот что на самом деле представляют собой ветви:
...--F--G--H <-- master
\
I <-- dev
Здесь история в dev
начинается (заканчивается?) С I
, затем возвращается к H
, а затем G
и т. Д.,История в master
начинается (заканчивается?) С H
, возвращается к G
и так далее.Поскольку мы добавляем больше коммитов, мы добавляем больше истории, и если мы делаем это следующим образом:
K--L <-- master
/
...--F--G--H
\
I--J <-- dev
, тогда история двух ветвей расходится .Теперь master
начинается с L
и работает в обратном направлении, а dev
начинается с J
и работает в обратном направлении.Есть два коммита на dev
, которые не на master
, и два коммита на , которые *1179* не на dev
, а затем все от H
на спине - на обеих ветвях.
Эта дивергенция - коммиты, которые не на некоторой ветви - это то, где рабочие линии расходятся.Ветвь names до сих пор помнит только по одному коммиту каждый , в частности tip или last коммит каждой линии разработки.Git запустится с этого коммита, используя сохраненный хеш-идентификатор, и будет использовать сохраненный родительский хеш-код этого коммита для перехода назад, по одному коммиту за раз.Там, где соединяются линии, история воссоединяется.Это все, что есть в репозитории, за исключением следующего раздела.
Объединяет историю объединения (и снимки)
Теперь вы можете сделать коммит слияния .Основной способ сделать коммит слияния - это использование команды git merge
.Это состоит из двух частей:
- объединение работы , где Git вычисляет, что изменилось в каждой линии разработки;и
- создание коммита слияния , который является коммитом только с одной специальной функцией.
Чтобы сделать слияние, вы начинаете с выбора одного кончика ветви.Вы запускаете git checkout master
или git checkout dev
здесь.Какой бы из них вы ни выбрали, это коммит, который у вас есть сейчас, и Git прикрепляет специальное имя HEAD
к названию этой ветви, чтобы запомнить, какое из них вы выбрали:
K--L <-- master (HEAD)
/
...--F--G--H
\
I--J <-- dev
Теперь вы запускаете git merge
и даетеэто идентификатор для выбора коммита слияния .Если вы используете master
= L
, вы можете использовать dev
= J
в качестве коммита для слияния:
git merge dev # or git merge --no-ff dev
Git теперь будет ходить по графику как обычно, чтобынайдите лучший общий коммит - лучший коммит, который находится на обеих ветвях, для использования в качестве отправной точки для этого слияния.Здесь это коммит H
, где две ветви сначала расходятся.
Теперь Git будет сравнивать сохраненный снимок с коммитом H
- базой слияния - с текущим коммитом L
. Что бы не отличалось , вы, должно быть, изменились на master
. Git помещает эти изменения в один список:
git diff --find-renames <hash-of-H> <hash-of-L> # what we changed
Git повторяет это, но с их коммитом J
:
git diff --find-renames <hash-of-H> <hash-of-J> # what they changed
Теперь Git объединяет два набора изменений . Что бы мы ни изменили, мы хотим измениться. Что бы они ни изменили, мы тоже хотим использовать эти изменения. Если они изменились README.md
, а мы нет, мы примем их изменение. Если мы изменили файл, а они этого не сделали, мы примем наши изменения. Если мы оба изменили один и тот же файл , Git попытается объединить эти изменения. Если Git преуспевает, у нас есть объединенное изменение для этого файла.
В любом случае, Git теперь берет все объединенные изменения и применяет их к снимку в H
. Если не было конфликтов, Git автоматически делает новый коммит из результата. Если в были конфликты, Git по-прежнему применяет объединенные изменения к H
, но оставляет нас с грязным результатом, и мы должны исправить его и сделать окончательный коммит самостоятельно; но давайте предположим, что конфликтов не было.
Git теперь делает новый коммит с одной специальной функцией. Вместо просто , запомнившего наш предыдущий коммит L
, Git имеет коммит слияния запоминание двух предыдущих коммитов, L
и J
:
K--L <-- master (HEAD)
/ \
...--F--G--H M
\ /
I--J <-- dev
Затем, как всегда, Git обновляет нашу текущую ветку, чтобы запомнить хэш-идентификатор нового коммита:
K--L
/ \
...--F--G--H M <-- master (HEAD)
\ /
I--J <-- dev
Обратите внимание, что если мы выполним слияние, запустив git checkout dev; git merge master
, Git сделает те же две разницы и получит такой же коммит слияния M
(ну, пока мы сделали это в ту же секунду, чтобы отметки времени совпадали). Но тогда Git записал бы хэш-идентификатор M
в dev
, а не в master
.
В любом случае, если мы теперь спросим об истории master
, Git начнется с M
. Затем он вернется к и L
и J
и покажет оба из них. (Он должен выбрать один, чтобы показать первым, и у git log
есть много флагов, которые помогут вам выбрать , который должен показывать первым.) Затем он отойдет от того, который выбрал первым, так что теперь он должен показывать оба K
и J
, или оба L
и I
. Затем он вернется от того, кого выбрал для показа.
В большинстве случаев Git показывает всех детей раньше, чем кто-либо из родителей, то есть, в конце концов, он покажет все четыре из I
, J
, K
и L
и будет иметь только H
показывать. Итак, отсюда Git покажет H
, затем G
и так далее - теперь есть только одна цепочка, по которой нужно идти, один коммит за раз. Но имейте в виду, что когда вы возвращаетесь из слияния, вы сталкиваетесь с , который обязуется показать следующую проблему.
git merge
не всегда делает коммит слияния
Предположим, у вас есть эта история:
...--F--G--H <-- master
\
I--J <-- dev
То есть, нет дивергенции , dev
просто строго впереди master
. Вы делаете git checkout master
, чтобы выбрать коммит H
:
...--F--G--H <-- master (HEAD)
\
I--J <-- dev
, а затем git merge dev
, чтобы объединить работу, которую вы проделали после создания базы слияния, с работой , которую они выполнили после создания базы слияния.
База слияния - лучший общий коммит. То есть мы начинаем с H
и продолжаем возвращаться по мере необходимости, а также начинаем с dev
и продолжаем возвращаться по мере необходимости, пока не достигнем общей отправной точки. Итак, с J
мы возвращаемся к I
и H
, а с H
мы просто сидим тихо в H
, пока J
не вернется сюда.
База слияния, другими словами, - это текущий коммит . Если Git побежал:
git diff --find-renames <hash-of-H> <hash-of-H>
было бы без изменений . Акт объединения без изменений (с H
до H
через master
) с с некоторыми изменениями (с H
до J
через dev
), затем применение этих изменений к H
будет таким же, как в J
. Git говорит: ну, это было слишком просто , и вместо нового коммита он просто двигается имя master
вперед в противоположном от обычного обратном направлении. (На самом деле, Git действительно работал задом наперед - с J
до I
до H
- для того, чтобы понять это. Он просто помнит, что он начался с J
.) Итак, что вы получаете по умолчанию Это 1400 *
...--F--G--H
\
I--J <-- dev, master (HEAD)
Когда Git может перемещать метку наподобие master
вперед, как это, он называет эту операцию fast-forward . Когда вы делаете это с git merge
, Git называет это ускоренным слиянием , но на самом деле это не слияние вообще. На самом деле Git извлек commit J
и заставил master
указать J
.
Во многих случаях это нормально! История теперь: Для master
, начните с J
и идите назад. Для dev
начните с J
и идите назад. Если это все, что вам нужно и вам нужно, это нормально. Но если вы хотите реальный коммит слияния - так что вы можете, например, позже разнести master
и dev
друг от друга - вы можете сказать Git: Даже если вы можете сделать быструю перемотку вперед вместо слияния, все равно сделайте реальное слияние. Git пойдет дальше и сравнит H
с H
, затем сравнит H
с J
, объединит изменения и сделает новым коммит:
...--F--G--H------K <-- master (HEAD)
\ /
I--J <-- dev
Теперь вы получаете реальный коммит слияния K
, с двумя родителями, необходимыми для коммита слияния. Первый родитель H
, как обычно, а второй J
, как обычно для коммита слияния. История master
сейчас включает историю dev
, но остается отличной от истории dev
, потому что история dev
не включает коммит K
.
Обратите внимание, что если вы теперь переключитесь обратно на dev
и сделаете больше коммитов, результат будет выглядеть следующим образом:
...--F--G--H------K <-- master
\ /
I--J--L--M--N <-- dev (HEAD)
Теперь вы можете git checkout master
и git merge dev
снова. На этот раз вам не понадобится --no-ff
, потому что есть коммит на master
, который не на dev
, а именно K
, и, конечно, есть коммиты на dev that are not on
master , namely
LMN . The *merge base* this time is shared commit
J (not
H —
H is also shared, but
J` лучше ). Так что Git объединит изменения, выполнив:
git diff --find-renames <hash-of-J> <hash-of-K> # what did we change?
git diff --find-renames <hash-of-J> <hash-of-N> # what did they change?
Что сделал мы изменили с J
на K
? (Это упражнение для вас, читатель.)
Предполагая, что Git может объединить изменения самостоятельно, эта операция слияния будет выполнена успешно, что приведет к:
...--F--G--H------K--------O <-- master (HEAD)
\ / /
I--J--L--M--N <-- dev
, где новый коммит слияния O
объединяет изменения J
-vs- K
с изменениями J
-vs- N
. История master
начинается с O
и будет включать N
и M
и L
и K
и J
и I
и H
и так далее. История dev
начинается с N
и включает в себя M
и L
и J
(не K
!) И I
и H
и так далее. Git всегда работает в обратном направлении , от ребенка к родителю. Слияния позволяют / заставляют Git работать в обратном направлении вдоль обеих строк одновременно (но показываются вам по одному, в некотором порядке, в зависимости от аргументов, которые вы предоставляете git log
).