合并子目录中的git仓库


83

我想在我的工作git仓库中合并一个远程git仓库作为它的子目录。我希望生成的存储库包含两个存储库的合并历史记录,并且希望合并后的存储库中的每个文件都保留其历史记录,就像在远程存储库中一样。我尝试使用如何使用子树合并策略中所述的子树策略,但是按照该步骤进行操作后,尽管生成的存储库确实包含两个存储库的合并历史记录,但来自远程文件库的单个文件并未保留其历史记录(其中任何一个上的“ git log”仅显示一条消息“ Merged branch ...”)。

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

是否可以将远程git存储库作为子目录合并到另一个目录中,而该远程git存储库中的单个文件会保留其历史记录?

非常感谢您的帮助。

编辑:我目前正在尝试使用git filter-branch重写合并的存储库历史记录的解决方案。它似乎确实有效,但我需要对其进行更多测试。我将返回报告我的发现。

编辑2:希望我能更清楚地给出与git的子树策略一起使用的确切命令,这显然会导致丢失远程存储库文件的历史记录。假设A是我当前正在使用的git repo,而B是我想作为其子目录并入A的git repo。它执行以下操作:

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."

在执行这些命令并进入目录subdir / Iwant / to / put / B / in之后,我看到了B的所有文件,但是git log在其中任何一个文件上仅显示了提交消息“将B合并为subdir / Iwant / to / put中的子目录” / B / in。” 他们在B中的文件历史记录丢失了。

什么似乎工作(因为我在混帐初学者我可能是错的)如下:

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路径。


这是什么gitk说的历史?我过去曾成功使用过git subtree merge。也许您可以显示您的确切命令?我不确定git-filter-branch是正确的方法。我可能建议尝试git-fast-export和git-fast-import来综合新的历史记录。
塞斯·罗伯逊

完成子树过程后,gitk显示了两个存储库在其提示上合并而在其初始提交中不相关。(如果我发布gitk的历史视图的屏幕快照会有所帮助吗?可以吗?)不幸的是,如果我在终端中进行了操作,则远程存储库的各个文件都不会保留其历史记录git log <file-from-remote-repo>。我调查git-fast-exportgit-fast-import; 我是git的新手。我将编辑问题,以确切显示与git子树一起使用的命令。非常感谢您的回复。
christosc

@christosc:您的第二种方法非常漂亮而且非常简单,非常感谢!我只需要更改subdir / Iwant / to / put / B / in /并使其成为一个一体机(因为Windows上的msysgit似乎不支持带有的命令中的行返回):git filter-branch --index-filter'git ls文件-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
令人赞叹的

@ user1121352很高兴为您提供帮助。
christosc

Answers:


37

在获得关于所发生情况的完整解释之后,我想我理解了,无论如何在底部,我都有一个解决方法。具体来说,我相信发生的事情是重命名检测被与--prefix合并的子树所欺骗。这是我的测试用例:

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有关将在子树合并中进行的重命名。不幸的是,git-read-tree对子树合并非常挑剔,因此我们必须遍历一个临时目录,但是在提交之前,它可能会消失。之后,我们可以查看完整的历史记录。

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

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”存储库并进行一些提交:

cd ../b
git init
echo B>B
git add B
git commit -m B
echo BB>>B
git commit -a -m BB

完成这项工作的技巧是:通过创建子目录并将其内容移入其中,强制Git识别重命名。

mkdir bdir
git mv B bdir
git commit -a -m bdir-rename

返回存储库“ A”并获取并合并“ B”的内容:

cd ../a
git remote add -f B ../b
git merge -s ours --no-commit B/master
# According to Alex Brown and pjvandehaar, newer versions of git need --allow-unrelated-histories
# git merge -s ours --allow-unrelated-histories --no-commit B/master
git read-tree --prefix= -u B/master
git commit -m "subtree merge B into bdir"

为了表明它们已经合并:

cd bdir
echo BBB>>B
git commit -a -m BBB

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

git log --follow B

完成此操作后,我们就会获得历史记录,但是问题是,如果您实际上是在保留旧的“ b”存储库并偶尔将其合并(例如,它实际上是由第三方单独维护的存储库),则您会遇到麻烦,因为该第三方不会进行重命名。您必须尝试使用​​重命名将新更改合并到您的b版本中,我担心这样做不会顺利。但是如果b消失了,您就赢了。


确实,@ Seth可行!而且我不必像滤镜分支那样诉诸历史重写,这使得历史有些具有欺骗性(例如,在查看时git log --stat)。我也没有--follow在git log的文档中注意到切换。重命名似乎非常方便。非常感谢您如此详尽和翔实的回复!
christosc

2
如果示例代码被分解为可读行,而不是单个用分号分隔的单行代码,则此响应将更加有用。;)
jwadsa​​ck 2012年

我想将“ b”合并为“ a”,同时保留其完整历史记录。我该怎么办?
emeraldhieu


1
正如@AlexBrown所提到的,在此新版本上git会产生该消息fatal: refusing to merge unrelated histories,因此您必须git merge -s ours --allow-unrelated-histories --no-commit B/master改为运行。
pjvandehaar

61

git-subtree是一个脚本,专门用于在保留历史记录(和/或拆分子树的历史记录,但似乎与该问题无关)的同时将多个存储库合并为一个用例。自1.7.11版本以来,它作为git树的一部分分发。

要将<repo>修订版的存储库合并<rev>为子目录<prefix>,请使用git subtree add以下命令:

git subtree add -P <prefix> <repo> <rev>

git-subtree以更加用户友好的方式实现了子树合并策略

缺点是,在合并后的历史文件前缀的(不是在子目录中)。假设您将存储库合并a到中b。结果git log a/f1将显示除合并历史记录之外的所有更改(如果有)。你可以做:

git log --follow -- f1

但这不会在合并历史记录中显示其他更改。

换句话说,如果您不更改a仓库中的文件b,则需要指定--follow和不带前缀的路径。如果您在两个存储库中都进行了更改,那么您将拥有2条命令,这些命令都不显示所有更改。

这里更多。


真好!这正是我需要的一行。谢谢,未来!
iameli 2015年

这是在子方向上将另一个存储库合并到我的存储库中的完美解决方案。
eitch

1
请注意,这不适用于位于的现有子目录<prefix>。例如,为了合并已手动移动到它自己的存储库somewhen一个子目录,并希望在合并回来。
理查德·基弗

6

我想

  1. 保持线性历史记录而无需显式合并,并且
  2. 使合并后的存储库的文件看起来总是存在于子目录中,并且附带的一个影响是git log -- file不使用--follow

步骤1:在源存储库中重写历史记录,以使其看起来所有文件始终都存在于子目录下。

为重写的历史记录创建一个临时分支。

git checkout -b tmp_subdir

然后git filter-branch按照“如何重写历史记录”中的说明使用,以便除我已移动的文件以外的所有文件都在子目录中?

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步:切换到目标存储库。在源存储库中将源存储库添加为远程存储库并获取其内容。

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

步骤3merge --onto用于将重写的源存储库的提交添加到目标存储库的顶部。

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

您可以检查日志,以确保它确实为您提供了所需的东西。

git log --stat

步骤4:重新设置基准后,您将处于“分离头”状态。您可以将主控快进到新的位置。

git checkout -b tmp_merged
git checkout master
git merge tmp_merged
git branch -d tmp_merged

步骤5:最后进行清理:删除临时遥控器。

git remote rm sourcerepo

git rebase似乎不允许将指定的选项放在一起:“错误:无法合并交互式选项(-交互式,-exec,-rebase-merges,-preserve-merges,-keep-empty,-root +- -onto)和am选项(--committer-date-is-author-date)”
萨姆

有趣!尝试下降--committer-date-is-author-date。最近,在git v2.19.0(github.com/git/git/commit/…)中添加了对不兼容选项的检查。从描述中听起来好像在--committer-date-is-author-date被默默忽略之前。
hfs

而不是使用旧filter-branch命令,而是使用git filter-repo --to-subdirectory-filter <dir>,它变得更快,更容易。
威廉

5

如果您真的想将东西缝合在一起,请查找嫁接。您还应该使用git rebase --preserve-merges --onto。还有一个选项可以保留作者日期以提供提交者信息。


@adymitruk谢谢您的答复。我真的是git的新手,所以我将研究您提出的解决方案。我尝试过git filter-branch,它似乎可以工作,但也许您的效果更好。我会尝试一下。
christosc

@adymitruk我可以对两个互不相关的存储库使用rebase作为分支吗?我的意思是我要合并的两个存储库没有共同的初始提交...
christosc

谢谢@adymitruk。我不确定是否可以使用两个不相关的存储库进行变基。它肯定会有用……
christosc

但是不要害怕过滤器分支。它为我们节省了很多倍。只需事先建立另一个分支,您就可以随时返回。那,或者使用reflog。
亚当·迪米特鲁克

我明白了……无论如何,我最好阅读一些有关这些git概念和命令的文档。在VCS(即svn)方面只有很少的经验,但是git使我有些不知所措。虽然它的力量似乎值得。
christosc

4

我发现以下解决方案对我来说是可行的。首先,我进入项目B,创建一个新分支,其中所有文件都将移至新的子目录。然后,我将这个新分支推向起源。接下来,我进入项目A,添加并获取B的远程对象,然后签出已移动的分支,然后回到master并合并:

# 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专家,所以我无法评论这是一个特别好的解决方案还是有警告,但是到目前为止,一切似乎还不错。


人们似乎在这里upvoting这种方法:stackoverflow.com/questions/1683531/...
nacross

3

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

我提到它,因为您没有。


1
感谢您的回答阿比伯恩。实际上,我确实希望将两个存储库历史记录合并为一个;我不希望它们分开,这就是为什么我没有提到子模块的原因。
christosc

0

假设您要将存储库合并a到其中b(假设它们彼此并排放置):

cd a
git filter-repo --to-subdirectory-filter a
cd ..
cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

为此,您需要git-filter-repo安装(filter-branch劝阻)。

合并两个大型存储库,然后将其中一个放入子目录的示例:https : //gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

更多关于它 这里

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.