web-dev-qa-db-fra.com

Ajouter un sous-répertoire du dépôt distant avec git-subtree

Existe-t-il un moyen d'ajouter un sous-répertoire d'un référentiel distant dans un sous-répertoire de mon référentiel avec git-subtree?

Supposons que j'ai ce dépôt main:

/
    dir1
    dir2

Et ce bibliothèque référentiel:

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

Je veux importer bibliothèque/libdir dans principal/dir1 pour qu'il ressemble à ceci:

/
    dir1
        some-file
    dir2

En utilisant git-subtree, je peux spécifier d'importer dans dir1 avec le --prefix argument, mais puis-je également spécifier de ne prendre que le contenu d'un répertoire spécifique dans le sous-arbre?

La raison d'utiliser git-subtree est que je peux plus tard synchroniser les deux référentiels.

38
Yogu

J'ai expérimenté cela et trouvé des solutions partielles, bien qu'aucune ne soit tout à fait parfaite.

Pour ces exemples, je vais envisager de fusionner les quatre fichiers de contrib/completion/ de https://github.com/git/git.git dans third_party/git_completion/ du référentiel local.

1. git diff | git appliquer

C'est probablement le meilleur moyen que j'ai trouvé. J'ai seulement testé la fusion unidirectionnelle; Je n'ai pas essayé de renvoyer les modifications au référentiel en amont.

# 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

Comme il est difficile de se souvenir du dernier SHA1 de validation que vous avez fusionné à partir du référentiel en amont, j'ai écrit cette fonction Bash qui fait tout le travail pour vous (en la récupérant dans le journal 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
}

Utilisez-le comme ceci:

# 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

Si vous n'allez jamais apporter de modifications locales aux fichiers fusionnés, c'est-à-dire que vous êtes toujours heureux de remplacer le sous-répertoire local avec la dernière version en amont, alors une approche similaire mais plus simple consiste à utiliser 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

J'ai trouvé un article de blog qui prétendait pouvoir fusionner (sans écraser) en utilisant une technique similaire, mais cela n'a pas fonctionné lorsque je l'ai essayé.

3. sous-arbre git

J'ai trouvé une solution qui utilise git subtree, grâce à http://jrsmith3.github.io/merging-a-subdirectory-from-another-repo-via-git-subtree.html , mais c'est incroyablement lent (chaque git subtree split la commande ci-dessous me prend 9 minutes pour un dépôt de 28 Mo avec 39000 commits sur un double Xeon X5675, alors que les autres solutions que j'ai trouvées prennent moins d'une seconde).

Si vous pouvez vivre avec la lenteur, cela devrait être réalisable:

# 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

Notez que je passe --squash pour éviter de polluer le référentiel local avec de nombreuses validations, mais vous pouvez supprimer --squash si vous préférez conserver l'historique des validations.

Il est possible que les divisions ultérieures puissent être effectuées plus rapidement en utilisant --rejoin (voir https://stackoverflow.com/a/16139361/691281 ) - Je n'ai pas testé cela.

4. Sous-arbre entier repo git

Le PO a clairement indiqué qu'il souhaitait fusionner un sous-répertoire d'un référentiel en amont dans un sous-répertoire du référentiel local. Si toutefois vous souhaitez fusionner tout un référentiel en amont dans un sous-répertoire de votre référentiel local, il existe une alternative plus simple, plus propre et mieux prise en charge:

# 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

Ou si vous préférez éviter de répéter l'URL du référentiel, vous pouvez l'ajouter en tant que télécommande:

# 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

Voir également:

5. Sous-module entier repo git

Une technique connexe est git submodules , mais ils comportent des mises en garde ennuyeuses (par exemple, les personnes qui clonent votre référentiel ne cloneront pas les sous-modules à moins d'appeler git clone --recursive), donc je n'ai pas cherché à savoir s'ils pouvaient prendre en charge les sous-chemins.

Edit: git-subtrac (de l'auteur du git-subtree précédent) semble résoudre certains des problèmes avec les sous-modules git. Donc, cela pourrait être une bonne option pour fusionner un référentiel en amont entier dans un sous-répertoire, mais il ne semble toujours pas prendre en charge l'inclusion uniquement d'un sous-répertoire du référentiel en amont.

48
John Mellor

J'ai pu faire quelque chose comme ça en ajoutant :dirname à la commande read-tree. (Notez que j'essaie juste d'apprendre git et git-subtrees moi-même cette semaine, et d'essayer de configurer un environnement similaire à la façon dont j'avais mes projets dans Subversion en utilisant svn: externals - mon point de vue étant qu'il pourrait y avoir une meilleure ou plus simple que les commandes que je montre ici ...)

Ainsi, par exemple, en utilisant votre exemple de structure ci-dessus:

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
1
DotNetSparky