如何合并两个 Git 存储库?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1425892/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 07:02:57  来源:igfitidea点击:

How do you merge two Git repositories?

gitmergerepositorygit-subtree

提问by static_rtti

Consider the following scenario:

考虑以下场景:

I have developed a small experimental project A in its own Git repo. It has now matured, and I'd like A to be part of larger project B, which has its own big repository. I'd now like to add A as a subdirectory of B.

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

How do I merge A into B, without losing history on any side?

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

采纳答案by Simon Perepelitsa

A single branch of another repository can be easily placed under a subdirectory retaining its history. For example:

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

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

This will appear as a single commit where all files of Rails master branch are added into "rails" directory. However the commit's title contains a reference to the old history tree:

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

Add 'rails/' from commit <rev>

从提交中添加“rails/” <rev>

Where <rev>is a SHA-1 commit hash. You can still see the history, blame some changes.

<rev>SHA-1 提交哈希在哪里。你仍然可以看到历史,怪一些变化。

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

Note that you can't see the directory prefix from here since this is an actual old branch left intact. You should treat this like a usual file move commit: you will need an extra jump when reaching it.

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

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

There are more complex solutions like doing this manually or rewriting the history as described in other answers.

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

The git-subtree command is a part of official git-contrib, some packet managers install it by default (OS X Homebrew). But you might have to install it by yourself in addition to git.

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

回答by Andresch Serj

If you want to merge project-ainto project-b:

如果你想合并project-aproject-b

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

Taken from: git merge different repositories?

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

This method worked pretty well for me, it's shorter and in my opinion a lot cleaner.

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

In case you want to put project-ainto a subdirectory, you can use git-filter-repo(filter-branchis discouraged). Run the following commands before the commands above:

如果你想要把project-a到子目录中,你可以使用git-filter-repofilter-branch劝阻)。在上述命令之前运行以下命令:

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

An example of merging 2 big repositories, putting one of them into a subdirectory: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

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

Note:The --allow-unrelated-historiesparameter only exists since git >= 2.9. See Git - git merge Documentation / --allow-unrelated-histories

注意:--allow-unrelated-histories参数只存在于 git >= 2.9 之后。参见Git - git merge 文档/--allow-unrelated-history

Update: Added --tagsas suggested by @jstadler in order to keep tags.

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

回答by Jakub Nar?bski

Here are two possible solutions:

以下是两种可能的解决方案:

Submodules

子模块

Either copy repository A into a separate directory in larger project B, or (perhaps better) clone repository A into a subdirectory in project B. Then use git submoduleto make this repository a submoduleof a repository B.

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

This is a good solution for loosely-coupled repositories, where development in repository A continues, and the major portion of development is a separate stand-alone development in A. See also SubmoduleSupportand GitSubmoduleTutorialpages on Git Wiki.

这是松耦合的仓库,其中一个仓库继续发展一个很好的解决方案,以及发展的主要部分是又见一个单独的独立发展SubmoduleSupportGitSubmoduleTutorial上的Git维基网页。

Subtree merge

子树合并

You can merge repository A into a subdirectory of a project B using the subtree mergestrategy. This is described in Subtree Merging and Youby Markus Prinz.

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

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

(Option --allow-unrelated-historiesis needed for Git >= 2.9.0.)

--allow-unrelated-historiesGit >= 2.9.0 需要选项。)

Or you can use git subtreetool (repository on GitHub) by apenwarr (Avery Pennarun), announced for example in his blog post A new alternative to Git submodules: git subtree.

或者您可以使用apenwarr (Avery Pennarun) 的git subtree工具(GitHub 上的存储库),例如在他的博客文章中宣布的Git 子模块的新替代方案: git subtree



I think in your case (A is to be part of larger project B) the correct solution would be to use subtree merge.

我认为在您的情况下(A 将成为更大项目 B 的一部分)正确的解决方案是使用subtree merge

回答by Greg Hewgill

The submodule approach is good if you want to maintain the project separately. However, if you really want to merge both projects into the same repository, then you have a bit more work to do.

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

The first thing would be to use git filter-branchto rewrite the names of everything in the second repository to be in the subdirectory where you would like them to end up. So instead of foo.c, bar.html, you would have projb/foo.cand projb/bar.html.

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

Then, you should be able to do something like the following:

然后,您应该能够执行以下操作:

git remote add projb [wherever]
git pull projb

The git pullwill do a git fetchfollowed by a git merge. There should be no conflicts, if the repository you're pulling to does not yet have a projb/directory.

git pull会做一个git fetch接着一个git merge。如果您要拉到的存储库还没有projb/目录,则应该没有冲突。

Further searching indicates that something similar was done to merge gitkinto git. Junio C Hamano writes about it here: http://www.mail-archive.com/[email protected]/msg03395.html

进一步搜索表明已完成类似的操作以合并gitkgit. Junio C Hamano 在这里写到:http: //www.mail-archive.com/[email protected]/msg03395.html

回答by Paul Draper

git-subtreeis nice, but it is probably not the one you want.

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

For example, if projectAis the directory created in B, after git subtree,

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

git log projectA

lists only onecommit: the merge. The commits from the merged project are for different paths, so they don't show up.

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

Greg Hewgill's answer comes closest, although it doesn't actually say how to rewrite the paths.

Greg Hewgill 的回答最接近,尽管它实际上并没有说明如何重写路径。



The solution is surprisingly simple.

解决方案出奇的简单。

(1) In A,

(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

Note: This rewrites history, so if you intend to continue using this repo A, you may want to clone (copy) a throwaway copy of it first.

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

Note Bene: You have to modify the substitute script inside the sed command in the case that you use non-ascii characters (or white characters) in file names or path. In that case the file location inside a record produced by "ls-files -s" begins with quotation mark.

注意:如果在文件名或路径中使用非 ASCII 字符(或白色字符),则必须修改 sed 命令中的替换脚本。在这种情况下,“ls-files -s”生成的记录内的文件位置以引号开头。

(2) Then in B, run

(2) 然后在 B 中,运行

git pull path/to/A

Voila! You have a projectAdirectory in B. If you run git log projectA, you will see all commits from A.

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



In my case, I wanted two subdirectories, projectAand projectB. In that case, I did step (1) to B as well.

就我而言,我想要两个子目录,projectA以及projectB. 在那种情况下,我也对 B 执行了步骤 (1)。

回答by Smar

If both repositories have same kind of files (like two Rails repositories for different projects), you can fetch data of the secondary repository to your current repository:

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

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

and then merge it to current repository:

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

git merge --allow-unrelated-histories branch_name

If your Git version is smaller than 2.9, remove --allow-unrelated-histories.

如果您的 Git 版本小于 2.9,请删除--allow-unrelated-histories.

After this, conflicts may occur. You can resolve them for example with git mergetool. kdiff3can be used solely with keyboard, so 5 conflict file takes when reading the code just few minutes.

在此之后,可能会发生冲突。例如,您可以使用git mergetool. kdiff3可以单独与键盘一起使用,因此在阅读代码时只需几分钟即可获得 5 个冲突文件。

Remember to finish the merge:

记得完成合并:

git commit

回答by Calahad

I kept losing history when using merge, so I ended up using rebase since in my case the two repositories are different enough not to end up merging at every commit:

我在使用合并时一直丢失历史记录,所以我最终使用了 rebase,因为在我的情况下,这两个存储库不同,不会在每次提交时都合并:

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

=> resolve conflicts, then continue, as many times as needed...

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

git rebase --continue

Doing this leads to one project having all commits from projA followed by commits from projB

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

回答by Radon Rosborough

In my case, I had a my-pluginrepository and a main-projectrepository, and I wanted to pretend that my-pluginhad always been developed in the pluginssubdirectory of main-project.

就我而言,我有一个my-plugin仓库和一个main-project仓库,我想假装my-plugin一直被发达plugins的子目录main-project

Basically, I rewrote the history of the my-pluginrepository so that it appeared all development took place in the plugins/my-pluginsubdirectory. Then, I added the development history of my-plugininto the main-projecthistory, and merged the two trees together. Since there was no plugins/my-plugindirectory already present in the main-projectrepository, this was a trivial no-conflicts merge. The resulting repository contained all history from both original projects, and had two roots.

基本上,我重写了my-plugin存储库的历史记录,以便所有开发都发生在plugins/my-plugin子目录中。然后,我将 的发展历史添加my-pluginmain-project历史中,并将两棵树合并在一起。由于存储库中不存在plugins/my-plugin目录main-project,因此这是一个微不足道的无冲突合并。生成的存储库包含两个原始项目的所有历史记录,并且有两个根。

TL;DR

TL; 博士

$ 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

Long version

长版

First, create a copy of the my-pluginrepository, because we're going to be rewriting the history of this repository.

首先,创建my-plugin存储库的副本,因为我们将重写此存储库的历史记录。

Now, navigate to the root of the my-pluginrepository, check out your main branch (probably master), and run the following command. Of course, you should substitute for my-pluginand pluginswhatever your actual names are.

现在,导航到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

Now for an explanation. git filter-branch --tree-filter (...) HEADruns the (...)command on every commit that is reachable from HEAD. Note that this operates directly on the data stored for each commit, so we don't have to worry about notions of "working directory", "index", "staging", and so on.

现在解释一下。git filter-branch --tree-filter (...) HEAD(...)可从HEAD. 请注意,这直接对为每个提交存储的数据进行操作,因此我们不必担心“工作目录”、“索引”、“暂存”等概念。

If you run a filter-branchcommand that fails, it will leave behind some files in the .gitdirectory and the next time you try filter-branchit will complain about this, unless you supply the -foption to filter-branch.

如果你运行一个filter-branch失败的命令,它会在.git目录中留下一些文件,下次你尝试时filter-branch它会抱怨这个,除非你提供-f选项filter-branch

As for the actual command, I didn't have much luck getting bashto do what I wanted, so instead I use zsh -cto make zshexecute a command. First I set the extended_globoption, which is what enables the ^(...)syntax in the mvcommand, as well as the glob_dotsoption, which allows me to select dotfiles (such as .gitignore) with a glob (^(...)).

至于实际的命令,我没有太多运气bash去做我想做的事情,所以我使用zsh -cmakezsh执行命令。首先我设置extended_glob选项,它是启用命令中的^(...)语法mvglob_dots选项,以及允许我选择.gitignore带有 glob ( ^(...)) 的点文件(例如)的选项。

Next, I use the mkdir -pcommand to create both pluginsand plugins/my-pluginat the same time.

接下来,我用mkdir -p命令同时创建plugins,并plugins/my-plugin在同一时间。

Finally, I use the zsh"negative glob" feature ^(.git|plugins)to match all files in the root directory of the repository except for .gitand the newly created my-pluginfolder. (Excluding .gitmight not be necessary here, but trying to move a directory into itself is an error.)

最后,我使用zsh“negative glob”功能^(.git|plugins)来匹配存储库根目录中除.git新创建的my-plugin文件夹之外的所有文件。(.git此处可能不需要排除,但尝试将目录移动到自身中是错误的。)

In my repository, the initial commit did not include any files, so the mvcommand returned an error on the initial commit (since nothing was available to move). Therefore, I added a || trueso that git filter-branchwould not abort.

在我的存储库中,初始提交不包含任何文件,因此该mv命令在初始提交时返回了错误(因为没有可移动的内容)。因此,我添加了一个|| true这样git filter-branch就不会中止。

The --alloption tells filter-branchto rewrite the history for allbranches in the repository, and the extra --is necessary to tell gitto interpret it as a part of the option list for branches to rewrite, instead of as an option to filter-branchitself.

--all选项告诉filter-branch重写存储库中所有分支的历史记录,并且--需要额外说明git将其解释为要重写的分支的选项列表的一部分,而不是作为其filter-branch自身的选项。

Now, navigate to your main-projectrepository and check out whatever branch you want to merge into. Add your local copy of the my-pluginrepository (with its history modified) as a remote of main-projectwith:

现在,导航到您的main-project存储库并检查要合并到的任何分支。添加my-plugin存储库的本地副本(修改其历史记录)作为远程main-project

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

You will now have two unrelated trees in your commit history, which you can visualize nicely using:

您现在将在提交历史记录中有两个不相关的树,您可以使用以下方法很好地可视化:

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

To merge them, use:

要合并它们,请使用:

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

Note that in pre-2.9.0 Git, the --allow-unrelated-historiesoption does not exist. If you are using one of these versions, just omit the option: the error message that --allow-unrelated-historiesprevents wasalso added in 2.9.0.

请注意,在 2.9.0 之前的 Git 中,该--allow-unrelated-histories选项不存在。如果您正在使用这些版本之一,只需省略选项:--allow-unrelated-histories阻止的错误消息也在2.9.0 中添加。

You should not have any merge conflicts. If you do, it probably means that either the filter-branchcommand did not work correctly or there was already a plugins/my-plugindirectory in main-project.

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

Make sure to enter an explanatory commit message for any future contributors wondering what hackery was going on to make a repository with two roots.

确保为任何未来的贡献者输入一个解释性的提交消息,他们想知道黑客正在做什么来创建一个具有两个根的存储库。

You can visualize the new commit graph, which should have two root commits, using the above git logcommand. Note that only the masterbranch will be merged. This means that if you have important work on other my-pluginbranches that you want to merge into the main-projecttree, you should refrain from deleting the my-pluginremote until you have done these merges. If you don't, then the commits from those branches will still be in the main-projectrepository, but some will be unreachable and susceptible to eventual garbage collection. (Also, you will have to refer to them by SHA, because deleting a remote removes its remote-tracking branches.)

你可以使用上面的git log命令可视化新的提交图,它应该有两个根提交。请注意,只会master合并分支。这意味着如果您my-plugin要合并到main-project树中的其他分支上有重要工作,则在my-plugin完成这些合并之前,您应该避免删除远程。如果你不这样做,那么来自这些分支的提交仍将在main-project存储库中,但有些将无法访问并且容易受到最终的垃圾收集。(此外,您必须通过 SHA 引用它们,因为删除远程会删除其远程跟踪分支。)

Optionally, after you have merged everything you want to keep from my-plugin, you can remove the my-pluginremote using:

或者,在合并了要保留的所有内容后my-plugin,您可以my-plugin使用以下方法删除遥控器:

$ git remote remove my-plugin

You can now safely delete the copy of the my-pluginrepository whose history you changed. In my case, I also added a deprecation notice to the real my-pluginrepository after the merge was complete and pushed.

您现在可以安全地删除my-plugin您更改其历史记录的存储库副本。就我而言,我还在my-plugin合并完成并推送后向真实存储库添加了弃用通知。



Tested on Mac OS X El Capitan with git --version 2.9.0and zsh --version 5.2. Your mileage may vary.

在 Mac OS X El Capitan 上使用git --version 2.9.0和测试zsh --version 5.2。你的旅费可能会改变。

References:

参考:

回答by Rian

I've been trying to do the same thing for days, I am using git 2.7.2. Subtree does not preserve the history.

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

You can use this method if you will not be using the old project again.

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

I would suggest that you branch B first and work in the branch.

我建议你先分支 B 并在分支中工作。

Here are the steps without branching:

以下是没有分支的步骤:

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

If you now log any of the files in subdir A you will get the full history

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

git log --follow A/<file>

This was the post that help me do this:

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

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

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

回答by Finn Haakansson

If you want to put the files from a branch in repo B in a subtreeof repo A andalso preserve the history, keep reading. (In the example below, I am assuming that we want repo B's master branch merged into repo A's master branch.)

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

In repo A, first do the following to make repo B available:

在 repo A 中,首先执行以下操作以使 repo B 可用:

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

Now we create a brand new branch (with only one commit) in repo A that we call new_b_root. The resulting commit will have the files that were committed in the first commit of repo B's master branch but put in a subdirectory called path/to/b-files/.

现在我们在 repo A 中创建一个全新的分支(只有一次提交),我们称之为new_b_root。结果提交将包含在 repo B 的 master 分支的第一次提交中提交的文件,但放在名为path/to/b-files/.

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

Explanation: The --orphanoption to the checkout command checks out the files from A's master branch but doesn't create any commit. We could have selected any commit because next we clear out all the files anyway. Then, without committing yet (-n), we cherry-pick the first commit from B's master branch. (The cherry-pick preserves the original commit message which a straight checkout doesn't seem to do.) Then we create the subtree where we want to put all files from repo B. We then have to move all files that were introduced in the cherry-pick to the subtree. In the example above, there's only a READMEfile to move. Then we commit our B-repo root commit, and, at the same time, we also preserve the timestamp of the original commit.

说明:--orphancheckout 命令的选项从 A 的 master 分支检出文件,但不创建任何提交。我们可以选择任何提交,因为接下来我们无论如何都要清除所有文件。然后,在尚未提交 ( -n) 的情况下,我们从 B 的主分支中挑选第一个提交。(cherry-pick 保留了原始提交消息,而直接结帐似乎没有这样做。)然后我们创建子树,我们要将所有来自 repo B 的文件放在其中。然后我们必须移动在樱桃采摘到子树。在上面的例子中,只有一个README文件要移动。然后我们提交我们的 B-repo 根提交,同时,我们还保留原始提交的时间戳。

Now, we'll create a new B/masterbranch on top of the newly created new_b_root. We call the new branch b:

现在,我们将B/master在新创建的new_b_root. 我们称新分支为b

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

Now, we merge our bbranch into A/master:

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

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

Finally, you can remove the Bremote and temporary branches:

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

git remote remove B
git branch -D new_b_root b

The final graph will have a structure like this:

最终的图形将具有如下结构:

enter image description here

在此处输入图片说明