使用 git-subtree 添加远程仓库的子目录

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23937436/
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-19 10:03:44  来源:igfitidea点击:

Add subdirectory of remote repo with git-subtree

gitgit-subtree

提问by Yogu

Is there a way to add a subdirectory of a remote repository into a subdirectory of my repository with git-subtree?

有没有办法使用 git-subtree 将远程存储库的子目录添加到我的存储库的子目录中?

Suppose I have this mainrepository:

假设我有这个存储库:

/
    dir1
    dir2

And this libraryrepository:

这个存储库:

/
    libdir
        some-file
    some-file-to-be-ignored

I want to import library/libdir into main/dir1 so that it looks like this:

我想将/libdir 导入/dir1,使其看起来像这样:

/
    dir1
        some-file
    dir2

Using git-subtree, I can specify to import into dir1with the --prefixargument, but can I also specify to only take the contents of a specific directory in the subtree?

使用 git-subtree,我可以指定使用参数导入到dir1--prefix,但我也可以指定只获取子树中特定目录的内容吗?

The reason for using git-subtree is that I can later synchronize the two repositories.

使用 git-subtree 的原因是我以后可以同步两个存储库。

回答by John Mellor

I've been experimenting with this, and found some partial solutions, though none are quite perfect.

我一直在试验这个,并找到了一些部分解决方案,尽管没有一个是完美的。

For these examples, I'll consider merging the four files from contrib/completion/of https://github.com/git/git.gitinto third_party/git_completion/of the local repository.

在这些例子中,我会考虑从四个文件合并contrib/completion/https://github.com/git/git.gitthird_party/git_completion/本地存储库中。

1. git diff | git apply

1. git 差异 | 申请

This is probably the best way I've found. I only tested one-way merging; I haven't tried sending changes back to the upstream repository.

这可能是我找到的最好的方法。我只测试了单向合并;我还没有尝试将更改发送回上游存储库。

# Do this the first time:
$ git remote add -f -t master --no-tags gitgit https://github.com/git/git.git
# The next line is optional. Without it, the upstream commits get
# squashed; with it they will be included in your local history.
$ git merge -s ours --no-commit gitgit/master
# The trailing slash is important here!
$ git read-tree --prefix=third_party/git-completion/ -u gitgit/master:contrib/completion
$ git commit

# In future, you can merge in additional changes as follows:
# The next line is optional. Without it, the upstream commits get
# squashed; with it they will be included in your local history.
$ git merge -s ours --no-commit gitgit/master
# Replace the SHA1 below with the commit hash that you most recently
# merged in using this technique (i.e. the most recent commit on
# gitgit/master at the time).
$ git diff --color=never 53e53c7c81ce2c7c4cd45f95bc095b274cb28b76:contrib/completion gitgit/master:contrib/completion | git apply -3 --directory=third_party/git-completion
# Now fix any conflicts if you'd modified third_party/git-completion.
$ git commit

Since it's awkward having to remember the most recent commit SHA1 that you merged from the upstream repository, I've written this Bash function which does all the hard work for you (grabbing it from git log):

由于必须记住您从上游存储库合并的最新提交 SHA1 很尴尬,因此我编写了这个 Bash 函数来为您完成所有艰苦的工作(从 gi​​t log 中获取):

git-merge-subpath() {
    local SQUASH
    if [[  == "--squash" ]]; then
        SQUASH=1
        shift
    fi
    if (( $# != 3 )); then
        local PARAMS="[--squash] SOURCE_COMMIT SOURCE_PREFIX DEST_PREFIX"
        echo "USAGE: ${FUNCNAME[0]} $PARAMS"
        return 1
    fi

    # Friendly parameter names; strip any trailing slashes from prefixes.
    local SOURCE_COMMIT="" SOURCE_PREFIX="${2%/}" DEST_PREFIX="${3%/}"

    local SOURCE_SHA1
    SOURCE_SHA1=$(git rev-parse --verify "$SOURCE_COMMIT^{commit}") || return 1

    local OLD_SHA1
    local GIT_ROOT=$(git rev-parse --show-toplevel)
    if [[ -n "$(ls -A "$GIT_ROOT/$DEST_PREFIX" 2> /dev/null)" ]]; then
        # OLD_SHA1 will remain empty if there is no match.
        local RE="^${FUNCNAME[0]}: [0-9a-f]{40} $SOURCE_PREFIX $DEST_PREFIX$"
        OLD_SHA1=$(git log -1 --format=%b -E --grep="$RE" \
                   | grep --color=never -E "$RE" | tail -1 | awk '{print }')
    fi

    local OLD_TREEISH
    if [[ -n $OLD_SHA1 ]]; then
        OLD_TREEISH="$OLD_SHA1:$SOURCE_PREFIX"
    else
        # This is the first time git-merge-subpath is run, so diff against the
        # empty commit instead of the last commit created by git-merge-subpath.
        OLD_TREEISH=$(git hash-object -t tree /dev/null)
    fi &&

    if [[ -z $SQUASH ]]; then
        git merge -s ours --no-commit "$SOURCE_COMMIT"
    fi &&

    git diff --color=never "$OLD_TREEISH" "$SOURCE_COMMIT:$SOURCE_PREFIX" \
        | git apply -3 --directory="$DEST_PREFIX" || git mergetool

    if (( $? == 1 )); then
        echo "Uh-oh! Try cleaning up with |git reset --merge|."
    else
        git commit -em "Merge $SOURCE_COMMIT:$SOURCE_PREFIX/ to $DEST_PREFIX/

# Feel free to edit the title and body above, but make sure to keep the
# ${FUNCNAME[0]}: line below intact, so ${FUNCNAME[0]} can find it
# again when grepping git log.
${FUNCNAME[0]}: $SOURCE_SHA1 $SOURCE_PREFIX $DEST_PREFIX"
    fi
}

Use it like this:

像这样使用它:

# Do this the first time:
$ git remote add -f -t master --no-tags gitgit https://github.com/git/git.git
$ git-merge-subpath gitgit/master contrib/completion third_party/git-completion

# In future, you can merge in additional changes as follows:
$ git fetch gitgit
$ git-merge-subpath gitgit/master contrib/completion third_party/git-completion
# Now fix any conflicts if you'd modified third_party/git-completion.

2. git read-tree

2. git 读取树

If you're never going to make local changes to the merged in files, i.e. you're happy to always overwrite the local subdirectory with the latest version from upstream, then a similar but simpler approach is to use git read-tree:

如果您永远不会对合并的文件进行本地更改,即您很乐意始终使用上游的最新版本覆盖本地子目录,那么类似但更简单的方法是使用git read-tree

# Do this the first time:
$ git remote add -f -t master --no-tags gitgit https://github.com/git/git.git
# The next line is optional. Without it, the upstream commits get
# squashed; with it they will be included in your local history.
$ git merge -s ours --no-commit gitgit/master
$ git read-tree --prefix=third_party/git-completion/ -u gitgit/master:contrib/completion
$ git commit

# In future, you can *overwrite* with the latest changes as follows:
# As above, the next line is optional (affects squashing).
$ git merge -s ours --no-commit gitgit/master
$ git rm -rf third_party/git-completion
$ git read-tree --prefix=third_party/git-completion/ -u gitgit/master:contrib/completion
$ git commit

I found a blog postthat claimed to be able to merge (without overwriting) using a similar technique, but it didn't work when I tried it.

我发现了一篇博客文章,声称可以使用类似的技术进行合并(无需覆盖),但在我尝试时它不起作用。

3. git subtree

3. git子树

I did actually find a solution that uses git subtree, thanks to http://jrsmith3.github.io/merging-a-subdirectory-from-another-repo-via-git-subtree.html, but it's incredibly slow (each git subtree splitcommand below takes me 9 minutes for a 28 MB repo with 39000 commits on a dual Xeon X5675, whereas the other solutions I found take less than a second).

git subtree由于http://jrsmith3.github.io/merging-a-subdirectory-from-another-repo-via-git-subtree.html,我确实找到了一个使用 的解决方案,但它非常慢(git subtree split下面的每个命令都需要在双 Xeon X5675 上进行 39000 次提交的 28 MB 存储库需要 9 分钟,而我发现的其他解决方案只需不到一秒钟)。

If you can live with the slowness, it should be workable:

如果你能忍受缓慢,它应该是可行的:

# Do this the first time:
$ git remote add -f -t master --no-tags gitgit https://github.com/git/git.git
$ git checkout gitgit/master
$ git subtree split -P contrib/completion -b temporary-split-branch
$ git checkout master
$ git subtree add --squash -P third_party/git-completion temporary-split-branch
$ git branch -D temporary-split-branch

# In future, you can merge in additional changes as follows:
$ git checkout gitgit/master
$ git subtree split -P contrib/completion -b temporary-split-branch
$ git checkout master
$ git subtree merge --squash -P third_party/git-completion temporary-split-branch
# Now fix any conflicts if you'd modified third_party/git-completion.
$ git branch -D temporary-split-branch

Note that I pass in --squashto avoid polluting the local repository with lots of commits, but you can remove --squashif you'd prefer to preserve the commit history.

请注意,我传入是--squash为了避免大量提交污染本地存储库,但是--squash如果您希望保留提交历史记录,则可以将其删除。

It's possible that subsequent splits can be made faster using --rejoin(see https://stackoverflow.com/a/16139361/691281) - I didn't test that.

使用--rejoin(参见https://stackoverflow.com/a/16139361/691281)可以更快地进行后续拆分- 我没有测试过。

4. Whole repo git subtree

4.整个repo git子树

The OP clearly stated that they want to merge a subdirectory of an upstream repository into a subdirectory of the local repository. If however instead you want to merge an entire upstream repository into a subdirectory of your local repository, then there's a simpler, cleaner, and better supported alternative:

OP 明确表示他们希望将上游存储库的子目录合并到本地存储库的子目录中。但是,如果您想将整个上游存储库合并到本地存储库的子目录中,那么有一个更简单、更干净且支持更好的替代方案:

# Do this the first time:
$ git subtree add --squash --prefix=third_party/git https://github.com/git/git.git master

# In future, you can merge in additional changes as follows:
$ git subtree pull --squash --prefix=third_party/git https://github.com/git/git.git master

Or if you prefer to avoid repeating the repository URL, then you can add it as a remote:

或者,如果您不想重复存储库 URL,则可以将其添加为远程:

# Do this the first time:
$ git remote add -f -t master --no-tags gitgit https://github.com/git/git.git
$ git subtree add --squash --prefix=third_party/git gitgit/master

# In future, you can merge in additional changes as follows:
$ git subtree pull --squash --prefix=third_party/git gitgit/master

# And you can push changes back upstream as follows:
$ git subtree push --prefix=third_party/git gitgit/master
# Or possibly (not sure what the difference is):
$ git subtree push --squash --prefix=third_party/git gitgit/master

See also:

也可以看看:

5. Whole repo git submodule

5.整个repo git子模块

A related technique is git submodules, but they come with annoying caveats (for example people who clone your repository won't clone the submodules unless they call git clone --recursive), so I didn't investigate whether they can support subpaths.

一个相关的技术是git submodules,但它们带有令人讨厌的警告(例如,克隆您的存储库的人不会克隆子模块,除非他们调用git clone --recursive),所以我没有调查他们是否可以支持子路径。

Edit: git-subtrac(from the author of the earlier git-subtree) seems to solve some of the problems with git submodules. So this might be a good option for merging an entire upstream repository into a subdirectory, but it still doesn't appear to support including only a subdirectory of the upstream repository.

编辑:git-subtrac(来自早期 git-subtree 的作者)似乎解决了 git 子模块的一些问题。因此,这对于将整个上游存储库合并到一个子目录中可能是一个不错的选择,但它似乎仍然不支持仅包含上游存储库的子目录。

回答by DotNetSparky

I was able to do something like this by adding :dirnameto the read-tree command. (note that I'm actually just trying to learn git and git-subtrees myself this week, and trying to setup an environment similar to how I had my projects in subversion using svn:externals -- my point being that there might be a better or easier way than the commands I'm showing here...)

我可以通过添加:dirname到 read-tree 命令来做这样的事情。(请注意,这周我实际上只是在尝试自己学习 git 和 git-subtrees,并尝试设置一个类似于我使用 svn:externals 将项目置于 subversion 中的环境——我的观点是可能有更好的或者比我在这里展示的命令更简单的方法......)

So for example, using your example structure above:

例如,使用上面的示例结构:

git remote add library_remote _URL_TO_LIBRARY_REPO_
git fetch library_remote
git checkout -b library_branch library_remote/master
git checkout master
git read-tree --prefix=dir1 -u library_branch:libdir