Chuyển đổi hồi tố một thư mục Git thành một mô-đun con?


115

Khá thường xuyên xảy ra trường hợp bạn đang viết một dự án nào đó, và sau một thời gian, rõ ràng là một số thành phần của dự án thực sự hữu ích như một thành phần độc lập (có lẽ là một thư viện). Nếu bạn đã có ý tưởng đó từ sớm, thì rất có thể phần lớn mã đó nằm trong thư mục riêng của nó.

Có cách nào để chuyển đổi một trong các thư mục con trong dự án Git thành mô-đun con không?

Lý tưởng nhất là điều này sẽ xảy ra sao cho tất cả mã trong thư mục đó bị xóa khỏi dự án mẹ và dự án mô thức con được thêm vào vị trí của nó, với tất cả lịch sử thích hợp, và sao cho tất cả các dự án mẹ đều trỏ đến đúng cam kết của mô thức con .



Đây không phải là một phần của câu hỏi ban đầu, nhưng điều thú vị hơn sẽ là một cách để giữ lịch sử của các tệp đã bắt đầu bên ngoài thư mục và được chuyển vào đó. Hiện tại, tất cả các câu trả lời đều mất hết lịch sử trước khi di chuyển.
naught101

2
Liên kết của @ ggll bị lỗi. Đây là một bản sao lưu trữ.
s3cur3

Câu trả lời:


84

Để tách một thư mục con thành kho lưu trữ của riêng nó, hãy sử dụng filter-branchtrên một bản sao của kho lưu trữ ban đầu:

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

Sau đó, không có gì khác hơn là xóa thư mục gốc của bạn và thêm mô-đun con vào dự án mẹ của bạn.


18
Bạn có thể cũng muốn git remote rm <name>sau nhánh bộ lọc, và sau đó có thể thêm một điều khiển từ xa mới. Ngoài ra, nếu có các tệp bị bỏ qua, một tệp git clean -xd -fcó thể hữu ích
naught101

-- --allcó thể được thay thế bằng tên của một nhánh nếu mô-đun con chỉ nên được trích xuất từ ​​nhánh này.
adius

Không git clone <your_project> <your_submodule>chỉ tải file cho your_submodule?
Dominic

@DominicTobias: git clone source destinationchỉ cần cho Git biết vị trí đặt các tệp đã sao chép của bạn. Phép thuật thực tế để lọc các tệp của mô-đun con của bạn sau đó sẽ xảy ra trong filter-branchbước.
đan vào

filter-branchđang bị phản đối hiện nay. Bạn có thể sử dụng git clone --filter, nhưng máy chủ Git của bạn phải được định cấu hình để cho phép lọc, nếu không bạn sẽ nhận được warning: filtering not recognized by server, ignoring.
Matthias Braun

24

Đầu tiên thay đổi dir thành thư mục sẽ là một mô-đun con. Sau đó:

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
Điều này sẽ làm mất tất cả lịch sử của thư mục đó.
naught101

6
lịch sử của thư mục sẽ được lưu trong kho lưu trữ chính và các cam kết mới sẽ lưu lịch sử trong mô-đun con
zednight

11

Tôi biết đây là một chủ đề cũ, nhưng các câu trả lời ở đây sẽ loại bỏ mọi cam kết liên quan trong các nhánh khác.

Một cách đơn giản để sao chép và giữ lại tất cả các nhánh và cam kết bổ sung đó:

1 - Đảm bảo bạn có bí danh git này

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

2 - Sao chép điều khiển từ xa, kéo tất cả các nhánh, thay đổi điều khiển từ xa, lọc danh bạ của bạn, đẩy

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
Ban đầu của tôi đã có một liên kết đến một ý chính thay vì nhúng mã ở đây trên SO
oodavid

1

Nó có thể được thực hiện, nhưng nó không đơn giản. Nếu bạn tìm kiếm git filter-branch, subdirectorysubmodule, có một số bài viết tốt về quá trình này. Về cơ bản, nó đòi hỏi phải tạo ra hai bản sao của dự án của bạn, sử dụng git filter-branchđể xóa mọi thứ ngoại trừ một thư mục con trong một và chỉ xóa thư mục con đó trong thư mục khác. Sau đó, bạn có thể thiết lập kho lưu trữ thứ hai như một mô-đun con của kho lưu trữ thứ nhất.


0

Hiện trạng

Giả sử chúng ta có một kho lưu trữ được gọi là repo-oldchứa một thư mục sub con mà chúng ta muốn chuyển đổi thành một mô-đun con với kho lưu trữ của chính nó repo-sub.

Ngoài ra, còn có ý định repo-oldchuyển đổi repo gốc thành repo đã sửa đổi repo-newtrong đó tất cả các cam kết chạm vào thư mục con hiện có trước đây subsẽ trỏ đến các cam kết tương ứng của repo mô thức con được trích xuất của chúng tôi repo-sub.

Hãy thay đổi

Có thể đạt được điều này với sự trợ giúp của git filter-branchquy trình hai bước:

  1. Trích xuất thư mục con từ repo-oldđến repo-sub(đã được đề cập trong câu trả lời được chấp nhận )
  2. Thay thế thư mục con từ repo-oldthành repo-new(với ánh xạ cam kết thích hợp)

Lưu ý : Tôi biết rằng câu hỏi này đã cũ và nó đã được đề cập đến git filter-branchloại không được dùng nữa và có thể nguy hiểm. Nhưng mặt khác, nó có thể giúp những người khác có kho lưu trữ cá nhân dễ xác thực sau khi chuyển đổi. Vì vậy, hãy cảnh báo ! Và vui lòng cho tôi biết nếu có bất kỳ công cụ nào khác làm điều tương tự mà không bị phản đối và an toàn để sử dụng!

Tôi sẽ giải thích cách tôi nhận ra cả hai bước trên linux với git phiên bản 2.26.2 bên dưới. Các phiên bản cũ hơn có thể hoạt động với một số phần mở rộng nhưng điều đó cần được kiểm tra.

Vì đơn giản, tôi sẽ hạn chế bản thân trong trường hợp chỉ có một masternhánh và một originđiều khiển từ xa trong repo gốc repo-old. Cũng được cảnh báo rằng tôi dựa vào các thẻ git tạm thời có tiền tố temp_sẽ bị xóa trong quá trình này. Vì vậy, nếu đã có các thẻ được đặt tên tương tự, bạn có thể muốn điều chỉnh tiền tố bên dưới. Và cuối cùng xin lưu ý rằng tôi chưa thử nghiệm rộng rãi điều này và có thể có những trường hợp góc khiến công thức không thành công. Vì vậy, hãy sao lưu mọi thứ trước khi tiếp tục !

Các đoạn mã bash sau có thể được nối thành một tập lệnh lớn, sau đó sẽ được thực thi trong cùng một thư mục nơi repo repo-orgtồn tại. Không nên sao chép và dán mọi thứ trực tiếp vào cửa sổ lệnh (mặc dù tôi đã kiểm tra điều này thành công)!

0. Chuẩn bị

Biến

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

Tập lệnh lọc

# 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. Trích xuất thư mục con

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. Thay thế thư mục con

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"

Lưu ý: Nếu kho lưu trữ mới được tạo repo-newbị treo trong khi git submodule update --initđó, hãy thử sao chép lại kho lưu trữ một cách đệ quy một lần thay thế:

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

Điều này thực hiện chuyển đổi tại chỗ, bạn có thể sao lưu nó như bạn làm với bất kỳ nhánh bộ lọc nào (tôi sử dụng git fetch . +refs/original/*:*).

Tôi có một dự án với một utilsthư viện bắt đầu hữu ích trong các dự án khác và muốn tách lịch sử của nó thành một mô-đun con. Không nghĩ đến việc xem xét SO trước vì vậy tôi đã viết lịch sử của riêng mình, nó xây dựng lịch sử cục bộ nên nhanh hơn một chút, sau đó nếu bạn muốn, bạn có thể thiết lập .gitmodulestệp lệnh của trình trợ giúp và tương tự, và tự đẩy lịch sử mô-đun con ở bất kỳ đâu bạn muốn.

Bản thân lệnh đã loại bỏ nằm ở đây, tài liệu nằm trong các nhận xét, trong lệnh không được đánh dấu theo sau. Chạy nó dưới dạng lệnh của riêng nó, với subdirset, giống như subdir=utils git split-submodulekhi bạn đang chia nhỏ utilsthư mục. Thật khó vì nó chỉ diễn ra một lần, nhưng tôi đã thử nghiệm nó trên thư mục con Documentation trong lịch sử 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}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.