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

问题描述 投票:37回答:2

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

假设我有这个存储库:

/
    dir1
    dir2

而这个存储库:

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

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

/
    dir1
        some-file
    dir2

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

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

git git-subtree
2个回答
45
投票

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

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

1. git diff | 适用

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

# 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日志中获取它):

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

我发现了一篇声称能够使用类似技术合并(不覆盖)的博客文章 ,但是当我尝试它时它没有用。

3. git子树

我确实找到了一个使用git subtree的解决方案,感谢http://jrsmith3.github.io/merging-a-subdirectory-from-another-repo-via-git-subtree.html ,但它非常慢(每个git subtree split下面的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子模块 ,但它们带来了烦人的警告(例如克隆你的存储库的人不会克隆子模块,除非他们调用git clone --recursive ),所以我没有调查它们是否可以支持子路径。


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
© www.soinside.com 2019 - 2024. All rights reserved.