Объедините два разных репозитория git, чередуя коммиты - PullRequest
2 голосов
/ 27 апреля 2019

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

Предположим, что наша текущая структура следующая, где project и tests - два отдельных репозитория git:

project
    /src
    /include
tests
    /short
    /long

Я бы хотел получить один репозиторий git с двумя каталогами project и tests.

Я не могу просто объединить эти два репозитория, используя методы, описанные в этом ответе , этом или этом сайте : они приводят к репозиториям, которые у вас есть две разные истории до слияния, и при проверке прошлого коммита у вас есть либо src и include, либо short и long, но у вас нет всех четырех из них, как они появились в в это время.

Если я извлекаю коммит, который был создан в project 4 месяца назад, я хотел бы видеть project/src и project/include, как они появились в этом коммите, но я также хотел бы иметь tests/short и test/long, как они были одновременно в (тогда отдельном) test хранилище.

Я понимаю, что порядок коммитов между обоими репозиториями будет зависеть только от времени и может быть не очень точным. Но для меня этого достаточно. И, конечно, я знаю, что не могу сохранить исходные идентификаторы Git из каждого репо. Это нормально, потому что эти два репозитория на самом деле являются свежим импортом из другого RCS, и поэтому нигде не было записано ни одного git id.

Должна выполнимо извлекать по одному все коммиты из каждого репо, упорядоченные по времени между репозиториями, и фиксировать полученные файлы. Уже есть инструмент, который бы это сделал?

Ответы [ 3 ]

3 голосов
/ 28 апреля 2019

Редактировать: подход, основанный на дате, который делает это довольно простым, но предполагает, что один из двух репозиториев будет «контролировать», какие коммиты поступают из другого репозитория, см. ответ jthill . В итоге вы получаете историю коммитов, которая точно соответствует истории проекта, возможно, уничтожая некоторые из истории тестов. Приведенный ниже ответ более уместен, если вам нужно добавить префикс к обоим наборам историй или вы хотите перемежать их (например, нужны два разных "тестовых" обновления для одной и той же фиксации "проекта").


phd-ответ хорошо, но если бы я делал это сам и хотел сделать его действительно аккуратным и чистым, я бы использовал другой подход.

Если деревья для двух репозиториев не перекрываются, это, безусловно, возможно сделать, и, обходя обычные механизмы Git, переходя прямо к базовым git read-tree командам, вы можете автоматизировать это. (Именно здесь недавний комментарий VonC отвергает мое утверждение о том, что Git и Mercurial очень похожи, верно: если вы пропустите команды Git верхнего уровня, вы получите то, что вы не можете получить почти так же легко в Mercurial.)

Как и в ответе phd , вы начнете этот процесс, объединив две базы данных фиксации репозитория с помощью git fetch. (Вы можете сделать это в третьем репо, который я бы порекомендовал, так как это упрощает перезапуск процесса с нуля, если вы решите, что хотите настроить некоторые параметры, или добавив репо A к репо B или репо B к репо А.) Но после этого все расходится.

Теперь у вас есть два независимых DAG коммита:

        D--...--K
       /         \
A--B--C           M--N   <-- repoA/master
       \         /
        E--...--L

O--P--Q--...--Z   <-- repoB/master

(Если repoA и repoB имеют более одного наконечника ветви, нарисуйте любую упрощенную диаграмму их коммитов, более подходящую.)

Ваш следующий шаг - перечислить все коммиты в каждом из двух непересекающихся групп DAG, используя git rev-list --topo-order --reverse и любые другие параметры сортировки, которые вам нравятся. Когда и требуется ли --topo-order, зависит от топологии и другой информации сортировки, но, как правило, вы хотите, чтобы родительский коммит был указан перед любым из его дочерних элементов.

Учитывая эти два линеаризованных списка хеш-идентификаторов коммитов, теперь у вас есть сложная часть: построение графа новых объединенных деревьев, которые вы хотите зафиксировать. Каждый новый коммит будет создаваться путем объединения одного коммита из каждого из двух старых графов. Если один из графиков является сложным (как для repoA выше) с ответвлениями и слияниями, а другой - нет (как для repoB выше), это может быть особенно сложно.

Я сделал свои собственные настройки для этого, где у меня есть очень простой график:

A--B   <-- A/master

O--P   <-- B/master

В моей упрощенной настройке я хотел бы сделать свой первый коммит на моем новом мастере коммитом C, который объединяет деревья A и O:

C   <-- master

Тогда я хотел бы сделать, как мой второй коммит на master, комбинацию A и P (не A и O и не B и O либо) и, как мой последний коммит, комбинация B и P, так что я получаю:

C--D--E   <-- master

with:
    C = A+O
    D = A+P
    E = B+P

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

$ git log --all --graph --decorate --format='%h%d %s' --name-status | sed '/^[| ] $/d'
* 7b9921a (B/master) commit-P
| A B/another
* 51955b1 commit O
  A B/start
* 69597d3 (A/master) commit-B
| A A/new
* ff40069 commit-A
  A A/file

(Я случайно не переносил коммит O, но переносил все остальные. sed - удалить некоторые пустые строки, которые в действительности не помогают читать.)

$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

Теперь мы строим новые коммиты, по одному, используя git read-tree, чтобы заполнить индекс для совершения коммитов. Начнем с пустого индекса (который у нас есть сейчас):

$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

Мы хотим, чтобы наш первый коммит объединил A и O, поэтому давайте теперь прочитаем эти два коммита в индекс. Если бы нам пришлось добавить префикс к дереву в A, мы могли бы сделать это здесь:

$ git read-tree --prefix= ff40069
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
$ git read-tree --prefix= 51955b1
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start

Мы можем сделать коммит, который нам нужен:

$ git commit -m combine-A-and-O
[master (root-commit) 7c629d8] combine-A-and-O
 2 files changed, 2 insertions(+)
 create mode 100644 A/file
 create mode 100644 B/start

Теперь нам нужно сделать следующий коммит, а значит, нам нужно построить правильное дерево в индексе. Чтобы сделать это, мы сначала должны очистить это; в противном случае следующий git read-tree --prefix потерпит неудачу с жалобой на перекрывающиеся файлы и Cannot bind. Так что теперь мы очищаем индекс, затем читаем коммиты A и P:

$ git read-tree --empty
$ git read-tree --prefix= ff40069
$ git read-tree --prefix= 7b9921a

Если хотите, вы можете проверить результат, используя git ls-file --stage еще раз:

$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
100644 d7941926464291df213061d48784da98f8602d6c 0       B/another
100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start

В любом случае теперь они могут быть зафиксированы как новый коммит:

$ git commit -m 'combine A and P'
[master eb8fa3c] combine A and P
 1 file changed, 1 insertion(+)
 create mode 100644 B/another

(теперь вы можете видеть, как я получаю непоследовательные переносы :-)). Наконец, мы повторяем процесс, опустошая индекс, читая в двух желаемых фиксациях (B + P) и фиксируя результат:

$ git read-tree --empty
$ git read-tree --prefix= A/master
$ git read-tree --prefix= B/master
$ git ls-files --stage
100644 7a1c6130c652b6ea92f4d19183693727e32c9ac4 0       A/file
100644 8e0c97794a6e80c2d371f9bd37174b836351f6b4 0       A/new
100644 d7941926464291df213061d48784da98f8602d6c 0       B/another
100644 f6284744575ecfc520293b33122d4a99548045e4 0       B/start
$ git commit -m 'combine B and P'
[master fad84f8] combine B and P
 1 file changed, 1 insertion(+)
 create mode 100644 A/new

(Я использовал здесь символические имена для получения двух последних коммитов, но хэш-идентификаторы из git rev-list, конечно, работали бы хорошо.) Теперь мы можем видеть три коммита, все на master:

$ git log --decorate --oneline --graph
* fad84f8 (HEAD -> master) combine B and P
* eb8fa3c combine A and P
* 7c629d8 combine-A-and-O

и теперь безопасно удалить ссылки A/master и B/master (и два пульта дистанционного управления). Есть одна особенность: поскольку мы выполняли всю работу непосредственно в индексе, не заботясь о рабочем дереве, оно все еще совершенно пустое:

$ ls
$ git status -s
 D A/file
 D A/new
 D B/another
 D B/start

Чтобы это исправить в конце, нам нужно просто запустить git checkout HEAD -- .:

$ git checkout HEAD -- .
$ git status -s
$ git status
On branch master
nothing to commit, working tree clean

Как написать собственный скрипт автоматизации

На практике вы, вероятно, захотите использовать git write-tree и git commit-tree, а не git commit, чтобы делать новые коммиты. Вы должны написать небольшой скрипт (на любом языке, который вам нравится) для запуска git rev-list, чтобы собрать хеш-идентификаторы коммитов для объединения. Сценарий должен проверять эти коммиты - например, просматривая авторство и даты, или содержимое файла, или что-то еще - чтобы решить, как переплетать коммиты. Затем, приняв решение о переплетении и о том, какие структуры ветвления и слияния предоставить, скрипт может начать процесс многократного выполнения следующих шагов:

  • Очистить индекс.
  • Дергать в дереве из коммита в подграфе из репо-A, с любым подходящим параметром --prefix - в вашем случае это --prefix=, т. Е. Пустая строка, но в других случаях это будет именем каталога с косой чертой).
  • Дергать в дереве из коммита в подграфе из репо-B с другим подходящим --prefix, чтобы не было коллизий между записями из A и B.
  • Используйте git write-tree, чтобы написать дерево. Его вывод - идентификатор хеша дерева для следующего шага.
  • Используйте git commit-tree с соответствующими -p аргументом (ами) для установки родителя (ей) нового коммита. Передайте ему соответствующий (комбинированный или любой другой) текст сообщения о фиксации. Используйте переменные окружения GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_AUTHOR_DATE, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL и GIT_COMMITTER_DATE для управления именами и датами авторов и коммиттеров. Выходные данные из git commit-tree - это хэш-идентификатор, который является родителем некоторого последующего коммита

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

git branch <name> <hash>

для каждого такого хэш-идентификатора.

3 голосов
/ 28 апреля 2019

[задано все project содержимое в src и include и все tests содержимое в short и long,]

Если я извлекаю коммит, созданный в проекте 4 месяца назад, я бы хотел видеть project/src и project/include в том виде, в каком они появились в этом коммите, но я также хотел бы иметь tests/short и tests/long как они были одновременно в (тогда отдельном) тестовом хранилище. [...]

Уже есть инструмент, который бы это сделал?

Да, он называется git filter-branch. Безусловно, самое простое для реализации - это просмотреть историю project и отыскать «соответствующий» контент tests коммита, вот эскиз:

git init junk
cd junk
git remote add project /path/to/project
git remote add tests /path/to/tests
git remote update

git filter-branch --index-filter '
        mydate=`git show -s --date=raw --pretty=%ad $GIT_COMMIT`
        thetest=`git rev-list -1 --before="$mydate" --remotes=tests`
        [[ -n $thetest ]] && git read-tree --prefix= $thetest
' -- --remotes=project

, который замедлится, если в вашей истории "тестов" будет много тысяч коммитов, если вы говорите о репозитории linux или о чем-то подобном, было бы дешевле предварительно создать список тестов, отсортированных по дате, и пройти через это .

2 голосов
/ 27 апреля 2019

Я думаю, вам следует объединить два хранилища, создав 2 ветки (git fetch без слияния).Затем интерактивно перебазируйте одну ветку, остановитесь на каждом коммите и выполните git cherry-pick соответствующий коммит в текущей ветке.Затем продолжите интерактивное перебазирование до следующего коммита (это сохранит «отредактированный» коммит без изменений).

Возможно, это можно даже автоматизировать.Вместо интерактивной перебазировки и ручного выбора вишни вы, вероятно, можете использовать git rebase --interactive -x, выполняя git cherry-pick после каждого коммита.Проблема в том, как выяснить, что совершают вишневые кирки.Я думаю, что это должно быть second-branch~count.Счет может быть отредактирован перед интерактивной перебазировкой при редактировании файла rebase-todo.

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