542

我在一个名为XXX的文件夹中有一个 Git 存储库,我有第二个名为YYY的 Git 存储库。

我想将XXX存储库作为名为ZZZ的子目录导入YYY存储库,并将所有XXX的更改历史记录添加到YYY

之前的文件夹结构:

├── XXX
│   ├── .git
│   └── (project files)
└── YYY
    ├── .git
    └── (project files)

之后的文件夹结构:

YYY
├── .git  <-- This now contains the change history from XXX
├──  ZZZ  <-- This was originally XXX
│    └── (project files)
└──  (project files)

这可以做到吗,还是我必须求助于使用子模块?

4

17 回答 17

476

可能最简单的方法是将XXX的东西拉到YYY中的一个分支中,然后将其合并到 master 中:

YYY

git remote add other /path/to/XXX
git fetch other
git checkout -b ZZZ other/master
mkdir ZZZ
git mv stuff ZZZ/stuff                      # repeat as necessary for each file/dir
git commit -m "Moved stuff to ZZZ"
git checkout master                
git merge ZZZ --allow-unrelated-histories   # should add ZZZ/ to master
git commit
git remote rm other
git branch -d ZZZ                           # to get rid of the extra branch before pushing
git push                                    # if you have a remote, that is

实际上,我只是用我的几个存储库尝试了这个,它可以工作。与Jörg 的回答不同,它不会让您继续使用其他存储库,但我认为您无论如何都没有指定。

注意:由于这最初是在 2009 年编写的,因此 git 添加了下面答案中提到的子树合并。我今天可能会使用这种方法,尽管这种方法当然仍然有效。

于 2009-11-06T00:47:59.360 回答
394

如果您想保留第二个存储库的确切提交历史记录,并因此还保留将来轻松合并上游更改的能力,那么这就是您想要的方法。它会导致未修改的子树历史被导入到您的存储库中,加上一个合并提交以将合并的存储库移动到子目录。

git remote add XXX_remote <path-or-url-to-XXX-repo>
git fetch XXX_remote
git merge -s ours --no-commit --allow-unrelated-histories XXX_remote/master
git read-tree --prefix=ZZZ/ -u XXX_remote/master
git commit -m "Imported XXX as a subtree."

您可以像这样跟踪上游更改:

git pull -s subtree XXX_remote master

Git 在进行合并之前会自行确定根的位置,因此您无需在后续合并中指定前缀。

缺点是在合并的历史中,文件没有前缀(不在子目录中)。结果git log ZZZ/a将向您显示所有更改(如果有),但合并历史记录中的更改除外。你可以做:

git log --follow -- a

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

换句话说,如果您不更改ZZZ存储库中的文件XXX,那么您需要指定--follow一个不带前缀的路径。如果您在两个存储库中更改它们,那么您有 2 个命令,其中没有一个显示所有更改。

2.9 之前的 Git 版本:您不需要将--allow-unrelated-histories选项传递给git merge.

另一个答案中使用read-tree和跳过该merge -s ours步骤的方法实际上与使用 cp 复制文件并提交结果没有什么不同。

原始来源来自github 的“Subtree Merge”帮助文章。还有另一个有用的链接

于 2011-12-06T06:54:18.470 回答
154

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

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

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

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

对于您的情况,在存储库 YYY 内,您将运行:

git subtree add -P ZZZ /path/to/XXX.git master

缺点是在合并的历史中,文件没有前缀(不在子目录中)。结果git log ZZZ/a将向您显示所有更改(如果有),但合并历史记录中的更改除外。你可以做:

git log --follow -- a

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

换句话说,如果您不更改ZZZ存储库中的文件XXX,那么您需要指定--follow一个不带前缀的路径。如果您在两个存储库中更改它们,那么您有 2 个命令,其中没有一个显示所有更改。

更多关于它的信息

于 2015-09-20T21:29:42.060 回答
52

在 Git 存储库本身中有一个众所周知的实例,它在 Git 社区中被统称为“有史以来最酷的合并”(在发送给 Git 邮件列表的电子邮件中使用的主题行 Linus Torvalds 之后,描述了这一点合并)。在这种情况下,gitk现在作为 Git 本身一部分的 Git GUI 实际上曾经是一个单独的项目。Linus 设法以一种方式将该存储库合并到 Git 存储库中

  • 它出现在 Git 存储库中,就好像它一直是作为 Git 的一部分开发的,
  • 所有的历史都完好无损
  • 它仍然可以在其旧存储库中独立开发,只需git pull编辑更改即可。

这封电子邮件包含了重现所需的步骤,但不适合胆小的人:首先,Linus编写了Git,所以他可能比你或我更了解它,其次,这几乎是 5 年前的事了从那时起, Git 有了很大的改进所以现在可能要容易得多。

特别是,我想现在人们会在这种特定情况下使用 gitk 子模块。

于 2009-11-05T23:39:34.630 回答
13

简单的方法是使用 git format-patch。

假设我们有 2 个 git 存储库foobar

foo包含:

  • foo.txt
  • .git

包含:

  • 吧.txt
  • .git

我们希望以包含bar历史记录和这些文件的foo结尾:

  • foo.txt
  • .git
  • foob​​ar/bar.txt

所以要做到这一点:

 1. create a temporary directory eg PATH_YOU_WANT/patch-bar
 2. go in bar directory
 3. git format-patch --root HEAD --no-stat -o PATH_YOU_WANT/patch-bar --src-prefix=a/foobar/ --dst-prefix=b/foobar/
 4. go in foo directory
 5. git am PATH_YOU_WANT/patch-bar/*

如果我们想重写 bar 中的所有消息提交,我们可以这样做,例如在 Linux 上:

git filter-branch --msg-filter 'sed "1s/^/\[bar\] /"' COMMIT_SHA1_OF_THE_PARENT_OF_THE_FIRST_BAR_COMMIT..HEAD

这将在每条提交消息的开头添加“[bar]”。

于 2010-11-10T08:51:53.837 回答
10

此功能将远程仓库克隆到本地仓库目录,合并后所有提交将被保存,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"

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

于 2017-04-11T12:17:26.350 回答
9

让我使用名称a(代替XXXand ZZZ)和b(代替YYY),因为这样可以使描述更易于阅读。

假设您要将存储库合并ab(我假设它们彼此并排):

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

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

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

更多关于它的信息

于 2020-05-30T14:47:15.047 回答
7

根据这篇文章,使用子树对我有用,并且只传输了适用的历史记录。如果有人需要这些步骤,请在此处发布(确保将占位符替换为适用于您的值):

在您的源存储库中将子文件夹拆分为一个新分支

git subtree split --prefix=<source-path-to-merge> -b subtree-split-result

在您的目标仓库中合并拆分结果分支

git remote add merge-source-repo <path-to-your-source-repository>
git fetch merge-source-repo
git merge -s ours --no-commit merge-source-repo/subtree-split-result
git read-tree --prefix=<destination-path-to-merge-into> -u merge-source-repo/subtree-split-result

验证您的更改并提交

git status
git commit

不要忘记

subtree-split-result通过删除分支进行清理

git branch -D subtree-split-result

删除您添加的遥控器以从源存储库中获取数据

git remote rm merge-source-repo

于 2015-04-29T15:57:08.207 回答
3

添加另一个答案,因为我认为这有点简单。将 repo_dest 拉入 repo_to_import ,然后 push --set-upstream url:repo_dest master 完成。

这种方法对我将几个较小的存储库导入到一个较大的存储库中很有效。

如何导入:repo1_to_import 到 repo_dest

# checkout your repo1_to_import if you don't have it already 
git clone url:repo1_to_import repo1_to_import
cd repo1_to_import

# now. pull all of repo_dest
git pull url:repo_dest
ls 
git status # shows Your branch is ahead of 'origin/master' by xx commits.
# now push to repo_dest
git push --set-upstream url:repo_dest master

# repeat for other repositories you want to import

在进行导入之前,将文件和目录重命名或移动到原始存储库中的所需位置。例如

cd repo1_to_import
mkdir topDir
git add topDir
git mv this that and the other topDir/
git commit -m"move things into topDir in preparation for exporting into new repo"
# now do the pull and push to import

以下链接中描述的方法启发了这个答案。我喜欢它,因为它看起来更简单。但当心!有龙!https://help.github.com/articles/importing-an-external-git-repository git push --mirror url:repo_dest将本地仓库历史和状态推送到远程(url:repo_dest)。但它删除了遥控器的旧历史和状态。乐趣随之而来!:-E

于 2014-06-20T00:02:21.303 回答
1

在我的情况下,我只想从其他存储库 (XXX) 导入一些文件。子树对我来说太复杂了,其他解决方案都不起作用。这就是我所做的:

ALL_COMMITS=$(git log --reverse --pretty=format:%H -- ZZZ | tr '\n' ' ')

这为您提供了一个以空格分隔的列表,其中列出了影响我要以相反顺序导入的文件 (ZZZ) 的所有提交(您可能还必须添加 --follow 来捕获重命名)。然后我进入目标存储库(YYY),将另一个存储库(XXX)添加为远程,从中获取,最后:

git cherry-pick $ALL_COMMITS

它将所有提交添加到您的分支,因此您将拥有所有文件及其历史记录,并且可以对它们做任何您想做的事情,就好像它们一直在这个存储库中一样。

于 2013-02-27T09:16:10.350 回答
1

请参阅本文中的基本示例并考虑在存储库上进行此类映射:

  • A<-> YYY,
  • B<->XXX

在本章描述的所有活动之后(合并后),删除分支B-master

$ git branch -d B-master

然后,推送更改。

这个对我有用。

于 2016-02-18T16:59:27.393 回答
1

这是可以立即运行的脚本。

#!/bin/bash -xe
# script name: merge-repo.sh
# To merge repositories into the current.
# To see the log of the new repo use 'git log --follow -- unprefixed-filename'
# So if the file is repo/test.cpp use 'git log --follow -- test.cpp'
# I'm not sure how this will work when two files have the same name.
#
# `git branch -a` will show newly created branches.
# You can delete them if you want.
merge_another() {
    repo="$1" # url of the remote repo
    rn="$2"   # new name of the repo, you can keep the same name as well.
    git remote add ${rn} ${repo}
    git fetch ${rn}
    git merge -s ours --no-commit --allow-unrelated-histories ${rn}/master
    git read-tree --prefix=${rn}/ -u ${rn}/master
    git commit -m "Imported ${rn} as a subtree."
    git pull -s subtree ${rn} master
}

merge_another $1 $2

运行脚本。转到您希望合并其他 repo 的 repo,然后运行脚本。

cd base-repo
./merge-repo.sh git@github.com:username/repo-to-be-merged.git repo-to-be-merged-new-name

现在将主分支上的更改推送到远程/源。根据您要执行的操作,可能不需要此步骤。

git push origin master
于 2021-03-15T04:37:27.247 回答
0

我处于我正在寻找的情况,-s theirs但当然,这种策略不存在。我的历史是我在 GitHub 上 fork 一个项目,现在由于某种原因,我的本地master无法合并,upstream/master尽管我没有对这个分支进行本地更改。(真的不知道那里发生了什么——我猜上游在幕后做了一些肮脏的推动,也许吧?)

我最终做的是

# as per https://help.github.com/articles/syncing-a-fork/
git fetch upstream
git checkout master
git merge upstream/master
....
# Lots of conflicts, ended up just abandonging this approach
git reset --hard   # Ditch failed merge
git checkout upstream/master
# Now in detached state
git branch -d master # !
git checkout -b master   # create new master from upstream/master

所以现在我master的再次同步upstream/master(你可以对你也想类似同​​步的任何其他分支重复上述操作)。

于 2015-02-05T11:16:00.950 回答
0

我可以为您的问题建议另一种解决方案(替代git-submodules) - gil (git links) 工具

它允许描述和管理复杂的 git 存储库依赖项。

它还为git 递归子模块依赖问题提供了解决方案。

考虑您有以下项目依赖项: 示例 git 存储库依赖关系图

然后您可以.gitlinks使用存储库关系描述定义文件:

# Projects
CppBenchmark CppBenchmark https://github.com/chronoxor/CppBenchmark.git master
CppCommon CppCommon https://github.com/chronoxor/CppCommon.git master
CppLogging CppLogging https://github.com/chronoxor/CppLogging.git master

# Modules
Catch2 modules/Catch2 https://github.com/catchorg/Catch2.git master
cpp-optparse modules/cpp-optparse https://github.com/weisslj/cpp-optparse.git master
fmt modules/fmt https://github.com/fmtlib/fmt.git master
HdrHistogram modules/HdrHistogram https://github.com/HdrHistogram/HdrHistogram_c.git master
zlib modules/zlib https://github.com/madler/zlib.git master

# Scripts
build scripts/build https://github.com/chronoxor/CppBuildScripts.git master
cmake scripts/cmake https://github.com/chronoxor/CppCMakeScripts.git master

每行以以下格式描述 git 链接:

  1. 存储库的唯一名称
  2. 存储库的相对路径(从 .gitlinks 文件的路径开始)
  3. Git 存储库,将在 git clone 命令中使用 Repository 分支进行结帐
  4. 不解析空行或以 # 开头的行(视为注释)。

最后,您必须更新您的根示例存储库:

# Clone and link all git links dependencies from .gitlinks file
gil clone
gil link

# The same result with a single command
gil update

结果,您将克隆所有必需的项目并以适当的方式将它们相互链接。

如果您想提交某个存储库中的所有更改以及子链接存储库中的所有更改,您可以使用单个命令来完成:

gil commit -a -m "Some big update"

拉、推命令的工作方式类似:

gil pull
gil push

Gil (git links) 工具支持以下命令:

usage: gil command arguments
Supported commands:
    help - show this help
    context - command will show the current git link context of the current directory
    clone - clone all repositories that are missed in the current context
    link - link all repositories that are missed in the current context
    update - clone and link in a single operation
    pull - pull all repositories in the current directory
    push - push all repositories in the current directory
    commit - commit all repositories in the current directory

更多关于git 递归子模块依赖问题

于 2018-12-30T19:28:14.540 回答
0

没有足够的代表来为 x-yuri 的答案添加评论,但它工作得很好并且保留了历史。我正在使用两个工作的本地仓库并收到此错误:

中止:拒绝破坏性地覆盖回购历史,因为这看起来不像是一个新的克隆。(预计新包装的回购)请改为在新的克隆上操作。如果您仍想继续,请使用 --force。

我没有担心--force标志的含义,而是首先在本地克隆了 repo:

cd tempDir
git clone <location of repo to be merged> --no-local

并将这个新克隆的副本用于 x-yuri 布置的一系列命令。最后, in:git filter-repo --to-subdirectory-filter aa您为要导入的存储库指定的根文件夹的名称。

于 2021-04-25T02:24:09.853 回答
-2

我不知道有什么简单的方法可以做到这一点。你可以这样做:

  1. 使用 git filter-branch 在 XXX 存储库上添加一个 ZZZ 超级目录
  2. 将新分支推送到 YYY 仓库
  3. 将推送的分支与 YYY 的主干合并。

如果这听起来很吸引人,我可以编辑细节。

于 2009-11-05T21:06:03.310 回答
-3

我认为你可以使用“git mv”和“git pull”来做到这一点。

我是一个公平的 git noob - 所以要小心你的主存储库 - 但我只是在临时目录中尝试过这个,它似乎工作。

首先 - 重命名 XXX 的结构以匹配它在 YYY 中时的外观:

cd XXX
mkdir tmp
git mv ZZZ tmp/ZZZ
git mv tmp ZZZ

现在 XXX 看起来像这样:

XXX
 |- ZZZ
     |- ZZZ

现在使用 'git pull' 来获取更改:

cd ../YYY
git pull ../XXX

现在 YYY 看起来像这样:

YYY
 |- ZZZ
     |- ZZZ
 |- (other folders that already were in YYY)
于 2009-11-05T21:05:09.223 回答