合并子目录中的Git存储库


Merge git repository in subdirectory

我想将工作中的Git存储库中的远程Git存储库合并为它的子目录。我希望得到的存储库包含两个存储库的合并历史,并且合并后的存储库的每个文件都保留其在远程存储库中的历史。我尝试使用子树策略,如如何使用子树合并策略中所述,但在执行该过程之后,虽然生成的存储库确实包含两个存储库的合并历史,但来自远程存储库的单个文件没有保留它们的历史(其中任何一个存储库上的"git log"只显示一条消息"merged分支……"。

另外,我不想使用子模块,因为我不希望两个组合的Git存储库再分开。

是否可以将远程Git存储库合并到另一个存储库中作为子目录,并保留来自远程存储库的各个文件的历史记录?

非常感谢你的帮助。

编辑:我目前正在尝试一种解决方案,该解决方案使用git filter分支重写合并后的知识库历史记录。它看起来确实有效,但我需要再测试一下。我会回去报告我的发现。

编辑2:希望我能更清楚地说明,我给出了与Git子树策略一起使用的确切命令,这会导致远程存储库文件的历史记录明显丢失。假设a是我目前正在工作的git repo,b是我想作为它的子目录合并到a中的git repo。它执行了以下操作:

1
2
3
4
git remote add -f B <url-of-B>
git merge -s ours --no-commit B/master
git read-tree --prefix=subdir/Iwant/to/put/B/in/ -u B/master
git commit -m"Merge B as subdirectory in subdir/Iwant/to/put/B/in."

在这些命令之后,进入目录subdirectory/iwant/to/put/b/in,我看到b的所有文件,但是其中任何一个文件上的git log只显示提交消息"merge b as subdirectory in subdirector/iwant/to/put/b/in."它们在b中的文件历史记录丢失。

似乎有用的(因为我是Git的初学者,我可能错了)是:

1
2
3
4
5
6
7
8
9
git remote add -f B <url-of-B>
git checkout -b B_branch B/master  # make a local branch following B's master
git filter-branch --index-filter \
   'git ls-files -s | sed"s-\t"*-&subdir/Iwant/to/put/B/in/-" |
        GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
                git update-index --index-info &&
        mv"$GIT_INDEX_FILE.new""$GIT_INDEX_FILE"' HEAD
git checkout master
git merge B_branch

上面关于filter branch的命令来自git help filter-branch,在这里我只更改了subdir路径。


git-subtree是一个专门为这个用例设计的脚本,它将多个存储库合并为一个存储库,同时保留历史(和/或拆分子树的历史,尽管这似乎与这个问题无关)。自1.7.11版本以来,它作为Git树的一部分进行分发。

要将修订版中的存储库合并为子目录,请使用git subtree add,如下所示:

1
git subtree add -P [cc] <repo> <rev>

Git子树以更加用户友好的方式实现子树合并策略。


在得到了对正在发生的事情的更全面的解释之后,我想我理解它,在任何情况下,在底部我都有一个解决办法。具体地说,我相信正在发生的是重命名检测被子树合并--prefix所愚弄。这是我的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mkdir -p z/a z/b
cd z/a
git init
echo A>A
git add A
git commit -m A
echo AA>>A
git commit -a -m AA
cd ../b
git init
echo B>B
git add B
git commit -m B
echo BB>>B
git commit -a -m BB
cd ../a
git remote add -f B ../b
git merge -s ours --no-commit B/master
git read-tree --prefix=bdir -u B/master
git commit -m"subtree merge B into bdir"
cd bdir
echo BBB>>B
git commit -a -m BBB

我们制作Git目录A和B,每个目录有几个提交。我们进行子树合并,然后进行新子树中的最后一次提交。

运行gitk(z/a)表明历史确实出现了,我们可以看到。运行git log表明历史确实出现了。但是,查看特定文件有一个问题:git log bdir/B

好吧,我们可以玩一个把戏。我们可以使用--follow查看特定文件的预重命名历史记录。git log --follow -- B。这很好,但不太好,因为它无法将预合并的历史记录与后合并相链接。

我试着玩-m和-c,但我没能让它按照一个特定的文件。

所以,我觉得解决方案是告诉Git将作为子树合并的一部分发生的重命名。不幸的是,gitreadtree对于子树合并非常挑剔,所以我们必须通过一个临时目录来工作,但是在提交之前,这可能会消失。之后,我们可以看到完整的历史。

首先,创建一个"A"存储库并进行一些提交:

1
2
3
4
5
6
7
8
mkdir -p z/a z/b
cd z/a
git init
echo A>A
git add A
git commit -m A
echo AA>>A
git commit -a -m AA

其次,创建一个"B"存储库并进行一些提交:

1
2
3
4
5
6
7
cd ../b
git init
echo B>B
git add B
git commit -m B
echo BB>>B
git commit -a -m BB

实现这一点的诀窍是:通过创建子目录并将内容移动到其中,强制Git识别重命名。

1
2
3
mkdir bdir
git mv B bdir
git commit -a -m bdir-rename

返回到存储库"A",获取并合并"B"的内容:

1
2
3
4
5
cd ../a
git remote add -f B ../b
git merge -s ours --no-commit B/master
git read-tree --prefix= -u B/master
git commit -m"subtree merge B into bdir"

要显示它们现在已合并:

1
2
3
cd bdir
echo BBB>>B
git commit -a -m BBB

为了证明完整的历史记录保存在一个连接的链中:

1
git log --follow B

这样做之后,我们就可以了解历史了,但问题是,如果您确实保留了旧的"B"回购协议,并且偶尔从中合并(假设它实际上是第三方单独维护的回购协议),则会遇到麻烦,因为第三方不会进行重命名。您必须尝试将新的更改合并到您的B版本和重命名,我担心这不会顺利进行。但是如果B要离开,你就赢了。


如果你真的想把东西缝在一起,那就去找嫁接吧。您还应该使用git rebase --preserve-merges --onto。还可以选择保留提交者信息的作者日期。


我想

  • 保持线性历史而不显式合并,以及
  • 使其看起来像合并存储库的文件一直存在于子目录中,并且作为副作用,使git log -- file在没有--follow的情况下工作。
  • 步骤1:重写源存储库中的历史记录,使其看起来所有文件都始终存在于子目录下。

    为重写的历史创建临时分支。

    1
    git checkout -b tmp_subdir

    然后使用git filter-branch,如我如何重写历史记录中所述,以便除已移动的文件外,所有文件都位于子目录中?:

    1
    2
    3
    4
    5
    git filter-branch --prune-empty --tree-filter '
    if [ ! -e foo/bar ]; then
        mkdir -p foo/bar
        git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files foo/bar
    fi'

    步骤2:切换到目标存储库。将源存储库作为远程存储添加到目标存储库中并获取其内容。

    1
    2
    git remote add sourcerepo .../path/to/sourcerepo
    git fetch sourcerepo

    步骤3:使用merge --onto在目标存储库的顶部添加重写源存储库的提交。

    1
    git rebase --preserve-merges --onto master --root sourcerepo/tmp_subdir

    你可以查看日志,看看这真的得到了你想要的。

    1
    git log --stat

    步骤4:在重新平衡之后,您处于"分离的头部"状态。你可以把大师快进新的头脑。

    1
    2
    3
    4
    git checkout -b tmp_merged
    git checkout master
    git merge tmp_merged
    git branch -d tmp_merged

    步骤5:最后进行一些清理:删除临时远程。

    1
    git remote rm sourcerepo


    我发现以下解决方案对我可行。首先,我进入项目B,创建一个新的分支,其中已经存在的所有文件都将移动到新的子目录中。然后我把这个新的分支推到原点。接下来,我转到项目A,添加并获取b的远程文件,然后签出移动的分支,返回master并合并:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #&nbsp;in local copy of project B
    git checkout -b prepare_move
    mkdir subdir
    git mv <files_to_move> subdir/
    git commit -m 'move files to subdir'
    git push origin prepare_move

    # in local copy of project A
    git remote add -f B_origin <remote-url>
    git checkout -b from_B B_origin/prepare_move
    git checkout master
    git merge from_B

    如果我转到subdir子目录,我可以使用git log --follow,并且仍然有历史。

    我不是一个git专家,所以我不能评论这是一个特别好的解决方案,或者它是否有警告,但到目前为止,一切似乎都很好。


    您是否尝试将额外的存储库添加为Git子模块?它不会将历史记录与包含存储库合并,事实上,它将是一个独立的存储库。

    我提过,因为你没有。