如何将现有的Git仓库导入到另一个仓库中?

580

我有一个名为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)

这可行吗,还是我必须使用子模块?


2
在Github上,现在可以通过Web界面在创建新存储库时执行此操作。 - Ben G
可能是如何合并两个git仓库?的重复问题。 - BuZZ-dEE
1
@bgcode的评论对我非常有帮助 - 谢谢。你可以直接从GitHub的UI导入另一个仓库,这样可以节省大量工作。 - ChumKui
17个回答

505

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

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

我实际上只是尝试了一下这个方法,对我的一些repo起作用了。与Jörg的回答不同的是,它不会让您继续使用其他repo,但我认为您也没有指定那样做。

注意:自2009年起,git已添加了下面答案中提到的子树合并功能。今天我可能会使用那种方法,但当然这种方法仍然有效。


2
谢谢。我使用了您的技巧的稍微修改版:我在XXX上创建了一个“暂存”分支,在其中创建了ZZZ文件夹,并将“东西”移动到其中。然后我将XXX合并到YYY中。 - Vijay Patel
1
这对我非常有效。我所做的唯一更改是:1)在推送之前使用“git branch -d ZZZ”,因为我不想让这个临时分支挂起来。2)“git push”给了我一个错误:“没有共同的参考和未指定的参考;什么也没做。也许你应该指定一个分支,比如'master'。”(我要推送到的源是一个空的裸仓库。)但是“git push --all”像冠军一样工作。 - CrazyPyro
4
@SebastianBlask,我刚刚在我的两个代码库中进行了一些尝试,并意识到有一个缺失的步骤,尽管我多年来一直得到了点赞,但似乎没有人注意到它。 :-) 我提到了将其合并到主分支,但实际上没有展示。现在正在编辑... - ebneter
1
@Danra 我不这么认为——你正在改变文件的位置,所以没有绕过这个问题的方法。你可以使用ColinM下面的子树合并解决方案,但我不确定日志中的历史记录是什么样子,因为我从未尝试过。 - ebneter
2
当将文件移动到子文件夹时,您可以添加以下内容:git mv $(ls|grep -v <your foldername>) <your foldername>/这将把所有文件和文件夹复制到您的新文件夹中。 - serup
显示剩余11条评论

405
如果你想保留第二个仓库的完整提交历史,并且希望将来能够轻松地合并上游的更改,那么这是你想要的方法。它会将子树的历史无修改地导入到你的仓库中,并添加一个合并提交来将合并的仓库移动到子目录中。
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

但这样做只会显示合并历史中的变更,而不会显示其他变更。
换句话说,如果您不在代码库XXX中更改ZZZ的文件,则需要指定--follow和无前缀的路径。如果您在两个代码库中都更改了它们,那么您需要使用两个命令,但都无法显示所有的变更。
Git 2.9之前的版本:您不需要在git merge命令中添加--allow-unrelated-histories选项。
另一个答案中使用read-tree并跳过merge -s ours步骤的方法实际上与使用cp复制文件并提交结果没有什么区别。
原始来源是来自于github的"Subtree Merge"帮助文章。还有另一个有用的链接

11
看起来这项操作没有保留历史记录...如果我在任何一个拉取的文件上执行 git log,我只看到了单个合并提交以及它在另一个仓库中之前的历史记录。Git版本为1.8.0。 - Anentropic
9
啊哈!如果我使用导入文件的旧路径,即省略它被导入到的子目录,那么Git日志将为我提供提交历史记录,例如 git log -- myfile 而不是 git log -- rack/myfile - Anentropic
2
@FrancescoFrassinelli,这不是很理想吗?将历史记录带入其中是此方法的一个特性 - pattivacek
5
如果你不想保留历史记录,为什么不直接复制呢?我在思考如果不是因为历史记录,你会被什么吸引到使用这种方法 -- 因为保留历史记录是我使用这种方法的唯一原因! - pattivacek
8
自 Git 2.9 版本起,在进行合并操作时需要添加选项 --allow-unrelated-histories - stuXnet
显示剩余18条评论

176

git-subtree是一个旨在将多个仓库合并为一个同时保留历史记录(或将子树的历史记录拆分,但这似乎与此问题无关)的脚本。自1.7.11版本发布以来,它已作为Git树的一部分进行分发。

要将<rev>版本的<repo>仓库合并为子目录<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

但这只会在合并历史记录中显示更改。

换句话说,如果您没有更改存储库XXXZZZ的文件,则需要指定--follow和一个无前缀的路径。如果您在两个存储库中都更改了它们,则有2个命令,其中没有一个显示所有更改。

更多信息请参见此处


4
如果你有一个目录需要合并而不是裸库或远程库,可以使用以下命令:git subtree add -P 想要的前缀名称 ~/没有.git后缀的git库位置 分支名称。请注意,此命令将把另一个仓库的指定分支合并到当前分支下的指定目录中。 - Tatsh
2
新手经验:在一个刚初始化的本地非裸库中,当我尝试使用git(版本2.9.0.windows.1)时,它会响应“fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree”,但是在我真正开始新的仓库之后,也就是添加了一个普通文件并按常规方式提交后,它就可以正常工作了。 - Stein
2
我的情况下运行得非常好。 - Johnny Utahh

52

在Git存储库本身中,有一个众所周知的例子,在Git社区中被称为“史上最酷的合并”(以Linus Torvalds在发送邮件给Git邮件列表时使用的主题行命名)。在这种情况下,现在作为Git正式组成部分的gitk Git GUI实际上曾经是一个单独的项目。Linus成功地将该存储库合并到Git存储库中,使得

  • 它在Git存储库中的表现好像一直都是作为Git的一部分开发的,
  • 所有历史记录都保持完整,
  • 它仍然可以在其旧存储库中独立开发,只需使用git pull进行更改即可。

邮件中包含了重现步骤,但这不适合新手:首先,Linus 编写了 Git,因此他可能比你和我更了解它,其次,这已经近5年前了,Git自那以后已经显着地改进,因此现在可能更容易了。

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


3
顺便提一句,如果需要合并其他分支,使用的策略称为“子树合并”,而第三方工具git-subtree可以帮助您完成此操作:http://github.com/apenwarr/git-subtree。 - Jakub Narębski
谢谢,我忘记了。subtree合并策略,特别是与git-subtree工具结合使用,是子模块的一个不错、甚至更好的替代方案。 - Jörg W Mittag
1
那个链接对我来说是坏的,这个链接可以使用(目前):https://marc.info/?l=git&m=111947722514210&w=2 - BallpointBen

19

让我使用名称a(代替XXXZZZ)和b(代替YYY),因为这样描述会更容易阅读。

假设您想将存储库a合并到b中(我假设它们位于彼此旁边):

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

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

更多信息here


2
很好。历史记录在git log中显示没有问题,不像使用git subtree add -P ...的解决方案。 - Martin Jambon
1
原始请求者想要的唯一一件事情就是让XXX在文件夹ZZZ中。因此使用git mv stuff ZZZ/stuff命令。我不明白你的解决方案如何满足这个要求。 - Jim
1
@Jim 子文件夹部分已经由 filter-repo --to-subdirectory-filter 处理,详见 https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html。我自己也使用了这种方法,效果完美(因为我在 Windows 上,需要根据 https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html 修复 PATH)。 - Ohad Schneider

14
简单的方法是使用git format-patch。
假设我们有两个git仓库foo和bar。
foo包含: - foo.txt - .git
bar包含: - bar.txt - .git
我们想让foo包含bar的历史记录和这些文件: - foo.txt - .git - foobar/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]”。


如果原始存储库包含分支和合并,则“git am”可能会失败。 - Adam Monsen
1
小问题:git am 会从提交消息中剥离 [ ] 中的任何内容。因此,您应该使用与 [bar] 不同的标记。 - HRJ
对我没有起作用。出现了“错误:foobar/mySubDir/test_host1:在索引中不存在。失败的补丁副本位于: /home/myuser/src/proj/.git/rebase-apply/patch 解决此问题后,运行“git am --continue”。这是在应用11个补丁(共60个)之后发生的。 - oligofren
1
这篇博客提供了一个类似的答案,回答了一个略有不同的问题(仅移动选定的文件)。 - Jesse Glick
我看到一个缺点,所有的提交都会被添加到目标仓库的HEAD。 - CSchulz

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="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版本。


2
对于使用 OS X 的用户,请安装 gnu-sed 以使 git-add-repo 函数正常工作。再次感谢 Andrey! - ptaylor

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


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

2

这是一个立即可用的脚本。

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

运行脚本。前往您想要合并其他存储库的存储库,并运行脚本。

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

1
运行良好,谢谢! - bric3

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