您如何合并两个Git存储库?


1619

请考虑以下情形:

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

如何将A合并为B,而又不会丢失任何历史记录?


8
如果你只是想两个仓库合并成一个,而不需要同时保存库,看看这个问题:stackoverflow.com/questions/13040958/...
Flimm

要在自定义目录中合并git repo并保存所有内容,请使用stackoverflow.com/a/43340714/1772410
Andrey Izman

Answers:


436

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

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

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

从提交中添加“ rails /” <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之外,您可能还必须自己安装它。


2
以下是有关如何安装Git SubTree(截至2013年6月)的说明:stackoverflow.com/a/11613541/694469 (我替换git co v1.7.11.3 ... v1.8.3)。
KajMagnus

1
感谢您对以下答案的注意。从git 1.8.4开始,仍然不包括“子树”(至少在Ubuntu 12.04 git ppa(ppa:git-core / ppa)上不包含)
Matt Klein

1
我可以确认,此后,git log rails/somefile除了合并提交外,将不会显示该文件的提交历史记录。如@artfulrobot所建议,请检查Greg Hewgill的答案。您可能需要git filter-branch在要包含的存储库上使用。
张继峰

6
或阅读埃里克·李的“合并两个Git仓库进入一个资源库,而不会失去文件历史记录” saintgimp.org/2013/01/22/...
骑缝章

4
正如其他人所说,git subtree可能做不到您的想法!请参阅此处以获得更完整的解决方案。
Paul Draper

1906

如果要合并project-aproject-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

更新--tags@jstadler建议添加,以保留标签。


8
这为我做生意。第一次像魅力一样工作,.gitignore文件中只有一个冲突!它完美地保留了提交历史。除了简单之外,与其他方法相比,最大的优点是,无需持续引用合并的回购协议。但是,要注意的一件事-如果您是像我这样的iOS开发人员-则要非常小心地将目标存储库的项目文件放入工作区中。
Max MacLeod

30
谢谢。为我工作。我需要将合并的目录移动到子文件夹中,所以在按照我刚才使用的上述步骤操作之后git mv source-dir/ dest/new-source-dir
Sid 2016年

13
git merge步骤在此处失败fatal: refusing to merge unrelated histories--allow-unrelated-historiesdocs中所述修复了该问题。
ssc 2016年

19
--allow-unrelated-histories是在git 2.9中引入的。在早期版本中,它是默认行为。
道格拉斯·罗伊斯

11
简称:git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD
jthill

614

这是两个可能的解决方案:

子模块

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

这是松耦合的仓库,其中一个仓库继续发展一个很好的解决方案,以及发展的主要部分是又见一个单独的独立发展SubmoduleSupportGitSubmoduleTutorial上的Git维基网页。

子树合并

您可以使用子树合并策略将存储库A合并到项目B的子目录中。这在Markus Prinz 撰写的Subtree Merging and You中进行了描述。

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

--allow-unrelated-historiesGit> = 2.9.0需要此选项。)

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


我认为在您的情况下(A将成为较大项目B的一部分),正确的解决方案是使用subtree merge


1
这行得通,似乎可以保留历史记录,但是您不能使用它来对文件进行差异化或通过合并平分。我错过了一步吗?
jettero 2012年

55
这是不完整的。是的,您将获得大量的提交,但是它们不再引用正确的路径。git log dir-B/somefile除了一次合并,不会显示任何内容。请参阅Greg Hewgill的答案引用了此重要问题。
artfulrobot 2012年

2
重要信息:git pull --no-rebase -s子树Bproject master如果您不这样做,并且您将pull设置为自动变基,则最终会出现“无法解析对象”的信息。参见osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman-abstracto-

4
这个答案可能令人困惑,因为当问题为A时,它具有B作为合并的子树。复制粘贴的结果?
vfclists 2012年

11
如果您只是想将两个存储库简单地粘合在一起,则子模块和子树合并是错误的工具,因为它们不能保留所有文件历史记录(如其他评论者所指出的)。参见stackoverflow.com/questions/13040958/…
Eric Lee

194

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

第一件事是使用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
子树合并将是更好的解决方案,并且不需要重写包含项目的历史记录
JakubNarębski09年

8
我想知道如何使用它git filter-branch来实现这一目标。在手册页中,它说明了相反的方法:使subdir /成为根,但没有相反的方法。
artfulrobot 2012年

31
如果它解释了如何使用过滤器分支来达到期望的结果,则此答案将是很好的
Anentropic 2013年

14
我发现如何在这里使用过滤器的分支:stackoverflow.com/questions/4042816/...
大卫小调

3
有关Greg大纲的实现,请参见此答案
Paul Draper

75

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,则可能要先克隆(复制)该存储库的一次性副本。

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

(2)然后在B中运行

git pull path/to/A

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


就我而言,我需要两个子目录projectAprojectB。在这种情况下,我也对B执行了步骤(1)。


1
看来您是从stackoverflow.com/a/618113/586086复制了答案的?
安德鲁·毛

1
@AndrewMao,我想是……我真的不记得了。我已经使用了很多这个脚本。
Paul Draper

6
我要补充一点,\ t在OS X上不起作用,您必须输入<tab>
Muneeb Ali 2014年

2
"$GIT_INDEX_FILE"必须加引号(两次),否则,例如,如果路径包含空格,则您的方法将失败。
罗布W

4
如果您想在osx中​​插入<tab>,则需要Ctrl-V <tab>
casey 2015年

48

如果两个存储库都具有相同类型的文件(例如,两个用于不同项目的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 mergetoolkdiff3只能与键盘一起使用,因此仅需几分钟即可读取5个冲突文件。

记住要完成合并:

git commit

25

使用合并时,我一直丢失历史记录,因此最终使用了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的提交


25

就我而言,我有一个my-plugin存储库和一个main-project存储库,并且我想假装my-plugin总是在的plugins子目录中开发的main-project

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

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它将抱怨此问题,除非您向提供了该-f选项filter-branch

至于实际的命令,我没有多少运气得到bash做我想要的东西,所以不是我用zsh -czsh执行命令。首先,我设置extended_glob选项,该选项将启用命令中的^(...)语法mv,以及glob_dots选项,该选项使我可以.gitignore使用glob(^(...))选择点文件(例如)。

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

最后,我使用zsh“ negative glob”功能^(.git|plugins)来匹配存储库的根目录中的所有文件,但不包括.git新创建的my-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-histories2.9.0 添加了阻止该错误的消息

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

确保输入任何将来的贡献者都想知道的黑客正在做什么的骇客消息,以说明为什么要建立两个根的存储库。

您可以使用上面的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.0和进行了测试zsh --version 5.2。你的旅费可能会改变。

参考文献:


1
哪里--allow-unrelated-histories来的?
xpto

3
@MarceloFilho检查man git-merge默认情况下,git merge命令拒绝合并不具有共同祖先的历史记录。合并两个独立开始的项目的历史记录时,可以使用此选项来覆盖此安全性。由于这是非常罕见的情况,因此默认情况下不存在任何配置变量来启用此功能,因此不会添加。
Radon Rosborough

应该可用git version 2.7.2.windows.1吗?
xpto

2
@MarceloFilho这是在2.9.0中添加的,但是在较旧的版本中,您不必传递该选项(它将起作用)。github.com/git/git/blob/...
氡Rosborough

这很好。而且我能够使用filter分支将文件名重写为合并前在树中想要的位置。我想如果需要除master分支之外的其他历史记录,还需要进行更多工作。
code Dr

9

几天来我一直在尝试做同样的事情,我正在使用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/


8

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

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

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

现在,我们在repo 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))"

说明:--orphancheckout命令的选项从A的master分支中检出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们还是要清除所有文件。然后,在尚未提交(-n)的情况下,我们从B的master分支中挑选了第一个提交。(cherry-pick保留了原来的提交消息,而直接检出似乎没有。)然后,我们在该子树中创建了要存储库B中所有文件的子树。然后,我们必须移动该文件中引入的所有文件。采摘子树。在上面的示例中,只有一个README文件要移动。然后,我们提交B-repo根提交,与此同时,我们还保留原始提交的时间戳。

现在,我们将B/master在新创建的上创建一个新分支new_b_root。我们称新分支为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

最终的图形将具有以下结构:

在此处输入图片说明


好答案,谢谢!我真的错过了Andresch Serj提供的“ git subtree”或“ merge --allow-unrelated-histories”的其他答案,该子目录没有日志。
伊伦迪尔

8

我在此处收集了有关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

该脚本本身有很多注释,应解释其功能。


而不是引导读者找到答案,请在此处发布答案(也可以将您在该评论中所说的内容编辑成该答案)。
josliber

1
当然,只是认为最好不要重复自己... =)
eitch 2015年

如果您认为此问题与另一个问题相同,则可以使用问题本身下方的“标志”链接并指出另一个问题,将其标记为重复项。如果这不是一个重复的问题,但是您认为可以使用完全相同的答案来解决这两个问题,那么只需对两个问题都发布相同的答案(就像您现在所做的那样)。感谢您的贡献!
josliber

惊人!在Windows bash提示符下不起作用,但是它完美无缺地从运行ubuntu的Vagrant盒中运行。节省时间!
xverges

乐于

7

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

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

2
这正是我想要的。谢谢!但是,我不得不将第22行更改为:if [[ $dirname =~ ^.*\.git$ ]]; then
heyman 2013年

2
^。* blarg $是贪婪的RE。最好说.blarg $并跳过前锚。
jettero

7

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


1
您的解决方案仅适用于新存储库,但是如何在文件冲突的情况下将存储库合并到另一个存储库中呢?
Andrey Izman'4

6

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

  1. 向指向仓库A的仓库B添加一个远程服务器(git remote add ...)
  2. 拉当前分支(我们未使用master进行错误修复)(git pull remoteForRepoA bugFixBranch)
  3. 推送合并到github

工作了请客:)


5

与@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的文章


5

合并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

4

当你想在合并三个或三个以上的项目提交,执行步骤,在其他的答案中描述(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”。

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


1
等待,为什么要在一次提交中合并三个项目?
史蒂夫·本内特

我从存储库,存储库客户端和建模器开始,作为单独的git项目。这对于同事来说很困难,因此我将他们加入了一个git项目。为了使新项目的“根”源自其他三个项目,我想进行一次合并提交。
koppor

4

要将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)然后回到母版和两个分支之间的经典合并:

git checkout master
git merge projectA

2

此功能会将远程仓库复制到本地仓库目录中,合并所有提交后,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"

注意
路径替换了via sed,因此请确保合并后它沿正确的路径移动。
--allow-unrelated-histories参数仅从git> = 2.9开始存在。


1

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

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

1

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

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

cp -R myotherproject newdirectory
git add newdirectory

未来的历史

git fetch path_or_url_to_other_repo

告诉git合并上次获取的东西的历史

echo 'FETCH_HEAD' > .git/MERGE_HEAD

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

git commit

0

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

这种感觉的方式比过滤分支那么可怕和方式更加清洁。当然,它可能并不适用于所有情况。

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.