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

Posted

技术标签:

【中文标题】使用 git-subtree 添加远程仓库的子目录【英文标题】:Add subdirectory of remote repo with git-subtree 【发布时间】:2014-07-19 04:48:51 【问题描述】:

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

假设我有这个 ma​​in 存储库:

/
    dir1
    dir2

还有这个存储库:

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

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

/
    dir1
        some-file
    dir2

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

之所以使用git-subtree,是为了以后可以同步两个仓库。

【问题讨论】:

【参考方案1】:

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

对于这些示例,我将考虑将 contrib/completion/ 的 https://github.com/git/git.git 的四个文件合并到本地存储库的 third_party/git_completion/ 中。

1。混帐差异 | git 申请

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

# 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

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

git-merge-subpath() 
    local SQUASH
    if [[ $1 == "--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="$1" 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 $2')
    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

像这样使用它:

# 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 读取树

如果您永远不会对合并的文件进行本地更改,即您乐于始终使用上游的最新版本覆盖本地子目录,那么类似但更简单的方法是使用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

我找到了一个blog post,它声称可以使用类似的技术进行合并(不覆盖),但我尝试时却没有成功。

3。 git 子树

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

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

# 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

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

使用--rejoin(请参阅https://***.com/a/16139361/691281)可能会更快地进行后续拆分 - 我没有对此进行测试。

4。整个仓库 git 子树

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

或者,如果您希望避免重复存储库 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

另见:

http://blogs.atlassian.com/2013/05/alternatives-to-git-submodule-git-subtree/ https://hpc.uni.lu/blog/2014/understanding-git-subtree/ http://getlevelten.com/blog/tom-mccracken/smarter-drupal-projects-projects-management-git-subtree

5。整个 repo git 子模块

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

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

【讨论】:

不错的细节,尽管尝试将这些解决方案应用于 OP 的问题有点令人困惑,因为远程路径和本地路径在您的所有示例中都不匹配。 IE。很难看到库 repo 中的 some_file 来自哪里以及如何将其放入主 repo 的 dir1 中。 从另一个仓库合并子目录的任何方法是否支持稍后将更改推回原始仓库?我预计 3: "git subtree" 可以做到,但jrsmith3.github.io/… 说,“git-subtree 将导致目标 repo 的最终状态使得对目标 repo 的更改不会被推回上游来源。” “它非常慢” - 这是因为你有太多的提交(39000)吗?并且 git 必须遍历整个提交历史来拆分属于子树的那些? @MichaelFreidgeim,好问题。恐怕我没有测试将更改推回上游,但如果有一个适用于此的解决方案,那就太好了。 @BruceSun,当然 - 对于较小的存储库来说会更快。但是39000并不是很多; Linux 内核正在接近一百万次提交 :)【参考方案2】:

我可以通过在 read-tree 命令中添加 :dirname 来完成类似的操作。 (请注意,这周我实际上只是想自己学习 git 和 git-subtrees,并尝试设置一个类似于我使用 svn:externals 在 subversion 中进行项目的环境——我的意思是可能会有更好的或者比我在这里显示的命令更简单的方法...)

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

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

【讨论】:

好的,我刚刚读到 git-subtree 与 subtree-merge 不同(我认为这就是我上面展示的内容)。同样的事情可能仍然有效(在适当的位置添加“:libdir”),但我不能肯定地说。我现在正在阅读子树文档。

以上是关于使用 git-subtree 添加远程仓库的子目录的主要内容,如果未能解决你的问题,请参考以下文章

Git本地仓库远程仓库操作和分支操作命令

git添加本地仓库与远程仓库连接

使用Git把项目添加到远程仓库

Git如何克隆远程仓库

git添加远程库基本操作

Git-- 远程仓库的使用