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

时间:2014-05-29 15:49:19

标签: git git-subtree

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

假设我有存储库:

/
    dir1
    dir2

存储库:

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

我想将 / libdir导入 main / dir1,以便它看起来像这样:

/
    dir1
        some-file
    dir2

使用git-subtree,我可以指定使用--prefix参数导入 dir1 ,但是我是否还可以指定仅获取子树中特定目录的内容? / p>

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

2 个答案:

答案 0 :(得分:44)

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

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

1。 git diff | git apply

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

# 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函数,它可以为您完成所有的工作(从git 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 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

我发现blog post声称能够使用类似的技术合并(不会覆盖),但在我尝试时它并没有起作用。

3。 git子树

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

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

# 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://stackoverflow.com/a/16139361/691281) - 我没有对此进行测试。

4。整个repo 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

另见:

5。整个repo git子模块

相关技术是git submodules,但是它们带有恼人的警告(例如,克隆你的存储库的人不会克隆子模块,除非他们调用git clone --recursive),所以我没有&#39 ; t调查他们是否可以支持子路径。

答案 1 :(得分:1)

通过向read-tree命令添加:dirname,我能够做到这一点。 (请注意,本周我实际上只是想尝试学习git和git-subtree,并尝试使用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