แปลงโฟลเดอร์ Git เป็นโมดูลย่อยย้อนหลังหรือไม่


115

บ่อยครั้งที่คุณกำลังเขียนโปรเจ็กต์บางประเภทและหลังจากนั้นไม่นานก็เห็นได้ชัดว่าองค์ประกอบบางส่วนของโปรเจ็กต์นั้นมีประโยชน์ในฐานะคอมโพเนนต์แบบสแตนด์อโลน (อาจเป็นไลบรารี) หากคุณมีความคิดนั้นตั้งแต่เนิ่นๆก็มีโอกาสที่รหัสส่วนใหญ่จะอยู่ในโฟลเดอร์ของตัวเอง

มีวิธีแปลงหนึ่งในไดเร็กทอรีย่อยในโปรเจ็กต์ Git เป็นโมดูลย่อยหรือไม่?

ตามหลักการแล้วสิ่งนี้จะเกิดขึ้นโดยที่โค้ดทั้งหมดในไดเร็กทอรีนั้นถูกลบออกจากโปรเจ็กต์พาเรนต์และโปรเจ็กต์โมดูลย่อยจะถูกเพิ่มเข้ามาแทนที่พร้อมกับประวัติที่เหมาะสมทั้งหมดและโปรเจ็กต์พาเรนต์ทั้งหมดจะชี้ไปที่โมดูลย่อยที่ถูกต้อง .


stackoverflow.com/questions/1365541/…อาจช่วยได้บ้าง :)
Rob Parker

นี่ไม่ใช่ส่วนหนึ่งของคำถามเดิม แต่สิ่งที่จะเจ๋งกว่านั้นคือวิธีการเก็บประวัติของไฟล์ที่เริ่มต้นนอกโฟลเดอร์และถูกย้ายเข้าไปในนั้น ในขณะนี้คำตอบทั้งหมดจะสูญเสียประวัติทั้งหมดก่อนการย้าย
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>หลังจากแยกสาขาตัวกรองแล้วอาจเพิ่มรีโมทใหม่ นอกจากนี้หากมีไฟล์ที่ถูกละเว้น a git clean -xd -fอาจมีประโยชน์
naught101

-- --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ของคุณจะต้องได้รับการกำหนดให้การกรองมิฉะนั้นคุณจะได้รับ
Matthias Braun

24

ขั้นแรกให้เปลี่ยน dir เป็นโฟลเดอร์ซึ่งจะเป็นโมดูลย่อย แล้ว:

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
ประวัติของโฟลเดอร์จะถูกบันทึกไว้ในที่เก็บหลักและ
คอมมิต

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-subrepo

มีวัตถุประสงค์เพิ่มเติมว่า repo เดิมrepo-oldควรถูกแปลงเป็น repo repo-newที่แก้ไขโดยที่การกระทำทั้งหมดที่สัมผัสกับไดเรกทอรีย่อยที่มีอยู่ก่อนหน้านี้subจะชี้ไปที่คอมมิตที่เกี่ยวข้องของ repo โมดูลย่อยที่แยกออกมาของเราrepo-subrepo

เปลี่ยนกันเถอะ

เป็นไปได้ที่จะบรรลุสิ่งนี้ด้วยความช่วยเหลือของgit filter-branchกระบวนการสองขั้นตอน:

  1. การแยกไดเรกทอรีย่อยจากrepo-oldถึงrepo-sub(กล่าวถึงแล้วในคำตอบที่ยอมรับ)
  2. การเปลี่ยนไดเร็กทอรีย่อยจากrepo-oldถึงrepo-new(ด้วยการแมปคอมมิตที่เหมาะสม)

หมายเหตุ : ฉันรู้ว่าคำถามนี้เก่าและมีการพูดถึงไปแล้วว่าgit filter-branchเป็นคำถามที่เลิกใช้แล้วและอาจเป็นอันตราย แต่ในทางกลับกันอาจช่วยให้ผู้อื่นมีที่เก็บส่วนบุคคลที่ตรวจสอบความถูกต้องหลังการแปลงได้ง่าย จึงขอเตือน ! และโปรดแจ้งให้เราทราบหากมีเครื่องมืออื่นที่ทำเช่นเดียวกันโดยไม่ถูกเลิกใช้งานและปลอดภัยต่อการใช้งาน!

ฉันจะอธิบายว่าฉันรู้ทั้งสองขั้นตอนบน linux ด้วย git เวอร์ชัน 2.26.2 ด้านล่างได้อย่างไร เวอร์ชันที่เก่ากว่าอาจใช้งานได้กับการขยาย แต่ต้องมีการทดสอบ

เพื่อประโยชน์ของความเรียบง่ายที่ผมจะ จำกัด ตัวเองให้กรณีที่มีเพียงmasterสาขาและoriginห่างไกลใน repo-oldrepo ขอเตือนด้วยว่าฉันใช้แท็ก git ชั่วคราวที่มีคำนำหน้าtemp_ซึ่งกำลังจะถูกลบออกในกระบวนการ ดังนั้นหากมีแท็กที่ชื่อคล้ายกันอยู่แล้วคุณอาจต้องการปรับคำนำหน้าด้านล่าง และสุดท้ายโปรดทราบว่าฉันยังไม่ได้ทดสอบสิ่งนี้อย่างครอบคลุมและอาจมีบางกรณีที่สูตรล้มเหลว ดังนั้นโปรดสำรองข้อมูลทุกอย่างก่อนดำเนินการต่อ !

ตัวอย่าง bash ต่อไปนี้สามารถเชื่อมต่อกันเป็นสคริปต์ขนาดใหญ่หนึ่งสคริปต์ซึ่งควรดำเนินการในโฟลเดอร์เดียวกับที่ repo 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 ที่สร้างขึ้นใหม่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ไฟล์คำสั่งผู้ช่วยและสิ่งนั้นและผลักดันประวัติโมดูลย่อยได้ทุกที่ คุณต้องการ.

คำสั่ง stripped อยู่ที่นี่เอกสารอยู่ในความคิดเห็นในคำสั่งที่ไม่ถูกตัดที่ตามมา เรียกใช้เป็นคำสั่งของตัวเองพร้อมsubdirset เช่นsubdir=utils git split-submoduleถ้าคุณกำลังแยกutilsไดเร็กทอรี มันแฮ็คเพราะมันเป็นเพียงครั้งเดียว แต่ฉันทดสอบกับไดเรกทอรีย่อยเอกสารในประวัติ 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}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.