Convertire una cartella Git in un sottomodulo in modo retrospettivo?


115

Molto spesso accade che tu stia scrivendo un progetto di qualche tipo, e dopo un po 'diventa chiaro che qualche componente del progetto è effettivamente utile come componente autonomo (una libreria, forse). Se hai avuto questa idea sin dall'inizio, allora ci sono buone probabilità che la maggior parte di quel codice si trovi nella sua cartella.

C'è un modo per convertire una delle sottodirectory in un progetto Git in un sottomodulo?

Idealmente ciò accadrebbe in modo tale che tutto il codice in quella directory venga rimosso dal progetto padre e il progetto del sottomodulo venga aggiunto al suo posto, con tutta la cronologia appropriata, e in modo tale che tutti i commit del progetto padre puntino al commit sottomodulo corretto .



Questo non fa parte della domanda originale, ma ciò che sarebbe ancora più interessante sarebbe un modo per mantenere la cronologia dei file che erano iniziati fuori dalla cartella e sono stati spostati in essa. Al momento, tutte le risposte perdono tutta la cronologia prima del trasferimento.
nought101

2
Il collegamento di @ ggll non è attivo. Ecco una copia archiviata.
s3cur3

Risposte:


84

Per isolare una sottodirectory nel proprio repository, utilizzare filter-branchsu un clone del repository originale:

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

Quindi non è altro che eliminare la directory originale e aggiungere il sottomodulo al progetto padre.


18
Probabilmente vuoi anche git remote rm <name>dopo il ramo del filtro e quindi forse aggiungere un nuovo telecomando. Inoltre, se ci sono file ignorati, git clean -xd -fpuò essere utile
nought101

-- --allpuò essere sostituito con il nome di un ramo se il sottomodulo deve essere estratto solo da questo ramo.
adius

Non git clone <your_project> <your_submodule>scaricare solo i file per your_submodule?
Dominic

@DominicTobias: git clone source destinationdice semplicemente a Git la posizione in cui mettere i file clonati. La vera magia per filtrare i file del tuo sottomodulo avviene quindi nel filter-branchpassaggio.
maglia

filter-branchè deprecato al giorno d'oggi. Puoi usare git clone --filter, ma il tuo server Git deve essere configurato per consentire il filtraggio, altrimenti otterrai warning: filtering not recognized by server, ignoring.
Matthias Braun

24

Per prima cosa cambia la directory nella cartella che sarà un sottomodulo. Poi:

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
Ciò perderà tutta la cronologia di quella cartella.
nought101

6
la cronologia della cartella verrà salvata nel repository principale e i nuovi commit salveranno la cronologia nel sottomodulo
zednight

11

So che questo è un vecchio thread, ma le risposte qui eliminano qualsiasi commit correlato in altri rami.

Un modo semplice per clonare e mantenere tutti quei branch e commit extra:

1 - Assicurati di avere questo 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 - Clona il telecomando, estrai tutti i rami, cambia il telecomando, filtra la tua directory, premi

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
Il mio originale aveva un collegamento a un gist invece di incorporare il codice qui su SO
oodavid

1

Si può fare, ma non è semplice. Se cerchi git filter-branch, subdirectorye submodule, ci sono alcuni commenti decenti sul processo. Si tratta essenzialmente di creare due cloni del progetto, utilizzando git filter-branchper rimuovere tutto tranne una sottodirectory in una e rimuovere solo quella sottodirectory nell'altra. Quindi puoi stabilire il secondo repository come sottomodulo del primo.


0

Status quo

Supponiamo che abbiamo un repository chiamato repo-oldche contiene una sub directory sub che vorremmo convertire in un sub modulo con il proprio repo repo-sub.

È inoltre inteso che il repo originale repo-olddovrebbe essere convertito in un repo modificato in repo-newcui tutti i commit che toccano la sottodirectory esistente in precedenza subdevono ora puntare ai commit corrispondenti del nostro repo del sottomodulo estratto repo-sub.

Facciamo cambio

È possibile ottenere ciò con l'aiuto di git filter-branchun processo in due fasi:

  1. Estrazione di sottodirectory da repo-olda repo-sub(già menzionato nella risposta accettata )
  2. Sostituzione di sottodirectory da repo-olda repo-new(con mappatura commit corretta)

Nota : so che questa domanda è vecchia ed è già stato detto che git filter-branchè un po 'deprecata e potrebbe essere pericolosa. Ma d'altra parte potrebbe aiutare gli altri con archivi personali che sono facili da convalidare dopo la conversione. Quindi state attenti ! E per favore fatemi sapere se c'è qualche altro strumento che fa la stessa cosa senza essere deprecato ed è sicuro da usare!

Spiegherò come ho realizzato entrambi i passaggi su Linux con git versione 2.26.2 di seguito. Le versioni precedenti potrebbero funzionare in qualche modo, ma devono essere testate.

Per semplicità mi limiterò al caso in cui nel repository originale ci siano solo un masterramo e un origintelecomando repo-old. Inoltre, tieni presente che mi affido a tag git temporanei con il prefisso temp_che verranno rimossi durante il processo. Quindi, se ci sono già tag con nomi simili, potresti voler regolare il prefisso di seguito. Infine, tieni presente che non l'ho testato ampiamente e potrebbero esserci casi d'angolo in cui la ricetta fallisce. Quindi per favore fai il backup di tutto prima di procedere !

I seguenti frammenti di bash possono essere concatenati in un unico grande script che dovrebbe quindi essere eseguito nella stessa cartella in cui repo-orgrisiede il repository . Non è consigliabile copiare e incollare tutto direttamente in una finestra di comando (anche se l'ho testato con successo)!

0. Preparazione

variabili

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

Filtra script

# 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. Estrazione di sottodirectory

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. Sostituzione di sottodirectory

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"

Nota: se il repository appena creato si repo-newblocca durante, git submodule update --initprovare a ri-clonare il repository in modo ricorsivo una volta:

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

Questo fa la conversione sul posto, puoi ritirarla come faresti con qualsiasi ramo di filtro (io uso git fetch . +refs/original/*:*).

Ho un progetto con un file utils libreria che ha iniziato a essere utile in altri progetti e volevo suddividere la sua storia in sottomoduli. Non pensavo di guardare prima SO, quindi ho scritto il mio, costruisce la cronologia localmente quindi è un po 'più veloce, dopodiché se vuoi puoi impostare il .gitmodulesfile del comando di supporto e simili, e spingere le cronologie del sottomodulo stesse ovunque tu vuoi.

Il comando spogliato stesso è qui, il documento è nei commenti, in quello unstripped che segue. Eseguilo come un proprio comando, con subdirset, come subdir=utils git split-submodulese stessi dividendo la utilsdirectory. È hacky perché è una tantum, ma l'ho testato nella sottodirectory Documentation nella cronologia di 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}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.