Ретроспективно перетворити папку Git в підмодуль?


115

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

Чи є спосіб перетворити один із підкаталогів у проекті Git в підмодуль?

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


stackoverflow.com/questions/1365541/… може допомогти деяким :)
Роб Паркер

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

2
@ ggll посилання не працює. Ось архівна копія.
s3cur3

Відповіді:


84

Щоб виділити підкаталог у власне сховище, використовуйте filter-branchна клоні оригінального сховища:

git clone <your_project> <your_submodule>
cd <your_submodule>
git filter-branch --subdirectory-filter 'path/to/your/submodule' --prune-empty -- --all

Тоді це не що інше, як видалення оригінального каталогу та додавання підмодуля до батьківського проекту.


18
Ви, ймовірно, також хочете git remote rm <name>після гілки фільтра, а потім, можливо, додати новий пульт. Також, якщо є ігноровані файли, git clean -xd -fможе бути корисним
naught101

-- --allможе бути замінено назвою гілки, якщо підмодуль повинен бути вилучений лише з цієї гілки.
adius

Є чи git clone <your_project> <your_submodule>тільки завантажувати файли для your_submodule?
Домінік

@DominicTobias: git clone source destinationпросто повідомляє Git про місце, куди потрібно класти клоновані файли. Дійсна магія фільтрувати файли вашого підмодуля відбувається тоді на filter-branchкроці.
knittl

filter-branchє застарілим в даний час. Можна використовувати git clone --filter, але ваш сервер Git повинен бути налаштований так, щоб дозволити фільтрацію, інакше ви отримаєте warning: filtering not recognized by server, ignoring.
Маттіас Браун

24

Спочатку змініть dir у папку, яка буде підмодулем. Тоді:

git init
git remote add origin repourl
git add .
git commit -am'first commit in submodule'
git push -u origin master
cd ..
rm -rf folder wich will be a submodule
git commit -am'deleting folder'
git submodule add repourl folder wich will be a submodule
git commit -am'adding submodule'

9
Це втратить всю історію цієї папки.
naught101

6
історія папки буде збережена в головному сховищі, а нові коміти збережуть історію в підмодулі
zednight

11

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

Простий спосіб клонувати та зберігати всі ці додаткові гілки та зобов'язання:

1 - Переконайтеся, що у вас є цей псевдонім git

git config --global alias.clone-branches '! git branch -a | sed -n "/\/HEAD /d; /\/master$/d; /remotes/p;" | xargs -L1 git checkout -t'

2 - Клоніруйте пульт, потягніть всі гілки, змініть пульт, фільтруйте каталог, натисніть

git clone git@github.com:user/existing-repo.git new-repo
cd new-repo
git clone-branches
git remote rm origin
git remote add origin git@github.com:user/new-repo.git
git remote -v
git filter-branch --subdirectory-filter my_directory/ -- --all
git push --all
git push --tags

1
У моєму оригіналі було посилання на суть замість вставки коду тут на SO
oodavid

1

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


0

Статус кво

Давайте припустимо , що у нас є сховище під назвою , repo-oldякий містить допоміжний каталог sub , який ми хотіли б перетворити в суб модуль з його власним репо repo-sub.

Крім того, передбачається, що оригінальний РЕПО repo-oldповинен бути перетворений в модифікований РЕПО, repo-newде всі комісії, що торкаються раніше існуючого підкаталогу sub, тепер повинні вказувати на відповідні комітети нашого видобутого підмодуля репо repo-sub.

Давайте змінимось

Домогтися цього можна за допомогою git filter-branchдвоступеневого процесу:

  1. Вилучення підкаталогу від repo-oldдо repo-sub(вже згадувалося у прийнятій відповіді )
  2. Заміна підкаталога з repo-oldна repo-new(з належним картографуванням фіксації)

Зауваження : Я знаю, що це питання давнє, і вже згадувалося, що воно git filter-branchє виснаженим і може бути небезпечним. Але з іншого боку, це може допомогти іншим із особистими сховищами, які легко перевірити після конверсії. Тож будьте попереджені ! І, будь ласка, повідомте мене, чи є якийсь інший інструмент, який робить те саме, без застарілого та безпечного використання!

Я поясню, як я реалізував обидва кроки на Linux з git версією 2.26.2 нижче. Старіші версії можуть працювати певною мірою, але це потрібно перевірити.

Для простоти я обмежуся випадком, коли в оригінальному репо є лише masterгілка та originпульт repo-old. Також слід попередити, що я покладаюсь на тимчасові теги git з префіксом, temp_які буде видалено в процесі. Отже, якщо вже є теги, названі подібним чином, ви можете скоригувати префікс нижче. І нарешті, будь ласка, майте на увазі, що я цього ще не пройшов тестування, і, можливо, є певні випадки, коли рецепт виходить з ладу. Тому, будь ласка, резервні копії всього, перш ніж продовжувати !

Наступні фрагменти bash можна об'єднати в один великий сценарій, який потім повинен бути виконаний у тій же папці, де repo-orgживе репо . Не рекомендується копіювати та вставляти все безпосередньо у командне вікно (хоча я це успішно перевірив)!

0. Підготовка

Змінні

# Root directory where repo-org lives
# and a temporary location for git filter-branch
root="$PWD"
temp='/dev/shm/tmp'

# The old repository and the subdirectory we'd like to extract
repo_old="$root/repo-old"
repo_old_directory='sub'

# The new submodule repository, its url
# and a hash map folder which will be populated
# and later used in the filter script below
repo_sub="$root/repo-sub"
repo_sub_url='https://github.com/somewhere/repo-sub.git'
repo_sub_hashmap="$root/repo-sub.map"

# The new modified repository, its url
# and a filter script which is created as heredoc below
repo_new="$root/repo-new"
repo_new_url='https://github.com/somewhere/repo-new.git'
repo_new_filter="$root/repo-new.sh"

Сценарій фільтра

# The index filter script which converts our subdirectory into a submodule
cat << EOF > "$repo_new_filter"
#!/bin/bash

# Submodule hash map function
sub ()
{
    local old_commit=\$(git rev-list -1 \$1 -- '$repo_old_directory')

    if [ ! -z "\$old_commit" ]
    then
        echo \$(cat "$repo_sub_hashmap/\$old_commit")
    fi
}

# Submodule config
SUB_COMMIT=\$(sub \$GIT_COMMIT)
SUB_DIR='$repo_old_directory'
SUB_URL='$repo_sub_url'

# Submodule replacement
if [ ! -z "\$SUB_COMMIT" ]
then
    touch '.gitmodules'
    git config --file='.gitmodules' "submodule.\$SUB_DIR.path" "\$SUB_DIR"
    git config --file='.gitmodules' "submodule.\$SUB_DIR.url" "\$SUB_URL"
    git config --file='.gitmodules' "submodule.\$SUB_DIR.branch" 'master'
    git add '.gitmodules'

    git rm --cached -qrf "\$SUB_DIR"
    git update-index --add --cacheinfo 160000 \$SUB_COMMIT "\$SUB_DIR"
fi
EOF
chmod +x "$repo_new_filter"

1. Підрозділ вилучення

cd "$root"

# Create a new clone for our new submodule repo
git clone "$repo_old" "$repo_sub"

# Enter the new submodule repo
cd "$repo_sub"

# Remove the old origin remote
git remote remove origin

# Loop over all commits and create temporary tags
for commit in $(git rev-list --all)
do
    git tag "temp_$commit" $commit
done

# Extract the subdirectory and slice commits
mkdir -p "$temp"
git filter-branch --subdirectory-filter "$repo_old_directory" \
                  --tag-name-filter 'cat' \
                  --prune-empty --force -d "$temp" -- --all

# Populate hash map folder from our previously created tag names
mkdir -p "$repo_sub_hashmap"
for tag in $(git tag | grep "^temp_")
do
    old_commit=${tag#'temp_'}
    sub_commit=$(git rev-list -1 $tag)

    echo $sub_commit > "$repo_sub_hashmap/$old_commit"
done
git tag | grep "^temp_" | xargs -d '\n' git tag -d 2>&1 > /dev/null

# Add the new url for this repository (and e.g. push)
git remote add origin "$repo_sub_url"
# git push -u origin master

2. Заміна підкаталога

cd "$root"

# Create a clone for our modified repo
git clone "$repo_old" "$repo_new"

# Enter the new modified repo
cd "$repo_new"

# Remove the old origin remote
git remote remove origin

# Replace the subdirectory and map all sliced submodule commits using
# the filter script from above
mkdir -p "$temp"
git filter-branch --index-filter "$repo_new_filter" \
                  --tag-name-filter 'cat' --force -d "$temp" -- --all

# Add the new url for this repository (and e.g. push)
git remote add origin "$repo_new_url"
# git push -u origin master

# Cleanup (commented for safety reasons)
# rm -rf "$repo_sub_hashmap"
# rm -f "$repo_new_filter"

Зауваження: Якщо новостворене репо repo-newвисить під час, git submodule update --initспробуйте повторно клонувати сховище рекурсивно один раз:

cd "$root"

# Clone the new modified repo recursively
git clone --recursive "$repo_new" "$repo_new-tmp"

# Now use the newly cloned one
mv "$repo_new" "$repo_new-bak"
mv "$repo_new-tmp" "$repo_new"

# Cleanup (commented for safety reasons)
# rm -rf "$repo_new-bak"

0

Це робить перетворення на місці, ви можете створити резервне копіювання, як і будь-який фільтр-гілка (я використовую git fetch . +refs/original/*:*).

У мене є проект з utilsбібліотекою, який став корисним для інших проектів і хотів розділити його історію на підмодулі. Не думав спочатку дивитися на SO, тому я написав свою власну, вона будує історію локально, так що це трохи швидше, після чого, якщо ви хочете, ви можете встановити .gitmodulesфайл допоміжної команди і таке інше , а самі перенести історії підмодуля куди завгодно ти хочеш.

Сама знімається команда тут, документ у коментарях, у розгорнутій наступній. Запустіть його як власну команду з subdirнабором, як, наприклад, subdir=utils git split-submoduleякщо ви розділяєте utilsкаталог. Це хакі, тому що він одноразовий, але я перевірив його у підкаталозі Documentation в історії Git.

#!/bin/bash
# put this or the commented version below in e.g. ~/bin/git-split-submodule
${GIT_COMMIT-exec git filter-branch --index-filter "subdir=$subdir; ${debug+debug=$debug;} $(sed 1,/SNIP/d "$0")" "$@"}
${debug+set -x}
fam=(`git rev-list --no-walk --parents $GIT_COMMIT`)
pathcheck=(`printf "%s:$subdir\\n" ${fam[@]} \
    | git cat-file --batch-check='%(objectname)' | uniq`)
[[ $pathcheck = *:* ]] || {
    subfam=($( set -- ${fam[@]}; shift;
        for par; do tpar=`map $par`; [[ $tpar != $par ]] &&
            git rev-parse -q --verify $tpar:"$subdir"
        done
    ))
    git rm -rq --cached --ignore-unmatch  "$subdir"
    if (( ${#pathcheck[@]} == 1 && ${#fam[@]} > 1 && ${#subfam[@]} > 0)); then
        git update-index --add --cacheinfo 160000,$subfam,"$subdir"
    else
        subnew=`git cat-file -p $GIT_COMMIT | sed 1,/^$/d \
            | git commit-tree $GIT_COMMIT:"$subdir" $(
                ${subfam:+printf ' -p %s' ${subfam[@]}}) 2>&-
            ` &&
        git update-index --add --cacheinfo 160000,$subnew,"$subdir"
    fi
}
${debug+set +x}

#!/bin/bash
# Git filter-branch to split a subdirectory into a submodule history.

# In each commit, the subdirectory tree is replaced in the index with an
# appropriate submodule commit.
# * If the subdirectory tree has changed from any parent, or there are
#   no parents, a new submodule commit is made for the subdirectory (with
#   the current commit's message, which should presumably say something
#   about the change). The new submodule commit's parents are the
#   submodule commits in any rewrites of the current commit's parents.
# * Otherwise, the submodule commit is copied from a parent.

# Since the new history includes references to the new submodule
# history, the new submodule history isn't dangling, it's incorporated.
# Branches for any part of it can be made casually and pushed into any
# other repo as desired, so hooking up the `git submodule` helper
# command's conveniences is easy, e.g.
#     subdir=utils git split-submodule master
#     git branch utils $(git rev-parse master:utils)
#     git clone -sb utils . ../utilsrepo
# and you can then submodule add from there in other repos, but really,
# for small utility libraries and such, just fetching the submodule
# histories into your own repo is easiest. Setup on cloning a
# project using "incorporated" submodules like this is:
#   setup:  utils/.git
#
#   utils/.git:
#       @if _=`git rev-parse -q --verify utils`; then \
#           git config submodule.utils.active true \
#           && git config submodule.utils.url "`pwd -P`" \
#           && git clone -s . utils -nb utils \
#           && git submodule absorbgitdirs utils \
#           && git -C utils checkout $$(git rev-parse :utils); \
#       fi
# with `git config -f .gitmodules submodule.utils.path utils` and
# `git config -f .gitmodules submodule.utils.url ./`; cloners don't
# have to do anything but `make setup`, and `setup` should be a prereq
# on most things anyway.

# You can test that a commit and its rewrite put the same tree in the
# same place with this function:
# testit ()
# {
#     tree=($(git rev-parse `git rev-parse $1`: refs/original/refs/heads/$1));
#     echo $tree `test $tree != ${tree[1]} && echo ${tree[1]}`
# }
# so e.g. `testit make~95^2:t` will print the `t` tree there and if
# the `t` tree at ~95^2 from the original differs it'll print that too.

# To run it, say `subdir=path/to/it git split-submodule` with whatever
# filter-branch args you want.

# $GIT_COMMIT is set if we're already in filter-branch, if not, get there:
${GIT_COMMIT-exec git filter-branch --index-filter "subdir=$subdir; ${debug+debug=$debug;} $(sed 1,/SNIP/d "$0")" "$@"}

${debug+set -x}
fam=(`git rev-list --no-walk --parents $GIT_COMMIT`)
pathcheck=(`printf "%s:$subdir\\n" ${fam[@]} \
    | git cat-file --batch-check='%(objectname)' | uniq`)

[[ $pathcheck = *:* ]] || {
    subfam=($( set -- ${fam[@]}; shift;
        for par; do tpar=`map $par`; [[ $tpar != $par ]] &&
            git rev-parse -q --verify $tpar:"$subdir"
        done
    ))

    git rm -rq --cached --ignore-unmatch  "$subdir"
    if (( ${#pathcheck[@]} == 1 && ${#fam[@]} > 1 && ${#subfam[@]} > 0)); then
        # one id same for all entries, copy mapped mom's submod commit
        git update-index --add --cacheinfo 160000,$subfam,"$subdir"
    else
        # no mapped parents or something changed somewhere, make new
        # submod commit for current subdir content.  The new submod
        # commit has all mapped parents' submodule commits as parents:
        subnew=`git cat-file -p $GIT_COMMIT | sed 1,/^$/d \
            | git commit-tree $GIT_COMMIT:"$subdir" $(
                ${subfam:+printf ' -p %s' ${subfam[@]}}) 2>&-
            ` &&
        git update-index --add --cacheinfo 160000,$subnew,"$subdir"
    fi
}
${debug+set +x}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.