Konwertować folder Git do modułu podrzędnego retrospektywnie?


115

Dość często jest tak, że piszesz jakiś projekt i po chwili staje się jasne, że jakiś komponent projektu jest faktycznie przydatny jako samodzielny komponent (być może biblioteka). Jeśli miałeś ten pomysł od samego początku, istnieje spora szansa, że ​​większość tego kodu znajduje się w swoim własnym folderze.

Czy istnieje sposób na przekonwertowanie jednego z podkatalogów w projekcie Git na moduł podrzędny?

Idealnie byłoby tak, że cały kod w tym katalogu zostanie usunięty z projektu nadrzędnego, a projekt modułu podrzędnego zostanie dodany w jego miejsce, z całą odpowiednią historią, i tak, że wszystkie zatwierdzenia projektu nadrzędnego wskazują na poprawny moduł podrzędny .



To nie jest część pierwotnego pytania, ale jeszcze fajniejszy byłby sposób na zachowanie historii plików, które rozpoczęły się poza folderem i zostały do ​​niego przeniesione. W tej chwili wszystkie odpowiedzi tracą całą historię sprzed przeprowadzki.
naught101

2
Link @ ggll nie działa. Oto zarchiwizowana kopia.
s3cur3

Odpowiedzi:


84

Aby wyodrębnić podkatalog do własnego repozytorium, użyj filter-branchna klonie oryginalnego repozytorium:

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

To nic innego jak usunięcie oryginalnego katalogu i dodanie modułu podrzędnego do projektu nadrzędnego.


18
Prawdopodobnie chcesz również git remote rm <name>po gałęzi filtru, a następnie być może dodać nowego pilota. Ponadto, jeśli są zignorowane pliki, git clean -xd -fmoże być przydatny
no101

-- --allmożna zastąpić nazwą gałęzi, jeśli podmoduł ma być wyodrębniony tylko z tej gałęzi.
adius

Czy git clone <your_project> <your_submodule>pobiera pliki tylko dla your_submodule?
Dominic

@DominicTobias: git clone source destinationpo prostu informuje Git o lokalizacji, w której należy umieścić sklonowane pliki. Właściwa magia filtrowania plików twojego modułu podrzędnego odbywa się następnie w filter-branchkroku.
knittl

filter-branchjest obecnie przestarzałe . Możesz użyć git clone --filter, ale serwer Git musi być skonfigurowany tak, aby zezwalał na filtrowanie, w przeciwnym razie otrzymasz warning: filtering not recognized by server, ignoring.
Matthias Braun

24

Najpierw zmień katalog na folder, który będzie modułem podrzędnym. Następnie:

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
Spowoduje to utratę całej historii tego folderu.
naught101

6
historia folderu zostanie zapisana w głównym repozytorium, a nowe zatwierdzenia
zapiszą

11

Wiem, że to stary wątek, ale odpowiedzi tutaj ograniczają wszelkie powiązane zatwierdzenia w innych gałęziach.

Prosty sposób na sklonowanie i zachowanie tych wszystkich dodatkowych gałęzi i zatwierdzeń:

1 - Upewnij się, że masz ten alias git

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

2 - Sklonuj pilota, ściągnij wszystkie gałęzie, zmień pilota, przefiltruj katalog, wciśnij

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
Mój oryginał miał link do istoty zamiast osadzania kodu tutaj na SO
oodavid

1

Można to zrobić, ale nie jest to proste. Jeśli szukać git filter-branch, subdirectoryi submoduleistnieje kilka przyzwoitych write-up na procesie. Zasadniczo pociąga to za sobą utworzenie dwóch klonów twojego projektu, użycie git filter-branchdo usunięcia wszystkiego oprócz jednego podkatalogu w jednym i usunięcie tylko tego podkatalogu w drugim. Następnie możesz ustawić drugie repozytorium jako podmoduł pierwszego.


0

Status quo

Załóżmy, że mamy repozytorium o nazwie repo-old, która zawiera sub katalogu sub , który chcielibyśmy przekształcić sub modułu z własnym repo repo-sub.

Zamiarem jest ponadto repo-oldprzekształcenie oryginalnego repozytorium w zmodyfikowane repozytorium, w repo-newktórym wszystkie zatwierdzenia dotykające wcześniej istniejącego podkatalogu subbędą teraz wskazywać na odpowiednie zatwierdzenia naszego wyodrębnionego repozytorium podmodułów repo-sub.

Zmieńmy się

Można to osiągnąć za pomocą git filter-branchdwuetapowego procesu:

  1. Ekstrakcja podkatalogu z repo-olddo repo-sub(już wspomniana w zaakceptowanej odpowiedzi )
  2. Zamiana podkatalogów z repo-oldna repo-new(z odpowiednim mapowaniem zatwierdzeń)

Uwaga : wiem, że to pytanie jest stare i zostało już wspomniane, że git filter-branchjest trochę przestarzałe i może być niebezpieczne. Ale z drugiej strony może pomóc innym dzięki osobistym repozytoriom, które można łatwo zweryfikować po konwersji. Więc uważaj ! I daj mi znać, jeśli istnieje inne narzędzie, które robi to samo, ale nie jest przestarzałe i jest bezpieczne w użyciu!

Poniżej wyjaśnię, jak zrealizowałem oba kroki na Linuksie z wersją git 2.26.2. Starsze wersje mogą działać w pewnym stopniu, ale należy to przetestować.

Dla uproszczenia ograniczę się do przypadku, gdy w oryginalnym repozytorium jest tylko mastergałąź i originpilot repo-old. Ostrzegam również, że korzystam z tymczasowych tagów git z prefiksem, temp_które zostaną usunięte w trakcie procesu. Jeśli więc istnieją już tagi o podobnych nazwach, możesz chcieć dostosować przedrostek poniżej. I na koniec, proszę, pamiętaj, że nie testowałem tego szczegółowo i mogą wystąpić narożne przypadki, w których przepis się nie powiedzie. Dlatego przed kontynuowaniem wykonaj kopię zapasową wszystkiego !

Następujące fragmenty basha można połączyć w jeden duży skrypt, który następnie powinien zostać wykonany w tym samym folderze, w którym znajduje się repozytorium repo-org. Nie zaleca się kopiowania i wklejania wszystkiego bezpośrednio do okna poleceń (mimo że pomyślnie przetestowałem to)!

0. Przygotowanie

Zmienne

# 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"

Skrypt filtru

# 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. Ekstrakcja podkatalogów

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. Zastąpienie podkatalogu

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"

Uwaga: Jeśli nowo utworzone repozytorium repo-newzawiesza się podczas, git submodule update --initspróbuj ponownie sklonować repozytorium rekurencyjnie raz zamiast tego:

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

To robi konwersję na miejscu, możesz ją wycofać, jak każdą gałąź filtra (używam git fetch . +refs/original/*:*).

Mam projekt z utilsbiblioteką, która zaczęła być użyteczna w innych projektach i chciałem podzielić jej historię na podmoduły. Nie pomyślałem, aby najpierw spojrzeć na SO, więc napisałem własną, buduje historię lokalnie, więc jest trochę szybsza, po czym, jeśli chcesz, możesz skonfigurować .gitmodulesplik polecenia pomocnika i tym podobne, i wrzucić historie podmodułów w dowolne miejsce chcesz.

Tutaj znajduje się samo polecenie pozbawione elementów, dokument w komentarzach, w następnym, bez ściągania. Uruchom go jako własne polecenie z subdirset, tak jak w subdir=utils git split-submoduleprzypadku dzielenia utilskatalogu. Jest to hacking, ponieważ jest jednorazowy, ale przetestowałem go w podkatalogu Documentation w historii 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}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.