【问题标题】:Detach many subdirectories into a new, separate Git repository将许多子目录分离到一个新的、单独的 Git 存储库中
【发布时间】:2010-06-05 20:51:53
【问题描述】:

本题基于Detach subdirectory into separate Git repository

我想分离一对,而不是分离单个子目录。例如,我当前的目录树如下所示:

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

而我想要这个:

/apps
  /AAA
/libs
  /XXX

git filter-branch 的 --subdirectory-filter 参数不起作用,因为它在第一次运行时会删除除给定目录之外的所有内容。我认为对所有不需要的文件使用 --index-filter 参数会起作用(尽管很乏味),但如果我尝试多次运行它,我会收到以下消息:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

有什么想法吗? TIA

【问题讨论】:

    标签: git git-filter-branch


    【解决方案1】:

    不必处理 subshel​​l 和使用 ext glob(如 kynan suggested),试试这个更简单的方法:

    git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all
    

    正如void.pointer's comment 所述,这将从当前存储库中删除除apps/AAA 和libs/XXX 之外的所有内容。

    修剪空的合并提交

    这会留下很多空的合并。正如raphinesse 在他的answer 中所描述的那样,这些可以通过另一遍删除:

    git filter-branch --prune-empty --parent-filter \
    'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'
    

    ⚠️ 警告:以上必须使用sed 和xargs 的GNU 版本,否则它将删除所有提交,因为xargs 失败。 brew install gnu-sed findutils 然后使用gsed 和gxargs:

    git filter-branch --prune-empty --parent-filter \
    'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
    

    【讨论】:

    • 另外,应该将 --ignore-unmatch 标志传递给 git rm,否则它在第一次提交时失败(在我的情况下,存储库是使用 git svn clone 创建的)
    • 假设您有标签,您应该将--tag-name-filter cat 添加到您的参数中
    • 您能否添加更多信息来解释这个冗长的命令在做什么?
    • 我很惊喜,这在使用 git bash 的 Windows 上完美运行,唷!
    • @BurhanAli 对于历史上的每一次提交,它都会删除除您要保留的文件之外的所有文件。一切完成后,您只剩下您指定的树的一部分,以及该历史记录。
    【解决方案2】:

    使用简单 git 命令的手动步骤

    计划是将各个目录拆分为自己的存储库,然后将它们合并在一起。以下手动步骤没有使用极客使用的脚本,而是使用易于理解的命令,并且可以帮助将额外的 N 个子文件夹合并到另一个存储库中。

    除法

    假设您的原始仓库是:original_repo

    1 - 拆分应用:

    git clone original_repo apps-repo
    cd apps-repo
    git filter-branch --prune-empty --subdirectory-filter apps master
    

    2 - 拆分库

    git clone original_repo libs-repo
    cd libs-repo
    git filter-branch --prune-empty --subdirectory-filter libs master
    

    如果您有 2 个以上的文件夹,请继续。现在您将拥有两个新的临时 git 存储库。

    通过合并应用程序和库来征服

    3 - 准备全新的 repo:

    mkdir my-desired-repo
    cd my-desired-repo
    git init
    

    并且您将需要至少进行一次提交。如果应该跳过以下三行,您的第一个 repo 将立即显示在 repo 的根目录下:

    touch a_file_and_make_a_commit # see user's feedback
    git add a_file_and_make_a_commit
    git commit -am "at least one commit is needed for it to work"
    

    提交临时文件后,后面部分中的merge 命令将按预期停止。

    根据用户的反馈,您可以选择添加.gitignore或README.md等随机文件,而不是添加a_file_and_make_a_commit这样的随机文件。

    4 - 先合并应用程序仓库:

    git remote add apps-repo ../apps-repo
    git fetch apps-repo
    git merge -s ours --no-commit apps-repo/master # see below note.
    git read-tree --prefix=apps -u apps-repo/master
    git commit -m "import apps"
    

    现在您应该会在新存储库中看到 apps 目录。 git log 应该显示所有相关的历史提交消息。

    注意:正如 Chris 在下面的 cmets 中所述,对于较新版本 (>=2.9) 的 git,您需要指定 --allow-unrelated-histories 和 git merge

    5 - 以同样的方式合并 libs repo:

    git remote add libs-repo ../libs-repo
    git fetch libs-repo
    git merge -s ours --no-commit libs-repo/master # see above note.
    git read-tree --prefix=libs -u libs-repo/master
    git commit -m "import libs"
    

    如果要合并的存储库超过 2 个,请继续。

    参考:Merge a subdirectory of another repository with git

    【讨论】:

    • 从 git 2.9 开始,您需要在合并命令上使用 --allow-unrelated-histories。否则这对我来说似乎很有效。
    • 天才!非常感谢你做的这些。我看到的最初答案是在一个非常大的存储库上使用树过滤器,git 预测需要 26 小时才能完成 git 重写。对这种简单但可重复的方法更满意,并且已成功将 4 个子文件夹移动到具有所有预期提交历史的新存储库中。
    • 您可以将第一次提交用于添加.gitignore 和README.md 文件的“初始提交”。
    • 不幸的是,这种方法似乎破坏了 git merge .. git read-tree 步骤中添加的文件的跟踪历史记录,因为它将它们记录为新添加的文件,并且我所有的 git gui 都没有连接到他们之前的提交。
    • @ksadjad,说实话,不知道。手动合并的中心点是选择目录以形成新的 repo 并保留它们的提交历史。我不确定如何处理提交将文件放入 dirA、dirB、dirDrop 并且只为新 repo 选择 dirA 和 dirB 的这种情况,提交历史应该如何与原始历史相关。
    【解决方案3】:

    一个简单的解决方案:git-filter-repo

    我遇到了类似的问题,在查看了此处列出的各种方法后,我发现了git-filter-repo。建议在官方 git 文档here 中作为 git-filter-branch 的替代品。

    要从现有存储库中的目录子集创建新存储库,您可以使用以下命令:

    git filter-repo --path <file_to_keep>
    

    通过链接过滤多个文件/文件夹:

    git filter-repo --path keepthisfile --path keepthisfolder/
    

    所以,要回答原始问题,使用 git-filter-repo 您只需要以下命令:

    git filter-repo --path apps/AAA/ --path libs/XXX/
    

    【讨论】:

    • 这绝对是一个很好的答案。所有其他解决方案的问题是我无法提取目录所有分支的内容。但是,git filter-repo 从所有分支中检索了该文件夹并完美地重写了历史记录,就像清理整个树中我不需要的所有内容一样。
    • 我用了你的答案。这是一个很好的解决方案。然后不知何故,它在许多打开的标签中丢失了。我不得不从我的浏览器历史记录中逐字搜索才能找到你并说声谢谢。
    【解决方案4】:

    为什么要多次运行filter-branch?您可以一次性完成所有操作,因此无需强制执行(请注意,您需要在 shell 中启用 extglob 才能使其工作):

    git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all
    

    这应该消除不需要的子目录中的所有更改并保留所有分支和提交(除非它们仅影响修剪子目录中的文件,凭借--prune-empty) - 重复提交等没有问题。

    此操作后,不需要的目录将被git status 列为未跟踪。

    $(ls ...) 是必要的。 extglob 由您的 shell 评估,而不是索引过滤器,它使用 sh 内置 eval (其中 extglob 不可用)。有关详细信息,请参阅How do I enable shell options in git?。

    【讨论】:

    • 有趣的想法。我有类似的问题,但无法正常工作,请参阅stackoverflow.com/questions/8050687/…
    • 这几乎是我所需要的,虽然我在我的仓库中洒了文件和文件夹......谢谢:)
    • 嗯。即使打开了 extglob,我的括号附近也出现错误:意外标记附近的语法错误 `(' 我的命令看起来像: git filter-branch -f --index-filter "git rm -r -f --cached - -ignore-unmatch src/css/themes/!(some_theme*)" --prune-empty --all an ls with src/css/themes/!(some_theme*) 返回所有其他主题,因此 extglob 确实看起来正在工作......
    • @MikeGraf 我认为这不会产生预期的结果:转义将匹配文字“!”等等。
    • @david-smiley 的(更新的)答案使用了非常相似的方法,但具有完全依赖 git 命令的优势,因此不易受到 @987654334 方式差异的影响@Bae 发现,@ 被跨操作系统解释。
    【解决方案5】:

    在这里回答我自己的问题...经过大量试验和错误。

    我设法使用git subtree 和git-stitch-repo 的组合来做到这一点。这些说明基于:

    首先,我将要保留的目录提取到它们自己的单独存储库中:

    cd origRepo
    git subtree split -P apps/AAA -b aaa
    git subtree split -P libs/XXX -b xxx
    
    cd ..
    mkdir aaaRepo
    cd aaaRepo
    git init
    git fetch ../origRepo aaa
    git checkout -b master FETCH_HEAD
    
    cd ..
    mkdir xxxRepo
    cd xxxRepo
    git init
    git fetch ../origRepo xxx
    git checkout -b master FETCH_HEAD
    

    然后我创建了一个新的空存储库,并将最后两个导入/缝合到其中:

    cd ..
    mkdir newRepo
    cd newRepo
    git init
    git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import
    

    这将创建两个分支,master-A 和 master-B,每个分支都包含其中一个拼接存储库的内容。将它们合并并清理:

    git checkout master-A
    git pull . master-B
    git checkout master
    git branch -d master-A 
    git branch -d master-B
    

    现在我不太确定这是如何/何时发生的,但是在第一个 checkout 和 pull 之后,代码神奇地合并到了 master 分支中(感谢您对这里发生的事情的任何见解!)

    一切似乎都按预期工作,除了如果我查看newRepo 提交历史记录,当变更集同时影响apps/AAA 和libs/XXX 时存在重复。如果有办法删除重复项,那就完美了。

    【讨论】:

    • 您在这里找到的简洁工具。洞察“结帐”:“git pull”与“git fetch && git merge”相同。 “获取”部分是无害的,因为您是“在本地获取”。所以我觉得这个结账命令和“git merge master-B”是一样的,这样就更不言而喻了。见kernel.org/pub/software/scm/git/docs/git-pull.html
    • 不幸的是 git-stitch-repo 工具由于现在的不良依赖关系而损坏。
    • @Henrik 您到底遇到了什么问题?它对我有用,尽管我必须将 export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/" 添加到我的 bash 配置中,以便它可以找到 Git.pm。然后我用cpan安装了它。
    • 可以使用git subtree add 来执行此任务。见stackoverflow.com/a/58253979/1894803
    【解决方案6】:

    我写了一个 git 过滤器来解决这个问题。 它有 git_filter 这个奇妙的名字,位于 github:

    https://github.com/slobobaby/git_filter

    它基于优秀的libgit2。

    我需要拆分一个包含许多提交 (~100000) 的大型存储库,并且基于 git filter-branch 的解决方案需要几天时间才能运行。 git_filter 需要一分钟来做同样的事情。

    【讨论】:

      【解决方案7】:

      使用 'git splits' git 扩展

      git splits 是一个 bash 脚本,它是我作为 git 扩展创建的 git branch-filter 的包装器,基于 jkeating's solution。

      它正是为这种情况而制作的。对于您的错误,请尝试使用 git splits -f 选项强制删除备份。因为git splits 在一个新的分支上运行,它不会重写你当前的分支,所以备份是无关的。有关更多详细信息,请参阅自述文件并确保在您的 repo 的副本/克隆上使用它(以防万一!)。

      1. 安装git splits。
      2. 将目录拆分为本地分支 #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
        #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

      3. 在某处创建一个空仓库。我们假设我们在 GitHub 上创建了一个名为 xyz 的空仓库,其路径为:git@github.com:simpliwp/xyz.git

      4. 推送到新的仓库。 #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

      5. 将新创建的远程仓库克隆到新的本地目录
        #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone git@github.com:simpliwp/xyz.git

      【讨论】:

      • 似乎无法将文件添加到拆分并稍后更新,对吧?
      • 这似乎在我的 repo 上运行缓慢,有大量提交
      • git-split 似乎使用git --index filter 与--subdirectory-filter 相比非常慢。对于某些存储库,它可能仍然是一个可行的选择,但对于大型存储库(数 GB,6 位提交)--index-filter 有效地需要数周才能运行,即使在专用云硬件上也是如此。
      【解决方案8】:
      git clone git@example.com:thing.git
      cd thing
      git fetch
      for originBranch in `git branch -r | grep -v master`; do
          branch=${originBranch:7:${#originBranch}}
          git checkout $branch
      done
      git checkout master
      
      git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all
      
      git remote set-url origin git@example.com:newthing.git
      git push --all
      

      【讨论】:

      • 通读所有其他 cmets 让我走上了正轨。但是,您的解决方案可以正常工作。它导入所有分支,并与多个目录一起使用!太好了!
      • for 循环值得承认,因为其他类似的答案不包括它。如果您的克隆中没有每个分支的本地副本,则filter-branch 不会将它们作为其重写的一部分,这可能会排除在其他分支中引入但尚未与您当前分支合并的文件. (尽管在您之前签出的任何分支上进行git fetch 也是值得的,以确保它们保持最新状态。)
      【解决方案9】:

      是的。在后续调用filter-branch 时使用-f 标志强制覆盖备份以覆盖该警告。 :) 否则我认为您有解决方案(即使用filter-branch 一次根除不需要的目录)。

      【讨论】:

        【解决方案10】:

        如消息所示,删除 refs/original 中 .git 目录下的备份。目录已隐藏。

        【讨论】:

          猜你喜欢
          • 2015-10-28
          • 1970-01-01
          • 2022-01-22
          • 2010-09-26
          • 2010-11-21
          • 2023-03-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多