如何合并两个 Git 存储库?

发布于 2024-08-05 07:50:05 字数 149 浏览 5 评论 0原文

考虑以下场景:

我在自己的 Git 存储库中开发了一个小型实验项目 A。它现在已经成熟了,我希望 A 成为更大的项目 B 的一部分,该项目有自己的大存储库。我现在想将 A 添加为 B 的子目录。

如何将 A 合并到 B 中,而不丢失任何一侧的历史记录?

Consider the following scenario:

I have developed a small experimental project A in its own Git repo. It has now matured, and I'd like A to be part of larger project B, which has its own big repository. I'd now like to add A as a subdirectory of B.

How do I merge A into B, without losing history on any side?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(27

樱娆 2024-08-12 07:50:05

如果要将分支 some-branchproject-a 合并到 project-b

cd path/to/project-a
git checkout some-branch

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/some-branch
git remote remove project-a

取自:git 合并不同的存储库?

这种方法对我来说效果很好,它更短,在我看来更干净。

如果您想将 project-a 放入子目录中,可以使用 git-filter-repofilter-branch不鼓励)。在上述命令之前运行以下命令:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

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

注意:
--allow-unlated-histories 参数仅自 git >= 2.9 起存在。请参阅 Git - git merge 文档 / --allow-无关历史

更新
按照@jstadler的建议添加了 --tags 以保留标签。

If you want to merge branch some-branch from project-a into project-b:

cd path/to/project-a
git checkout some-branch

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/some-branch
git remote remove project-a

Taken from: git merge different repositories?

This method worked pretty well for me, it's shorter and in my opinion a lot cleaner.

In case you want to put project-a into a subdirectory, you can use git-filter-repo (filter-branch is discouraged). Run the following commands before the commands above:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

An example of merging 2 big repositories, putting one of them into a subdirectory: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Note:
The --allow-unrelated-histories parameter only exists since git >= 2.9. See Git - git merge Documentation / --allow-unrelated-histories

Update:
Added --tags as suggested by @jstadler in order to keep tags.

谈下烟灰 2024-08-12 07:50:05

这里有两种可能的解决方案:

子模块

将存储库 A 复制到较大项目 B 中的单独目录中,或者(也许更好)将存储库 A 克隆到项目 B 中的子目录中。然后使用 git 子模块 以使该存储库是存储库 B 的子模块

对于松散耦合的存储库来说,这是一个很好的解决方案,其中存储库 A 中的开发继续进行,并且开发的主要部分是单独的A 中的独立开发。另请参阅 SubmoduleSupportGitSubmoduleTutorial Git Wiki 上的页面。

子树合并

您可以使用子树合并策略将存储库 A 合并到项目 B 的子目录中。 子树合并与您对此进行了描述马库斯·普林茨。

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(Git >= 2.9.0 需要选项 --allow-unlated-histories。)

或者您可以使用 git subtree 工具 (GitHub 上的存储库)由 apenwarr (Avery Pennarun) 发布,例如在他的博客文章中Git 子模块的新替代方案:git subtree


我认为在你的情况下(A是更大的项目B的一部分),正确的解决方案是使用子树合并

Here are two possible solutions:

Submodules

Either copy repository A into a separate directory in larger project B, or (perhaps better) clone repository A into a subdirectory in project B. Then use git submodule to make this repository a submodule of a repository B.

This is a good solution for loosely-coupled repositories, where development in repository A continues, and the major portion of development is a separate stand-alone development in A. See also SubmoduleSupport and GitSubmoduleTutorial pages on Git Wiki.

Subtree merge

You can merge repository A into a subdirectory of a project B using the subtree merge strategy. This is described in Subtree Merging and You by Markus Prinz.

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(Option --allow-unrelated-histories is needed for Git >= 2.9.0.)

Or you can use git subtree tool (repository on GitHub) by apenwarr (Avery Pennarun), announced for example in his blog post A new alternative to Git submodules: git subtree.


I think in your case (A is to be part of larger project B) the correct solution would be to use subtree merge.

人海汹涌 2024-08-12 07:50:05

另一个存储库的单个分支可以轻松地放置在保留其历史记录的子目录下。例如:

git subtree add --prefix=rails git://github.com/rails/rails.git master

这将显示为单个提交,其中 Rails master 分支的所有文件都添加到“rails”目录中。
然而,提交的标题包含对旧历史树的引用:

从提交中添加“rails/”

其中 是 SHA-1 提交哈希。你仍然可以看到历史,归咎于一些变化。

git log <rev>
git blame <rev> -- README.md

请注意,您无法从这里看到目录前缀,因为这是一个完整的实际旧分支。
您应该将其视为通常的文件移动提交:到达它时您将需要额外的跳转。

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

还有更复杂的解决方案,例如手动执行此操作或重写历史记录,如其他答案中所述。

git-subtree 命令是官方 git-contrib 的一部分,一些数据包管理器默认安装它(OS X Homebrew)。
但除了 git 之外,你可能还需要自己安装它。

A single branch of another repository can be easily placed under a subdirectory retaining its history. For example:

git subtree add --prefix=rails git://github.com/rails/rails.git master

This will appear as a single commit where all files of Rails master branch are added into "rails" directory.
However the commit's title contains a reference to the old history tree:

Add 'rails/' from commit <rev>

Where <rev> is a SHA-1 commit hash. You can still see the history, blame some changes.

git log <rev>
git blame <rev> -- README.md

Note that you can't see the directory prefix from here since this is an actual old branch left intact.
You should treat this like a usual file move commit: you will need an extra jump when reaching it.

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

There are more complex solutions like doing this manually or rewriting the history as described in other answers.

The git-subtree command is a part of official git-contrib, some packet managers install it by default (OS X Homebrew).
But you might have to install it by yourself in addition to git.

⒈起吃苦の倖褔 2024-08-12 07:50:05

如果您想单独维护项目,子模块方法是很好的选择。但是,如果您确实想将两个项目合并到同一个存储库中,那么您还有更多工作要做。

第一件事是使用 git filter-branch 重写第二个存储库中所有内容的名称,使其位于您希望它们结束的子目录中。因此,您将拥有 projb/foo.cprojb/bar.html<,而不是 foo.cbar.html /代码>。

然后,您应该能够执行如下操作:

git remote add projb [wherever]
git pull projb

git pull 将执行 git fetch,然后执行 git merge。如果您要拉取的存储库还没有 projb/ 目录,则应该不会发生冲突。

进一步搜索表明,进行了类似的操作将 gitk 合并到 git 中。 Junio C Hamano 在这里写到: http://www.mail -archive.com/[电子邮件受保护]/msg03395.html

The submodule approach is good if you want to maintain the project separately. However, if you really want to merge both projects into the same repository, then you have a bit more work to do.

The first thing would be to use git filter-branch to rewrite the names of everything in the second repository to be in the subdirectory where you would like them to end up. So instead of foo.c, bar.html, you would have projb/foo.c and projb/bar.html.

Then, you should be able to do something like the following:

git remote add projb [wherever]
git pull projb

The git pull will do a git fetch followed by a git merge. There should be no conflicts, if the repository you're pulling to does not yet have a projb/ directory.

Further searching indicates that something similar was done to merge gitk into git. Junio C Hamano writes about it here: http://www.mail-archive.com/[email protected]/msg03395.html

舟遥客 2024-08-12 07:50:05

git-subtree 很好,但它可能不是您想要的。

例如,如果 projectA 是在 B 中创建的目录,则之后git 子树

git log projectA

仅列出一个提交:合并。合并项目的提交针对不同的路径,因此它们不会显示。

Greg Hewgill 的答案最接近,尽管它实际上没有说明如何重写路径。


解决方案出奇地简单。

(1) 在A中,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

注:这重写了历史;您可能需要先备份 A。

注意:如果您在文件名或路径中使用非 ascii 字符(或白色字符),则必须修改 sed 命令内的替代脚本。在这种情况下,由“ls-files -s”生成的记录内的文件位置以引号开头。

(2) 然后在 B 中运行

git pull path/to/A

瞧!您在 B 中有一个 projectA 目录。如果您运行 git log projectA,您将看到来自 A 的所有提交。


在我的例子中,我想要两个子目录,projectAprojectB。在这种情况下,我也对 B 执行了步骤 (1)。

git-subtree is nice, but it is probably not the one you want.

For example, if projectA is the directory created in B, after git subtree,

git log projectA

lists only one commit: the merge. The commits from the merged project are for different paths, so they don't show up.

Greg Hewgill's answer comes closest, although it doesn't actually say how to rewrite the paths.


The solution is surprisingly simple.

(1) In A,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

Note: This rewrites history; you may want to first make a backup of A.

Note Bene: You have to modify the substitute script inside the sed command in the case that you use non-ascii characters (or white characters) in file names or path. In that case the file location inside a record produced by "ls-files -s" begins with quotation mark.

(2) Then in B, run

git pull path/to/A

Voila! You have a projectA directory in B. If you run git log projectA, you will see all commits from A.


In my case, I wanted two subdirectories, projectA and projectB. In that case, I did step (1) to B as well.

把昨日还给我 2024-08-12 07:50:05

如果两个存储库具有相同类型的文件(例如不同项目的两个 Rails 存储库),您可以将辅助存储库的数据提取到当前存储库:

git fetch git://repository.url/repo.git master:branch_name

然后将其合并到当前存储库:

git merge --allow-unrelated-histories branch_name

如果您的 Git 版本小于 2.9,请删除 <代码>--允许不相关的历史。

此后,可能会发生冲突。例如,您可以使用 git mergetool 来解决它们。 kdiff3 可以单独使用键盘,因此读取代码时只需几分钟即可获得 5 个冲突文件。

记得完成合并:

git commit

If both repositories have same kind of files (like two Rails repositories for different projects), you can fetch data of the secondary repository to your current repository:

git fetch git://repository.url/repo.git master:branch_name

and then merge it to current repository:

git merge --allow-unrelated-histories branch_name

If your Git version is smaller than 2.9, remove --allow-unrelated-histories.

After this, conflicts may occur. You can resolve them for example with git mergetool. kdiff3 can be used solely with keyboard, so 5 conflict file takes when reading the code just few minutes.

Remember to finish the merge:

git commit
Spring初心 2024-08-12 07:50:05

使用合并时我不断丢失历史记录,因此我最终使用了 rebase,因为在我的情况下,两个存储库足够不同,不会在每次提交时最终合并:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=>解决冲突,然后根据需要多次继续...

git rebase --continue

这样做会导致一个项目拥有来自 projA 的所有提交,然后是来自 projB 的提交

I kept losing history when using merge, so I ended up using rebase since in my case the two repositories are different enough not to end up merging at every commit:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> resolve conflicts, then continue, as many times as needed...

git rebase --continue

Doing this leads to one project having all commits from projA followed by commits from projB

笔芯 2024-08-12 07:50:05

就我而言,我有一个 my-plugin 存储库和一个 main-project 存储库,我想假装 my-plugin 始终已在 main-projectplugins 子目录中开发。

基本上,我重写了 my-plugin 存储库的历史,以便所有开发都发生在 plugins/my-plugin 子目录中。然后,我将 my-plugin 的开发历史添加到 main-project 历史中,并将两棵树合并在一起。由于 main-project 存储库中不存在 plugins/my-plugin 目录,因此这是一个简单的无冲突合并。生成的存储库包含两个原始项目的所有历史记录,并且有两个根。

TL;DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

长版本

首先,创建 my-plugin 存储库的副本,因为我们将重写此存储库的历史记录。

现在,导航到 my-plugin 存储库的根目录,检查您的主分支(可能是 master),然后运行以下命令。当然,无论您的实际名称是什么,您都应该替换 my-pluginplugins

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

现在进行解释。 git filter-branch --tree-filter (...) HEAD 在每次可从 HEAD 访问的提交上运行 (...) 命令代码>.请注意,这直接对每次提交存储的数据进行操作,因此我们不必担心“工作目录”、“索引”、“暂存”等概念。

如果您运行 filter-branch 命令失败,它会在 .git 目录中留下一些文件,并在您下次尝试 filter-branch 时使用code> 它将抱怨这一点,除非您向 filter-branch 提供 -f 选项。

至于实际的命令,我没有太多运气让 bash 来做我想做的事情,所以我使用 zsh -c 来制作 zsh 执行命令。首先,我设置了 extended_glob 选项,该选项在 mv 命令中启用 ^(...) 语法,以及 < code>glob_dots 选项,它允许我使用 glob (^(...)) 选择点文件(例如 .gitignore)。

接下来,我使用 mkdir -p 命令同时创建 pluginsplugins/my-plugin

最后,我使用 zsh“负 glob”功能 ^(.git|plugins) 来匹配存储库根目录中除 .git 之外的所有文件 和新创建的 my-plugin 文件夹。 (此处可能不需要排除 .git,但尝试将目录移动到其自身中是一个错误。)

在我的存储库中,初始提交不包含任何文件,因此 mv< /code> 命令在初始提交时返回错误(因为没有任何内容可以移动)。因此,我添加了一个|| true 这样 git filter-branch 就不会中止。

--all 选项告诉 filter-branch 重写存储库中所有分支的历史记录,以及额外的 --< /code> 有必要告诉 git 将其解释为要重写的分支选项列表的一部分,而不是作为 filter-branch 本身的选项。

现在,导航到您的 main-project 存储库并检查您想要合并到的任何分支。将 my-plugin 存储库的本地副本(其历史记录已修改)添加为 main-project 的远程版本:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

您的提交历史记录中现在将有两个不相关的树,您可以使用以下方式很好地可视化:

$ git log --color --graph --decorate --all

要合并它们,请使用:

$ git merge my-plugin/master --allow-unrelated-histories

请注意,在 2.9.0 之前的 Git 中,--allow-unlated-histories 选项不存在。如果您使用的是这些版本之一,只需省略该选项即可:2.9.0 中还添加了 --allow-unlated-histories 阻止的错误消息。 em>

你不应该有任何合并冲突。如果这样做,则可能意味着 filter-branch 命令无法正常工作,或者 main-project 中已经存在 plugins/my-plugin 目录

确保为任何未来的贡献者输入解释性提交消息,他们想知道黑客是如何创建具有两个根的存储库的。

您可以使用上面的 git log 命令可视化新的提交图,它应该有两个根提交。请注意,只有 master 分支会被合并。这意味着,如果您在其他 my-plugin 分支上进行了重要工作并希望将其合并到 main-project 树中,则应避免删除 my-plugin 分支。 -plugin 远程,直到完成这些合并。如果不这样做,那么来自这些分支的提交仍将位于 main-project 存储库中,但有些分支将无法访问并且容易受到最终垃圾回收的影响。 (此外,您必须通过 SHA 引用它们,因为删除远程会删除其远程跟踪分支。)

或者,在合并了想要从 my-plugin 中保留的所有内容后,您可以使用以下方法远程删除 my-plugin

$ git remote remove my-plugin

您现在可以安全地删除您更改了历史记录的 my-plugin 存储库的副本。就我而言,在合并完成并推送后,我还在真正的 my-plugin 存储库中添加了弃用通知。


在 Mac OS X El Capitan 上使用 git --version 2.9.0zsh --version 5.2 进行了测试。您的里程可能会有所不同。

参考文献:

In my case, I had a my-plugin repository and a main-project repository, and I wanted to pretend that my-plugin had always been developed in the plugins subdirectory of main-project.

Basically, I rewrote the history of the my-plugin repository so that it appeared all development took place in the plugins/my-plugin subdirectory. Then, I added the development history of my-plugin into the main-project history, and merged the two trees together. Since there was no plugins/my-plugin directory already present in the main-project repository, this was a trivial no-conflicts merge. The resulting repository contained all history from both original projects, and had two roots.

TL;DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

Long version

First, create a copy of the my-plugin repository, because we're going to be rewriting the history of this repository.

Now, navigate to the root of the my-plugin repository, check out your main branch (probably master), and run the following command. Of course, you should substitute for my-plugin and plugins whatever your actual names are.

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

Now for an explanation. git filter-branch --tree-filter (...) HEAD runs the (...) command on every commit that is reachable from HEAD. Note that this operates directly on the data stored for each commit, so we don't have to worry about notions of "working directory", "index", "staging", and so on.

If you run a filter-branch command that fails, it will leave behind some files in the .git directory and the next time you try filter-branch it will complain about this, unless you supply the -f option to filter-branch.

As for the actual command, I didn't have much luck getting bash to do what I wanted, so instead I use zsh -c to make zsh execute a command. First I set the extended_glob option, which is what enables the ^(...) syntax in the mv command, as well as the glob_dots option, which allows me to select dotfiles (such as .gitignore) with a glob (^(...)).

Next, I use the mkdir -p command to create both plugins and plugins/my-plugin at the same time.

Finally, I use the zsh "negative glob" feature ^(.git|plugins) to match all files in the root directory of the repository except for .git and the newly created my-plugin folder. (Excluding .git might not be necessary here, but trying to move a directory into itself is an error.)

In my repository, the initial commit did not include any files, so the mv command returned an error on the initial commit (since nothing was available to move). Therefore, I added a || true so that git filter-branch would not abort.

The --all option tells filter-branch to rewrite the history for all branches in the repository, and the extra -- is necessary to tell git to interpret it as a part of the option list for branches to rewrite, instead of as an option to filter-branch itself.

Now, navigate to your main-project repository and check out whatever branch you want to merge into. Add your local copy of the my-plugin repository (with its history modified) as a remote of main-project with:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

You will now have two unrelated trees in your commit history, which you can visualize nicely using:

$ git log --color --graph --decorate --all

To merge them, use:

$ git merge my-plugin/master --allow-unrelated-histories

Note that in pre-2.9.0 Git, the --allow-unrelated-histories option does not exist. If you are using one of these versions, just omit the option: the error message that --allow-unrelated-histories prevents was also added in 2.9.0.

You should not have any merge conflicts. If you do, it probably means that either the filter-branch command did not work correctly or there was already a plugins/my-plugin directory in main-project.

Make sure to enter an explanatory commit message for any future contributors wondering what hackery was going on to make a repository with two roots.

You can visualize the new commit graph, which should have two root commits, using the above git log command. Note that only the master branch will be merged. This means that if you have important work on other my-plugin branches that you want to merge into the main-project tree, you should refrain from deleting the my-plugin remote until you have done these merges. If you don't, then the commits from those branches will still be in the main-project repository, but some will be unreachable and susceptible to eventual garbage collection. (Also, you will have to refer to them by SHA, because deleting a remote removes its remote-tracking branches.)

Optionally, after you have merged everything you want to keep from my-plugin, you can remove the my-plugin remote using:

$ git remote remove my-plugin

You can now safely delete the copy of the my-plugin repository whose history you changed. In my case, I also added a deprecation notice to the real my-plugin repository after the merge was complete and pushed.


Tested on Mac OS X El Capitan with git --version 2.9.0 and zsh --version 5.2. Your mileage may vary.

References:

壹場煙雨 2024-08-12 07:50:05

几天来我一直在尝试做同样的事情,我使用的是 git 2.7.2。子树不保留历史记录。

如果您不再使用旧项目,可以使用此方法。

我建议你先去B分行,然后在分行工作。

以下是不分支的步骤:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

如果您现在记录子目录 A 中的任何文件,您将获得完整的历史记录

git log --follow A/<file>

这是帮助我执行此操作的帖子:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into -one-repository-without-losing-file-history/

I've been trying to do the same thing for days, I am using git 2.7.2. Subtree does not preserve the history.

You can use this method if you will not be using the old project again.

I would suggest that you branch B first and work in the branch.

Here are the steps without branching:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

If you now log any of the files in subdir A you will get the full history

git log --follow A/<file>

This was the post that help me do this:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/

空城之時有危險 2024-08-12 07:50:05

如果您想将存储库 B 中的分支中的文件放入存储库 A 的子树中,并且也保留历史,请继续阅读。 (在下面的示例中,我假设我们希望将存储库 B 的主分支合并到存储库 A 的主分支中。)

在存储库 A 中,首先执行以下操作以使存储库 B 可用:

git remote add B ../B # Add repo B as a new remote.
git fetch B

现在我们创建一个全新的分支(只有一次提交) )在存储库 A 中,我们称之为 new_b_root。生成的提交将包含在存储库 B 的 master 分支的第一次提交中提交的文件,但放入名为 path/to/b-files/ 的子目录中。

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

说明:checkout 命令的 --orphan 选项从 A 的 master 分支中检出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们无论如何都会清除所有文件。然后,在尚未提交的情况下 (-n),我们从 B 的 master 分支中挑选第一个提交。 (cherry-pick 保留了原始提交消息,而直接签出似乎无法做到这一点。)然后我们创建子树,将存储库 B 中的所有文件放入其中。然后,我们必须移动在存储库 B 中引入的所有文件。樱桃挑选到子树。在上面的示例中,只有一个 README 文件需要移动。然后我们提交 B-repo 根提交,同时我们还保留原始提交的时间戳。

现在,我们将在新创建的 new_b_root 之上创建一个新的 B/master 分支。我们将新分支称为 b

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

现在,我们将 b 分支合并到 A/master

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

最后,您可以删除 B 远程和临时分支:

git remote remove B
git branch -D new_b_root b

最终图表将具有如下结构:

在此处输入图像描述

If you want to put the files from a branch in repo B in a subtree of repo A and also preserve the history, keep reading. (In the example below, I am assuming that we want repo B's master branch merged into repo A's master branch.)

In repo A, first do the following to make repo B available:

git remote add B ../B # Add repo B as a new remote.
git fetch B

Now we create a brand new branch (with only one commit) in repo A that we call new_b_root. The resulting commit will have the files that were committed in the first commit of repo B's master branch but put in a subdirectory called path/to/b-files/.

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

Explanation: The --orphan option to the checkout command checks out the files from A's master branch but doesn't create any commit. We could have selected any commit because next we clear out all the files anyway. Then, without committing yet (-n), we cherry-pick the first commit from B's master branch. (The cherry-pick preserves the original commit message which a straight checkout doesn't seem to do.) Then we create the subtree where we want to put all files from repo B. We then have to move all files that were introduced in the cherry-pick to the subtree. In the example above, there's only a README file to move. Then we commit our B-repo root commit, and, at the same time, we also preserve the timestamp of the original commit.

Now, we'll create a new B/master branch on top of the newly created new_b_root. We call the new branch b:

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

Now, we merge our b branch into A/master:

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

Finally, you can remove the B remote and temporary branches:

git remote remove B
git branch -D new_b_root b

The final graph will have a structure like this:

enter image description here

葬心 2024-08-12 07:50:05

我在这里收集了有关 StackOverFlow 等的大量信息,并设法将一个脚本放在一起,为我解决了问题。

需要注意的是,它只考虑每个存储库的“开发”分支,并将其合并到全新存储库中的单独目录中。

标签和其他分支将被忽略 - 这可能不是您想要的。

该脚本甚至可以处理功能分支和标签 - 在新项目中重命名它们,以便您知道它们来自哪里。

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            [email protected]
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=

您还可以从 http://paste.ubuntu.com/11732805 获取它。

首先创建一个文件,其中包含每个存储库的 URL,例如:

[email protected]:eitchnet/ch.eitchnet.parent.git
[email protected]:eitchnet/ch.eitchnet.utils.git
[email protected]:eitchnet/ch.eitchnet.privilege.git

然后调用脚本,给出项目名称和脚本路径:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

脚本本身有很多注释,应该解释它的作用。

\n' # Detect proper usage if [ "$#" -ne "2" ] ; then echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>" exit 1 fi ## Script variables PROJECT_NAME="${1}" PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}" TIMESTAMP="$(date +%s)" LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log" REPO_FILE="${2}" REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}" # Script functions function failed() { echo -e "ERROR: Merging of projects failed:" echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1 echo -e "$1" exit 1 } function commit_merge() { current_branch="$(git symbolic-ref HEAD 2>/dev/null)" if [[ ! -f ".git/MERGE_HEAD" ]] ; then echo -e "INFO: No commit required." echo -e "INFO: No commit required." >>${LOG_FILE} 2>&1 else echo -e "INFO: Committing ${sub_project}..." echo -e "INFO: Committing ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}" fi fi } # Make sure the REPO_URL_FILE exists if [ ! -e "${REPO_URL_FILE}" ] ; then echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!" exit 1 fi # Make sure the required directories don't exist if [ -e "${PROJECT_PATH}" ] ; then echo -e "ERROR: Project ${PROJECT_NAME} already exists!" exit 1 fi # create the new project echo -e "INFO: Logging to ${LOG_FILE}" echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 cd ${ROOT_DIR} mkdir ${PROJECT_NAME} cd ${PROJECT_NAME} git init echo "Initial Commit" > initial_commit # Since this is a new repository we need to have at least one commit # thus were we create temporary file, but we delete it again. # Deleting it guarantees we don't have conflicts later when merging git add initial_commit git commit --quiet -m "[Project] Initial Master Repo Commit" git rm --quiet initial_commit git commit --quiet -m "[Project] Initial Master Repo Commit" echo # Merge all projects into the branches of this project echo -e "INFO: Merging projects into new repository..." echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 for url in $(cat ${REPO_URL_FILE}) ; do if [[ "${url:0:1}" == '#' ]] ; then continue fi # extract the name of this project export sub_project=${url##*/} sub_project=${sub_project%*.git} echo -e "INFO: Project ${sub_project}" echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1 echo -e "----------------------------------------------------" echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1 # Fetch the project echo -e "INFO: Fetching ${sub_project}..." echo -e "INFO: Fetching ${sub_project}..." >>${LOG_FILE} 2>&1 git remote add "${sub_project}" "${url}" if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then failed "Failed to fetch project ${sub_project}" fi # add remote branches echo -e "INFO: Creating local branches for ${sub_project}..." echo -e "INFO: Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1 while read branch ; do branch_ref=$(echo $branch | tr " " "\t" | cut -f 1) branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-) echo -e "INFO: Creating branch ${branch_name}..." echo -e "INFO: Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1 # create and checkout new merge branch off of master if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi # Merge the project echo -e "INFO: Merging ${sub_project}..." echo -e "INFO: Merging ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}" fi # And now see if we need to commit (maybe there was a merge) commit_merge "${sub_project}/${branch_name}" # relocate projects files into own directory if [ "$(ls)" == "${sub_project}" ] ; then echo -e "WARN: Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." echo -e "WARN: Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1 else echo -e "INFO: Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." echo -e "INFO: Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1 mkdir ${sub_project} for f in $(ls -a) ; do if [[ "$f" == "${sub_project}" ]] || [[ "$f" == "." ]] || [[ "$f" == ".." ]] ; then continue fi git mv -k "$f" "${sub_project}/" done # commit the moving if ! git commit --quiet -m "[Project] Move ${sub_project} files into sub directory" ; then failed "Failed to commit moving of ${sub_project} files into sub directory" fi fi echo done < <(git ls-remote --heads ${sub_project}) # checkout master of sub probject if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then failed "sub_project ${sub_project} is missing master branch!" fi # copy remote tags echo -e "INFO: Copying tags for ${sub_project}..." echo -e "INFO: Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1 while read tag ; do tag_ref=$(echo $tag | tr " " "\t" | cut -f 1) tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3) # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0 tag_name="${tag_name_unfixed%%^*}" tag_new_name="${sub_project}/${tag_name}" echo -e "INFO: Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." echo -e "INFO: Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1 if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then echo -e "WARN: Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" echo -e "WARN: Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1 fi done < <(git ls-remote --tags --refs ${sub_project}) # Remove the remote to the old project echo -e "INFO: Removing remote ${sub_project}..." echo -e "INFO: Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1 git remote rm ${sub_project} echo done # Now merge all project master branches into new master git checkout --quiet master echo -e "INFO: Merging projects master branches into new repository..." echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 for url in $(cat ${REPO_URL_FILE}) ; do if [[ ${url:0:1} == '#' ]] ; then continue fi # extract the name of this project export sub_project=${url##*/} sub_project=${sub_project%*.git} echo -e "INFO: Merging ${sub_project}..." echo -e "INFO: Merging ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then failed "Failed to merge branch ${sub_project}/master into master" fi # And now see if we need to commit (maybe there was a merge) commit_merge "${sub_project}/master" echo done # Done cd ${ROOT_DIR} echo -e "INFO: Done." echo -e "INFO: Done." >>${LOG_FILE} 2>&1 echo exit 0

您还可以从 http://paste.ubuntu.com/11732805 获取它。

首先创建一个文件,其中包含每个存储库的 URL,例如:

然后调用脚本,给出项目名称和脚本路径:

脚本本身有很多注释,应该解释它的作用。

I have gathered a lot of information here on Stack OverFlow, etc., and have manage to put a script together which solves the problem for me.

The caveat is that it only takes into account the 'develop' branch of each repository and merges it into a separate directory in a completely new repository.

Tags and other branches are ignored - this might not be what you want.

The script even handles feature branches and tags - renaming them in the new project so you know where they came from.

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            [email protected]
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=

You can also get it from http://paste.ubuntu.com/11732805

First create a file with the URL to each repository, e.g.:

[email protected]:eitchnet/ch.eitchnet.parent.git
[email protected]:eitchnet/ch.eitchnet.utils.git
[email protected]:eitchnet/ch.eitchnet.privilege.git

Then call the script giving a name of the project and the path to the script:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

The script itself has a lot of comments which should explain what it does.

\n' # Detect proper usage if [ "$#" -ne "2" ] ; then echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>" exit 1 fi ## Script variables PROJECT_NAME="${1}" PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}" TIMESTAMP="$(date +%s)" LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log" REPO_FILE="${2}" REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}" # Script functions function failed() { echo -e "ERROR: Merging of projects failed:" echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1 echo -e "$1" exit 1 } function commit_merge() { current_branch="$(git symbolic-ref HEAD 2>/dev/null)" if [[ ! -f ".git/MERGE_HEAD" ]] ; then echo -e "INFO: No commit required." echo -e "INFO: No commit required." >>${LOG_FILE} 2>&1 else echo -e "INFO: Committing ${sub_project}..." echo -e "INFO: Committing ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}" fi fi } # Make sure the REPO_URL_FILE exists if [ ! -e "${REPO_URL_FILE}" ] ; then echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!" exit 1 fi # Make sure the required directories don't exist if [ -e "${PROJECT_PATH}" ] ; then echo -e "ERROR: Project ${PROJECT_NAME} already exists!" exit 1 fi # create the new project echo -e "INFO: Logging to ${LOG_FILE}" echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 cd ${ROOT_DIR} mkdir ${PROJECT_NAME} cd ${PROJECT_NAME} git init echo "Initial Commit" > initial_commit # Since this is a new repository we need to have at least one commit # thus were we create temporary file, but we delete it again. # Deleting it guarantees we don't have conflicts later when merging git add initial_commit git commit --quiet -m "[Project] Initial Master Repo Commit" git rm --quiet initial_commit git commit --quiet -m "[Project] Initial Master Repo Commit" echo # Merge all projects into the branches of this project echo -e "INFO: Merging projects into new repository..." echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 for url in $(cat ${REPO_URL_FILE}) ; do if [[ "${url:0:1}" == '#' ]] ; then continue fi # extract the name of this project export sub_project=${url##*/} sub_project=${sub_project%*.git} echo -e "INFO: Project ${sub_project}" echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1 echo -e "----------------------------------------------------" echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1 # Fetch the project echo -e "INFO: Fetching ${sub_project}..." echo -e "INFO: Fetching ${sub_project}..." >>${LOG_FILE} 2>&1 git remote add "${sub_project}" "${url}" if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then failed "Failed to fetch project ${sub_project}" fi # add remote branches echo -e "INFO: Creating local branches for ${sub_project}..." echo -e "INFO: Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1 while read branch ; do branch_ref=$(echo $branch | tr " " "\t" | cut -f 1) branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-) echo -e "INFO: Creating branch ${branch_name}..." echo -e "INFO: Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1 # create and checkout new merge branch off of master if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi # Merge the project echo -e "INFO: Merging ${sub_project}..." echo -e "INFO: Merging ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}" fi # And now see if we need to commit (maybe there was a merge) commit_merge "${sub_project}/${branch_name}" # relocate projects files into own directory if [ "$(ls)" == "${sub_project}" ] ; then echo -e "WARN: Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." echo -e "WARN: Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1 else echo -e "INFO: Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." echo -e "INFO: Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1 mkdir ${sub_project} for f in $(ls -a) ; do if [[ "$f" == "${sub_project}" ]] || [[ "$f" == "." ]] || [[ "$f" == ".." ]] ; then continue fi git mv -k "$f" "${sub_project}/" done # commit the moving if ! git commit --quiet -m "[Project] Move ${sub_project} files into sub directory" ; then failed "Failed to commit moving of ${sub_project} files into sub directory" fi fi echo done < <(git ls-remote --heads ${sub_project}) # checkout master of sub probject if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then failed "sub_project ${sub_project} is missing master branch!" fi # copy remote tags echo -e "INFO: Copying tags for ${sub_project}..." echo -e "INFO: Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1 while read tag ; do tag_ref=$(echo $tag | tr " " "\t" | cut -f 1) tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3) # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0 tag_name="${tag_name_unfixed%%^*}" tag_new_name="${sub_project}/${tag_name}" echo -e "INFO: Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." echo -e "INFO: Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1 if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then echo -e "WARN: Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" echo -e "WARN: Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1 fi done < <(git ls-remote --tags --refs ${sub_project}) # Remove the remote to the old project echo -e "INFO: Removing remote ${sub_project}..." echo -e "INFO: Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1 git remote rm ${sub_project} echo done # Now merge all project master branches into new master git checkout --quiet master echo -e "INFO: Merging projects master branches into new repository..." echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1 echo -e "====================================================" echo -e "====================================================" >>${LOG_FILE} 2>&1 for url in $(cat ${REPO_URL_FILE}) ; do if [[ ${url:0:1} == '#' ]] ; then continue fi # extract the name of this project export sub_project=${url##*/} sub_project=${sub_project%*.git} echo -e "INFO: Merging ${sub_project}..." echo -e "INFO: Merging ${sub_project}..." >>${LOG_FILE} 2>&1 if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then failed "Failed to merge branch ${sub_project}/master into master" fi # And now see if we need to commit (maybe there was a merge) commit_merge "${sub_project}/master" echo done # Done cd ${ROOT_DIR} echo -e "INFO: Done." echo -e "INFO: Done." >>${LOG_FILE} 2>&1 echo exit 0

You can also get it from http://paste.ubuntu.com/11732805

First create a file with the URL to each repository, e.g.:

Then call the script giving a name of the project and the path to the script:

The script itself has a lot of comments which should explain what it does.

殊姿 2024-08-12 07:50:05

我知道这已经是很久以后的事了,但我对在这里找到的其他答案并不满意,所以我写了这个:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

I know it's long after the fact, but I wasn't happy with the other answers I found here, so I wrote this:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done
蓝戈者 2024-08-12 07:50:05

如果您尝试简单地将两个存储库粘合在一起,则子模块和子树合并是错误的工具,因为它们不会保留所有文件历史记录(正如人们在其他答案中指出的那样)。请参阅此处的答案,了解简单而正确的方法。

If you're trying to simply glue two repositories together, submodules and subtree merges are the wrong tool to use because they don't preserve all of the file history (as people have noted on other answers). See this answer here for the simple and correct way to do this.

家住魔仙堡 2024-08-12 07:50:05

我遇到了类似的挑战,但就我而言,我们在存储库 A 中开发了一个版本的代码库,然后将其克隆到新的存储库(存储库 B)中,以用于新版本的产品。修复了存储库 A 中的一些错误后,我们需要将更改修改到存储库 B 中。最终执行了以下操作:

  1. 向存储库 B 添加指向存储库 A 的远程(git remote add...)
  2. 拉取当前分支(我们是不使用 master 进行错误修复)(git pull remoteForRepoA bugFixBranch)
  3. 将合并推送到 github

很不错:)

I had a similar challenge, but in my case, we had developed one version of the codebase in repo A, then cloned that into a new repo, repo B, for the new version of the product. After fixing some bugs in repo A, we needed to FI the changes into repo B. Ended up doing the following:

  1. Adding a remote to repo B that pointed to repo A (git remote add...)
  2. Pulling the current branch (we were not using master for bug fixes) (git pull remoteForRepoA bugFixBranch)
  3. Pushing merges to github

Worked a treat :)

沫离伤花 2024-08-12 07:50:05

合并 2 个存储库

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

Merging 2 repos

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master
晨与橙与城 2024-08-12 07:50:05

与 @Smar 类似,但使用文件系统路径,在 PRIMARY 和 SECONDARY 中设置:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

然后手动合并。

(改编自 Anar Manafov 的帖子

Similar to @Smar but uses file system paths, set in PRIMARY and SECONDARY:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

Then you manually merge.

(adapted from post by Anar Manafov)

半步萧音过轻尘 2024-08-12 07:50:05

当您想要在单个提交中合并三个或更多项目时,请执行其他答案中描述的步骤(remote add -fmerge)。然后,(软)将索引重置为旧头(没有发生合并)。添加所有文件 (git add -A) 并提交它们(消息“将项目 A、B、C 和 D 合并为一个项目)。这现在是 master 的提交 ID。

现在,创建.git/info/grafts 包含以下内容:

<commit-id of master> <list of commit ids of all parents>

运行 git filter-branch -- head^..head head^2..head head^3..head 如果。如果您有三个以上的分支,只需添加与分支数量一样多的 head^n..head 即可。要更新标签,请附加 --tag-name-filter cat。不要总是添加它,因为这可能会导致重写某些提交。有关详细信息,请参阅 filter-branch 的手册页,搜索“grafts”。

现在,您的最后一次提交已关联正确的父级。

When you want to merge three or more projects in a single commit, do the steps as described in the other answers (remote add -f, merge). Then, (soft) reset the index to old head (where no merge happened). Add all files (git add -A) and commit them (message "Merging projects A, B, C, and D into one project). This is now the commit-id of master.

Now, create .git/info/grafts with following content:

<commit-id of master> <list of commit ids of all parents>

Run git filter-branch -- head^..head head^2..head head^3..head. If you have more than three branches, just add as much head^n..head as you have branches. To update tags, append --tag-name-filter cat. Do not always add that, because this might cause a rewrite of some commits. For details see man page of filter-branch, search for "grafts".

Now, your last commit has the right parents associated.

公布 2024-08-12 07:50:05

要将 A 合并到 B 中:

1) 在项目 A 中

git fast-export --all --date-order > /tmp/ProjectAExport

2) 在项目 B 中

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

在此分支中执行您需要执行的所有操作并提交它们。

C) 然后回到 master 并在两个分支之间进行经典合并:

git checkout master
git merge projectA

To merge a A within B:

1) In the project A

git fast-export --all --date-order > /tmp/ProjectAExport

2) In the project B

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

In this branch do all operations you need to do and commit them.

C) Then back to the master and a classical merge between the two branches:

git checkout master
git merge projectA
离线来电— 2024-08-12 07:50:05

https://github.com/hraban/tomono 作为基于脚本的解决方案的另一个提及。

我不是作者,但使用了它并且它完成了工作。

一个积极的方面是您可以将所有分支和所有历史记录放入最终存储库中。对于我的存储库(存储库中没有重复的文件夹 - 实际上,它们来自 tfs2git 迁移),没有冲突,一切都是自动化运行的。

它主要用于(参见名称)创建 monorepos。

对于Windows用户:git bash可以执行.sh文件。它带有标准的 git 安装。

https://github.com/hraban/tomono as another mention of a script-based solution.

I am not the author but used it and it does the job.

One positive aspect is that you get all the branches and all the history into the final repo. For my repos (no duplicate folders in repos - actually, they came out of tfs2git migration) there were no conflicts and everything ran automated.

It is mainly used (see name) to create monorepos.

For Windows users: git bash can execute the .sh file. It comes with the standard git installation.

老街孤人 2024-08-12 07:50:05

Google 使用 Copybara 工具来处理更复杂的用例 - https://github.com/google/copybara< /a>

There is a Copybara tool used by Google for more complex use cases - https://github.com/google/copybara

丢了幸福的猪 2024-08-12 07:50:05

我稍微手动合并项目,这使我可以避免需要处理合并冲突。

首先,根据需要从其他项目中复制文件。

cp -R myotherproject newdirectory
git add newdirectory

下一次拉入历史记录

git fetch path_or_url_to_other_repo

告诉 git 合并到上次获取的历史记录中,

echo 'FETCH_HEAD' > .git/MERGE_HEAD

现在提交,但是您通常会提交

git commit

I merge projects slightly manually, which allows me to avoid needing to deal with merge conflicts.

first, copy in the files from the other project however you want them.

cp -R myotherproject newdirectory
git add newdirectory

next pull in the history

git fetch path_or_url_to_other_repo

tell git to merge in the history of last fetched thing

echo 'FETCH_HEAD' > .git/MERGE_HEAD

now commit however you normally would commit

git commit
檐上三寸雪 2024-08-12 07:50:05

我今天必须按如下方式解决它:
项目 A 位于 bitbucket 中,项目 B 位于代码提交中.. 两者是相同的项目,但必须合并从 A 到 B 的更改。(技巧是在项目 A 中创建相同名称的分支,与项目 B 中的相同)

  • git签出项目A
  • git远程删除原点
  • git远程添加原点项目B
  • git签出分支
  • git add *
  • git commit -m“我们已经移动了代码”
  • git Push

I had to solve it as follows today:
Project A was in bitbucket and Project B was in code commit .. both are the same projects but had to merge changes from A to B. (The trick is to create the same name branch in Project A, same as in Project B)

  • git checkout Project A
  • git remote remove origin
  • git remote add origin Project B
  • git checkout branch
  • git add *
  • git commit -m "we have moved the code"
  • git push
木緿 2024-08-12 07:50:05

该功能将远程仓库克隆到本地仓库目录,合并后所有提交将被保存,git log将显示原始提交和正确的路径:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

如何使用:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

如果做一点更改,您甚至可以将合并的存储库的文件/目录移动到不同的路径,例如:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

通知
路径通过 sed 替换,因此请确保合并后它移动到正确的路径中。
--allow-unlated-histories 参数仅自 git >= 2.9 起存在。

This function will clone remote repo into local repo dir, after merging all commits will be saved, git log will be show the original commits and proper paths:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

How to use:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

If make a little changes you can even move files/dirs of merged repo into different paths, for example:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

Notices
Paths replaces via sed, so make sure it moved in proper paths after merging.
The --allow-unrelated-histories parameter only exists since git >= 2.9.

指尖上得阳光 2024-08-12 07:50:05

除了使用远程添加的所有答案 -> 获取 -> merge 策略:如果您想保留其他存储库中的标签,但又不想将它们全部溢出到公共命名空间中(并且可能会发生冲突),您可能需要稍微更改一下 fetch 命令:

git fetch --no-tags other_repo
git fetch --no-tags other_repo 'refs/tags/*:refs/tags/other_repo/*'

第一个命令照常获取所有分支,但省略附加到提交的标签,第二个命令也省略通常的标签获取机制(git help fetch 了解更多),并获取映射它们的所有标签使用 git 的 refspec 功能将 X 转换为 other_repo/X

引用(分支、标签)只是 git 中的文件,您可以使用目录进行命名空间。上面的两个命令将按原样保留第一个存储库中的标签,而另一个存储库中的标签将以 other_repo/ 为前缀。

操作完成后,最好删除另一个远程存储库,这样您就不需要不要意外地以正常方式获取标签并弄乱。

In addition to all the answers using remote add -> fetch -> merge strategy: if you want to preserve tags from the other repository but don't want to spill them all into a common namespace (and possibly get collisions) you might want to change the fetch command a little bit:

git fetch --no-tags other_repo
git fetch --no-tags other_repo 'refs/tags/*:refs/tags/other_repo/*'

First command fetches all the branches as usual, but omits tags attached to commits, the second one also omits the usual tag fetching mechanism (git help fetch for more), and fetches all the tags mapping them from X to other_repo/X using git's refspec functionality.

References (branches, tags) are just files in git, and you can use directories for namespacing. The two commands above will preserve tags from the first repository as-is, and the ones from the other one will be prefixed with other_repo/

After the operation it's best to remove the other remote, so you don't accidentally fetch the tags the normal way and make a mess.

失眠症患者 2024-08-12 07:50:05

给定命令是我建议的最佳解决方案。

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master

Given command is the best possible solution I suggest.

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master
凉墨 2024-08-12 07:50:05

我想将一个小项目移动到一个大项目的子目录中。由于我的小项目没有很多提交,因此我使用了 git format-patch --output-directory /path/to/patch-dir 。然后在更大的项目中,我使用了 git am --directory=dir/in/project /path/to/patch-dir/* 。

这感觉比过滤器分支没那么可怕,而且更干净。当然,它可能并不适用于所有情况。

I wanted to move a small project to a subdirectory of a larger one. Since my small project did not have many commits, I used git format-patch --output-directory /path/to/patch-dir. Then on the larger project, I used git am --directory=dir/in/project /path/to/patch-dir/*.

This feels way less scary and way more cleaner than a filter-branch. Granted, it may not be applicable to all cases.

命比纸薄 2024-08-12 07:50:05

注意:此解决方案可能会推送不需要的 LFS 二进制文件(合并的祖先提交未使用这些二进制文件),因此您可能需要改进此解决方案。

完成良好的 Andresch Serj 的回答,在 project-a 使用 Git LFS 以便能够推送的情况下我必须首先执行以下操作:

cd /path/to/project-a
git lfs fetch --all # some old LFS objects might only be on the server of project-a and not in your clone of project-a

# possible alternative to the following two commands (untested): do a git lfs fetch from path/to/project-b
git remote add project-b-origin url-to-project-b-server
git lfs push --all project-b-origin

在我的情况下,这修复了尝试将合并结果合并到项目克隆目录中时遇到的错误:

remote: GitLab: LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".

以及

Unable to find source for object <hash> (try running `git lfs fetch --all`)
Git LFS upload failed:
    (missing) <path> (<hash>)

Note: this solution may push uneeded LFS binaries (that are not used by the merge's ancestor commits), so you might want to improve this solution.

To complete the good answer by Andresch Serj in the case the project-a uses Git LFS in order to be able to push I had to first do:

cd /path/to/project-a
git lfs fetch --all # some old LFS objects might only be on the server of project-a and not in your clone of project-a

# possible alternative to the following two commands (untested): do a git lfs fetch from path/to/project-b
git remote add project-b-origin url-to-project-b-server
git lfs push --all project-b-origin

In my case this fixed the errors I got when attempting to git push the merge result in project-a clone directory:

remote: GitLab: LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".

and

Unable to find source for object <hash> (try running `git lfs fetch --all`)
Git LFS upload failed:
    (missing) <path> (<hash>)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文