Git 폴더를 소급하여 하위 모듈로 변환 하시겠습니까?


115

꽤 자주 당신이 어떤 종류의 프로젝트를 작성하는 경우이고, 잠시 후 프로젝트의 일부 구성 요소가 실제로 독립형 구성 요소 (아마도 라이브러리)로 유용하다는 것이 분명해집니다. 처음부터 그 아이디어가 있었다면 대부분의 코드가 자체 폴더에있을 가능성이 큽니다.

Git 프로젝트의 하위 디렉터리 중 하나를 하위 모듈로 변환하는 방법이 있습니까?

이상적으로는 해당 디렉토리의 모든 코드가 상위 프로젝트에서 제거되고 하위 모듈 프로젝트가 적절한 기록과 함께 그 자리에 추가되고 모든 상위 프로젝트 커밋이 올바른 하위 모듈 커밋을 가리 키도록 발생합니다. .


stackoverflow.com/questions/1365541/...는 일부 :) 도움이 될 수 있습니다
롭 파커

이것은 원래 질문의 일부가 아니지만 더 멋진 것은 폴더 외부에서 시작되어 폴더로 이동 된 파일의 기록을 유지하는 방법입니다. 현재 모든 답변은 이동 이전의 모든 기록을 잃습니다.
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
폴더의 기록은 주 저장소에 저장되고 새 커밋은 하위 모듈에 기록을 저장합니다
zednight

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하위 디렉토리 를 포함 하는 저장소가 있다고 가정 해 보겠습니다.subrepo-sub .

또한 원래의 저장소 repo-old는 수정 된 저장소로 변환되어야하며 repo-new, 이전에 존재하는 하위 디렉토리를 터치하는 모든 커밋은 sub이제 추출 된 하위 모듈 저장소의 해당 커밋을 가리 킵니다.repo-sub .

바꾸자

git filter-branch두 단계의 프로세스 를 통해이를 달성 할 수 있습니다.

  1. 에서 하위 디렉토리 추출 repo-oldrepo-sub(이미 허용에 언급 대답 )
  2. 에서 repo-oldrepo-new(적절한 커밋 매핑 사용)로 하위 디렉터리 교체

비고 :이 질문은 오래되었고 이미 git filter-branch사용되지 않으며 위험 할 수있는 것으로 이미 언급되었습니다 . 그러나 다른 한편으로는 변환 후 검증하기 쉬운 개인 리포지토리로 다른 사람들에게 도움이 될 수 있습니다. 그래서 경고하십시오 ! 그리고 더 이상 사용되지 않고 사용하기에 안전한 다른 도구가 있으면 알려주세요!

아래 git 버전 2.26.2를 사용하여 Linux에서 두 단계를 모두 실현 한 방법을 설명하겠습니다. 이전 버전은 일부 확장 될 수 있지만 테스트가 필요합니다.

단순함을 위해 원래 저장소에 master브랜치와 origin리모트 만있는 경우로 제한하겠습니다 repo-old. 또한 temp_프로세스에서 제거 될 접두사가 있는 임시 git 태그에 의존한다는 점에 유의 하십시오. 따라서 유사한 이름의 태그가 이미있는 경우 아래 접두사를 조정하는 것이 좋습니다. 그리고 마지막으로 이것을 광범위하게 테스트하지 않았으며 레시피가 실패하는 코너 케이스가있을 수 있습니다. 계속하기 전에 모든 것을 백업 하십시오 !

다음 bash 스 니펫을 하나의 큰 스크립트로 연결 한 다음 저장소가있는 동일한 폴더에서 실행해야 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-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다른 프로젝트에서 유용하기 시작한 라이브러리 가있는 프로젝트가 있으며 그 역사를 하위 모듈로 나누고 싶었습니다. 처음에는 그렇게 생각하지 않았기 때문에 내가 직접 작성 했으므로 로컬로 기록을 작성하므로 조금 더 빠릅니다. 원하는 경우 도우미 명령의 .gitmodules파일 등을 설정하고 하위 모듈 기록을 어디서나 푸시 할 수 있습니다 당신이 원합니다.

제거 된 명령 자체는 여기에 있으며, 주석에있는 문서는 다음에 나오는 제거되지 않은 명령에 있습니다. 디렉토리를 분할하는 subdir것처럼 세트 를 사용하여 자체 명령으로 실행하십시오 . 일회성이기 때문에 해키이지만 Git 히스토리의 Documentation 하위 디렉토리에서 테스트했습니다.subdir=utils git split-submoduleutils

#!/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.