将大型Git仓库拆分为许多小型仓库

95

在成功将一个SVN仓库转换为Git之后,我现在有一个非常大的Git仓库,我想将其分割成多个较小的仓库并保留历史记录。

因此,可以有人帮忙分解类似这样的仓库:

MyHugeRepo/
   .git/
   DIR_A/
   DIR_B/
   DIR_1/
   DIR_2/

分为两个看起来像这样的存储库:

MyABRepo/
   .git
   DIR_A/
   DIR_B/

My12Repo/
   .git
   DIR_1/
   DIR_2/

我尝试按照之前的这个问题的指示操作,但当尝试将多个目录放入单独的仓库时,它并不是很适合 (Detach (move) subdirectory into separate Git repository)。


13
当您对答案满意时,请将其标记为已接受。 - Ben Fowler
1
对于任何想要将多个(嵌套)目录拆分成新存储库的人(而不是试图删除多个目录,这可能对某些项目更难),这个答案对我很有帮助:https://dev59.com/dGIj5IYBdhLWcg3wuHXP#19957874 - thaddeusmt
6个回答

81

这将设置MyABRepo; 当然你也可以类似地设置My12Repo。

git clone MyHugeRepo/ MyABRepo.tmp/
cd MyABRepo.tmp
git filter-branch --prune-empty --index-filter 'git rm --cached --ignore-unmatch DIR_1/* DIR_2/*' HEAD 

仍有对 .git/refs/original/refs/heads/master 的引用。您可以使用以下命令将其删除:

cd ..
git clone MyABRepo.tmp MyABRepo

如果一切顺利,您可以删除MyABRepo.tmp。


如果出现与.git-rewrite相关的错误,您可以尝试以下方法:

git clone MyHugeRepo/ MyABRepo.tmp/
cd MyABRepo.tmp
git filter-branch -d /tmp/git-rewrite.tmp --prune-empty --index-filter 'git rm --cached --ignore-unmatch DIR_1/* DIR_2/*' HEAD 
cd ..
git clone MyABRepo.tmp MyABRepo

这将创建并使用/tmp/git-rewrite.tmp作为临时目录,而不是.git-rewrite。

当然,您可以将任何路径替换为/tmp/git-rewrite.tmp,只要您具有写权限,并且该目录不存在即可。


-d <path-on-another-physical-disk> 对我有用,并解决了在 --tree-filter 中出现的奇怪的 'mv' 失败问题。 - Vertigo
你有想法如何获取第一个提交记录吗?如果它与被排除的路径相关(例如 DIR_A),该怎么办? - bitmask
@bitmask:你尝试过git rebase -i --root $tip(适用于git 1.7.12+),或者这种较旧的方法吗? - unutbu
3
我没有意识到 filter-branch 的全部后果。对于那些不了解的人来说,它会重新编写历史记录,因此如果你在执行此操作后计划推送存储库,提交哈希值现在将不同,并且无法正常工作。 - thaddeusmt
使用这种方法,新存储库中将保留哪些历史记录?新存储库中所有现有文件的历史记录?目录DIR_ADIR_B所有更改(包括已删除的文件)的历史记录? 外部文件移动的历史记录会发生什么,例如从DIR_1/README.md -> DIR_A/README.md内部文件移动如DIR_A/MyClass.java -> DIR_1/NewNameClass.java呢? - Snackoverflow
显示剩余3条评论

9
你可以使用 git filter-branch --index-filtergit rm --cached 从克隆/拷贝的仓库中删除不需要的目录。
例如:
trim_repo() { : trim_repo src dst dir-to-trim-out...
  : uses printf %q: needs bash, zsh, or maybe ksh
  git clone "$1" "$2" &&
  (
    cd "$2" &&
    shift 2 &&

    : mirror original branches &&
    git checkout HEAD~0 2>/dev/null &&
    d=$(printf ' %q' "$@") &&
    git for-each-ref --shell --format='
      o=%(refname:short) b=${o#origin/} &&
      if test -n "$b" && test "$b" != HEAD; then 
        git branch --force --no-track "$b" "$o"
      fi
    ' refs/remotes/origin/ | sh -e &&
    git checkout - &&
    git remote rm origin &&

    : do the filtering &&
    git filter-branch \
      --index-filter 'git rm --ignore-unmatch --cached -r -- '"$d" \
      --tag-name-filter cat \
      --prune-empty \
      -- --all
  )
}
trim_repo MyHugeRepo MyABRepo DIR_1 DIR_2
trim_repo MyHugeRepo My12Repo DIR_A DIR_B

您需要手动删除每个存储库中不需要的分支或标签(例如,如果您有一个 feature-x-for-AB 分支,则可能需要从“12”存储库中删除该分支)。


1
在bash中,:不是注释字符。你应该使用 # 代替。 - Daenyth
4
@Daenyth,“:”是一个传统的内置命令(也在POSIX中指定)。它包含在bash中,但不是一个注释。我特意使用它来代替“#”,因为并非所有的shell在所有上下文中都将“#”作为注释引入符号(例如,没有启用INTERACTIVE_COMMENTS选项的交互式zsh)。使用“:”使整个文本适用于粘贴到任何交互式shell中以及保存在脚本文件中。 - Chris Johnsen
1
太棒了!这是我找到的唯一一个保持所有分支完整的解决方案。 - pheelicks
对我来说,它在 git remote rm origin 处停止了,这似乎总是返回 1。因此,我将此行中的 && 替换为 ; - kynan
很好,$@ 在需要时可以用于两个以上的目录。 完成后,我会调用 git remote add origin $TARGET; git push origin master - Walter A
文件重命名历史记录丢失了,但这是git处理重命名的方式。无论如何,如果您想保留某个目录并删除其余部分,也有“官方”方法,使用git subtree。请参见https://dev59.com/cXRC5IYBdhLWcg3wROpQ#17864475。 - user133408

6
git_split 项目是一个简单的脚本,可以实现您所需要的功能。 https://github.com/vangorra/git_split
它可以将 git 仓库中的某个文件夹变成独立的仓库,并将该文件夹所有的历史记录复制到新的仓库中,而不使用子树(subtree)。
./git_split.sh <src_repo> <src_branch> <relative_dir_path> <dest_repo>
        src_repo  - The source repo to pull from.
        src_branch - The branch of the source repo to pull from. (usually master)
        relative_dir_path   - Relative path of the directory in the source repo to split.
        dest_repo - The repo to push to.

这与上面的“filter-branch”答案相同,是吗?如果是的话,我假设它有类似的问题,即重写整个历史记录? - éclairevoyant

6

虽然在问题提出时,Ubuntu的回答是最好的选择,但现在即使是Git本身也推荐使用https://github.com/newren/git-filter-repo

它的速度比原先快了几个数量级,而且相对来说非常容易使用

例如,在这里你可以执行以下操作:

git clone MyHugeRepo/ MyABRepo.tmp/
cd MyABRepo.tmp
git filter-repo --path DIR_A/ --path DIR_B/

你可以在https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html#EXAMPLES上看到更多示例。

3

1

感谢您的回答,但我最终只是复制了存储库两次,然后从每个存储库中删除了不需要的文件。稍后我将使用filter-branch命令过滤掉已删除文件的所有提交,因为它们已经在其他地方进行版本控制。

cp -R MyHugeRepo MyABRepo
cp -R MyHugeRepo My12Repo

cd MyABRepo/
rm -Rf DIR_1/ DIR_2/
git add -A
git commit -a

这对我需要的工作非常有效。

编辑:当然,在My12Repo中也对A和B目录进行了同样的操作。这给了我两个具有相同历史记录的存储库,直到我删除不想要的目录为止。


3
这不会保留提交历史记录。 - Daenyth
怎么会呢?即使是对于已删除的文件,我仍然拥有所有历史记录。 - MikeM
2
由于您的要求并不是仓库A必须假装仓库B从未存在过,我认为这(保留仅影响仓库B的提交记录)是一个合适的解决方案。最好复制一些历史记录,而不是破坏它。 - Steve Clay
“我打算在以后的某个时间使用filter-branch”意味着,不仅会改变分割点之前的所有历史记录,还会改变之后的所有历史记录(包括提交哈希)。因此,这是一种通常应避免使用的处理方式。 - undefined

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