你如何合并两个Git存储库?

考虑以下场景:

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

我如何将A和B合并,而不丢失任何一方的历史?


如果你想合并project-aproject-b:

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/master # or whichever branch you want to merge
git remote remove project-a

摘自:git合并不同的存储库?

这个方法对我很有效,它更短,在我看来更干净。

注意:--allow-unrelated-histories参数只存在于git >= 2.9之后。参见Git - Git合并文档/——允许不相关的历史记录

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


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

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

对于松耦合存储库,这是一个很好的解决方案,其中存储库a中的开发将继续进行,而开发的主要部分是在a中单独的独立开发。请参阅Git Wiki上的SubmoduleSupport和GitSubmoduleTutorial页面。

子树合并

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

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 >= 2.9.0需要选择--allow-unrelated-histories)

或者您也可以使用apenwarr (Avery Pennarun)的git子树工具(GitHub上的repository),比如他在博客文章中宣布了git子模块的一个新替代方案:git子树。

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


可以很容易地将另一个存储库的单个分支放在保留其历史记录的子目录下。例如:

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

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

Add 'rails/' from commit

其中是SHA-1提交散列。你还能看到历史,怪一些变化。

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

注意,您不能从这里看到目录前缀,因为这是一个完整的实际旧分支。您应该像对待一个普通的文件移动提交一样对待它:当到达它时,您将需要一个额外的跳转。

1
2
3
4
5
# 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自制程序)。但是除了git之外,您可能还必须自己安装它。


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

第一件事是使用git filter-branch重写第二个存储库中所有内容的名称,使它们位于子目录中,您希望它们位于子目录中。因此,不是foo.c,而是bar.html,而是projb/foo.cprojb/bar.html

然后,您应该能够做如下事情:

1
2
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/[email protected]/msg03395.html


git-subtree很好,但是它可能不是你想要的那个。

例如,如果projectA是在B中创建的目录,在git subtree之后,

1
git log projectA

只列出一个提交:合并。合并项目的提交针对不同的路径,因此它们不会出现。

格雷格?休吉尔(Greg Hewgill)的答案最为接近,不过他并没有具体说明如何重写路径。

解决方案出奇地简单。

(1)在一个,

1
2
3
4
5
6
7
8
PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed"s,\t,&amp;'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &amp;&amp;
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

注意:这将重写历史记录,因此如果您打算继续使用这个repo A,您可能想首先克隆(复制)它的一次性副本。

然后在B中,运行

1
git pull path/to/A

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

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


如果两个储存库都有相同的文件类型(例如两个Rails储存库用于不同的项目),您可以将辅助储存库的数据提取到当前储存库:

1
git fetch git://repository.url/repo.git master:branch_name

然后将其合并到当前存储库:

1
git merge --allow-unrelated-histories branch_name

如果Git版本小于2.9,则删除--allow-unrelated-histories

在此之后,可能会发生冲突。您可以使用git mergetool解析它们。kdiff3可以单独使用键盘,所以5冲突文件读取代码时只需几分钟。

记住要完成合并:

1
git commit

在使用merge时,我不断丢失历史记录,所以我最终使用了rebase,因为在我的例子中,这两个存储库足够不同,不会在每次提交时都合并:

1
2
3
4
5
6
7
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

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

1
git rebase --continue

这样做将导致一个项目拥有来自projA的所有提交,然后是来自projB的提交


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

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

博士

TL;

1
2
3
4
5
6
7
8
9
$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter"zsh -c 'setopt extended_glob &amp;&amp; setopt glob_dots &amp;&amp; mkdir -p plugins/my-plugin &amp;&amp; (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

1
$ git filter-branch -f --tree-filter"zsh -c 'setopt extended_glob &amp;&amp; setopt glob_dots &amp;&amp; mkdir -p plugins/my-plugin &amp;&amp; (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选项,它允许我使用glob (^(...))选择dotfiles(例如.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的远程添加到:

1
$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

在提交历史中,您现在将有两个不相关的树,您可以很好地使用它们:

1
$ git log --color --graph --decorate --all

要合并它们,请使用:

1
$ git merge my-plugin/master --allow-unrelated-histories

注意,在pre-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远程:

1
$ 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-branchhttps://unix.stackexchange.com/questions/6393/how-do-you-move-all-files-including-hidden-from-one-directory-to-anotherhttp://www.refining-linux.org/archives/37/ZSH-Gem-2-Extended-globbing-and-expansion/从Git repo清除文件失败,无法创建新的备份git,所有分支上的过滤器分支


几天来我一直在尝试做同样的事情,我使用的是git 2.7.2。子树不保存历史。

如果不再使用旧项目,可以使用此方法。

我建议你先在B分行工作。

以下是没有分支的步骤:

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

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

1
git log --follow A/<file>

这篇文章帮助我做到了这一点:

Merging Two Git Repositories Into One Repository Without Losing File History


我在这里收集了许多关于stack&c;OverFlow等的信息,并设法编写了一个脚本来解决这个问题。

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

标记和其他分支被忽略——这可能不是您想要的。

该脚本甚至处理特性分支和标记——在新项目中重新命名它们,以便您知道它们来自何处。

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
254
#!/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
##&nbsp;  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=$'
'

# 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>&amp;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>&amp;1
  else
    echo -e"INFO:   Committing ${sub_project}..."
    echo -e"INFO:   Committing ${sub_project}...">>${LOG_FILE} 2>&amp;1
    if ! git commit -m"[Project] Merged branch '$1' of ${sub_project}">>${LOG_FILE} 2>&amp;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>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;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>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;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>&amp;1
  echo -e"----------------------------------------------------"
  echo -e"----------------------------------------------------">>${LOG_FILE} 2>&amp;1

  # Fetch the project
  echo -e"INFO:   Fetching ${sub_project}..."
  echo -e"INFO:   Fetching ${sub_project}...">>${LOG_FILE} 2>&amp;1
  git remote add"${sub_project}""${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&amp;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>&amp;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>&amp;1

    # create and checkout new merge branch off of master
    if ! git checkout -b"${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&amp;1 ; then failed"Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed"Failed preparing ${branch_name}">>${LOG_FILE} 2>&amp;1 ; fi
    if ! git clean -d --force ; then failed"Failed preparing ${branch_name}">>${LOG_FILE} 2>&amp;1 ; fi

    # Merge the project
    echo -e"INFO:   Merging ${sub_project}..."
    echo -e"INFO:   Merging ${sub_project}...">>${LOG_FILE} 2>&amp;1
    if ! git merge --allow-unrelated-histories --no-commit"remotes/${sub_project}/${branch_name}">>${LOG_FILE} 2>&amp;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>&amp;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>&amp;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})


  #&nbsp;checkout master of sub probject
  if ! git checkout"${sub_project}/master">>${LOG_FILE} 2>&amp;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>&amp;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>&amp;1
    if ! git tag"${tag_new_name}""${tag_ref}">>${LOG_FILE} 2>&amp;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>&amp;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>&amp;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>&amp;1
echo -e"===================================================="
echo -e"====================================================">>${LOG_FILE} 2>&amp;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>&amp;1
  if ! git merge --allow-unrelated-histories --no-commit"${sub_project}/master">>${LOG_FILE} 2>&amp;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>&amp;1
echo

exit 0

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

首先创建一个文件与每个存储库的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
./mergeGitRepositories.sh eitchnet_test eitchnet.lst

脚本本身有很多注释,可以解释它的功能。


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

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
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 ] &amp;&amp; git commit -m"merge made by $me"
        cd ..

    x=$(( x + 1 ))
done


如果您试图将两个存储库简单地粘合在一起,则使用子模块和子树合并是错误的工具,因为它们不能保存所有的文件历史记录(正如人们在其他答案中指出的那样)。看这里的答案,找到简单而正确的方法。


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

在回购A中,首先执行以下步骤使回购B可用:

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

现在我们在repo a中创建了一个全新的分支(只有一个commit),我们称之为new_b_root。生成的提交将包含在repo B主分支的第一次提交中提交的文件,但这些文件将放在名为path/to/b-files/的子目录中。

1
2
3
4
5
6
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的主分支中选择第一个提交。(chry -pick保留了原始的提交消息,而直接签出似乎不会这样做。)然后,我们创建子树,将repo b中的所有文件放到子树中。然后,我们必须将chry -pick中引入的所有文件移动到子树中。在上面的例子中,只需要移动一个README文件。然后我们提交B-repo根提交,同时,我们还保存原始提交的时间戳。

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

1
2
git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

现在,我们将我们的b分支合并为A/master:

1
2
3
git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

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

1
2
git remote remove B
git branch -D new_b_root b

最终的图表结构如下:

enter image description here


我遇到过类似的挑战,但在我的例子中,我们在repo a中开发了一个版本的代码库,然后将其克隆到一个新的repo, repo B,用于产品的新版本。在修复了repo A中的一些bug后,我们需要将更改FI到repo b中。

向指向repo a的repo B添加远程(git远程添加…)绘制当前分支(我们没有使用master进行bug修复)(git绘制remoteForRepoA bugFixBranch)推动合并到github

工作待遇:)


将a a合并到B中:

1)在项目A中

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

2)在项目B中

1
2
git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

在这个分支中,执行所有需要执行的操作并提交它们。

C)然后回到大师和经典的融合之间的两个分支:

1
2
git checkout master
git merge projectA

合并2回购

1
2
3
4
5
6
7
8
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

类似于@Smar,但使用文件系统路径,设置在主要和次要:

1
2
3
4
5
PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY &amp;&amp; git fetch test
git merge test/master

然后手动合并。

(改编自Anar Manafov的文章)


当您想在一次提交中合并三个或更多的项目时,按照其他答案(remote add -fmerge)中描述的步骤执行。然后,(soft)将索引重置为old head(没有合并的地方)。添加所有文件(git add -A)并提交它们(消息"将项目A、B、C和D合并到一个项目中")。这现在是master的commit-id。

现在,用以下内容创建.git/info/grafts:

1
<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。不要总是添加它,因为这可能导致重写一些提交。有关详细信息,请参见过滤器-分支的手册页,搜索"grafts"。

现在,您的上一个提交关联了正确的父类。


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

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


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

这感觉比滤网树枝要干净得多,也不那么可怕。当然,它可能并不适用于所有情况。


该函数将远程repo克隆到本地repo目录,合并所有提交后保存,git log将显示原始提交和正确路径:

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
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,&amp;'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &amp;&amp;
        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"
}

如何使用:

1
2
cd current/package
git-add-repo https://github.com/example/example dir/to/save

如果做一些小小的改变,你甚至可以把合并后的回购文件/dirs移动到不同的路径,例如:

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
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 &amp;&amp;
    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之后。


我稍微手动地合并项目,这允许我避免处理合并冲突。

首先,按照您希望的方式从其他项目复制文件。

1
2
cp -R myotherproject newdirectory
git add newdirectory

历史上的下一站

1
git fetch path_or_url_to_other_repo

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

1
echo 'FETCH_HEAD' > .git/MERGE_HEAD

现在按照您通常的方式提交

1
git commit