如何拆分一个git仓库并保留子目录?

21
我想要的类似于this question。不过,我希望被拆分成独立仓库的目录仍然是该仓库中的子目录:
我有这样的内容:
foo/
  .git/
  bar/
  baz/
  qux/

我想把它分成两个完全独立的代码库:
foo/
  .git/
  bar/
  baz/

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

如何在git中操作?
如果有一种方法可以将所有新的存储库内容移动到一个子目录中并保留历史记录,那么我可以使用this answer中的方法。
7个回答

21
你确实可以使用子目录过滤器,然后是索引过滤器将内容放回子目录中,但为什么要费事呢?当你只需使用索引过滤器即可。

以下是man页面的示例:

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目录而非仓库顶层运行index-filter。
  • grep可能过度了,但我认为这不是速度问题。
  • --all将此应用于所有引用;我想你确实需要它。(--将其与filter-branch选项分隔开)
  • -z-0告诉ls-tree、grep和xargs使用NUL终止处理文件名中的空格。

编辑,很久以后:Thomas提供了一个有用的方法来删除现在为空的提交,但这已经过时了。如果您使用的是旧版git,请查看编辑历史记录,但使用现代git,您只需要添加以下选项:

--prune-empty

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


@Jefromi:index-filter参数应该更容易表达为git rm -r -f --cached --ignore-unmatch $(ls !(directory-to-keep)),请参见我的答案https://dev59.com/9l3Ua4cB1Zd3GeqP8gNq#8079852和https://dev59.com/_HA85IYBdhLWcg3wBOtO#7849648。 - kynan
@kynan:这并不能正确地处理隐藏文件,也不够易于扩展。 - Cascabel
1
如果你的文件名带有空格,那么你可以在 ls-tree| grep 之间添加 | tr "\n" "\0" (将换行符转换为 NUL),将 grep -v 更改为 grep -zv,并将 xargs 更改为 xargs -0(使 grep 和 xargs 期望 NUL 作为分隔符)。 - idbrii
1
@pydave 如果文件名包含换行符,则这并没有帮助。正确的解决方案是在ls-tree中使用-z而不是| tr "\n" "\0",这样整个流水线从开始到结束都没有歧义。(因为在符合POSIX标准的文件系统中,NUL/是唯一不允许出现在文件名中的两个字符。) - ssokolow
如果你有一个短文件列表,那么不要删除其他所有内容,只需键入 git read-tree --empty; git reset $GIT_COMMIT -- $your $files $here 就可以了。 - jthill
显示剩余6条评论

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的答案和将子目录分离(移动)到单独的Git存储库,再加上这里在SO的许多评论。
Jefromi的解决方案之所以对我不起作用,是因为我的仓库中有文件和文件夹名称包含特殊字符(主要是空格)。此外,git rm抱怨存在未匹配的文件(使用--ignore-unmatch解决)。
您可以使过滤器对目录保持不可知,而不是在存储库的根目录中或被移动。
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

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

令人惊讶的是,这比我最初预计的要更加困难,因此我决定在此发布。


1
非常感谢分享!这在我的测试仓库上起作用了。我还添加了 if [ "$(cat $1)" == "" ]; then echo "No content in exclude file" exit 1 fi 来检查是否提供了文件。此外,似乎需要提供排除文件的完整路径。 - Denis
PS. 排除文件应该有最后一行为空/垃圾。 - Denis
我喜欢挑选要保留的文件的想法...但是按照设计,这将需要超过20个小时才能在一个有30K提交的仓库上运行... - Linas

3

使用git-filter-repo 这不是 git 的一部分,需要 git 2.22.0 及以上版本和 Python3 (>=3.5)。

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次提交的代码库,git-filter-branch 花费了超过24小时,而 git-filter-repo 则只需不到一分钟。


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参数中指定

0
如果您希望将单个目录拆分为独立的git存储库,git-filter-branch提供了--subdirectory-filter选项,比之前提到的解决方案更简单,只需执行以下操作:
git filter-branch --subdirectory-filter foodir -- --all

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

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小时


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接