Einen Git-Ordner nachträglich in ein Submodul konvertieren?

115

Sehr oft schreiben Sie ein Projekt, und nach einer Weile wird klar, dass eine Komponente des Projekts tatsächlich als eigenständige Komponente (möglicherweise eine Bibliothek) nützlich ist. Wenn Sie diese Idee von Anfang an hatten, besteht eine gute Chance, dass sich der größte Teil dieses Codes in einem eigenen Ordner befindet.

Gibt es eine Möglichkeit, eines der Unterverzeichnisse in einem Git-Projekt in ein Submodul zu konvertieren?

Im Idealfall geschieht dies so, dass der gesamte Code in diesem Verzeichnis aus dem übergeordneten Projekt entfernt wird und das Submodulprojekt an seiner Stelle mit dem entsprechenden Verlauf hinzugefügt wird und alle Commits des übergeordneten Projekts auf die richtigen Commits des Submoduls verweisen .

naught101
quelle
stackoverflow.com/questions/1365541/… kann einigen helfen :)
Rob Parker
Dies ist nicht Teil der ursprünglichen Frage, aber was noch cooler wäre, wäre eine Möglichkeit, den Verlauf von Dateien beizubehalten, die außerhalb des Ordners gestartet und in diesen verschoben wurden. Im Moment verlieren alle Antworten die gesamte Historie vor dem Umzug.
naught101
2
Der Link von @ ggll ist ausgefallen. Hier ist eine archivierte Kopie.
s3cur3

Antworten:

84

Verwenden Sie filter-branchauf einem Klon des ursprünglichen Repositorys Folgendes, um ein Unterverzeichnis in ein eigenes Repository zu isolieren :

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

Es ist dann nichts weiter als das Löschen Ihres ursprünglichen Verzeichnisses und das Hinzufügen des Submoduls zu Ihrem übergeordneten Projekt.

stricken
quelle
18
Sie möchten wahrscheinlich auch git remote rm <name>nach dem Filterzweig eine neue Fernbedienung hinzufügen. Auch wenn es ignorierte Dateien gibt, git clean -xd -fkann a nützlich sein
naught101
-- --allkann durch den Namen eines Zweigs ersetzt werden, wenn das Submodul nur aus diesem Zweig extrahiert werden soll.
Adius
Ist git clone <your_project> <your_submodule>nur Download - Dateien für your_submodule?
Dominic
@DominicTobias: git clone source destinationteilt Git einfach mit, wo sich Ihre geklonten Dateien befinden sollen. Die eigentliche Magie zum Filtern der Dateien Ihres Submoduls geschieht dann im filter-branchSchritt.
Knittl
filter-branchist heutzutage veraltet . Sie können verwenden git clone --filter, aber Ihr Git-Server muss so konfiguriert sein, dass Filterung möglich ist, sonst erhalten Sie warning: filtering not recognized by server, ignoring.
Matthias Braun
24

Ändern Sie zuerst das Verzeichnis in einen Ordner, der ein Submodul sein wird. Dann:

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'
Zednight
quelle
9
Dadurch geht der gesamte Verlauf dieses Ordners verloren.
naught101
6
Der Verlauf des Ordners wird im Haupt-Repository gespeichert und neue Commits speichern den Verlauf im Submodul
zednight
11

Ich weiß, dass dies ein alter Thread ist, aber die Antworten hier unterdrücken alle damit verbundenen Commits in anderen Branchen.

Eine einfache Möglichkeit, all diese zusätzlichen Zweige und Commits zu klonen und zu behalten:

1 - Stellen Sie sicher, dass Sie diesen Git-Alias ​​haben

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

2 - Klonen Sie die Fernbedienung, ziehen Sie alle Zweige, wechseln Sie die Fernbedienung, filtern Sie Ihr Verzeichnis und drücken Sie

git clone [email protected]:user/existing-repo.git new-repo
cd new-repo
git clone-branches
git remote rm origin
git remote add origin [email protected]:user/new-repo.git
git remote -v
git filter-branch --subdirectory-filter my_directory/ -- --all
git push --all
git push --tags
oodavid
quelle
1
Mein Original hatte einen Link zu einem Kern, anstatt den Code hier auf SO
oodavid
1

Es kann getan werden, aber es ist nicht einfach. Wenn Sie suchen git filter-branch, subdirectoryund submodulegibt es ein paar anständige Zuschreibung auf den Prozess. Im Wesentlichen müssen zwei Klone Ihres Projekts erstellt werden, mit denen git filter-branchalles außer dem einen Unterverzeichnis in einem entfernt wird und nur dieses Unterverzeichnis im anderen entfernt wird. Anschließend können Sie das zweite Repository als Submodul des ersten einrichten.

Twalberg
quelle
0

Status Quo

Nehmen wir an , wir haben ein Repository, genannt , repo-olddie eine Unter enthält Verzeichnis sub , dass wir in eine Unter umwandeln möchte Modul mit einem eigenen Repo repo-sub.

Es ist ferner beabsichtigt, das ursprüngliche Repo repo-oldin ein modifiziertes Repo umzuwandeln, repo-newwobei alle Commits, die das zuvor vorhandene Unterverzeichnis berühren sub, nun auf die entsprechenden Commits unseres extrahierten Submodul-Repos verweisenrepo-sub .

Lass uns ändern

Dies kann mit Hilfe git filter-brancheines zweistufigen Prozesses erreicht werden:

  1. Unterverzeichnis-Extraktion von repo-oldbis repo-sub(bereits in der akzeptierten Antwort erwähnt )
  2. Ersetzen des Unterverzeichnisses von repo-oldbis repo-new(mit ordnungsgemäßer Festschreibungszuordnung)

Bemerkung : Ich weiß, dass diese Frage alt ist und bereits erwähnt wurde, dass sie git filter-branchveraltet ist und gefährlich sein könnte. Auf der anderen Seite kann es anderen helfen, persönliche Repositorys zu erstellen, die nach der Konvertierung leicht zu validieren sind. Also sei gewarnt ! Und bitte lassen Sie mich wissen, ob es ein anderes Tool gibt, das dasselbe tut, ohne veraltet zu sein und sicher zu verwenden ist!

Ich werde erklären, wie ich beide Schritte unter Linux mit der Git-Version 2.26.2 realisiert habe. Ältere Versionen funktionieren möglicherweise bis zu einem gewissen Grad, dies muss jedoch getestet werden.

Der Einfachheit halber beschränke ich mich auf den Fall, dass das ursprüngliche Repo nur einen masterZweig und eine originFernbedienung enthält repo-old. Seien Sie auch gewarnt, dass ich mich auf temporäre Git-Tags mit dem Präfix verlasse, temp_die dabei entfernt werden. Wenn also bereits Tags mit ähnlichen Namen vorhanden sind, können Sie das folgende Präfix anpassen. Und zum Schluss beachten Sie bitte, dass ich dies nicht ausführlich getestet habe und es möglicherweise Eckfälle gibt, in denen das Rezept fehlschlägt. Bitte sichern Sie alles, bevor Sie fortfahren !

Die folgenden Bash-Snippets können zu einem großen Skript verkettet werden, das dann in demselben Ordner ausgeführt werden soll, in dem sich das Repo befindet repo-org. Es wird nicht empfohlen, alles direkt in ein Befehlsfenster zu kopieren und einzufügen (obwohl ich dies erfolgreich getestet habe)!

0. Vorbereitung

Variablen

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

Filterskript

# 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. Extraktion des Unterverzeichnisses

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. Austausch des Unterverzeichnisses

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"

Anmerkung: Wenn das neu erstellte Repo repo-newwährenddessen hängt, git submodule update --initversuchen Sie stattdessen, das Repository einmal rekursiv neu zu klonen:

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"
PB
quelle
0

Dies führt die Konvertierung direkt durch. Sie können sie wie jeden Filterzweig (den ich verwende git fetch . +refs/original/*:*) zurücksetzen .

Ich habe ein Projekt mit einer utilsBibliothek, die in anderen Projekten nützlich ist, und wollte ihre Geschichte in Submodule aufteilen. Ich habe nicht daran gedacht, zuerst auf SO zu schauen, also habe ich mein eigenes geschrieben. Es erstellt den Verlauf lokal, so dass es ein gutes Stück schneller ist. Danach können Sie, wenn Sie möchten, die Hilfsbefehle einrichten.gitmodules Datei Hilfsbefehls und dergleichen und die Submodulverläufe selbst an eine beliebige Stelle verschieben Sie wollen.

Der Befehl zum Entfernen ist hier, das Dokument in den Kommentaren, in dem folgenden, der nicht entfernt wurde. Führen Sie es als eigenen Befehl mit subdirset aus, als würden subdir=utils git split-submoduleSie das utilsVerzeichnis aufteilen . Es ist hacky, weil es einmalig ist, aber ich habe es im Unterverzeichnis Documentation im Git-Verlauf getestet.

#!/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}
jthill
quelle