Виділіть багато підкаталогів у новий, окремий сховище Git


135

Це питання засноване на підкаталозі Detach в окремому сховищі 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

Будь-які ідеї? ТІА

Відповіді:


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свої параметри
Yonatan

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"

Після введення файлу temp 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 - З'єднайте ліповий репо наступний таким же чином:

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"

Продовжуйте, якщо у вас є більше двох репостів для злиття.

Довідка: Об’єднайте підкаталог іншого сховища з git


4
Оскільки git 2.9 вам потрібно скористатись --allow-nepovezaними-історіями в командах злиття. Інакше це, здається, добре спрацювало для мене.
Кріс,

1
Геній! Дуже дякую за це. Початкові відповіді, які я переглянув, використовуючи деревний фільтр у дуже великому сховищі, мав передчуття, що передбачило більше 26 годин для завершення перезаписів git. Набагато щасливіший з цим простим, але повторюваним підходом і успішно перемістив 4 підпапки в нове репо з усіма очікуваними історіями фіксацій.
shuttsy

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? для отримання детальної інформації про це.


1
Цікава ідея. У мене аналогічна проблема , але не міг змусити його працювати, см stackoverflow.com/questions/8050687 / ...
Маноло

Це майже те, що мені було потрібно, хоча у мене було посипання файлів і папок по моїй репо ... Спасибі :)
notlesh

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 працювати ...
robdodson

2
@MikeGraf Я не думаю, що це дасть бажаний результат: втеча відповідатиме буквальному "!" тощо на вашому шляху.
kynan

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. Якщо є спосіб видалити дублікати, то це було б ідеально.


Акуратні інструменти ви знайшли тут. Інформація про "checkout": "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 config, щоб він міг знайти 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 займає хвилину, щоб зробити те саме.


7

Використовуйте розширення git 'git splits'

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


Здається, неможливо додати файли до спліту та оновити їх пізніше, правда?
Олексій

Це, здається, повільно працює на моїй репо з тоннами комісій
Shinta Smith

git-split, здається, використовує фільтр git --index, який надзвичайно повільний порівняно з - subdirectory-filter. Для деяких репостів це все ще може бути життєздатним варіантом, але для великих репостів (кілька гігабайт, 6-значний цифр) --index-фільтр ефективно потребує тижнів, навіть на спеціальному облачному обладнанні.
Jostein Kjønigsen

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/

Це, безумовно, чудова відповідь. Проблема з усіма іншими рішеннями полягає в тому, що мені не вдалося витягнути вміст ВСІХ гілок каталогу. Однак git filter-repo отримав папку з усіх гілок і прекрасно переписав історію, як очищення всього дерева від усього, що мені не потрібно.
Теодоро

3

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


-4

Видаліть наявну резервну копію в каталозі .git у refs / original, як це передбачає повідомлення. Каталог прихований.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.