如何在保留子目录的同时拆分git存储库?

时间:2010-05-09 09:30:47

标签: git split git-filter-branch

我想要的是this question。但是,我希望拆分为单独仓库的目录仍然是该仓库中的子目录:

我有这个:

foo/
  .git/
  bar/
  baz/
  qux/

我想把它分成两个完全独立的存储库:

foo/
  .git/
  bar/
  baz/

quux/
  .git/
  qux/  # Note: still a subdirectory

如何在git中执行此操作?

如果有办法将所有新repo的内容移动到整个历史记录的子目录中,我可以使用this answer中的方法。

7 个答案:

答案 0 :(得分:18)

你确实可以使用子目录过滤器后跟一个索引过滤器将内容放回到一个子目录中,但是为什么你只能单独使用索引过滤器呢?

以下是手册页中的示例:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD

这只删除一个文件名;你想要做的是删除除给定子目录之外的所有内容。如果你想谨慎,你可以明确列出要删除的每个路径,但如果你想全押,你可以这样做:

git filter-branch --index-filter 'git ls-tree -z --name-only --full-tree $GIT_COMMIT | grep -zv "^directory-to-keep$" | xargs -0 git rm --cached -r' -- --all

我希望有一种更优雅的方式;如果有人有东西请建议吧!

关于该命令的一些注释:

  • filter-branch在内部将GIT_COMMIT设置为当前提交SHA1
  • 我不希望--full-tree是必要的,但显然filter-branch会从.git-rewrite/t目录而不是repo的顶层运行索引过滤器。
  • grep可能有点矫枉过正,但我​​认为这不是速度问题。
  • --all适用于所有裁判;我想你真的很想要那个。 (--将其与过滤器分支选项分开)
  • -z-0告诉ls-tree,grep和xargs使用NUL终止来处理文件名中的空格。

编辑,很久以后:托马斯帮忙建议了一种删除现在空提交的方法,但它现在已经过时了。如果您有旧版本的git,请查看编辑历史记录,但使用现代git,您需要做的就是使用此选项:

--prune-empty

这将删除所有在应用索引过滤器后为空的提交。

答案 1 :(得分:3)

当我自己拥有它时,这就是我最终要解决的问题:

git filter-branch --index-filter \
'git ls-tree --name-only --full-tree $GIT_COMMIT | \
 grep -v "^directory-to-keep$" | \
 sed -e "s/^/\"/g" -e "s/$/\"/g" | \
 xargs git rm --cached -r -f --ignore-unmatch \
' \
--prune-empty -- --all

解决方案基于Jefromi的答案和Detach (move) subdirectory into separate Git repository以及SO上的许多评论。

Jefromi的解决方案对我不起作用的原因是,我的仓库中有文件和文件夹,其名称包含特殊字符(大多数是空格)。另外git rm抱怨有不匹配的文件(使用--ignore-unmatch解决)。

您可以将过滤禁止与不在repo的根目录中的目录或被移动的目录:

grep --invert-match "^.*directory-to-keep$"

最后,您可以使用它来过滤掉固定的文件或目录子集:

egrep --invert-match "^(.*file-or-directory-to-keep-1$|.*file-or-directory-to-keep-2$|…)"

要进行清理,您可以使用以下命令:

$ git reset --hard
$ git show-ref refs/original/* --hash | xargs -n 1 git update-ref -d
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

答案 2 :(得分:3)

我想做类似的事情,但由于我想要保留的文件列表很长,所以使用无数的greps来做这件事是没有意义的。我写了一个脚本,从文件中读取文件列表:

#!/bin/bash

# usage:
# git filter-branch --prune-empty --index-filter \
# 'this-script file-with-list-of-files-to-be-kept' -- --all

if [ -z $1 ]; then
    echo "Too few arguments."
    echo "Please specify an absolute path to the file"
    echo "which contains the list of files that should"
    echo "remain in the repository after filtering."
    exit 1
fi

# save a list of files present in the commit
# which is currently being modified.
git ls-tree -r --name-only --full-tree $GIT_COMMIT > files.txt

# delete all files that shouldn't be removed
while read string; do
    grep -v "$string" files.txt > files.txt.temp
    mv -f files.txt.temp files.txt
done < $1

# remove unwanted files (i.e. everything that remained in the list).
# warning: 'git rm' will exit with non-zero status if it gets
# an invalid (non-existent) filename OR if it gets no arguments.
# If something exits with non-zero status, filter-branch will abort.
# That's why we have to check carefully what is passed to git rm.
if [ "$(cat files.txt)" != "" ]; then
    cat files.txt | \
    # enclose filenames in "" in case they contain spaces
    sed -e 's/^/"/g' -e 's/$/"/g' | \
    xargs git rm --cached --quiet
fi

相当令人惊讶的是,这比我最初的预期要多得多,所以我决定将它发布在这里。

答案 3 :(得分:1)

更清洁的方法:

git filter-branch --index-filter '
                git read-tree --empty
                git reset $GIT_COMMIT path/to/dir
        ' \
        -- --all -- path/to/dir

或坚持使用核心命令,在git read-tree --prefix=path/to/dir/ $GIT_COMMIT:path/to/dir中进行重置。

在rev-list args上指定path/to/dir会尽早进行修剪,使用这个便宜的过滤器并不重要,但无论如何都要避免浪费精力。

答案 4 :(得分:1)

使用git-filter-repo 从2.25版开始,这不是git的一部分。 这需要Python3(> = 3.5)和git 2.22.0

mkdir new_repoA
mkdir new_repoB
git clone originalRepo newRepoA
git clone originalRepo newRepoB

pushd
cd new_repoA
git filter-repo --path foo/bar --path foo/baz

popd
cd new_repoB 
git filter-repo --path foo/qux

对于我的包含约12000次提交的repo, git-filter-branch 花了超过24小时,而 git-filter-repo 花了不到一分钟。

答案 5 :(得分:0)

如果您希望将单个目录拆分为单独的git存储库

git-filter-branch具有--subdirectory-filter选项,它比前面提到的解决方案简单得多,只是:

git filter-branch --subdirectory-filter foodir -- --all

此外,它可以更改路径并将目录内容放置在新存储库之上,而不仅仅是过滤和删除其他内容。

答案 6 :(得分:0)

我将git-filter-repofilename-callback一起使用。

stephen@B450-AORUS-M:~/source/linux$ git filter-repo --force --filename-callback '
  if b"it87.c" in filename:
    return filename
  else:
    # Keep the filename and do not rename it
    return None
  '
warning: Tag points to object of unexpected type tree, skipping.
warning: Tag points to object of unexpected type tree, skipping.
Parsed 935794 commitswarning: Omitting tag 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c,
since tags of trees (or tags of tags of trees, etc.) are not supported.
warning: Omitting tag 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c,
since tags of trees (or tags of tags of trees, etc.) are not supported.
Parsed 937142 commits
New history written in 177.03 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at a57e6edb85a3 treewide: Replace GPLv2 boilerplate/reference with SPDX - rule 157
Enumerating objects: 20210, done.
Counting objects: 100% (20210/20210), done.
Delta compression using up to 12 threads
Compressing objects: 100% (17718/17718), done.
Writing objects: 100% (20210/20210), done.
Total 20210 (delta 1841), reused 20038 (delta 1669), pack-reused 0
Completely finished after 179.76 seconds.

它没有删除空的合并提交,可能是由于一堆与树的一侧相关联的标签所致。

我尝试使用投票率最高的答案,但似乎并没有删除任何内容,并且花费了很长时间。

Rewrite 3e80e1395bd4f410b79dc0f17113f5b6b409c7d8 (329/937142) (8 seconds passed, remaining 22779 predicted)

22779秒= 6.3275小时