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


115

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

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

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



Это не часть первоначального вопроса, но что было бы еще круче, это способ сохранить историю файлов, которые были запущены вне папки и были перемещены в нее. На данный момент все ответы теряют всю историю до переезда.
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может быть полезен
a

-- --allможно заменить на имя ветки, если подмодуль должен быть извлечен только из этой ветки.
adius

Есть ли git clone <your_project> <your_submodule>только скачивать файлы для your_submodule?
Dominic

@DominicTobias: git clone source destinationпросто сообщает Git расположение, куда поместить ваши клонированные файлы. На этом filter-branchшаге происходит настоящая магия фильтрации файлов вашего подмодуля .
Knittl

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

24

Сначала измените каталог на папку, которая будет подмодулем. Затем:

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файл вспомогательной команды и тому подобное, и протолкнуть сами истории подмодулей куда угодно вы хотите.

Сама команда с разделением находится здесь, документ в комментариях, в следующем разделе без разделов. Запустите его как отдельную команду с subdirset, как 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.