Отсоединить много подкаталогов в новый отдельный Git-репозиторий

135

Этот вопрос основан на отсоединении подкаталога в отдельный Git-репозиторий

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

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

И я хотел бы это вместо этого:

/apps
  /AAA
/libs
  /XXX

--subdirectory-filterАргумент git filter-branchне будет работать , потому что он избавляется от всего для данного каталога , за исключением в первый раз он запущен. Я думал, что использование --index-filterаргумента для всех нежелательных файлов будет работать (хотя и утомительно), но если я попытаюсь запустить его несколько раз, я получу следующее сообщение:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

Любые идеи? ТИА

prisonerjohn
источник

Ответы:

155

Вместо того, чтобы иметь дело с подоболочкой и использовать ext glob (как предложил kynan), попробуйте этот гораздо более простой подход:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all

Как упомянуто void.pointer в его / ее комментарии , это удалит все, кроме apps/AAAи libs/XXXиз текущего репозитория.

Обрезать пустые коммиты слияния

Это оставляет много пустых слияний. Они могут быть удалены другим проходом, как описано raphinesse в его ответе :

git filter-branch --prune-empty --parent-filter \
'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'

⚠️ Предупреждение . Вышеприведенное должно использовать версию GNU sedи в xargsпротивном случае оно удалит все коммиты в случае xargsсбоя. brew install gnu-sed findutilsа затем используйте gsedи gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
Дэвид Смайли
источник
4
Кроме того, флаг --ignore-unmatch должен быть передан в git rm, иначе мне не удалось выполнить первый коммит (репозиторий был создан с использованием клона git svn в моем случае)
Pontomedon
8
Предполагая, что у вас есть теги в миксе, вы, вероятно, должны добавить --tag-name-filter catк своим параметрам
Йонатан
16
Не могли бы вы добавить больше информации, объясняющей, что делает эта длинная команда?
Бурхан Али
4
Я приятно удивлен, что это прекрасно работает на Windows, используя git bash, фу!
Дай
3
@BurhanAli Для каждого коммита в истории он удаляет все файлы, кроме тех, которые вы хотите сохранить. Когда все будет сделано, у вас останется только та часть дерева, которую вы указали, и только эта история.
void.pointer
39

Ручные шаги с простыми командами git

План состоит в том, чтобы разделить отдельные каталоги на собственные репозитории, а затем объединить их. В следующих шагах вручную не использовались скрипты, предназначенные для использования, но были понятные команды, и они могли помочь объединить дополнительные N подпапок в другой репозиторий.

Делить

Давайте предположим, что ваш оригинальный репо: original_repo

1 - Разделить приложения:

git clone original_repo apps-repo
cd apps-repo
git filter-branch --prune-empty --subdirectory-filter apps master

2 - Сплит либс

git clone original_repo libs-repo
cd libs-repo
git filter-branch --prune-empty --subdirectory-filter libs master

Продолжить, если у вас более 2 папок. Теперь у вас будет два новых и временных git-хранилища.

Покори , объединяя приложения и библиотеки

3 - Подготовьте новый репо:

mkdir my-desired-repo
cd my-desired-repo
git init

И вам нужно будет сделать хотя бы один коммит. Если следующие три строки должны быть пропущены, ваш первый репо появится сразу под корнем репо:

touch a_file_and_make_a_commit # see user's feedback
git add a_file_and_make_a_commit
git commit -am "at least one commit is needed for it to work"

При фиксированном временном файле mergeкоманда в следующем разделе остановится, как и ожидалось.

Принимая во внимание обратную связь с пользователем, вместо добавления случайного файла, такого как a_file_and_make_a_commit, вы можете добавить или .gitignore, и README.mdт. Д.

4 - Слияние приложений репо первым:

git remote add apps-repo ../apps-repo
git fetch apps-repo
git merge -s ours --no-commit apps-repo/master # see below note.
git read-tree --prefix=apps -u apps-repo/master
git commit -m "import apps"

Теперь вы должны увидеть каталог приложений внутри вашего нового хранилища. git logдолжен показывать все соответствующие исторические сообщения коммита.

Примечание: как отметил Крис ниже в комментариях, для более новой версии (> = 2.9) git необходимо указать --allow-unrelated-historiesс помощьюgit merge

5 - Объединить libs repo следующим образом:

git remote add libs-repo ../libs-repo
git fetch libs-repo
git merge -s ours --no-commit libs-repo/master # see above note.
git read-tree --prefix=libs -u libs-repo/master
git commit -m "import libs"

Продолжайте, если у вас есть более 2 репо для слияния.

Ссылка: объединить подкаталог другого репозитория с git

chfw
источник
4
Начиная с git 2.9 вам нужно использовать --allow-unrelated-history в командах слияния. В противном случае это, кажется, хорошо работает для меня.
Крис
1
Genius! Спасибо тебе большое за это. Первоначальные ответы, на которые я смотрел, используя древовидный фильтр в очень большом репозитории, имели предсказание git, занимавшее 26 часов для завершения переписывания git. Намного счастливее этот простой, но повторяемый подход, и мы успешно переместили 4 подпапки в новое хранилище со всей ожидаемой историей коммитов.
Шутцы
1
Вы можете использовать первый коммит для «Первоначального коммита», который добавляет .gitignoreи README.mdфайлы.
Джек Миллер
2
К сожалению, этот подход, похоже, нарушает историю отслеживания для файлов, добавленных на git merge .. git read-treeшаге, так как он записывает их как вновь добавленные файлы, и все мои git guis не устанавливают связь с их более ранними коммитами.
Дай
1
@ksadjad, не знаю, если честно. Центральная точка ручного слияния заключается в выборе каталогов для формирования нового репо и сохранении их истории коммитов. Я не уверен, как справиться с такой ситуацией, когда коммит помещает файлы в dirA, dirB, dirDrop и только dirA и dirB выбираются для нового репо, как история коммитов должна относиться к исходному.
Chfw
27

Почему вы хотите запустить filter-branchболее одного раза? Вы можете сделать все это за один раз, поэтому не нужно его форсировать (обратите внимание, что вам нужно extglobвключить его в своей оболочке, чтобы это работало):

git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all

Это должно избавить от всех изменений в нежелательных подкаталогах и сохранить все ваши ветки и коммиты (если только они не влияют только на файлы в сокращенных подкаталогах, в силу --prune-empty) - никаких проблем с дублирующимися коммитами и т. Д.

После этой операции нежелательные каталоги будут перечислены как неотслеживаемые git status.

$(ls ...)Необходимо ул extglobоценивается вместо вашей оболочки индексного фильтра, который использует shвстроенную команду eval(где extglobне доступен). Смотрите Как включить опции оболочки в git? для более подробной информации об этом.

kynan
источник
1
Интересная идея. У меня аналогичная проблема , но не мог заставить его работать, см stackoverflow.com/questions/8050687/...
Манол
Это в значительной степени то, что мне было нужно, хотя у меня было много файлов и папок в
репозитории
1
гектометр даже при включенном extglob я получаю ошибку рядом с круглыми скобками: синтаксическая ошибка рядом с неожиданным токеном `('моя команда выглядит так: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - - все ls с src / css / themes /! (some_theme *) возвращает все остальные темы, так что extglob действительно работать ...
Робдодсон
2
@MikeGraf Не думаю, что это даст желаемый результат: экранирование будет соответствовать буквальному символу "!" и т.д. на вашем пути.
Кинан
1
Ответ @ david-smiley (более поздний) использует очень похожий подход, но имеет преимущество, заключающееся в том, что он полагается исключительно на gitкоманды, и, следовательно, не так подвержен различиям в том, как lsинтерпретируется в операционных системах, как обнаружил @Bae.
Джереми Кейни
20

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

Мне удалось сделать это с помощью комбинации git subtreeи git-stitch-repo. Эти инструкции основаны на:

Сначала я вытащил каталоги, которые хотел сохранить, в отдельный репозиторий:

cd origRepo
git subtree split -P apps/AAA -b aaa
git subtree split -P libs/XXX -b xxx

cd ..
mkdir aaaRepo
cd aaaRepo
git init
git fetch ../origRepo aaa
git checkout -b master FETCH_HEAD

cd ..
mkdir xxxRepo
cd xxxRepo
git init
git fetch ../origRepo xxx
git checkout -b master FETCH_HEAD

Затем я создал новый пустой репозиторий и импортировал / сшил последние два в него:

cd ..
mkdir newRepo
cd newRepo
git init
git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import

Это создает две ветви, master-Aи master-B, каждая из которых содержит содержимое одного из прошитых РЕПО. Объединить их и очистить:

git checkout master-A
git pull . master-B
git checkout master
git branch -d master-A 
git branch -d master-B

Теперь я не совсем уверен, как / когда это произойдет, но после первого checkoutи pullвторого кода этот код волшебным образом сливается с основной ветвью (любое понимание того, что здесь происходит, приветствуется!)

Кажется, все работает так, как ожидалось, за исключением того, что, если я просматриваю newRepoисторию коммитов, появляются дубликаты, когда набор изменений влияет как на, так apps/AAAи на libs/XXX. Если есть способ удалить дубликаты, то это было бы идеально.

prisonerjohn
источник
Аккуратные инструменты, которые вы нашли здесь. Понятие «оформить заказ»: «git pull» аналогично «git fetch && git merge». Часть «выборка» безвредна, поскольку вы «выбираете локально». Поэтому я думаю, что эта команда извлечения такая же, как «git merge master-B», что немного более очевидно. См. Kernel.org/pub/software/scm/git/docs/git-pull.html
phord
1
К сожалению, инструмент git-stitch-repo в настоящее время не работает из-за плохих зависимостей.
Хенрик
@Henrik Какую проблему вы испытали? Это работает для меня, хотя я должен был добавить export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"к моей конфигурации bash, чтобы он мог найти Git.pm. Затем я установил его с помощью cpan.
Можно использовать git subtree addдля выполнения этой задачи. См stackoverflow.com/a/58253979/1894803
laconbass
7

Я написал фильтр git, чтобы решить именно эту проблему. Он имеет фантастическое имя git_filter и находится на github здесь:

https://github.com/slobobaby/git_filter

Он основан на превосходном libgit2.

Мне нужно было разбить большой репозиторий на множество коммитов (~ 100000), и решения, основанные на git filter-branch, заняли несколько дней. git_filter занимает минуту, чтобы сделать то же самое.

slobobaby
источник
7

Используйте расширение git 'split'

git splitsэто скрипт bash, который является оболочкой, git branch-filterкоторую я создал как расширение git на основе решения jkeating .

Это было сделано именно для этой ситуации. В случае вашей ошибки попробуйте использовать git splits -fопцию принудительного удаления резервной копии. Поскольку git splitsработает в новой ветви, она не будет перезаписывать вашу текущую ветку, поэтому резервное копирование постороннее. Смотрите readme для более подробной информации и обязательно используйте его на копии / клоне вашего репо (на всякий случай!) .

  1. установить git splits.
  2. Разделите каталоги в местный филиал #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

  3. Создайте пустой репо где-нибудь. Предположим, мы создали пустой репозиторий xyzна GitHub с указанием пути:git@github.com:simpliwp/xyz.git

  4. Нажмите на новый репо. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Клонировать только что созданный удаленный репо в новый локальный каталог
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone git@github.com:simpliwp/xyz.git

AndrewD
источник
Похоже, невозможно добавить файлы в раздел и обновить их позже, верно?
Алекс
Похоже, это замедляет работу моего репо с кучей
Шинта Смит
git-split использует фильтр git --index, который очень медленный по сравнению с --subdirectory-filter. Для некоторых репозиториев это все еще может быть приемлемым вариантом, но для больших репозиториев (несколько гигабайт, 6-значные коммиты) --index-filter эффективно работает несколько недель, даже на выделенном облачном оборудовании.
Йостейн Кьёнигсен
6
git clone git@example.com:thing.git
cd thing
git fetch
for originBranch in `git branch -r | grep -v master`; do
    branch=${originBranch:7:${#originBranch}}
    git checkout $branch
done
git checkout master

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all

git remote set-url origin git@example.com:newthing.git
git push --all
Ричард Барракло
источник
Прочитав все остальные комментарии, я оказался на правильном пути. Тем не менее, ваше решение просто работает. Он импортирует все ветки и работает с несколькими каталогами! Большой!
Jschober
1
forЦикл стоит признать, так как другие подобные ответы не включать его. Если у вас нет локальной копии каждой ветви в вашем клоне, filter-branchона не будет учитываться как часть ее перезаписи, что потенциально может исключить файлы, добавленные в другие ветви, но еще не объединенные с вашей текущей веткой. (Хотя это также стоит сделать для git fetchлюбых веток, которые вы ранее проверили, чтобы убедиться, что они остаются актуальными.)
Джереми Кейни
5

Простое решение: git-filter-repo

У меня была похожая проблема, и после рассмотрения различных подходов, перечисленных здесь, я обнаружил git-filter-repo . Рекомендуется в качестве альтернативы git-filter-branch в официальной документации git здесь .

Чтобы создать новый репозиторий из подмножества каталогов в существующем репозитории, вы можете использовать команду:

git filter-repo --path <file_to_remove>

Отфильтруйте несколько файлов / папок, объединяя их в цепочку:

git filter-repo --path keepthisfile --path keepthisfolder/

Итак, чтобы ответить на исходный вопрос , с git-filter-repo вам просто понадобится следующая команда:

git filter-repo --path apps/AAA/ --path libs/XXX/
Elmo
источник
Это определенно отличный ответ. Проблема со всеми другими решениями заключается в том, что мне не удалось извлечь содержимое ВСЕХ ветвей каталога. Тем не менее, git filter-repo извлек папку из всех веток и отлично переписал историю, как чистка всего дерева всего, что мне не нужно.
Теодоро
3

Да. Принудительно перезаписать резервную копию, используя -fфлаг при последующих вызовах, filter-branchчтобы переопределить это предупреждение. :) В противном случае я думаю, что у вас есть решение (то есть, уничтожить нежелательный каталог одновременно с filter-branch).

Якоб Борг
источник
-4

Удалите резервную копию из каталога .git в refs / original, как показано в сообщении. Каталог скрыт.

user5200576
источник