Здесь на самом деле нет никаких проблем: вы просто неправильно истолковываете то, что говорит Git. (Я полагаю, тот факт, что Git может быть неверно истолкован, может считаться проблемой, но на практике, будь то Git или какая-либо другая система контроля версий, этот материал является сложным и требует изучения и опыт.)
Есть некоторые ключевые вещи, которые необходимо знать о Git, файлах и фиксациях:
Что хранит Git на уровне, с которым вы взаимодействуете это, совершает . Такие имена веток, как master
, полезны, но на самом деле они просто помогают Git - и вы - находите коммиты Мы посмотрим, как это работает в данный момент.
Коммиты хранят файлы, но вы обычно работаете с целым коммитом за раз. Вы говорите Git: получить мне коммит X для некоторого X , который идентифицирует коммит, и вы получите все файлы для этого коммита. У вас либо есть фиксация - и, следовательно, все файлы - или у вас нет фиксации вообще, и, следовательно, у вас нет ни одного файла.
Каждый коммит имеет уникальный идентификатор , Этот идентификатор является га sh ID и представляет собой большую некрасивую строку случайных букв и цифр, таких как 9fadedd637b312089337d73c3ed8447e9f0aa775
. То, что ha sh ID, как только он существует, означает , что коммит, и никогда никакой другой коммит.
Содержимое любого коммита полностью, полностью, 100% только для чтения. Ни файлы, хранящиеся в коммите, ни метаданные коммита не могут быть изменены. (Причина этого заключается в том, что идентификатор ha sh является криптографической контрольной суммой c содержимого коммита. Если вы извлекаете коммит, изменяете любой из его битов и помещаете его обратно, вы получаете новый, другой коммит, с новым, другим идентификатором ha sh. Старый коммит все еще там: вы только что добавили один больше коммит.)
Снимок всех файлов каждого коммита - это просто снимок. То есть фиксирует не магазин изменения вообще.
Но когда вы посмотрите на коммит, Git часто показывает вам изменений . Это трюк! Но это также хорошая вещь, потому что это, как правило, более интересно. raw ha sh ID одного предыдущего или parent commit. Таким образом, с учетом любого одного коммита X , Git может выполнить резервное копирование на один шаг, чтобы найти коммит, который приходит до X . Этот коммит также имеет моментальный снимок.
Git может - и делает - просто извлекает два снимка, родительский и дочерний, и сравнивает их. Для каждого файла, который один и тот же , Git вообще ничего не говорит. Для каждого файла, который отличается , Git показывает рецепт: Начните с родительской копии файла. Добавьте эту строку здесь. Удалите это там. Повторите при необходимости, и когда вы закончите добавлять и удалять, у вас будет версия файла, которая находится в дочернем коммите.
Когда у вас есть простая строка коммитов, все в Строка, вы можете нарисовать их, или думать о них, как это:
... <-F <-G <-H ...
, где H
обозначает некоторый идентификатор ha sh, который находит коммит. Сам коммит H
содержит ha sh ID своего родителя, который мы просто назовем G
. Это позволяет Git найти G
. G
содержит идентификатор ha sh его родительский элемент, F
, что позволяет Git найти F
и т. Д.
A имя ветви подобно master
просто содержит идентификатор ha sh последнего коммита в цепочке . Последний коммит указывает назад на своего родителя, который снова указывает назад и так далее. Таким образом, мы можем нарисовать это как:
...--F--G--H <-- master
Нам не нужно рисовать соединительные стрелки из одного коммита в следующий как стрелки , так как они не могут измениться. Никакая часть любого коммита не может измениться. Так что они всегда будут указывать назад. Стрелка выходит из имени ветви , однако меняет . Мы можем начать с:
...--G--H <-- master
, а затем добавить новое имя ветви, чтобы мы могли делать новые коммиты, не касаясь нашего master
:
...--G--H <-- master, dev
, но в конечном итоге мы будем добавьте новый коммит в нашу ветку. Давайте добавим специальное имя HEAD
к dev
, чтобы помнить, что это имя, которое мы используем - имя, которое мы использовали при запуске git checkout dev
- и нарисуем его так:
...--G--H <-- master, dev (HEAD)
Теперь мы сделаем новый коммит. Он получит какой-то большой уродливый случайный идентификатор ha sh, но мы просто назовем его I
и нарисуем так:
I
/
...--G--H <-- master, dev (HEAD)
I
указывает на H
, потому что H
является текущим коммитом , когда мы делаем I
.
Теперь приходит хитрый трюк: Git записывает I
идентификатор ha sh в название филиала. Измененное имя ветви - это то, к которому HEAD
присоединено: dev
. Так что теперь dev
указывает на I
вместо H
:
I <-- dev (HEAD)
/
...--G--H <-- master
Нет существующий коммит изменился. (В конце концов, никто не может.) Но наш новый коммит I
теперь существует и указывает на существующий коммит H
, а теперь наше имя dev
указывает на коммит I
, что теперь текущий коммит.
Когда мы делаем новый коммит J
, Git делает то же самое, давая нам:
I--J <-- dev (HEAD)
/
...--G--H <-- master
На этом этапе, однако, мы можем запустить git checkout master
и git pull
(или git fetch && git merge
) и получить новые коммиты, сделанные кем-то другим. Просто для симметрии я нарисую два коммита, которые сделал кто-то другой. Это продвигает нашу master
вверх по сравнению с двумя новыми коммитами:
I--J <-- dev
/
...--G--H
\
K--L <-- master (HEAD)
Текущая ветвь теперь master
, а текущая фиксация сейчас L
. Вы можете удивиться, почему я нарисовал их в отдельной строке: в основном следует подчеркнуть, что коммиты до H
находятся на в обеих ветвях . Этот странный факт - фиксация может происходить более чем на одной ветви за раз - несколько присущ Git.
Теперь мы можем запустить git checkout dev
, чтобы подготовиться к слиянию master
в dev
. Этот первый шаг просто перемещает HEAD
к dev
:
I--J <-- dev (HEAD)
/
...--G--H
\
K--L <-- master
Теперь мы можем объединить две ветви. Мы действительно объединяем коммитов , потому что Git это все о коммитах, но давайте посмотрим, как это работает.
В наши коммиты I-J
, мы сделали некоторые изменения в некоторых файлах. В их коммитах K-L
они - кем бы они ни были - внесли некоторые изменения в некоторые файлы. Мы собираемся сделать новый коммит слияния , и этот коммит слияния будет содержать снимок, как и каждый коммит. Что должно go в этом снимке?
Ответ таков: нам бы хотелось, чтобы этот снимок сочетал нашу работу с их работой. То есть мы хотели бы начать с каждого файла из общего, общего коммита . Лучшая общая общая отправная точка ясна из диаграммы: это коммит H
. Этот коммит находится на обеих ветках. Так же и G
, но H
лучше лучше , потому что это самое близкое к J
и L
.
Итак, Git начнется с того, что находится в H
. Это будет сравнение H
против J
, чтобы увидеть, что мы изменили. У каждого файла, который мы изменили, есть рецепт: добавить несколько строк, удалить несколько строк. Затем Git начнется снова с того, что находится в H
, и сравните H
с L
, чтобы увидеть, что они изменились. У каждого файла, который они изменили, есть рецепт: добавить несколько строк, удалить несколько строк.
Git теперь объединяет этих рецептов изменений. Везде, где мы меняли файл, а они - нет, результатом является наш файл. Где бы они ни меняли файл, а мы нет, результатом является их файл. Если мы оба изменили один конкретный файл, Git объединит наших изменений. Это сложная часть слияния: объединение изменений.
Если линии , которые мы изменили, отличаются от линий, которые они изменили (и рецепты также не имеют смежных или примыкающих линий), Git сможет объединить эти изменения самостоятельно. Или, если мы и они сделаем точно такое же изменение в какую-то строку (и) - например, если мы оба исправим одну и ту же орфографическую ошибку где-то - Git просто возьмет одну копию изменения. В противном случае - если мы изменили строку не так, как они, - Git приведет к ошибке слияния для этого файла и оставит нам беспорядок для очистки.
Объединив все файлы в меру своих возможностей, Git теперь либо останавливается с конфликтами слияния, либо не имеет никаких конфликтов слияния и продолжает делать коммит слияния . Давайте предположим, что не было никаких конфликтов, чтобы упростить задачу.
Единственное, что является особенным в этом коммите слияния, это то, что вместо одного родителя у него есть два. Мы можем нарисовать его так:
I--J
/ \
...--G--H M <-- dev (HEAD)
\ /
K--L <-- master
Родитель first нового коммита M
- это commit J
, который продвигает ветку dev
на один шаг как обычно. Родитель second нового коммита M
является коммитом L
, который по-прежнему является коммитом коммита ветви master
. С именем master
ничего не происходит, и * существующий коммит не изменился (так как никто не может), но новый коммит слияния M
делает так, что коммиты K
и L
теперь находятся на ветке dev
тоже, вместе с коммитами до J
.
Почему слияния работают
Если мы сейчас спросим Git: , где появилась какая-то конкретная строка (скажем, строка 42) некоторого конкретного файла F из , Git может посмотреть снимок в M
, а затем и снимки в J
и L
. Если строка 42 F соответствует M
и J
, но отличается в M
и L
, то строка 42 "взята из" J
: объединить сохранить строку из J
. Git теперь откатится еще на один коммит до I
, чтобы увидеть, совпадает ли строка 42 в F с I
и J
. Если они там другие, Git скажет, что строка 42 пришла от человека, который сделал коммит I
, в день, когда он сделал коммит I
.
Если строка 42 из F
однако совпадает в M
и L
и отличается в J
, что означает, что слияние сохранило строку 42 из L
. Поэтому Git следует вернуться к L
, а затем K
и т. Д. При необходимости.
Если строка 42 совпадает с M
, L
, и J
, он, вероятно, прошел без изменений с H
, и Git продолжит маршировать назад, по одному коммиту за раз, чтобы увидеть, изменилось ли оно при переходе G
-to- H
, или оно пришло от еще более раннего изменения.
Команда, которая просматривает определенные строки одного конкретного файла, - git blame
(или git annotate
). Обратите внимание, что, как и многие другие команды Git, он должен выполнять коммиты, по одному шагу за раз, проходя назад во времени. Эти коммиты, , являются историей в хранилище. История совершает; коммиты - это история.
Вы не должны извлекать чужие изменения (если они не ошибочны)
Результат любого слияния автоматически правильный файл. Будущее слияние будет предполагать, что все, что вы вставили , правильно. Если вы извлекаете их изменения , это означает, что вы говорите, что их код был неверным и его следует забыть.
Если это действительно так, то вы можете удалить этот код, но вы вероятно, следует сделать это в другом отдельном коммите, а не делать это непосредственно в слиянии.
Дополнительные примечания о слияниях ускоренной перемотки
Хотя мы не рассмотрели это здесь должным образом, Ответ Чака Лу упоминает ускоренные слияния . Предположим, что мы рисуем серию коммитов, как это:
...--C--D--E <-- branch1 (HEAD)
\
F--G--H <-- branch2
, что указывает на то, что у нас есть ветвь branch1
, и, следовательно, коммит E
, проверенный прямо сейчас. Если мы запустим git merge branch2
, Git обнаружит, что наилучшим общим коммитом в обеих ветвях является текущий коммит E
. В этом случае Git не нужно выполнять слияние. С учетом этой опции Git вместо этого выполнит операцию fast-forward , по сути, выполнив git checkout
commit H
, но перетащив в ответ имя ветви branch1
вперед. :
...--C--D--E
\
F--G--H <-- branch1 (HEAD), branch2
(Теперь нет причин сохранять диагональную линию на чертеже; не стесняйтесь снимать ее, когда вы рисуете это самостоятельно.)
Когда Git делает это Операция также сравнивает снимок старого коммита E
с новым текущим коммитом H
. Для каждого файла, который изменился, он сообщает вам об этом изменении.
Вы можете увидеть то же самое сравнение, выполнив:
git diff --stat <hash-of-E> HEAD
Так как HEAD
теперь называет имена commit H
, этот git diff
сравнивает снимок в E
со снимком в H
- в точности то же самое, что git pull
сделал - и поэтому печатает ту же информацию снова.
Когда вы делаете реальное слияние (как мы сделали с M
), информация, которую вы видите прямо в то время, основана на сравнении вашего предыдущего коммита (J
) и этого в M
. Поскольку M
объединяет изменения с обеих "сторон" ветви, но J
имеет ваши изменения, что вы видите их изменения . Однако вы можете запустить git diff --stat master dev
для сравнения коммита L
с коммитом M
: на этот раз вы увидите, что произошло слияние с "вашей стороны" ветви.
Трудно увидеть что в реальном слиянии M
в общем из-за его двух родителей. Вам нужны две отдельные команды git diff
, чтобы увидеть это правильно, правда. Команда git show
может сделать это автоматически, если вы укажете флаг -m
, но мы не будем здесь это рассматривать.