如何合并两个Git存储库?

考虑以下场景:

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

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

650063 次浏览

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

第一件事是使用git filter-branch重写第二个存储库中所有内容的名称,使其位于您希望它们结束的子目录中。因此,您将拥有projb/foo.cprojb/bar.html,而不是foo.cbar.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

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

子模块

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

对于松耦合的存储库来说,这是一个很好的解决方案,其中存储库A中的开发继续进行,开发的主要部分是A中的单独独立开发。另请参阅Git Wiki上的子模块名称子模块教程页面。

子树合并

您可以使用子树合并策略将存储库A合并到项目B的子目录中。这在子树合并和你中由Markus Prinz描述。

git remote add -f Bproject /path/to/Bgit merge -s ours --allow-unrelated-histories --no-commit Bproject/mastergit read-tree --prefix=dir-B/ -u Bproject/mastergit 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子树工具(GitHub上的存储库),例如在他的博客文章Git子模块的新替代方案:git子树中宣布。


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

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

与@Smar类似,但使用文件系统路径,在PRIMary和SECONDary中设置:

PRIMARY=~/Code/project1SECONDARY=~/Code/project2cd $PRIMARYgit remote add test $SECONDARY && git fetch testgit merge test/master

然后手动合并。

(改编自作者:Anar Manafov

我也遇到过类似的挑战,但在我的案例中,我们在仓库A中开发了一个版本的代码库,然后将其克隆到新的仓库B中,用于新版本的产品。在修复了仓库A中的一些错误后,我们需要将更改提交到仓库B中。最终完成了以下操作:

  1. 向存储库B添加指向存储库A的远程(git远程添加…)
  2. 拉取当前分支(我们没有使用master进行bug修复)(git拉取远程ForRepoA错误修复分支)
  3. 将合并推送到github

工作治疗:)

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

me=$(basename $0)
TMP=$(mktemp -d /tmp/$me.XXXXXXXX)echoecho "building new repo in $TMP"echosleep 1
set -e
cd $TMPmkdir new-repocd new-repogit initcd ..
x=0while [ -n "$1" ]; dorepo="$1"; shiftgit clone "$repo"dirname=$(basename $repo | sed -e 's/\s/-/g')if [[ $dirname =~ ^git:.*\.git$ ]]; thendirname=$(echo $dirname | sed s/.git$//)fi
cd $dirnamegit remote rm origingit filter-branch --tree-filter \"(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"cd ..
cd new-repogit pull --no-commit ../$dirname[ $x -gt 0 ] && git commit -m "merge made by $me"cd ..
x=$(( x + 1 ))done

如果您想将project-a合并为project-b

cd path/to/project-bgit remote add project-a /path/to/project-agit fetch project-a --tagsgit merge --allow-unrelated-histories project-a/master # or whichever branch you want to mergegit remote remove project-a

图片来源:git合并不同的存储库?

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

如果您想将project-a放入子目录,您可以使用#1filter-branch气馁)。在上面的命令之前运行以下命令:

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

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

备注:--allow-unrelated-histories参数只存在于git>=2.9之后。参见git-git合并文档

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

如果你试图简单地将两个存储库粘合在一起,子模块和子树合并是错误的工具,因为它们不会保留所有的文件历史记录(正如人们在其他答案中指出的那样)。

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

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

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

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

其中<rev>是SHA-1提交哈希。您仍然可以看到历史记录,归咎于一些更改。

git log <rev>git blame <rev> -- README.md

请注意,您无法从这里看到目录前缀,因为这是一个实际的旧分支。您应该将其视为通常的文件移动提交:到达它时需要额外的跳转。

# finishes with all files added at once commitgit log rails/README.md
# then continue from original treegit log <rev> -- README.md

还有更复杂的解决方案,例如手动执行此操作或重写其他答案中描述的历史记录。

git-subtree命令是官方git-contrib的一部分,一些数据包管理器默认安装它(OS X Homebrew)。但是除了git之外,你可能还需要自己安装它。

当你想在单一提交中合并三个或更多项目时,按照其他答案(remote add -fmerge)中描述的步骤进行。然后,(软)将索引重置为旧的head(没有发生合并的地方)。添加所有文件(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分支的手册页,搜索“移植”。

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

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字符(或白色字符),则必须修改ses命令中的替代脚本。在这种情况下,由“ls-files-s”生成的记录中的文件位置以引号开头。

(2)在B中,运行

git pull path/to/A

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


在我的例子中,我想要两个子目录,projectAprojectB。在这种情况下,我也做了步骤(1)到B。

我在使用合并时不断丢失历史记录,所以我最终使用了rebase,因为在我的情况下,两个存储库的差异足以避免在每次提交时合并:

git clone git@gitorious/projA.git projAgit clone git@gitorious/projB.git projB
cd projBgit remote add projA ../projA/git fetch projAgit rebase projA/master HEAD

=>解决冲突,然后继续,根据需要多次…

git rebase --continue

这样做会导致一个项目具有来自项目A的所有提交,然后是来自项目B的提交

我在这里收集了很多关于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 variablesshopt -s -o nounset
# Script variablesdeclare SCRIPT_NAME="${0##*/}"declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"declare ROOT_DIR="$PWD"IFS=$'\n'
# Detect proper usageif [ "$#" -ne "2" ] ; thenecho -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"exit 1fi

## Script variablesPROJECT_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 functionsfunction failed() {echo -e "ERROR: Merging of projects failed:"echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1echo -e "$1"exit 1}
function commit_merge() {current_branch="$(git symbolic-ref HEAD 2>/dev/null)"if [[ ! -f ".git/MERGE_HEAD" ]] ; thenecho -e "INFO:   No commit required."echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1elseecho -e "INFO:   Committing ${sub_project}..."echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; thenfailed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"fifi}

# Make sure the REPO_URL_FILE existsif [ ! -e "${REPO_URL_FILE}" ] ; thenecho -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"exit 1fi

# Make sure the required directories don't existif [ -e "${PROJECT_PATH}" ] ; thenecho -e "ERROR: Project ${PROJECT_NAME} already exists!"exit 1fi

# create the new projectecho -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>&1echo -e "===================================================="echo -e "====================================================" >>${LOG_FILE} 2>&1cd ${ROOT_DIR}mkdir ${PROJECT_NAME}cd ${PROJECT_NAME}git initecho "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 merginggit add initial_commitgit commit --quiet -m "[Project] Initial Master Repo Commit"git rm --quiet initial_commitgit commit --quiet -m "[Project] Initial Master Repo Commit"echo

# Merge all projects into the branches of this projectecho -e "INFO: Merging projects into new repository..."echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1echo -e "===================================================="echo -e "====================================================" >>${LOG_FILE} 2>&1for url in $(cat ${REPO_URL_FILE}) ; do
if [[ "${url:0:1}" == '#' ]] ; thencontinuefi
# extract the name of this projectexport sub_project=${url##*/}sub_project=${sub_project%*.git}
echo -e "INFO: Project ${sub_project}"echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1echo -e "----------------------------------------------------"echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1
# Fetch the projectecho -e "INFO:   Fetching ${sub_project}..."echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1git remote add "${sub_project}" "${url}"if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; thenfailed "Failed to fetch project ${sub_project}"fi
# add remote branchesecho -e "INFO:   Creating local branches for ${sub_project}..."echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1while read branch ; dobranch_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 masterif ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fiif ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fiif ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
# Merge the projectecho -e "INFO:   Merging ${sub_project}..."echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; thenfailed "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 directoryif [ "$(ls)" == "${sub_project}" ] ; thenecho -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>&1elseecho -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>&1mkdir ${sub_project}for f in $(ls -a) ; doif  [[ "$f" == "${sub_project}" ]] ||[[ "$f" == "." ]] ||[[ "$f" == ".." ]] ; thencontinuefigit mv -k "$f" "${sub_project}/"done
# commit the movingif ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; thenfailed "Failed to commit moving of ${sub_project} files into sub directory"fifiechodone < <(git ls-remote --heads ${sub_project})

# checkout master of sub probjectif ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; thenfailed "sub_project ${sub_project} is missing master branch!"fi
# copy remote tagsecho -e "INFO:   Copying tags for ${sub_project}..."echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1while read tag ; dotag_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.0tag_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>&1if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; thenecho -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>&1fidone < <(git ls-remote --tags --refs ${sub_project})
# Remove the remote to the old projectecho -e "INFO:   Removing remote ${sub_project}..."echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1git remote rm ${sub_project}
echodone

# Now merge all project master branches into new mastergit checkout --quiet masterecho -e "INFO: Merging projects master branches into new repository..."echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1echo -e "===================================================="echo -e "====================================================" >>${LOG_FILE} 2>&1for url in $(cat ${REPO_URL_FILE}) ; do
if [[ ${url:0:1} == '#' ]] ; thencontinuefi
# extract the name of this projectexport sub_project=${url##*/}sub_project=${sub_project%*.git}
echo -e "INFO:   Merging ${sub_project}..."echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; thenfailed "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"
echodone

# Donecd ${ROOT_DIR}echo -e "INFO: Done."echo -e "INFO: Done." >>${LOG_FILE} 2>&1echo
exit 0

您也可以从http://paste.ubuntu.com/11732805获取它

首先创建一个带有每个存储库URL的文件,例如:

git@github.com:eitchnet/ch.eitchnet.parent.gitgit@github.com:eitchnet/ch.eitchnet.utils.gitgit@github.com:eitchnet/ch.eitchnet.privilege.git

然后调用脚本,给出项目的名称和脚本的路径:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

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

在B中合并A:

(1)项目A

git fast-export --all --date-order > /tmp/ProjectAExport

(2)项目B

git checkout -b projectAgit fast-import --force < /tmp/ProjectAExport

在此分支中执行您需要执行的所有操作并提交它们。

C)然后回到master和两个分支之间的经典合并:

git checkout mastergit merge projectA

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

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

几天来我一直在尝试做同样的事情,我使用的是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 dirmkdir B
# Move all files to B, till there is nothing in the dir but .git and Bgit mv <files> B
git add .
git commit -m "Moving content of project B in preparation for merge from A"

# Now merge A into Bgit remote add -f A <A repo url>
git merge A/<branch>
mkdir A
# move all the files into subdir A, excluding .gitgit mv <files> A
git commit -m "Moved A into subdir"

# Move B's files back to rootgit mv B/* ./
rm -rf B
git commit -m "Reset B to original state"
git push

如果您现在在subdir A中记录任何文件,您将获得完整的历史记录

git log --follow A/<file>

这是帮助我做到这一点的帖子:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/

在我的例子中,我有一个my-plugin存储库和一个main-project存储库,我想假装my-plugin一直是在main-projectplugins子目录中开发的。

基本上,我重写了my-plugin存储库的历史,使其看起来所有开发都发生在plugins/my-plugin子目录中。然后,我将my-plugin的开发历史添加到main-project历史中,并将两棵树合并在一起。由于main-project存储库中已经没有plugins/my-plugin目录,这是一次简单的无冲突合并。生成的存储库包含两个原始项目的所有历史,并且有两个根。

太长别读

$ 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时,它会抱怨这一点,除非您向filter-branch提供-f选项。

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

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

最后,我使用zsh“负全局”功能^(.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中,#0选项不存在。如果您使用的是这些版本之一,只需省略该选项:#0阻止的错误消息是添加于2.9.0。

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

确保为任何未来的贡献者输入解释性提交消息,这些贡献者想知道正在发生什么黑客行为来制作具有两个根的存储库。

您可以使用上面的git log命令可视化新的提交图,它应该有两个根提交。注意只有#1分支将被合并。这意味着如果您在其他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。您的里程可能会有所不同。

参考文献:

该函数将远程仓库克隆到本地仓库目录中,合并后将保存所有提交,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/packagegit-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个仓库

git clone ssh://<project-repo> project1cd project1git remote add -f project2 project2git merge --allow-unrelated-histories project2/mastergit remote rm project2
delete the ref to avoid errorsgit update-ref -d refs/remotes/project2/master

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

首先,从另一个项目中复制文件,无论您想要它们。

cp -R myotherproject newdirectorygit add newdirectory

历史上的下一次拉动

git fetch path_or_url_to_other_repo

告诉git在最后获取的东西的历史中合并

echo 'FETCH_HEAD' > .git/MERGE_HEAD

现在按照你通常会做的去做

git commit

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

在仓库A中,首先执行以下操作以使仓库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 mastergit rm -rf . # Remove all files.git cherry-pick -n `git rev-list --max-parents=0 B/master`mkdir -p path/to/b-filesgit mv README path/to/b-files/git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

说明:check out命令的--orphan选项从A的主分支签出文件,但不创建任何提交。我们本可以选择任何提交,因为接下来我们还是会清除所有文件。然后,还没有提交(-n),我们从B的主分支中挑选第一个提交。(樱桃挑选保留了原始提交消息,这是直接签出似乎做不到的。)然后我们创建子树,我们想把存储库B中的所有文件放在那里。然后我们必须将樱桃挑选中引入的所有文件移动到子树中。在上面的例子中,只有一个README文件要移动。然后我们提交我们的B-repo根提交,同时,我们还保留了原始提交的时间戳。

现在,我们将在新创建的new_b_root之上创建一个新的B/master分支。我们称之为新分支b

git checkout -b b B/mastergit rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

现在,我们将b分支合并到A/master

git checkout mastergit merge --allow-unrelated-histories --no-commit bgit commit -m 'Merge repo B into repo A.'

最后,您可以删除B远程和临时分支:

git remote remove Bgit branch -D new_b_root b

最终的图将具有这样的结构:

在此处输入图片描述

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

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

https://github.com/hraban/tomono作为另一个提到的基于脚本的解决方案。

我不是作者,但使用它,它做的工作。

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

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

对于Windows用户:git bash可以执行. sh文件。它随标准git安装一起提供。

我今天必须解决如下问题:项目A是在比特桶中,项目B是在代码提交中……两者都是相同的项目,但必须合并从A到B的更改(诀窍是在项目A中创建相同名称的分支,与项目B中相同)

  • git签出项目A
  • git远程删除原点
  • git远程添加来源项目B
  • git签出分支
  • git add*
  • git提交-m"我们已经移动了代码"
  • git推送

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

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

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

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

操作后,最好删除另一个遥控器,这样你就不会意外地以正常方式获取标签并弄得一团糟。

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