如何合并两个 Git 存储库?

Posted

技术标签:

【中文标题】如何合并两个 Git 存储库?【英文标题】:How do you merge two Git repositories? 【发布时间】:2022-01-10 22:13:25 【问题描述】:

考虑以下场景:

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

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

【问题讨论】:

如果您只是想将两个存储库合并为一个,而不需要保留两个存储库,请查看以下问题:***.com/questions/13040958/… 要在自定义目录中合并 git repo 并保存所有提交,请使用 ***.com/a/43340714/1772410 【参考方案1】:

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

第一件事是使用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

【讨论】:

子树合并会是更好的解决方案,并且不需要重写包含项目的历史 我想知道如何使用git filter-branch 来实现这一点。在手册页中,它说的是相反的方式:使 subdir/ 成为根,但不是相反。 如果它解释了如何使用 filter-branch 来达到预期的结果,这个答案会很棒 我在这里找到了如何使用过滤器分支:***.com/questions/4042816/… 参见this answer 了解 Greg 大纲的实现。【参考方案2】:

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

子模块

要么将存储库 A 复制到较大项目 B 中的单独目录中,要么(也许更好)将存储库 A 克隆到项目 B 中的子目录中。然后使用 git submodule 将此存储库设置为 子模块 em> 存储库 B.

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

子树合并

您可以使用 subtree merge 策略将存储库 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

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

或者您可以使用 apenwarr (Avery Pennarun) 的 git subtree 工具 (repository on GitHub),例如在他的博客文章 A new alternative to Git submodules: git subtree 中宣布。


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

【讨论】:

这有效并且似乎保留了历史记录,但不能让您使用它来区分文件或通过合并一分为二。我错过了一步吗? 这是不完整的。是的,您会收到大量提交,但它们不再引用正确的路径。 git log dir-B/somefile 除了一个合并之外不会显示任何内容。请参阅Greg Hewgill's answer 引用此重要问题。 重要: git pull --no-rebase -s subtree Bproject master 如果你不这样做,并且你已经将 pull 设置为自动变基,你最终会得到“无法解析对象”。见osdir.com/ml/git/2009-07/msg01576.html 这个答案可能会令人困惑,因为它在问题中是 A 时将 B 作为合并的子树。复制和粘贴的结果? 如果您试图简单地将两个存储库粘合在一起,则子模块和子树合并是错误的工具,因为它们不会保留所有文件历史记录(正如其他评论者所指出的那样)。见***.com/questions/13040958/…。【参考方案3】:

如果两个存储库具有相同类型的文件(例如不同项目的两个 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

【讨论】:

我喜欢这个解决方案的简单性,它看起来就像我正在寻找的东西,但它基本上不等同于git pull --allow-unrelated-histories吗? @Prometheus 有点像。我现在没有测试它,但可能 pull 需要将远程存储库添加为真正的远程,这只会将必要的内容获取到分支并合并该内容。【参考方案4】:

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

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

然后你手动合并。

(改编自post by Anar Manafov)

【讨论】:

【参考方案5】:

我也遇到过类似的挑战,但就我而言,我们在 repo A 中开发了一个版本的代码库,然后将其克隆到新的 repo 中,repo B 用于新版本的产品。在修复了 repo A 中的一些错误后,我们需要将更改 FI 到 repo B。最终做了以下事情:

    向 repo B 添加一个指向 repo A 的远程(git remote add...) 拉取当前分支(我们没有使用 master 修复错误)(git pull remoteForRepoA bugFixBranch) 将合并推送到 github

辛苦了:)

【讨论】:

【参考方案6】:

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

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

【讨论】:

这正是我想要的。谢谢!但是,我不得不将第 22 行更改为:if [[ $dirname =~ ^.*\.git$ ]]; then ^.*blarg$ 是浪费的贪婪 RE。最好说 .blarg$ 并跳过前面的锚。【参考方案7】:

如果要将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 merge different repositories?

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

如果你想把project-a放到一个子目录中,你可以使用git-filter-repofilter-branch是discouraged)。在上述命令之前运行以下命令:

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

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

注意: --allow-unrelated-histories 参数仅在 git >= 2.9 后才存在。见Git - git merge Documentation / --allow-unrelated-histories

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

【讨论】:

这为我做了生意。第一次像魅力一样工作,在 .gitignore 文件中只有一个冲突!它完美地保留了提交历史。除了简单性之外,其他方法的最大优点是不需要持续引用合并的 repo。但是需要注意的一件事——如果你是像我这样的 ios 开发人员——将目标 repo 的项目文件放入工作区时要非常小心。 谢谢。为我工作。我需要将合并的目录移动到一个子文件夹中,所以按照上述步骤后,我只使用了git mv source-dir/ dest/new-source-dir @sg 一种间接的方法是将 project-a 中的所有这些文件移动到 project-a 中的子目录中(这样 project-a 的顶层只有一个目录),然后按照以上过程。 git merge 步骤在此处失败,fatal: refusing to merge unrelated histories; --allow-unrelated-histories 修复了 docs 中解释的问题。 短:git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.【参考方案8】:

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

【讨论】:

您的解决方案仅适用于新存储库,但如何将 repo 合并到另一个存在文件冲突的存储库中?【参考方案9】:

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

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

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

从提交<rev>添加'rails/'

<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之外,你可能还得自己安装。

【讨论】:

以下是关于如何安装 Git SubTree 的说明(截至 2013 年 6 月):***.com/a/11613541/694469(我将 git co v1.7.11.3 替换为 ... v1.8.3)。 感谢您对以下答案的提醒。截至 git 1.8.4 'subtree' 仍然不包括在内(至少不在 Ubuntu 12.04 git ppa (ppa:git-core/ppa) 上) 我可以确认,在此之后,git log rails/somefile 将不会显示该文件的提交历史记录,但合并提交除外。正如@artfulrobot 建议的那样,检查Greg Hewgill's answer。您可能需要在要包含的存储库中使用git filter-branch 或阅读 Eric Lee 的“将两个 Git 存储库合并到一个存储库而不丢失文件历史记录”saintgimp.org/2013/01/22/… 正如其他人所说,git subtree 可能不会像你想的那样!如需更完整的解决方案,请参阅here。【参考方案10】:

如果您想在一个单个提交中合并三个或更多项目,请按照其他答案(remote add -fmerge)中描述的步骤进行操作。然后,(软)将索引重置为旧头(没有发生合并)。添加所有文件 (git add -A) 并提交它们(消息“将项目 A、B、C 和 D 合并到一个项目中)。这现在是 master 的 commit-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。不要总是添加它,因为这可能会导致重写某些提交。详情见man page of filter-branch,搜索“grafts”。

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

【讨论】:

等等,为什么要在一次提交中合并三个项目? 我从存储库、存储库客户端和建模器作为单独的 git 项目开始。这对同事来说很困难,所以我加入了他们的一个 git 项目。为了能够使新项目的“根”源自其他三个项目,我希望 single 合并提交。【参考方案11】:

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

【讨论】:

您似乎从***.com/a/618113/586086复制了您的答案? @AndrewMao,我想是的......我实际上不记得了。这个脚本我用过不少。 我要补充一点 \t 在 OS X 上不起作用,你必须输入 "$GIT_INDEX_FILE" 必须被引用(两次),否则您的方法将失败,例如路径包含空格。 如果你想在osx中​​插入一个,你需要Ctrl-V &lt;tab&gt;【参考方案12】:

我在使用合并时不断丢失历史记录,所以我最终使用了 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 的提交

【讨论】:

【参考方案13】:

我在这里收集了很多关于 Stack OverFlow 等的信息,并设法将脚本放在一起解决了我的问题。

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

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

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

#!/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

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

【讨论】:

请不要将读者引导至答案,而是在此处发布答案(也就是将您在该评论中所说的内容编辑到此答案中)。 当然,只是觉得最好不要重复自己... =) 如果您认为这个问题与另一个问题相同,那么您可以使用问题本身下方的“标记”链接将其标记为重复,并指出另一个问题。如果不是重复的问题,但您认为可以使用完全相同的答案来解决这两个问题,那么只需对这两个问题发布相同的答案(就像您现在所做的那样)。感谢您的贡献! 太棒了!在 Windows bash 提示符下不起作用,但它完美地从运行 ubuntu 的 Vagrant 盒子中运行。多么节省时间! 很高兴为您服务 =)【参考方案14】:

在 B 中合并 A:

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

【讨论】:

【参考方案15】:

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

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

【讨论】:

【参考方案16】:

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

【讨论】:

【参考方案17】:

就我而言,我有一个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 时它会抱怨这个,除非你提供-f 选项filter-branch.

至于实际的命令,我没有太多运气让bash 做我想做的事,所以我改用zsh -czsh 执行命令。首先,我设置了extended_glob 选项,这是在mv 命令中启用^(...) 语法的原因,以及glob_dots 选项,它允许我选择带有glob 的点文件(例如.gitignore) (^(...))。

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

最后,我使用zsh“负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-histories 阻止的错误消息也在 2.9.0 中添加。

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

确保为任何未来的贡献者输入解释性的提交消息,以便他们想知道什么hackery 正在创建具有两个根的存储库。

您可以使用上面的git log 命令可视化新的提交图,它应该有两个根提交。请注意,只会合并 master 分支。这意味着,如果您对要合并到 main-project 树中的其他 my-plugin 分支有重要工作,则在完成这些合并之前,您应该避免删除 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 进行测试。您的里程可能会有所不同。

参考资料:

https://git-scm.com/docs/git-filter-branch https://unix.stackexchange.com/questions/6393/how-do-you-move-all-files-including-hidden-from-one-directory-to-another http://www.refining-linux.org/archives/37/ZSH-Gem-2-Extended-globbing-and-expansion/ Purging file from Git repo failed, unable to create new backup git, filter-branch on all branches

【讨论】:

--allow-unrelated-histories 来自哪里? @MarceloFilho 检查man git-merge默认情况下,git merge 命令拒绝合并不共享共同祖先的历史。当合并两个独立开始的项目的历史时,此选项可用于覆盖此安全性。由于这种情况很少见,因此默认情况下不存在启用此功能的配置变量,因此不会添加。 应该可以在git version 2.7.2.windows.1上找到吗? @MarceloFilho 这是在 2.9.0 中添加的,但在旧版本中,您不必传递该选项(它会起作用)。 github.com/git/git/blob/… 这很好用。而且我能够使用过滤器分支将文件名重写到合并之前树中我想要的位置。如果您需要在 master 分支之外移动历史记录,我想还有更多工作要做。【参考方案18】:

此功能会将远程仓库克隆到本地仓库目录,合并后所有提交将被保存,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 的文件/目录移动到不同的路径中,例如:

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-unrelated-histories 参数仅在 git >= 2.9 后才存在。

【讨论】:

【参考方案19】:

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

【讨论】:

【参考方案20】:

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

首先,从其他项目中复制你想要的文件。

cp -R myotherproject newdirectory
git add newdirectory

下一次拉入历史

git fetch path_or_url_to_other_repo

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

echo 'FETCH_HEAD' > .git/MERGE_HEAD

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

git commit

【讨论】:

这是唯一有意义的解决方案,并且完全符合我的要求。【参考方案21】:

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

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

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

现在我们在 repo A 中创建一个全新的分支(只有一个提交),我们称之为 new_b_root。生成的提交将包含在 repo 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))"

解释:checkout 命令的--orphan 选项从 A 的主分支中检出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们无论如何都会清除所有文件。然后,还没有提交(-n),我们从 B 的主分支中挑选第一个提交。 (cherry-pick 保留了直接签出似乎无法做到的原始提交消息。)然后我们创建子树,我们希望将所有文件从 repo 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

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

【讨论】:

很好的答案,谢谢!我真的错过了来自 Andresch Serj 的“git subtree”或“merge --allow-unrelated-histories”的其他答案,即子目录没有日志。【参考方案22】:

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

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

【讨论】:

【参考方案23】:

https://github.com/hraban/tomono 再次提及基于脚本的解决方案。

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

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

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

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

【讨论】:

【参考方案24】:

我今天必须解决它: 项目 A 在 bitbucket 中,项目 B 在代码提交中 .. 两者都是相同的项目,但必须合并从 A 到 B 的更改。(诀窍是在项目 A 中创建同名分支,与项目 B 中相同)

git checkout 项目A git 远程删除原点 git remote add origin Project B git checkout 分支 git 添加 * git commit -m "我们已经移动了代码" git 推送

【讨论】:

【参考方案25】:

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

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

第一个命令像往常一样获取所有分支,但省略了附加到提交的标签,第二个命令也省略了通常的标签获取机制(git help fetch 更多),并获取从X 映射到@ 的所有标签987654327@ 使用 git 的 refspec 功能。

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

操作后最好把另一个遥控器拿掉,以免不小心按正常方式抓取标签而弄得一团糟。

【讨论】:

【参考方案26】:

Google 有一个 Copybara 工具用于更复杂的用例 - https://github.com/google/copybara

【讨论】:

以上是关于如何合并两个 Git 存储库?的主要内容,如果未能解决你的问题,请参考以下文章

如何将多个 Git 存储库合并为一个并交错历史

合并两个 Git 存储库而不破坏文件历史记录

在两个 git 存储库之间合并? [复制]

如何将 2 台机器上的 2 个本地 git 存储库合并为一个?

合并 Git 存储库的前两个提交?

如何删除 Git 合并? [复制]