如何合并两个Git仓库

如何合并两个Git仓库

技术背景

在软件开发过程中,有时需要将两个不同的Git仓库合并成一个。可能是因为项目重构、团队合并或代码整合等原因。合并仓库时,我们通常希望保留原仓库的历史记录,以便追溯代码的演变过程。

实现步骤

方法一:将project-asome-branch分支合并到project-b

  1. 切换到project-a仓库并检出some-branch分支:
1
2
cd path/to/project-a
git checkout some-branch
  1. 切换到project-b仓库,添加project-a为远程仓库,拉取标签并合并some-branch分支:
1
2
3
4
5
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/some-branch
git remote remove project-a

方法二:将project-a放入project-b的子目录

在上述步骤之前,先在project-a仓库中执行以下命令:

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

方法三:使用子模块

可以将仓库A复制到项目B的单独目录中,或者更好的做法是将仓库A克隆到项目B的子目录中,然后使用git submodule将其作为项目B的子模块:

1
git submodule add /path/to/repo-A path/to/subdirectory

方法四:使用子树合并

1
2
3
4
5
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仓库合并到一个新仓库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/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
## [email protected]
##
## 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

使用方法:

  1. 创建一个包含每个仓库URL的文件,例如:
1
2
3
[email protected]:eitchnet/ch.eitchnet.parent.git
[email protected]:eitchnet/ch.eitchnet.utils.git
[email protected]:eitchnet/ch.eitchnet.privilege.git
  1. 调用脚本并指定新项目的名称和URL文件的路径:
1
./mergeGitRepositories.sh eitchnet_test eitchnet.lst

核心代码

以下是将project-asome-branch分支合并到project-b的核心代码:

1
2
3
4
5
6
7
8
cd path/to/project-a
git checkout some-branch

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/some-branch
git remote remove project-a

最佳实践

  • 在合并仓库之前,建议备份原仓库,以防意外情况发生。
  • 尽量使用--allow-unrelated-histories参数,确保可以合并没有共同历史的仓库。
  • 在合并过程中,如果遇到冲突,应仔细检查并解决冲突,确保代码的正确性。

常见问题

--allow-unrelated-histories参数不可用

该参数从Git 2.9版本开始支持,如果使用的是较旧的版本,可能会遇到该参数不可用的问题。可以考虑升级Git版本,或者尝试其他合并方法。

合并后历史记录丢失

如果使用的合并方法不正确,可能会导致历史记录丢失。建议使用保留历史记录的合并方法,如上述介绍的方法一和方法二。

合并时出现冲突

冲突是合并过程中常见的问题,通常是由于两个仓库中的文件有相同的修改。可以使用git mergetool工具来解决冲突,也可以手动编辑冲突文件。


如何合并两个Git仓库
https://119291.xyz/posts/2025-05-16.how-to-merge-two-git-repositories/
作者
ww
发布于
2025年5月16日
许可协议