Ubah folder Git menjadi submodul secara retrospektif?


115

Seringkali Anda menulis sebuah proyek, dan setelah beberapa saat menjadi jelas bahwa beberapa komponen proyek sebenarnya berguna sebagai komponen yang berdiri sendiri (perpustakaan, mungkin). Jika Anda sudah memiliki ide itu sejak awal, maka ada kemungkinan besar sebagian besar kode itu ada di foldernya sendiri.

Apakah ada cara untuk mengubah salah satu sub direktori dalam proyek Git menjadi submodul?

Idealnya ini akan terjadi sedemikian rupa sehingga semua kode dalam direktori itu dihapus dari proyek induk, dan proyek submodule ditambahkan di tempatnya, dengan semua sejarah yang sesuai, dan sedemikian rupa sehingga semua proyek induk menunjuk ke submodul yang benar melakukan .



Ini bukan bagian dari pertanyaan awal, tetapi apa yang lebih keren lagi adalah cara untuk menyimpan riwayat file yang dimulai di luar folder, dan dipindahkan ke dalamnya. Saat ini, semua jawaban kehilangan semua riwayat sebelum pindah.
n nothing101

2
Tautan @ ggll tidak aktif. Ini salinan yang diarsipkan.
s3cur3

Jawaban:


84

Untuk mengisolasi subdirektori ke dalam repositori sendiri, gunakan filter-branchklon dari repositori asli:

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

Maka tidak lebih dari menghapus direktori asli Anda dan menambahkan submodule ke proyek induk Anda.


18
Anda mungkin juga ingin git remote rm <name>setelah cabang filter, dan kemudian mungkin menambahkan remote baru. Juga, jika ada file yang diabaikan, a git clean -xd -fmungkin berguna
naught101

-- --alldapat diganti dengan nama cabang jika submodul hanya diekstrak dari cabang ini.
adius

Apakah git clone <your_project> <your_submodule>hanya mengunduh file untuk modul_Anda?
Dominic

@DominicTobias: git clone source destinationcukup memberi tahu Git lokasi penyimpanan file kloning Anda. Keajaiban sebenarnya untuk memfilter file submodule Anda kemudian terjadi pada filter-branchlangkah tersebut.
rajutan

filter-branchsudah usang saat ini. Anda dapat menggunakan git clone --filter, tetapi server Git Anda harus dikonfigurasi untuk memungkinkan pemfilteran, jika tidak Anda akan mendapatkannya warning: filtering not recognized by server, ignoring.
Matthias Braun

24

Pertama ubah dir ke folder yang akan menjadi submodule. Kemudian:

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
Ini akan kehilangan semua riwayat folder itu.
n nothing101

6
sejarah folder akan disimpan di repositori utama dan komit baru akan menyimpan sejarah dalam submodule
zednight

11

Saya tahu ini adalah utas lama, tetapi jawaban di sini menghentikan semua komitmen terkait di cabang lain.

Cara sederhana untuk mengkloning dan menyimpan semua cabang dan komitmen ekstra itu:

1 - Pastikan Anda memiliki alias git ini

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

2 - Kloning remote, tarik semua cabang, ubah remote, filter direktori Anda, dorong

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

1

Itu bisa dilakukan, tapi tidak sederhana. Jika Anda mencari git filter-branch, subdirectorydan submodule, ada beberapa artikel yang layak dalam prosesnya. Ini pada dasarnya memerlukan pembuatan dua klon proyek Anda, menggunakan git filter-branchuntuk menghapus semuanya kecuali satu subdirektori di satu, dan menghapus hanya subdirektori di yang lain. Kemudian Anda dapat membuat repositori kedua sebagai submodul dari yang pertama.


0

Status quo

Anggaplah kita memiliki repositori bernama repo-oldyang berisi sub direktori sub yang ingin kita ubah menjadi sub modul dengan reponya sendiri repo-sub.

Lebih lanjut dimaksudkan bahwa repo asli repo-oldharus diubah menjadi repo yang dimodifikasi di repo-newmana semua komit yang menyentuh subdirektori yang ada sebelumnya subsekarang akan mengarah ke komit yang sesuai dari repo submodul yang kami ekstrak repo-sub.

Ayo ganti

Hal ini dimungkinkan untuk mencapai ini dengan bantuan git filter-branchdalam proses dua langkah:

  1. Ekstraksi subdirektori dari repo-oldke repo-sub(sudah disebutkan dalam jawaban yang diterima )
  2. Penggantian subdirektori dari repo-oldke repo-new(dengan pemetaan komit yang tepat)

Catatan : Saya tahu bahwa pertanyaan ini sudah lama dan sudah disebutkan, itu git filter-branchagak usang dan mungkin berbahaya. Tetapi di sisi lain, ini mungkin membantu orang lain dengan repositori pribadi yang mudah divalidasi setelah konversi. Jadi berhati - hatilah ! Dan beri tahu saya jika ada alat lain yang melakukan hal yang sama namun tetap aman digunakan!

Saya akan menjelaskan bagaimana saya menyadari kedua langkah di linux dengan git versi 2.26.2 di bawah. Versi yang lebih lama mungkin berfungsi sampai batas tertentu tetapi itu perlu diuji.

Demi kesederhanaan, saya akan membatasi diri pada kasus di mana hanya ada mastercabang dan originremote di repo asli repo-old. Juga diperingatkan bahwa saya mengandalkan tag git sementara dengan awalan temp_yang akan dihapus dalam prosesnya. Jadi jika sudah ada tag dengan nama yang mirip, Anda mungkin ingin menyesuaikan awalan di bawah ini. Dan akhirnya perlu diketahui bahwa saya belum menguji ini secara ekstensif dan mungkin ada kasus sudut di mana resep gagal. Jadi harap buat cadangan semuanya sebelum melanjutkan !

Cuplikan bash berikut dapat digabungkan menjadi satu skrip besar yang kemudian harus dieksekusi di folder yang sama tempat repo berada repo-org. Tidak disarankan untuk menyalin dan menempel semuanya langsung ke jendela perintah (meskipun saya telah berhasil mengujinya)!

0. Persiapan

Variabel

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

Filter skrip

# 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. Ekstraksi subdirektori

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. Penggantian subdirektori

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"

Keterangan: Jika baru dibuat repo repo-newhang selama git submodule update --initkemudian mencoba untuk re-clone repositori rekursif sekali bukan:

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

Ini melakukan konversi di tempat, Anda dapat mengembalikannya seperti yang Anda lakukan pada cabang filter (saya gunakan git fetch . +refs/original/*:*).

Saya memiliki proyek dengan utilsperpustakaan yang mulai berguna dalam proyek lain, dan ingin membagi sejarahnya menjadi submodul. Tidak berpikir untuk melihat SO terlebih dahulu jadi saya menulis sendiri, itu membangun sejarah secara lokal sehingga sedikit lebih cepat, setelah itu jika Anda mau, Anda dapat mengatur .gitmodulesfile perintah pembantu dan semacamnya, dan mendorong sejarah submodul sendiri di mana saja kamu ingin.

Perintah stripped itu sendiri ada di sini, dokumen ada di komentar, di perintah unstripped yang mengikuti. Jalankan sebagai perintahnya sendiri, dengan subdirset, seperti subdir=utils git split-submodulejika Anda memisahkan utilsdirektori. Ini hacky karena ini hanya satu kali, tetapi saya mengujinya di subdirektori Dokumentasi dalam sejarah 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}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.