Почему несвязанная история появляется при запуске "git log -m --follow" в файле после объединения нескольких репозиториев в один монолитный репозиторий?

У меня есть несколько разных репозиториев git, которые я хотел бы объединить в одно монолитное репо, сохранив при этом их историю. Я нашел способ сделать это, но меня немного смущает то, что журнал git показывает мне для истории отдельных файлов.

Вот вывод, который у меня был:

git log --oneline

выход из комбинированного репо

------- (HEAD -> master) Merge repoC into mono repo
------- Merge repoB into mono repo
------- Merge repoA into mono repo
------- initial commit
------- Add README to repoC
------- Add README to repoB
------- Add README to repoA

git log --oneline repoA/README.md

выход из комбинированного репо

------- Merge repoA into mono repo

git log --oneline -m --follow repoA/README.md

выход из комбинированного репо

 ------- (from -------) (HEAD -> master) Merge repoC into mono repo
 ------- (from -------) Merge repoB into mono repo
 ------- (from -------) Merge repoA into mono repo
 ------- (from -------) Merge repoA into mono repo
 ------- initial commit
 ------- Add README to repoC
 ------- Add README to repoB
 ------- Add README to repoA

Начиная со всех отдельных репо в виде пакетов, я делаю следующее для создания своего монолитного репо:

Для Repos A/B/C

git init
echo "repo" > README.md
git add .
git commit -m 'Add README to repo'
git bundle create ../repo{A,B,C}.bundle --all

Создать объединенный репозиторий git init echo "initial" > README.md git add . git commit -m 'начальный коммит'

Для каждого репо

mkdir repo{A,B,C}
git fetch ../repo{A,B,C}.bundle master
git merge --allow-unrelated-histories -s ours --no-commit FETCH_HEAD
git read-tree --prefix=repoA -u FETCH_HEAD
git commit -m "Merge repo{A,B,C} into mono repo"

Почему я получаю несвязанную историю git commit для определенных файлов при запуске с -m --follow? Я ожидаю увидеть только коммиты, которые относятся к файлу.

ОБНОВЛЕНО (пытается журналы для файлов с разными именами и содержанием):

  git log -m --follow --oneline repoB/sue.md`
  -------(from  -------) (HEAD -> master) Merge repo C into mono repo`
  -------(from  -------) Merge repo B into mono repo`
  -------(from -------) Merge repo B into mono repo`

1 ответ

Решение

Чтобы расширить комментарий Марка Адельсбергера, вы должны понимать, что в Git идентичность файла определяется довольно любопытно.

Идентификация файлов в системах контроля версий (VCSes) является основной концепцией. Как VCS должен знать этот файл include/lib.h является или не является "тем же" файлом как файл lib/lib.h?

Некоторые VCS используют такой подход, что когда файл впервые вводится в VCS, вы сообщаете VCS нечто особенное, например, hg add path, С тех пор каждый раз, когда файл переименовывается, вы также сообщаете VCS нечто особенное, например: hg mv [--after] old-name new-name, VCS может использовать это для отслеживания идентичности файла через некоторые серии коммитов: lib/lib.h в ревизии X есть или нет "тот же" файл, что и include/lib.h в версии R, в зависимости от того, сообщили ли вы VCS, что между R и X была операция переименования.

Git, с другой стороны, делает что-то радикально другое: он пытается идентифицировать пары файлов по заданным двум ревизиям по содержимому. То есть, учитывая ревизии R и X как пару, Git просматривает каждый файл в R и каждый файл в X. Если оба R и X имеют файлы с именем include/lib.h ну, это почти наверняка тот же файл, поэтому lib/lib.h (либо в R, либо в X) определенно не совпадает с файлом include/lib.h (в другой ревизии), но это может быть тот же файл, что и lib/lib.h (в другой ревизии). Однако, если ровно одна из двух ревизий имеет include/lib.h а другой имеет lib/lib.h этот файл мог быть переименован между этими двумя ревизиями.

В общем, по причинам, связанным с временем ЦП, с учетом любой пары ревизий, если какой-либо путь P существует в обеих ревизиях, Git предполагает, что файл не был переименован. С git diff -но нет git merge и не git log —Вы можете добавить флаг, чтобы не предполагать, что файлы не были переименованы только потому, что они существуют в обеих ревизиях. Это -B (разрыв пар).

Затем, пока включено обнаружение переименования (-M вариант в git diff, --follow в git log и другие различные условия): для всех файлов, которые не сопряжены, либо из-за -B или поскольку указанный путь существует только в одной из двух ревизий, Git ищет файлы с похожим содержимым, вычисляя для них "индекс сходства" и / или схожие имена. (Для соответствующих имен компонентов есть бонус +1, если оба файла заканчиваются на /lib.h например. В качестве ключевой оптимизации, поскольку ее легко выполнить внутренне и она работает хорошо, Git быстро объединит файлы со 100%-ным идентичным содержимым и только после этого скомпилирует индекс сходства.) Затем он сопоставит любые файлы с индексом сходства, который соответствует или превышает процентное требование, которое вы ему дали: -M50 по умолчанию, но вы можете потребовать "сходство 75%" с -M75, например.

Эти парные файлы являются "одинаковыми" файлами в двух ревизиях. Это верно для git diff, который затем производит diff между спаренными файлами, и для типичного git merge, который работает два git diff s, один от базы слияния до одного из двух коммитов наконечника, а затем второй от той же базы слияния к другому из двух коммитов наконечника. Самое главное, для --follow это правда для git log а также: парные имена файлов направляют --follow операция по изменению имени файла, который он ищет, если файл в более ранней ревизии имеет другое имя.

(Ваш merge -s ours это не типичное слияние: ours стратегия игнорирует все, кроме коммита HEAD, при вычислении исходного кода для нового коммита, поэтому она вообще не беспокоится о каких-либо различиях.)

Как это влияет git log --follow

За git log --follow path чтобы следовать файлу, путь к которому является путём при переименовании, Git должен выполнить эти разностные сравнения, чтобы он мог обнаружить, что файл действительно был переименован. Используемые пары являются родителями самих C и C, где C - это фиксация, найденная из-за обхода графа, т. Е. Фиксация, которая git log собирается показать или не показать, в зависимости от того, коснулся ли он файла, путь к которому - путь.

Коммит слияния представляет проблему здесь. Само определение коммит слияния состоит в том, что у него есть по крайней мере два родителя. Это где -m (разделить слияние) опция приходит: разделение слияния означает притворяться, на время этого git log Операция, в которой коммит слияния, с N родителями, на самом деле N отдельных коммитов. Первый из этих N коммитов имеет одного родителя: первого родителя слияния. Второй коммит имеет одного родителя: второй родитель слияния. N-й коммит имеет N-го родителя в качестве одиночного родителя и так далее. Поэтому, если у слияния три родителя, оно разделяется на три виртуальных коммита, каждый с одним родителем.

Это решает проблему сопряжения: у каждого из этих виртуальных коммитов теперь есть только один родительский элемент, и Git может запускать diff обычным способом, чтобы обнаружить любые переименования. Если Git находит переименование, это просто означает, что когда он показывает родительские коммиты - после завершения каждого из этих N виртуальных коммитов - он должен прекратить искать путь к имени и вместо этого начать искать файл с именем старое имя в дифф.

Так как вы ищете repoA/README.md, Git начинает искать этот конкретный путь. Git находит это имя, repoA/README.md в разделенном виртуальном коммите каждый раз, когда это выглядит. Родитель каждого разделенного виртуального коммита имеет этот файл под именем README.md так что после того, как Git напечатает разделенный виртуальный коммит один раз для каждого родителя - каждая пара родитель / потомок имеет repoA/README.md в нем, так как каждый такой дочерний коммит (само слияние) имеет repoA/README.md в нем - он переходит к родителям, по одному, теперь ищет файл с именем README.md, Он обнаруживает, что каждый родительский коммит имеет такой файл, поэтому он печатает каждый родительский коммит.

Другие вопросы по тегам