Как исправить историю мерзавцев и правильно объединить несвязанные истории - PullRequest
1 голос
/ 20 сентября 2019

У меня есть репозиторий Git, который содержит две несвязанные истории, как показано на этом графике:

* commit a577995ec16ae05c2f81adfdba5ce28e7b8ba150
(A)
|
*   commit d89ddb17122ab9eea72e7006461cb04a5a879770
|\  Merge: 95febfb f85c1bb
| |
| * commit 97b8dc2f7cf7e81d75fee5565423b554d191e4f3
| |
| * commit c86ff8d4695f63c30ba096a5a71ab8f50536a31c
| (B)
|
* commit a577995ec16ae05c2f81adfdba5ce28e7b8ba150
|
* commit 53c3a6a895c2732c8262e6467b586284fbe7c79d
(C)

Обратите внимание, что я пометил три точки в истории Git, A, B & C, на графике, простотак что я могу сослаться на них здесь, в тексте.

Две истории, начинающиеся с C и B, не связаны и начали свою жизнь как два совершенно разных репозитория Git.Затем истории были объединены таким способом, который неизвестен (но известно, что он включал git filter-branch).

Моя проблема

Если я проверяю в точке (A) в истории,файлы, созданные в точке C, некорректно отображаются в git log и других командах, как если бы они были добавлены коммитом слияния fe63b2f.Таким образом, команды git, такие как git show, git blame и другие, не могут рассказать мне об истинной истории этих файлов.Но они показывают мне истинную историю файлов, которые были добавлены в точке (B).

Дальнейшие наблюдения

Git show --name-status

Как предлагается в комментариях, если я запускаю это накоммит слияния, я вижу:

$ git show --oneline -m --name-status d89ddb17122ab9eea72e7006461cb04a5a879770
d89ddb1 (from 95febfb) Merge branch 'master' of ../jenkins into alex/jenkins
A       files_from_C
A       more_files_from_C
D       jenkins/files_from_B
D       jenkins/more_files_from_B
d89ddb1 (from f85c1bb) Merge branch 'master' of ../jenkins into alex/jenkins
A       files_from_C
A       more_files_from_C

Я включил реальный вывод как Gist здесь .

Журнал Git - name-status --full-history -follow v Git log

Дополнительная информация о журнале Git файлов, созданных в C. Простой журнал git:

$ git log TODO.md
commit 989011e2dee59f9502c369d8fac58b2b947ab4e6
Author: Alex Harvey <redacted>
Date:   Sat Sep 14 00:51:53 2019 +1000

    Some other commit

commit d89ddb17122ab9eea72e7006461cb04a5a879770
Merge: 95febfb f85c1bb
Author: Alex Harvey <redacted>
Date:   Wed Sep 11 12:18:54 2019 +1000

    Merge branch 'master' of ../jenkins into alex/jenkins

Но с --all --name-status --full-history --follow -- я вижу всю историю:

$ git log --all --oneline --name-status --full-history --follow -- TODO.md
989011e Add autogenerated helpers documentation
M       TODO.md
8766c98 Remove shunit2/_include.sh
M       TODO.md
15bf859 Remove shunit2/_include.sh
M       TODO.md
65ee7e5 Remove shunit2/_include.sh
M       TODO.md
2c601af Remove unused invalid_ami_stacks variable
M       TODO.md
d347137 Resolve unprintable characters in README
M       TODO.md
dc7068d TODO.md
M       TODO.md
etc

Мой вопрос

Есть ли способ, которым я могу отредактировать эту историю, чтобы git show, git blame и другие раскрыли истинную историю всех файлов этого репо, независимо от того, были ли они созданы в (B) или (C)?

Ответы [ 3 ]

2 голосов
/ 20 сентября 2019

В 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 для выбора начального коммита.

  • Пока очередь не пуста:

    1. Уберите первый элемент из очереди.(Этот коммит теперь посещен.)
    2. Решите, показывать ли этот коммит.Если это так, покажите его (также переписывание родителями, если оно включено - это совсем другая тема; это имеет значение, если вы используете --parents или --graph).
    3. Перечислите родителей этого коммита, применяяупрощение истории, если включено.Поместить выбранных родителей в очередь с приоритетами, если они уже присутствуют или уже посещеныЕсли у коммита нет родителей или они пропущены, очередь становится короче.Если несколько родителей попадают в очередь, очередь становится длиннее.«Приоритетная» часть приоритетной очереди определяет, какой коммит будет впереди, когда мы вернемся к шагу 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, он в конце концов вынужден делать что-то вродеупрощения истории, потому что нецелесообразно следовать каждому пути назад.

1 голос
/ 20 сентября 2019

Следуя наблюдениям @ torek, я нашел способ «исправить» то, что кажется надежным.

Повторное использование этого примера:

* commit beb7ea3351f50dd29899baa878ea2fa29c437ecc
(A)
|
*   commit ed3fef629f8d7268fe29c37029977443eea46494
|\  Merge: d8cf79f ed3fef6
| |
| * commit 820bea750c86c90443ca1068e08d6b72cbe317ca
| |
| * commit 19efa83244f1e19976b0e543bf391099bcc1b056
| (B)
|
* commit d8cf79f2103b7d25e6c4dbb96bbd3f672d30bae8
|
* commit fc4c8d8cb9df4c3ee892f0f0f691c71526668d55
(C)

Я определяю:

pointA=beb7ea3351f50dd29899baa878ea2fa29c437ecc
pointC=d8cf79f2103b7d25e6c4dbb96bbd3f672d30bae8
merge_commit=ed3fef629f8d7268fe29c37029977443eea46494

Затем я использую git replace:

▶ git replace -f --graft "$pointA" "$pointC" "$merge_commit"

Это говорит о том, что оба пункта C и коммит слияния являются родителями коммита A.

Мой новыйГрафик выглядит так:

▶ git log --graph 
*   commit beb7ea3351f50dd29899baa878ea2fa29c437ecc (HEAD -> master, replaced)
|\  Merge: d8cf79f ed3fef6
| | 
| *   commit ed3fef629f8d7268fe29c37029977443eea46494
| |\  Merge: d8cf79f 820bea7
|/ /  
| * commit 820bea750c86c90443ca1068e08d6b72cbe317ca
| | 
| * commit 19efa83244f1e19976b0e543bf391099bcc1b056
| 
* commit d8cf79f2103b7d25e6c4dbb96bbd3f672d30bae8
| 
* commit fc4c8d8cb9df4c3ee892f0f0f691c71526668d55

Полагаю, это сложнее, чем нужно, но хорошая часть - все мои команды: git log, git blame и т. д. показывают исправленные истории как вфайлы в точке B и в точке C.

Наконец, как заметил @VonC и более подробно объяснил в ответе @ torek здесь , это заменило только локальные ссылки.

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

▶ git filter-branch --tag-name-filter cat -- --all
0 голосов
/ 20 сентября 2019

Ваш гистолог показывает, что файл TODO.md существует в коммите слияния d89d и не существует ни у одного из родителей слияния, точно так же, как вам говорили git log и git show и так далее.Файл был добавлен вручную в этом слиянии, по причинам, которые вы должны будете узнать у автора слияния.Итак, где-то между корнем C и слиянием кто-то сделал TODO.md, а где-то между этим и слиянием кто-то удалил его (по причинам, которые вы должны выяснить у автора этого коммита).Аналогично с корнем B.Затем, кто бы ни сделал слияние, он сделал новое TODO.md во время слияния.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...