В Git каждый коммит 1 - это снимок плюс некоторые метаданные.Каждый коммит идентифицируется по его хэш-идентификатору.Метаданные в коммите включают в себя хэш-идентификаторы родительского коммита.Это формирует граф, в частности, направленный ациклический граф, или DAG, вершины (или узлы) которого являются коммитами, а ребра - односторонние дочерние к родительским ссылкам от каждого узла к его родителю (ям).
Это означает, что история в репозитории равна коммитам.Там нет истории файлов.Есть только коммиты.
В то время как git log
покажет вам предполагаемую историю файлов, если вы спросите об этом, на самом деле она просто составляет ее.Это делается путем сравнения каждого коммита с его родителем (ями).Для обычных коммитов с одним родителем это работает хорошо.Для слияний этот вид в основном работает для некоторых или большинства случаев, за исключением случаев, когда это не так.Ваше конкретное слияние является одним из тех, где оно работает не очень хорошо.
Вы можете использовать флаг -m
, как вы делаете, чтобы «разделить» слияние.Вместо выполнения комбинированного diff (как с -c
или --cc
) или вообще без diff (как по умолчанию), флаг -m
сообщает git log
, что при обнаруженииmerge - commit d89ddb17122ab9eea72e7006461cb04a5a879770
в вашем примере выше - сначала он должен сделать diff, используя parent # 1 и merge.Затем он делает второй diff, используя parent # 2 и объединение.В вашем случае parent # 1 либо 95febfb
, либо a577995ec16ae05c2f81adfdba5ce28e7b8ba150
(оба не могут быть истинными - вы должны что-то здесь опускать или git log
опускаете что-то здесь), а parent # 2 - либо f85c1bb
, либо 97b8dc2f7cf7e81d75fee5565423b554d191e4f3
.
(Команда git show
похожа на git log
, за исключением того, что она по умолчанию --cc
вместо того, чтобы ничего не показывать, и останавливается после показа именованного коммита. На основе вашего git show
это выглядит такболее короткие хэш-идентификаторы являются действительными.)
Теперь тот факт, что один конкретный git show
(или git diff --name-status
) вывод показывает:
A files_from_C
A more_files_from_C
D jenkins/files_from_B
D jenkins/more_files_from_B
просто означает, что в родительском файле были файлы с именами D
, а в дочернем - файлы с именами A
.Вероятно, здесь у вас отключено обнаружение переименования - обнаружение переименования отключено по умолчанию в версиях Git, предшествующих 2.9.0, и включено по умолчанию в 2.9.0 и более поздних версиях.Если вы включите его, Git может показывать их как «переименованные», а не как «удаленные и добавленные», если содержимое достаточно схоже.
То же самое относится ко второму git diff --name-status
выводу из git show
.Этот сравнивает снимок в родительском # 2 с снимком в merge-child.Важно понимать, что эти сравнения действительны сами по себе, но дают вам только небольшую картину.Случай true состоит в том, что есть два родителя с двумя снимками и один дочерний объект - коммит слияния - с одним снимком, и три снимка отличаются по-разному.
..с --all --name-status --full-history --follow --
Я вижу всю историю:
--follow
включает поиск переименования, но это ужасный взлом.Он может смотреть только на один файл.Вы говорите git log
начальное имя.Он просматривает первый коммит, который просматривает git log
, 2 выбирает родителя (ей) этого коммита.Если есть только один родитель, задание проще: как и раньше, Git сравнивает родителя с дочерним.Ни один файл, кроме названного, не интересен.Теперь происходит одно из трех:
Если diff (помните: с включенным переименованием) показывает, что файл изменен на месте, git log
показывает фиксацию и перемещаетсяon.
Если diff показывает, что файл не изменился, git log
не показывает фиксацию и продолжает.
Еслиdiff показывает, что файл переименован - изменен или нет - git log
показывает фиксацию.Затем он меняет имя, которое ищет , чтобы использовать имя "source" из родительского коммита.Затем он движется, как и прежде.
Этот же паттErn также используется для коммитов слияния!Однако коммиты слияния имеют очень ... интересное git log
поведение, что приводит нас к следующему пункту.(Пришло время остановиться для сносок.)
1 Точнее, коммит ссылается на снимок.Если два разных коммита имеют 100% идентичные моментальные снимки, они просто повторно используют один и тот же.
2 Порядок, в котором проходят коммиты, когда git log
дается --all
,это немного хитро.
Как git log
работает, когда для показа более одного коммита
Мы уже упоминали, что история - это коммитов.Когда цепочка коммитов является линейной:
... <-F <-G <-H ...
для Git довольно легко показать коммит H
(путем дифференцирования G
и H
), а затем просто перейти к отображению G
(поdiffing F
и G
), затем перейдем к отображению F
и так далее.За один раз можно показать только один коммит: вы начинаете с last , идентифицируемого по имени какой-то ветви, и работаете в обратном направлении, по одному коммиту за раз.
Это разбивается при слияниях,Это также проблема, когда вы указываете git log
начинать с двух или более коммитов, как обычно делает git log --all
.
Алгоритм git log
, который здесь фактически используется, включает очередь с приоритетами.Вы даете git log
некоторый набор начальных точек:
git log master develop origin/feature
например, разрешает каждое из трех имен, master
, develop
и origin/feature
, в хэш-идентификаторы (предположительно, фиксирует - и еслиэто имена веток и удаленного отслеживания, они являются коммитами).Предполагая, что есть три различных идентификатора хэша фиксации, 3 все три идентификатора фиксации идут в очередь приоритетов.
Теперь, когда очередь приоритета не пуста, Git выбирает first коммит из очереди.Какой из них первый?Это зависит от параметров сортировки, которые вы вводите в командной строке: --author-date-order
, --topo-order
и так далее.Отсутствие опций означает, что приоритетом является дата коммиттера: более поздние даты имеют более высокий приоритет.Чтобы узнать, что делает каждая опция сортировки, см. документацию git log
, но обратите внимание, что эта сортировка only происходит, когда в очереди более одного коммита ..
Команда git log
теперь показывает или не показывает выбранный коммит, основываясь на остальных критериях из командной строки.Обычно он помещает всех родителей коммитов в очередь приоритетов, если только эти родители не были посещены.Однако несколько параметров, в том числе перечисление имени файла, например TODO.md
, изменяют это поведение, включая упрощение истории .Когда упрощение истории включено, некоторые родители опускаются.Добавление --full-history
заставляет всех родителей вставляться в приоритетную очередь.
С --follow
это - --full-history
- не всегда полезно, как мы скоро увидим,Но давайте сначала разберемся с алгоритмом обхода графа.
Теперь мы можем посмотреть, как на самом деле работает git log
, более подробно:
Поместить команду-строковые аргументы, переведенные в необработанные хэш-идентификаторы, в очередь с приоритетами.Если аргумент командной строки не используется для выбора одного или нескольких начальных коммитов, используйте HEAD
для выбора начального коммита.
Пока очередь не пуста:
- Уберите первый элемент из очереди.(Этот коммит теперь посещен.)
- Решите, показывать ли этот коммит.Если это так, покажите его (также переписывание родителями, если оно включено - это совсем другая тема; это имеет значение, если вы используете
--parents
или --graph
). - Перечислите родителей этого коммита, применяяупрощение истории, если включено.Поместить выбранных родителей в очередь с приоритетами, если они уже присутствуют или уже посещеныЕсли у коммита нет родителей или они пропущены, очередь становится короче.Если несколько родителей попадают в очередь, очередь становится длиннее.«Приоритетная» часть приоритетной очереди определяет, какой коммит будет впереди, когда мы вернемся к шагу 1.
Thв основном весь алгоритм.Много странностей следует из шагов 2 и 3. Упрощение истории при слияниях, если не отключено с помощью --full-history
, состоит в следовании за некоторыми (случайно выбранными) родителями TREESAME, если они есть!(Понимание этого требует определения TREESAME. К счастью, вы используете --full-history
, поэтому нам не нужно этого делать.)
3 Если вы называете объекты тегов, git log
переводит имя тега в хеш-идентификатор коммита, почти как если бы вы использовали tag^{commit}
;подробности смотрите в документации git rev-parse
.Команда git log
в основном заинтересована в фиксации , поэтому она игнорирует попытки регистрировать хэши больших двоичных объектов и т. П.
Подумайте о том, как обнаружение переименования взаимодействует с очередью приоритетов.
Предположим, мы смотрим на следующую очень простую историю с коммитом M
в качестве HEAD
в нашей отдельной ветке master
:
M (merge commit)
|\
| B (parent #2)
A (parent #1)
Предположим, что есть точноодин файл в M
с именем final
.Его содержимое точно соответствует содержимому единственного файла - который называется A
- в коммите A
, и единственного файла - который называется B
- в коммите B
.
(Вотфактический git log --oneline ...
вывод:
* f11ea2a (HEAD -> master) merge A and B to final
|\
| * 811819b (B) B
* 50d92c7 A
, который будет полезен ниже. Мои хэш-идентификаторы, конечно, мои.)
Мы запускаем:
git log --name-status --oneline --follow --full-history -m -- final
(-m
требуется в этом случае, как я выяснил через тестирование).Git извлекает M
и первый из двух родителей и показывает их.Он обнаруживает, что с A
до M
происходит переименование с A
до final
.Так что покажет commit M
.Затем он изменяет свой следующий файл: он больше не ищет final
, а скорее A
.Теперь это diffs фиксирует B
и M
.Нет файла с именем A
, поэтому здесь он ничего не показывает.
Следующий коммит в очереди - B
(потому что у него более поздняя дата).Чтобы сравнить не родительский (корневой) коммит, Git будет сравнивать его с пустым деревом.Git diffs none-vs-commit- B
и находит, что мы добавили файл B
.Это не тот файл, который мы ищем, поэтому Git ничего не говорит.
Git теперь переходит к рассмотрению commit A
.Здесь он обнаруживает, что commit A
добавляет файл A
, который является тем файлом, который он ищет.
Окончательный вывод такой:
$ git log --name-status --oneline --follow --full-history -m -- final
f11ea2a (from 50d92c7) (HEAD -> master) merge A and B to final
R100 A final
50d92c7 A
A A
Сообщение f11ea2a (from 50d92c7)
говорит нам, что в следующей строке отображается коммит: virtual-split- f11ea2a with parent 50d92c7
(объединить M
с родителем A
).Строка R
сообщает нам, что файл A
был переименован в final
при слиянии.
Virtual-split- f11ea2a
для B не печатается, поскольку ни один из этих коммитов не имеет файла A
в нем, и мы уже ищем A
вместо final
.
Далее, 50d92c7
- это сам коммит A
.Следующая строка A
сообщает нам, что файл A
был добавлен в commit 50d92c7
(commit A
).
Commit B
опущен, хотя он тоже создал B
с нуля,и B
был затем переименован в final
.Или это было A
, которое было переименовано в final
?Ну, оба верны, а может и нет: может быть, я создал файл final
с нуля, отбросив два файла A
и B
.
Реальная точка зрения всехэто упражнение состоит в том, что не одна "реальная" история файлов.История only в этом Git-репозитории - это набор коммитов в репозитории с их родительскими / дочерними отношениями.Все остальное - выдумка!В некоторой степени мы можем получить полезную беллетристику от git log
, но у этой степени есть ограничения.
Заключение
Есть ли способ, которым яможете отредактировать эту историю так, чтобы git show, git blame и другие раскрыли реальную историю всех файлов этого репо, независимо от того, были ли они созданы в (B) или (C)?
Нетправда нет.Проблема в том, что нет файла истории.Вы можете расположить историю commit так, как вам нравится, зная теперь, что (например) git log
делает с точки зрения поиска операций переименования, потому что вы использовали -M
или установили diff.renames
на true
, либо используете Git версии 2.9 или более поздней, либо используете --follow
для фальсификации истории файлов с помощью довольно скудных, но иногда недостаточно адекватных методов Git.
Команда git show
то же самое, что и git log
, за исключением того, что при генерации различий по умолчанию используется --cc
для создания комбинированных различий.Комбинированный diff пропускает любой файл, который является таким же в any parent, как и в дочернем коммите.Предположим, что коммит слияния M имеет родителей P1 и P2, и все, кроме двух, файлы в M точно совпадают с файлами в P1.Предположим далее, что два файла в M, которые не не соответствуют P1, do точно соответствуют файлам в P2.Таким образом, объединенный diff покажет нет файлов, измененных.
Команда git blame
является более сложной.Он может искать строки, которые были скопированы или перемещены из любого файла в родительском файле: см. Параметр -C
.Я никогда не вникал в то, что он делает при коммитах слияния (он ищет строки, скопированные или перемещенные из любого родительского файла?), Но я предполагаю, что, как и git log
, он в конце концов вынужден делать что-то вродеупрощения истории, потому что нецелесообразно следовать каждому пути назад.