Здесь все выглядит вполне нормально.
Мы могли бы начать с этих наблюдений:
Git хранит, по большей части, фиксирует . То есть коммит - это базовая c единица хранения Git. (Можно разбить коммит на несколько частей, и иногда Git делает, но в основном мы работаем с целыми коммитами за раз.)
Когда создается каждый коммит, он получает уникальный га sh ID . Этот идентификатор ha sh всегда зарезервирован для этого коммита, не только в этом Git репозитории, но и в каждом Git репозитории повсюду. В некотором смысле, это было зарезервировано для этого даже за до того, как вы сделали коммит, за исключением того факта, что идентификатор га sh фиксации зависит от времени, вплоть до секунды, при вы делаете коммит.
(Эти идентификаторы ha sh очень большие и громоздкие. Они должны быть такими уникальными, как этот. Нет, человек не может сделать их правильно, но все в порядке: у нас есть компьютер , в конце концов.)
Каждый коммит содержит снимок всех ваших файлов. Это основные данные в коммите, но каждый коммит также содержит некоторые метаданные: некоторую информацию о самом коммите. Метаданные включают в себя имя и адрес электронной почты того, кто его сделал - в виде двух записей: автор и коммиттер - и некоторые отметки даты и времени, а также сообщение в журнале, где вы или кто бы ни записал причину того, что вы сделали коммит.
Ключевым элементом в этих метаданных является то, что каждый коммит хранит фактический идентификатор ha sh своего непосредственного предшествующего коммита или коммита , То есть каждый коммит имеет некоторое количество родительских коммитов - обычно только один - и дочерний коммит сохраняет необработанный идентификатор ha sh своего родителя или родителей.
Всякий раз, когда мы храним где-то идентификатор ha sh коммита, мы говорим, что все, что содержит этот га sh ID , указывает на коммит. Поэтому, если у вас есть где-нибудь записанное га sh ID H - например, на бумаге или на белой доске - мы можем сказать, что это указывает на commit H
. Поскольку каждый коммит хранит идентификатор ha sh своего родителя, каждый коммит указывает на своего родителя. Мы можем нарисовать это:
... <-F <-G <-H
, где H
- это некоторый коммит с некоторым ха sh ID H , и он указывает на своего родителя G
, который в свою очередь указывает к его родителю F
и т. д.
Если мы записываем последний га sh идентификатор некоторой длинной цепочки коммитов в имени ветви , например master
, достаточно для Git, чтобы найти всех этих коммитов. Имя указывает на последний коммит:
...--F--G--H <-- master
и оттуда Git может работать в обратном направлении: начиная с H
, он находит G
, затем F
и так далее. Это повторяется до тех пор, пока Git не найдет самый первый сделанный коммит, который больше не будет указывать назад, потому что он не может.
Чтобы сделать новый коммит, мы начинаем с извлечения коммита H
в рабочая зона. (Все коммиты доступны только для чтения на 100%, поэтому мы не можем изменить H
, но мы можем изменить материал, который копируем в рабочую область.) Затем мы редактируем файлы, как обычно, используем git add
, чтобы скопировать их в index - еще один ключевой элемент, но мы не будем go подробно описывать здесь - и используем git commit
, чтобы сделать новый коммит. Git упаковывает всех наших файлов в новый снимок, добавляет метаданные - например, наше имя и адрес электронной почты, а также текущую дату и время - и устанавливает родительский элемент для new совершить коммит H
. Этот новый коммит получает новый идентификатор, который слишком велик для нас, но мы просто назовем его I
. Новый коммит I
, следовательно, указывает на существующий коммит H
.
В качестве окончательного акта git commit
записывает идентификатор ha sh нового коммита в имя текущей ветви . Если текущее имя ветви master
, Git записывает I
ha sh ID в master
, так что master
теперь указывает на I
. Так как I
указывает на H
, вот что мы получаем:
...--F--G--H--I <-- master
Команда git log
начинается в конце и работает в обратном направлении
Обычная git log
- не git reflog
! - начинается с некоторой фиксации конечной точки с идентификатором ha sh, такой как поиск внутри имени master
. Всякий раз, когда git log
просматривает имя ветви , это, по определению, последний коммит в ветви. 1 Тогда git log
показывает нам этот коммит –Commit I
, в данном случае - и теперь git log
просто переходит к родителю коммита. Таким образом, next коммит вас см. это предыдущий коммит .
Журнал, другими словами, автоматически работает назад от конца к началу. Это тема в Git: все ее внутренние стрелки направлены назад. Git всегда должен начинаться с конца и работать в обратном направлении. Так что git log
показывает самый новый коммит по умолчанию, потому что по умолчанию он начинается с вашего текущего имени ветки, которое является самым новым коммитом в этой ветке.
Обратите внимание, что если вы используете git checkout
- или новый Git 2.23+ git switch
- чтобы переключиться на другое имя ветви, git log
будет начинаться с этого другого имени ветви, а не с master
. Или, если вы используете git checkout
или git switch
для переключения в режим detached HEAD , в котором специальное имя HEAD
указывает непосредственно на коммит, git log
по умолчанию будет начинаться с этого исторического коммита .
1 Определение имени ветви состоит в том, что оно содержит идентификатор ha sh последнего последнего коммита. Вот что могут сделать git branch -f
, git reset
и т. Д .: путем принудительного ввода другого га sh идентификатора в имя ветки это меняет то, что является последним коммитом, Вот почему git commit
записывает новый идентификатор кома ha sh в имя ветви: этот новый коммит по определению является последним коммитом, а по определению - именем ветви must содержит идентификатор ha sh последнего последнего коммита , поэтому git commit
обеспечивает совместную работу двух определений.
Когда git log
более сложный
Часто очень важно нарисовать график коммитов, рисуя каждый коммит и стрелку от него к его родителю (ам):
I--J <-- branch1
/
...--G--H
\
K--L <-- branch2
Здесь у нас есть две разные ветви имена, каждое из которых указывает на один коммит - J
для branch1
и L
для branch2
- при этом их один коммит направлен назад, как обычно, но в какой-то момент две ветви объединяются в в history: committs I
и K
оба указывают назад на фиксацию H
.
Команда git log
обычно начинается с текущей ветви * tip и работает в обратном направлении. Если мы выберем branch1
в качестве нашей текущей ветки, например, сначала мы увидим commit J
, затем I
, затем H
, затем G
и так далее. Если мы выберем branch2
в качестве текущей ветви, мы увидим L
, затем K
, затем H
и так далее. Обратите внимание, что в любом случае, мы не видим два из этих коммитов.
Однако мы можем сказать git log
, чтобы начать с оба branch1
и branch2
. Но git log
может показывать только один коммит за раз . Это покажет нам J
сначала или L
сначала?
Ответ на этот вопрос сложен. Иногда Git сначала показывает нам J
, а иногда сначала L
. Чтобы выбрать один из двух вариантов, git log
помещает оба в очередь с приоритетом . Приоритет по умолчанию - дата и время коммиттера , поэтому любой из J
или L
был сделан позже - тот, который мы увидим первым. Вы можете задать git log
различные параметры сортировки, которые меняют приоритет; в этом случае вы могли бы сначала увидеть другой коммит.
После удаления одного из двух коммитов из очереди и его показа, git log
теперь поместит родителя этого коммита в очередь с приоритетами. Затем он покажет вам коммит с наивысшим приоритетом, какой бы ни был следующий. Затем он поместит того родителя commit в очередь с приоритетами. В этой очереди все еще есть два коммита, так что еще раз, какой коммит вы видите после того, как эти первые два определяются очередью приоритетов.
В конце концов, это вероятно, или, в зависимости от флагов, которые вы задаете git log
, необходимо - чтобы git log
показывало J
, I
, L
и K
до , показывая коммит H
. В этот момент только приоритет H
будет находиться в очереди с приоритетами, поэтому git log
теперь покажет H
и поместит коммит G
в очередь. Теперь G
будет единственным коммитом в очереди, поэтому git log
покажет G
и поместит родителя G
в очередь и т. Д.
Другими словами, git log
иногда сортирует фиксирует. Он делает это только в том случае, если в очереди с приоритетами более одной записи , что происходит автоматически, если вы даете ему несколько начальных коммитов, таких как J
и L
здесь. Но посмотрите, что произойдет после того, как мы слим некоторые коммиты:
I--J
/ \
...--G--H M--N <-- master
\ /
K--L
Теперь, если мы начнем с master
и вернемся назад, git log
должны показать N
сначала - это единственный коммит в очереди - а затем M
. Но коммит M
имеет двух родителей, поэтому git log
теперь помещает в очередь как J
, так и L
. Теперь у него есть два коммита для показа, поэтому он выбирает один в зависимости от приоритета. Другими словами, теперь сортирует коммитов в соответствии с опциями, которые мы ему предоставляем. Мы вернулись в той же ситуации, в которой находились, когда у нас было два имени ветви, указывающих на коммиты J
и L
, и велели git log
показать оба коммита / ветви.
git log
show мы по одному коммиту за раз, даже если история более сложная
История, которую мы увидим с git log
, может вводить в заблуждение. Это может показать нам N
, затем M
, затем L
и K, then
I and
J`, в этом порядке. Если мы не знаем о факте ветвления и слияния, мы можем подумать, что коммиты были сделаны линейно, как это.
По этой причине вы можете git log
нарисовать сырой текстовый график. Этот график не всегда очень симпатичен, но он покажет, что коммиты L
и K
параллельны коммитам J
и I
: что они образуют структуру ветвления и слияния в история, как записано в коммитах.
История коммитов; коммиты - это история
Обратите внимание, что во всех этих процессах commit не изменяется. Мы можем просматривать коммиты по одному, в разных порядках с git log --sort=...
или git log --topo-order
и / или Git рисовать график с git log --graph
. Или мы можем нарисовать наш график на доске, или на бумаге, или с помощью различных программ для рисования графиков. Но что бы мы ни делали, обязуется никогда не меняться. Эти коммиты являются историей в вашем хранилище. Их связи существуют с того момента, как вы делаете коммиты: коммиты ребенка указывают на их родителей. Коммиты слияния - это коммиты с двумя (или более) родителями. Ветви «разветвляются», когда у родителя есть два или более потомка.
Вы всегда можете добавить больше коммитов на график. Git находит коммитов, используя имена веток, которые по определению являются последними коммитами в этой ветке. Вы всегда можете добавить новые имена ветвей, а также можете удалить имена ветвей. Если вы удалите имя only , которое позволяет вам найти некоторый коммит, например, если у вас есть:
...--o--o--o--o--...--o <-- master
\
H <-- branch
, и вы удалите имя branch
- что один конкретный коммит, в данном случае H
, становится трудным или, возможно, невозможным для поиска . Однако у Git есть и другие способы найти коммиты. Например, имена тегов могут указывать на коммиты. Git также хранит журнал предыдущих значений конкретной ветви и других имен. Этот журнал, называемый reflog , может позволить вам найти коммит H
.
Некоторые операции, такие как git rebase
, выполняются копированием существующей серии коммитов к новым и улучшенным коммитам, затем перемещая имя ветки. Поскольку имя содержит идентификатор ha sh последнего последнего коммита, если мы скопируем коммиты, а затем переместим имя , похоже, что мы каким-то образом изменили коммиты:
...--o--o--o--o--...--o <-- master
\
H--I--J <-- branch
становится:
...--o--o--o--o--...--o <-- master
\ \
H--I--J H'-I'-J' <-- branch
, где H'
является копией H
, I'
является копией I
, а J'
является копией J
. Хотя на самом деле мы не изменили . Оригиналы все еще там, в хранилище. Вы просто не можете найти их легко, потому что мы начинаем с name branch
до находим commit I'
сейчас и работаем в обратном направлении.
(Reflog для имени branch
содержит га sh ID коммита J
. Если мы решим, что sh решим "отменить" операцию rebase, мы можем принудительно ввести имя branch
чтобы запомнить J
снова, вместо J'
. Тогда в reflog для branch
будет храниться идентификатор * sh коммита ha J'
. Поскольку reflogs содержат несколько записей, мы получим несколько reflog записи, содержащие J
и / или J'
. Записи reflog со временем истекают , поэтому со временем мы забудем зафиксировать J
полностью, пока имя branch
указывает только на J'
. Имя ветви может указывать только на один коммит, и на самом деле должен указывать на один коммит всегда.)