如何合并两个Git仓库?

2213
考虑以下情况: 我已经在自己的Git repo中开发了一个小型实验项目A。现在它已经成熟,我想让A成为更大的项目B的一部分,B有自己的大型仓库。现在我想将A作为B的子目录添加进去。 如何将A合并到B中,而不会丢失任何一方的历史记录?

17
如果您只是尝试将两个存储库合并为一个,而不需要保留两个存储库,请参考此问题:https://dev59.com/CWcs5IYBdhLWcg3wOBRC - Flimm
27个回答

2797
如果你想将project-a合并到project-b中:
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/master # or whichever branch you want to merge
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

以下是两个大型仓库合并的示例,其中一个被放入一个子目录中:https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

注意: --allow-unrelated-histories 参数仅在 git >= 2.9 中存在。请参阅Git - git merge文档 / --allow-unrelated-histories

更新: 根据 @jstadler 的建议添加了 --tags 参数以保留标记。


14
这对我很有帮助,第一次使用就像魔法一样奏效,只在.gitignore文件中遇到了一个冲突!它完美地保留了提交历史记录。与其他方法相比,最大的优点是除了简单性外,不需要对合并后的仓库进行持续引用。但要注意的一件事是——如果你和我一样是iOS开发者,则一定要非常小心地将目标仓库的项目文件放入工作区。 - Max MacLeod
44
谢谢。对我有用。我需要将合并后的目录移动到子文件夹中,所以在按照上述步骤操作后,我只需使用 git mv source-dir/ dest/new-source-dir - Sid
7
一种间接的方法是将 project-a 中所有这些文件移动到 project-a 的一个子目录中(使得 project-a 的顶层目录只有这一个目录),然后按照上述过程进行操作。 - Sid
17
在这里使用 git merge 步骤失败,出现了“fatal: refusing to merge unrelated histories”的错误;根据文档说明,可以使用--allow-unrelated-histories来解决这个问题。 - ssc
17
简化版:git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD。翻译:执行这条命令可以将另一个项目(/path/to/project-a)的主分支(master)合并到当前项目中,命令为git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD - jthill
显示剩余36条评论

662

以下是两种可能的解决方案:

子模块

将仓库A复制到较大项目B的单独目录中,或者(可能更好的方式)将仓库A克隆到项目B的子目录中。然后使用git submodule使该仓库成为B仓库的子模块

这是一个适用于松散耦合的仓库的好解决方案,其中在仓库A中进行开发并继续进行开发,而主要开发部分是在A中作为单独独立的开发。也可以在Git Wiki上查看SubmoduleSupportGitSubmoduleTutorial页面。

子树合并

您可以使用子树合并策略将仓库A合并到项目B的子目录中。这在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

(需要在Git版本>=2.9.0中使用选项--allow-unrelated-histories)

或者您可以使用git子树工具(由apenwarr(Avery Pennarun)创建的GitHub上的存储库),例如在他的博客文章一个新的替代Git子模块的选择:git子树中宣布。


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


61
这是不完整的。是的,您会得到许多提交记录,但它们不再指向正确的路径。 git log dir-B/somefile 只会显示一个合并记录。参见 Greg Hewgill 的回答,提到了这个重要问题。 - artfulrobot
2
重要提示:git pull --no-rebase -s subtree Bproject master。如果您不这样做,并且您已将pull设置为自动rebase,那么您最终会遇到“无法解析对象”的问题。请参见http://osdir.com/ml/git/2009-07/msg01576.html。 - Eric Bowman - abstracto -
4
这个答案可能会让人感到困惑,因为在问题中合并的子树是A,但在答案中却是B。这是复制粘贴的结果吗? - vfclists
12
如果你只是想简单地将两个代码库合并在一起,使用子模块和子树合并是错误的工具,因为它们不能保存所有文件的历史记录(正如其他评论者所指出的)。请参考https://dev59.com/CWcs5IYBdhLWcg3wOBRC。 - Eric Lee
1
还有git stree - 据说比subtree更好:https://medium.com/@porteneuve/mastering-git-subtrees-943d29a798ec。 - tomekwi
显示剩余5条评论

515

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

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

这将出现为一个单一的提交,其中Rails主分支的所有文件都添加到“rails”目录中。 但提交的标题包含对旧历史记录树的引用:

Add 'rails/' from commit <rev>

其中<rev>是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 之外。


3
以下是关于如何安装Git SubTree的说明(截至2013年6月):https://dev59.com/dWgu5IYBdhLWcg3wEzHP#11613541(我将`git co v1.7.11.3替换为... v1.8.3`)。 - KajMagnus
1
感谢您提供以下答案的信息。截至git 1.8.4,'subtree'仍未包含在其中(至少在Ubuntu 12.04 git ppa(ppa:git-core / ppa)中没有)。 - Matt Klein
1
我可以确认,在此之后,git log rails/somefile 将不会显示该文件的提交历史记录,除了合并提交。正如 @artfulrobot 建议的那样,请查看 Greg Hewgill 的答案。你可能需要在要包含的仓库上使用 git filter-branch - Jifeng Zhang
8
请阅读Eric Lee的文章《将两个Git存储库合并为一个存储库,同时保留文件历史记录》,链接为http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/。 - Jifeng Zhang
6
正如其他人所说,git subtree 可能不会做你想要的事情!请参见 此处 获取更完整的解决方案。 - Paul Draper
显示剩余5条评论

213

如果您想要分别维护项目,则子模块方法很好。但是,如果您真的想将两个项目合并到同一个存储库中,则需要进行更多工作。

首先,您需要使用 git filter-branch 将第二个存储库中的所有内容的名称重写为您希望它们最终出现的子目录中的名称。因此,不是使用 foo.cbar.html,而是使用 projb/foo.cprojb/bar.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/git@vger.kernel.org/msg03395.html

4
子树合并会是更好的解决方案,并且不需要重写包含项目的历史记录。 - Jakub Narębski
9
我想知道如何使用git filter-branch来实现这一点。在手册页中,它提到了相反的方法:将子目录/变为根目录,但不能用于此情况。 - artfulrobot
40
如果这个答案能够解释如何使用filter-branch来达到预期的结果,那就太好了。 - Anentropic
14
我发现如何使用filter-branch在这里:https://dev59.com/S2855IYBdhLWcg3w_5mm - David Minor
3
请参考 这个答案 实现 Greg 的大纲。 - Paul Draper
显示剩余6条评论

81

git-subtree 很好用,但可能不是你想要的。

例如,如果 projectA 是在 B 中创建的目录,在使用 git subtree 后,

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)。


1
看起来你从 https://dev59.com/9XVC5IYBdhLWcg3wfhKL#618113 复制了你的答案? - Andrew Mao
1
@AndrewMao,我想是这样...实际上我记不清了。我已经用过这个脚本很多次了。 - Paul Draper
6
我想补充一点,在OS X上\t无法使用,你需要输入<tab>。 - Muneeb Ali
2
"$GIT_INDEX_FILE" 必须加引号(两次),否则如果路径中包含空格,您的方法将失败。 - Rob W
4
如果你想知道如何在OSX中插入一个<tab>,你需要使用Ctrl-V <tab> - casey
显示剩余8条评论

68

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

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

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

git merge --allow-unrelated-histories branch_name
如果您的Git版本小于2.9,请删除--allow-unrelated-histories。 此后可能会出现冲突。您可以使用git mergetool来解决这些冲突。 kdiff3可以仅使用键盘来完成,因此在阅读代码时处理5个冲突文件只需几分钟。 记得完成合并:
git commit

2
我喜欢这个解决方案的简洁性,它似乎就是我正在寻找的,但它基本上不就等同于 git pull --allow-unrelated-histories 吗? - Hashim Aziz
1
@Prometheus 有点这样。我现在没有测试过,但可能需要将pull添加到远程存储库作为真正的远程存储库,而这只会将必要的内容提取到一个分支并合并该内容。 - Smar

35

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

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的提交。


28
本人的情况是,有一个名为 my-plugin 的代码库和一个名为 main-project 的代码库,希望让 my-plugin 看起来一直是在 main-project/plugins 目录下开发的。实现方法是重写 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目录中留下一些文件。除非您提供-f选项给filter-branch,否则下一次尝试filter-branch时,它将抱怨这些文件。

至于实际命令,我没有成功地让bash执行我想要的操作,所以我使用zsh -c来使zsh执行一个命令。首先,我设置了extended_glob选项,这是启用mv命令中的^(...)语法以及允许我使用glob_dots选项来选择带有通配符(例如.gitignore)的点文件的选项。

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

最后,我使用zsh的“负通配符”特性^(.git|plugins)匹配存储库根目录中除了新创建的.gitmy-plugin文件夹之外的所有文件。(在这里排除.git可能是不必要的,但尝试将一个目录移动到其自身是一个错误。)

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

--all选项告诉filter-branch重写存储库中所有分支的历史记录,而额外的--则是告诉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-unrelated-histories选项。如果您使用其中一个版本,只需省略该选项: --allow-unrelated-histories导致的错误信息也在2.9.0中添加。

您不应该有任何合并冲突。如果有,请检查一下filter-branch命令是否正常工作或者main-project中是否已经存在plugins/my-plugin目录。

请确保为未来的贡献者输入解释性提交信息,让他们知道这个具有两个根的仓库是如何创建的。

您可以使用上述的git log命令可视化新的提交图,其应该有两个根提交。请注意,只有master分支将被合并。这意味着,如果您在其他my-plugin分支上有重要的工作需要合并到main-project树中,您应该先不要删除my-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。不同环境可能会有所差异。

参考文献:


1
“--allow-unrelated-histories” 是从哪里来的? - Idemax
3
请查看man git-merge。默认情况下,git merge命令拒绝合并没有共同祖先的历史记录。当合并两个独立起源的项目的历史记录时,可以使用此选项来覆盖此安全限制。由于这是非常罕见的情况,因此不存在启用此选项的配置变量,并且不会添加。 - Radon Rosborough
应该在 git version 2.7.2.windows.1 上可用吗? - Idemax
2
@MarceloFilho 这是在2.9.0中添加的,但在旧版本中,您不需要传递选项(它将正常工作)。 https://github.com/git/git/blob/a28705da929ad746abcb34270947f738549d3246/Documentation/RelNotes/2.9.0.txt#L11 - Radon Rosborough
@codeDr 我已经更新了我的答案,以解决重写所有分支的问题。 - Radon Rosborough
显示剩余3条评论

10

我在Stack OverFlow等网站上收集了很多信息,并成功地编写了一个脚本来解决我的问题。

但是,这个脚本只考虑每个存储库的“develop”分支,并将其合并到一个全新的存储库中的单独目录中。

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

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

#!/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
##            eitch@eitchnet.ch
##
## 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=$'\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的文件,例如:
git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git
然后,调用脚本时需要提供项目名称和脚本路径:
./mergeGitRepositories.sh eitchnet_test eitchnet.lst
脚本本身有很多注释,应该可以解释它的功能。

1
请不要直接引导读者去找答案,而是在这里发布答案(也就是将您在评论中所说的内容编辑到此答案中)。 - josliber
1
当然,我只是认为最好不要重复自己... =) - eitch
如果您认为这个问题与其他问题完全相同,那么您可以使用问题本身下方的“标记”链接将其标记为重复。如果不是重复的问题,但您认为完全相同的答案可以用来解决两个问题,那么只需在两个问题上发布相同的答案(就像您现在所做的那样)。感谢您的贡献! - josliber
太棒了!在Windows bash提示符上无法运行,但在运行Ubuntu的Vagrant框中完美运行。真是个时间节省者! - xverges
老旧但是.. 或许将警告信息加粗(和/或增大字体)是个好主意? - Pryftan
如果有人遇到了我遇到的“没有共同提交”的错误,可以通过在git fetch命令中添加“--force”来解决。 - h00ligan

10
如果您想将存储库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主分支第一次提交中被提交的文件,但这些文件将放置在名为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))"
解释:`--orphan` 选项是用于 `checkout` 命令的,它可以将 A 仓库的 master 分支文件检出,但不创建任何提交记录。我们可以选择任何一个提交记录,因为接下来我们会清空所有文件。然后,在没有提交之前(使用 `-n`),我们从 B 仓库的 master 分支上挑选第一个提交记录(`cherry-pick` 会保留原始的提交信息,而直接检出则不会)。接着,我们创建一个子树,用于放置来自 repo B 的所有文件。然后,我们需要将在 cherry-pick 中引入的所有文件移动到子树中。在上面的示例中,只有一个 README 文件需要移动。然后,我们提交了 B 仓库的根提交记录,同时我们还保留了原始提交的时间戳。

现在,我们将在新创建的 `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
最终图表的结构将如下所示:
enter image description here

非常好的答案,谢谢!我在其他答案中确实错过了Andresch Serj提到的“git subtree”或“merge --allow-unrelated-histories”,导致子目录没有日志。 - Ilendir

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