Питання Від'єднати (перемістити) підкаталог до окремого сховища Git


Я маю Гіт сховище, яке містить декілька підкаталогів. Тепер я виявив, що один з підкаталогів не пов'язаний з іншим і повинен бути відокремлений в окремому сховищі.

Як це зробити, зберігаючи історію файлів у підкаталозі?

Я думаю, я міг зробити клон і видалити небажані частини кожного клону, але я вважаю, що це дасть мені повне дерево при перевірці старої версії тощо. Це може бути прийнятним, але я б вважав за краще вдаватися до того, що два сховища не мають спільної історії.

Просто щоб зрозуміти, у мене є така структура:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Але я хотів би це натомість:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/

1595
2017-12-11 13:57


походження


Це тривіальне зараз з git filter-branch див. мою відповідь нижче. - jeremyjjbrown
@jeremyjjbrown правильно. Це вже не складно зробити, але важко знайти правильну відповідь на Google, тому що всі старі відповіді переважають над результатами. - Agnel Kurian


Відповіді:


Оновити: Цей процес настільки поширений, що команда git зробила його набагато простішим за допомогою нового інструмента, git subtree. Дивіться тут: Від'єднати (перемістити) підкаталог до окремого сховища Git


Ви хочете клонувати своє сховище, а потім використовувати git filter-branch щоб позначити все, окрім підкаталогу, який ви хочете в новому репо для збирання сміття.

  1. Щоб клонувати місцевий репозиторій:

    git clone /XYZ /ABC
    

    (Примітка: сховище буде клонувати за допомогою жорстких посилань, але це не проблема, оскільки жорстко пов'язані файли не будуть змінюватися самі по собі - нові будуть створені.)

  2. Тепер збережемо цікаві гілки, які ми також хочемо переписати, а потім видаліть походження, щоб уникнути натискання там і переконатися, що старі зобов'язання не будуть посилатися на джерело:

    cd /ABC
    for i in branch1 br2 br3; do git branch -t $i origin/$i; done
    git remote rm origin
    

    або для всіх віддалених відділень:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
    
  3. Тепер ви можете також видалити теги, які не мають відношення до підпроекту; ви також можете це зробити пізніше, але вам може знадобитися ще раз обрізати репо. Я цього не зробив і отримав WARNING: Ref 'refs/tags/v0.1' is unchanged для всіх міток (оскільки вони не були пов'язані з підпроектом); Крім того, після видалення таких міток більше буде відновлено місце. Очевидно git filter-branch повинен мати можливість переписати інші теги, але я не зміг підтвердити це. Якщо ви хочете видалити всі теги, скористайтеся git tag -l | xargs git tag -d.

  4. Потім використовуйте фільтр-гілку та скиньте, щоб виключити інші файли, тому їх можна обрізати. Давайте також додамо --tag-name-filter cat --prune-empty щоб видалити порожні команд і переписати теги (зверніть увагу, що для цього потрібно буде позбавити їх підпис):

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all
    

    або ж, щоб переписати гілку HEAD та ігнорувати теги та інші гілки:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Потім видаліть резервні копії резервного копіювання, щоб простір було дійсно відновлено (хоча зараз операція є руйнівною)

    git reset --hard
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --aggressive --prune=now
    

    і тепер у вас є місцевий сховище git підкаталогу ABC зі всією його історією.

Примітка. Для більшості застосувань git filter-branch має дійсно мати додатковий параметр -- --all. Так, це дійсно --простір--  all. Це має бути останнім параметром для команди. Як виявив Матлі, це означає, що гілки проекту та теги будуть включені до нового репо.

Редагувати: різні пропозиції з коментарів нижче були включені, щоб переконатися, наприклад, що репозиторій фактично зменшився (що раніше було не так).


1155
2017-07-25 17:10



Дуже хороша відповідь. Дякую! І щоб насправді отримати те, що я хотів, я додав команду "- --all" до фільтра-гілки. - matli
Чому ти потребуєшся --no-hardlinks? Видалення однієї твердої посилання не вплине на інший файл. Об'єкти Git також незмінні. Тільки якщо ви хочете змінити потрібні права власника / файлу --no-hardlinks. - vdboor
Додатковим кроком, який я рекомендую, буде "віддалений виклик git remote". Це призведе до того, що ми повернемося до початкового сховища, якщо я не помиляюся. - Tom
Інша команда додати до filter-branch є --prune-empty, щоб видалити тепер порожні коми. - Seth Johnson
Як і Пол, я не бажав, щоб теги проекту містилися в моєму новому репо, тому я не користувався -- --all. Я теж побіг git remote rm origin, і git tag -l | xargs git tag -d перед git filter-branch команда Це зменшило мою .git каталог від 60M до ~ 300K. Зауважте, що мені потрібно було запустити обидві ці команди, щоб отримати зменшення розміру. - saltycrane


Easy Way ™

Виявляється, це настільки загальноприйнята та корисна практика, що заступники гіт зробили це дуже просто, але вам потрібно мати нову версію git (> = 1.7.11 травня 2012 р.). Див додаток для того, як встановити останню версію git. Також є а реальний приклад в проходження нижче.

  1. Підготуйте стару репо

    pushd <big-repo>
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Примітка:  <name-of-folder> Не повинно містити провідних чи кінцевих символів. Наприклад, папка з ім'ям subproject ПОВИНЕН бути переданий як subprojectНЕ ./subproject/

    Примітка для користувачів Windows: коли ваша глибина папки> 1, <name-of-folder> повинен мати сепаратор папки типу nix (/). Наприклад, папка з ім'ям path1\path2\subproject ПОВИНЕН бути переданий як path1/path2/subproject

  2. Створіть новий репо

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Зв'яжіть нове репо з Github або де завгодно

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Прибирати, за бажанням

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Примітка: Це залишає всі історичні посилання в репозиторії. Див Додаток нижче, якщо ви дійсно стурбовані вчиненням пароля або вам потрібно зменшити розмір файлу вашої .git папка

...

Проходження

Це такі ті ж кроки, що описані вище, але слідуючи моїм конкретним крокам для мого сховища замість використання <meta-named-things>.

Ось проект, який я маю для реалізації модулів JavaScript браузера в вузлі:

tree ~/Code/node-browser-compat

node-browser-compat
├── ArrayBuffer
├── Audio
├── Blob
├── FormData
├── atob
├── btoa
├── location
└── navigator

Я хочу розділити одну папку, btoa, в окреме сховище git

pushd ~/Code/node-browser-compat/
git subtree split -P btoa -b btoa-only
popd

У мене зараз є нова філія, btoa-only, що тільки покладається на btoa і я хочу створити новий репозиторій.

mkdir ~/Code/btoa/
pushd ~/Code/btoa/
git init
git pull ~/Code/node-browser-compat btoa-only

Далі я створив нове репо на Github або bitbucket, або що завгодно, і додати його origin (до речі, "походження" - це лише конвенція, а не частина команди - можна назвати це "віддаленим сервером" або що завгодно)

git remote add origin git@github.com:node-browser-compat/btoa.git
git push origin -u master

Щасливий день!

Примітка: Якщо ви створили репо з README.md, .gitignore і LICENSE, вам спочатку треба потягнути:

git pull origin -u master
git push origin -u master

Нарешті, я хочу видалити папку з більшого репо

git rm -rf btoa

...

Додаток

Остання версія на OS X

Щоб отримати останню версію git:

brew install git

Щоб заварювати ОС X:

http://brew.sh

Остання гіта на Ubuntu

sudo apt-get update
sudo apt-get install git
git --version

Якщо це не працює (у вас є дуже стара версія ubuntu), спробуйте

sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git

Якщо це все одно не працює, спробуйте

sudo chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh
sudo ln -s \
/usr/share/doc/git/contrib/subtree/git-subtree.sh \
/usr/lib/git-core/git-subtree

Завдяки rui.araujo з коментарів.

очищення історії

За замовчуванням видалення файлів з git насправді не видаляє їх з git, він просто зобов'язує їх там більше не існує. Якщо ви хочете фактично видалити історичні посилання (наприклад, ви зробили пароль), вам потрібно це зробити:

git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD

Після цього ви можете перевірити, що ваш файл або папка більше не відображається в історії git взагалі

git log -- <name-of-folder> # should show nothing

Тим не менш не може "натиснути" видалити на github і тому подібне. Якщо ви спробуєте, ви отримаєте помилку, і вам доведеться git pull перш ніж зможеш git push - і тоді ти повернувся до того, що маєте все у вашій історії.

Отже, якщо ви хочете видалити історію з "походження" - тобто видалити його з github, bitbucket тощо - вам потрібно видалити репо та повторно натиснути обрізану копію репо. Але чекай - там більше! - Якщо ви дійсно стурбовані тим, як позбутися пароля або щось подібне, вам потрібно буде обрізати резервну копію (див. Нижче).

виготовлення .git менший

Вищезгадана команда видалення історії все ще залишає собою безліч файлів резервної копії - адже GIT - це занадто приємно, допомагаючи вам не випадково зруйнувати репо. Це врешті-решт видаляє сирітські файли протягом декількох днів і місяців, однак вони залишать їх на деякий час, якщо ви зрозумієте, що ви випадково видалили те, чого ви не хотіли.

Отже, якщо ти дійсно хочеш викиньте сміття до зменшити розмір клону від репо відразу ж ви повинні зробити все це дійсно дивне речі:

rm -rf .git/refs/original/ && \
git reflog expire --all && \
git gc --aggressive --prune=now

git reflog expire --all --expire-unreachable=0
git repack -A -d
git prune

Тим не менш, рекомендую не виконувати ці кроки, якщо не знаєте, що вам потрібно - лише у випадку, якщо ви обрізали неправильний підкаталог, знаєте? Файли резервної копії не повинні клонуватись, коли ви натискаєте репо, вони просто будуть у вашій локальній копії.

Кредит


1124
2018-06-05 13:15



git subtree як і раніше, є частиною папки contrib і не встановлюється за умовчанням у всіх дистрибутивах. github.com/git/git/blob/master/contrib/subtree - onionjake
@krlmlr sudo chmod + x /usr/share/doc/git/contrib/subtree/git-subtree.sh sudo ln -s /usr/share/doc/git/contrib/subtree/git-subtree.sh / usr / lib / git-core / git-subtree Щоб активувати в Ubuntu 13.04 - rui.araujo
Якщо ви натиснули пароль у загальнодоступний сховище, вам слід змінити пароль, не намагатися вилучити його з публічного репо та сподіватися, що ніхто його не бачив. - Miles Rout
це здається зробити новий репо з вмістом ABC/, але нова репо не містить папку ABC/ сама, як задано питання. Як би ти це зробив? - woojoo666
Це рішення не зберігає історію. - Cœur


Відповідь Павла створює новий репозиторій, що містить / ABC, але не видаляє / ABC з / XYZ. Наступна команда видаляє / ABC зсередини / XYZ:

git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD

Зрозуміло, спочатку перевірте його в сховищі "клон - нелегкий зв'язок", і слідкуйте за ним зі скиданням, командою gc і командами "Пруньки" списків "Павло".


131
2017-10-19 21:10



зробити це git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD і це буде багато чого швидше Індекс-фільтр працює на індексі, а дерев-фільтр - для перевірки та етапу все для кожної фіксації. - fmarc
в деяких випадках збиває історію репозиторію XYZ - це надмірна програма ... просто простий "rm -rf ABC; git rm -r ABC; git commit -m" витягне ABC у власну репо "" буде працювати краще для більшості людей. - Evgeny
Ви, напевно, хочете використовувати -f (силові) за цією командою, якщо ви це робите кілька разів, наприклад, щоб видалити два каталоги після того, як вони були розділені. В іншому випадку ви отримаєте "Неможливо створити нову резервну копію". - Brian Carlton
Якщо ви робите це --index-filter метод, ви також можете зробити це git rm -q -r -f, так що кожен виклик не буде друкувати рядок для кожного файлу, який він видаляє. - Eric Naeseth
Я б запропонував відредагувати відповідь Павла тільки тому, що Павло настільки досконалий. - Erik Aronesty


Я виявив, що для правильного видалення старої історії з нового сховища ви повинні зробити трохи більше роботи після filter-branch крок

  1. Зробіть клон та фільтр:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Видаліть усі посилання на стару історію. "Походження" відслідковував ваш клон, а "оригінал" - це місце збереження старого матеріалу:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. Навіть зараз ваша історія може застрягти в пакетному файлі, що fsck не торкнеться. Зірвайте це на шматки, створіть нову упаковку та видаліть невикористані об'єкти:

    git repack -ad
    

є пояснення цього в Інструкція для фільтрації.


94
2018-06-09 15:41



Я думаю, що ти хочеш git gc --aggressive --prune=now все ще відсутня, чи не так? - Albert
@Albert Команда repack піклується про це, і не буде ніяких вільних об'єктів. - Josh Lee
просто перепакування не працювало для мене, потрібно робити git gc - jsvnm
так git gc --aggressive --prune=now зменшив значну частину нового репо - Tomek Wyderka
Простий і елегантний. Дякую! - Marco Pelegrini


Редагування: додано сценарій Bash.

Відповіді, наведені тут, працювали частково для мене; Багато великих файлів залишилося в кеш-пам'яті. Що нарешті працювало (через години в #git на freenode):

git clone --no-hardlinks file:///SOURCE /tmp/blubb
cd blubb
git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT  --prune-empty --tag-name-filter cat -- --all
git clone file:///tmp/blubb/ /tmp/blooh
cd /tmp/blooh
git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

За попередніми рішеннями розмір сховища становив близько 100 Мб. Це привело його до 1,7 МБ. Може, це допомагає комусь :)


Наступний баш-скрипт автоматизує завдання:

!/bin/bash

if (( $# < 3 ))
then
    echo "Usage:   $0 </path/to/repo/> <directory/to/extract/> <newName>"
    echo
    echo "Example: $0 /Projects/42.git first/answer/ firstAnswer"
    exit 1
fi


clone=/tmp/${3}Clone
newN=/tmp/${3}

git clone --no-hardlinks file://$1 ${clone}
cd ${clone}

git filter-branch --subdirectory-filter $2  --prune-empty --tag-name-filter cat -- --all

git clone file://${clone} ${newN}
cd ${newN}

git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

38
2017-08-20 14:11





Це вже не так складно, ви можете просто скористатись гіт-фільтр-гілка команду на клон вашого репо, щоб видалити підкаталоги, які ви не бажаєте, а потім натисніть на новий пульт дистанційного керування.

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .

21
2018-03-22 20:55



Це працювало як чарівність. YOUR_SUBDIR у наведеному вище прикладі є підкаталог, який ви хочете зберегти, все інше буде видалено - J.T. Taylor
Оновлення на основі коментарів. - jeremyjjbrown
Це не відповідає на питання. З документів це говорить The result will contain that directory (and only that) as its project root. і це саме те, що ви отримаєте, тобто оригінальна структура проекту не збережена. - NicBright
@NicBright Чи можете ви проілюструвати свою проблему з XYZ та ABC як у питанні, щоб показати, що не так? - Adam
@jeremyjjbrown чи можна повторно використовувати клонований репо та не використовувати нову репо, тобто моє запитання тут stackoverflow.com/questions/49269602/... - Qiulang


Оновити: Модуль git-subtree був настільки корисний, що команда git витягнула його в основу і зробила це git subtree. Дивіться тут: Від'єднати (перемістити) підкаталог до окремого сховища Git

git-subtree може бути корисним для цього

http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (не підтримується)

http://psionides.jogger.pl/2010/02/04/sharing-code-between-projects-with-git-subtree/


19
2017-08-06 15:26



git-subtree тепер є частиною Git, хоча це і є в дереві contrib, так що не завжди встановлюється за замовчуванням. Я знаю, що вона встановлена ​​за формулою Homeclick Git, але без її сторінки. Таким чином, apenwarr називає його версію застарілою. - echristopherson


Ось невелика модифікація до CoolAJ86с Відповідь "Easy Way ™" для розколу кілька підпапок (скажімо sub1і sub2) в новий сховище git.

Easy Way ™ (кілька підпапок)

  1. Підготуйте стару репо

    pushd <big-repo>
    git filter-branch --tree-filter "mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Примітка:  <name-of-folder> Не повинно містити провідних чи кінцевих символів. Наприклад, папка з ім'ям subproject ПОВИНЕН бути переданий як subprojectНЕ ./subproject/

    Примітка для користувачів Windows: коли ваша глибина папки> 1, <name-of-folder> повинен мати сепаратор папки типу nix (/). Наприклад, папка з ім'ям path1\path2\subproject ПОВИНЕН бути переданий як path1/path2/subproject. Більш того, не використовуйте mvкоманда але move.

    Остаточна примітка: унікальна і велика різниця з базовою відповіддю - це друга лінія сценарію "git filter-branch..."

  2. Створіть новий репо

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Зв'яжіть нове репо з Github або де завгодно

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Прибирати, за бажанням

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Примітка: Це залишає всі історичні посилання в репозиторії. Див Додаток в оригінальній відповіді, якщо ви дійсно стурбовані вчиненням пароля, або вам потрібно зменшити розмір файлу вашої .git папка


13
2018-04-17 05:12



Це працювало для мене з невеликою зміною. Тому що мій sub1 і sub2 Папки не існували з початковою версією, мені довелося модифікувати мою --tree-filter скрипт наступним чином: "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi". Для другого filter-branch команда I замінила <sub1> з <sub2>, опустила створення <ім'я-з-папки>, і включено -f після filter-branch перевизначити попередження про існуючу резервну копію. - pglezen
Це не спрацює, якщо будь-який підрозділ змінився під час історії в git. Як це можна вирішити? - nietras
@nietras див. відповідь rogerdpack. Я взяв час, щоб знайти його після прочитання та поглинання всієї інформації в цих інших відповідях. - Adam


Початкове запитання вимагає, щоб файли XYZ / ABC / (*) ставали ABC / ABC / (* файлами). Після реалізації прийнятої відповіді на власний код я помітив, що він дійсно змінює XYZ / ABC / (* файли) на ABC / (* файли). Сторінка фільтра-гілка навіть говорить:

Результат буде містити цей каталог (і тільки це) як його корінь проекту"

Інакше кажучи, він просуває верхній рівень папки "вгору" на один рівень. Це важлива відмінність, оскільки, наприклад, в моїй історії я перейменував папку верхнього рівня. Просунувши папки "вгору" на один рівень, git втрачає спадкоємність на фіксації, де я перейменовував.

I lost contiuity after filter-branch

Тоді моя відповідь на це питання полягає в тому, щоб зробити 2 копії репозиторію та видалити вручну ті папки, які ви хочете зберегти в кожному. Сторінка людини підтримує таке:

[...] уникати використання [цієї команди], якщо для виправлення вашої проблеми вистачить простої одиночної дії


11
2017-07-25 10:01



Мені подобається стиль цього графа. Чи можу я запитати, який інструмент ви використовуєте? - Slipp D. Thompson
Башта для Mac. Мені дуже подобається. Це майже варто переключитися на Mac для себе. - MM.
Так, хоча в моєму випадку, моє підпапки targetdir був перейменований в якийсь момент і git filter-branch просто називав це день, видаливши всі зобов'язання, зроблені до перейменування! Шокуюча, розглядаючи, як майстер Git стежить за подібними речами і навіть міграцією окремих фрагментів змісту! - Jay Allen
О, також, якщо хтось опиняється на тому ж човні, ось команда, яку я використав. Не забувай, що git rm приймає кілька аргументів, тому немає підстав для запуску для кожного файлу / папки: BYEBYE="dir/subdir2 dir2 file1 dir/file2"; git filter-branch -f --index-filter "git rm -q -r -f --cached --ignore-unmatch $BYEBYE" --prune-empty -- --all - Jay Allen


Додати до Відповідь Павла, Я виявив, що, зрештою, відновлюючи простір, я повинен натискати HEAD на чистий сховище, а це зменшує розмір каталогу .git / objects / pack.

тобто

$ mkdir ... ABC.git
$ cd ... ABC.git
$ git init - березня

Після того як gc prune, також:

$ git push ... ABC.git HEAD

Тоді ти можеш це зробити

$ git clone ... ABC.git

і розмір ABC / .git зменшується

Насправді, деякі етапи, що витрачають час (наприклад, git gc), не потрібні, якщо натиснути для очищення сховища, тобто:

Клон $ git - без жорстких зв'язків / XYZ / ABC
$ git filter-branch --subdirectory-filter ABC HEAD
$ git скинути - серйозно
$ git push ... ABC.git HEAD

7
2017-11-12 13:22





Правильний шлях зараз:

git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]

GitHub тепер навіть є маленька стаття про такі випадки.

Але спочатку спочатку клонувати свій оригінальний репо для окремої директорії (оскільки він буде видаляти всі файли та інші каталоги, і ви, ймовірно, повинні працювати з ними).

Тому ваш алгоритм повинен бути таким:

  1. клонувати ваш віддалений репо в інший каталог
  2. використовуючи git filter-branch залишили лише файли під деякими підкаталогами, натискали на новий пульт дистанційного керування
  3. Створіть "Обов'язково", щоб видалити цей підкаталог з вашого оригінального віддаленого репо

5
2017-09-19 18:46