Fusionner, mettre à jour et extraire des branches Git sans utiliser d'extractions

629

Je travaille sur un projet qui a 2 branches, A et B. Je travaille généralement sur la branche A et je fusionne des éléments de la branche B. Pour la fusion, je fais généralement:

git merge origin/branchB

Cependant, je voudrais également conserver une copie locale de la branche B, car je peux parfois consulter la branche sans d'abord fusionner avec ma branche A. Pour cela, je ferais:

git checkout branchB
git pull
git checkout branchA

Existe-t-il un moyen de faire ce qui précède en une seule commande, et sans avoir à changer de branche d'avant en arrière? Dois-je utiliser git update-refpour cela? Comment?

Charles
la source
1
La réponse de Jakub à la première question liée explique pourquoi cela est en général impossible. Une autre explication (a posteriori) est que vous ne pouvez pas fusionner dans un référentiel nu, donc cela nécessite clairement l'arbre de travail.
Cascabel
3
@Eric: Les raisons courantes sont que les extractions prennent beaucoup de temps pour les grands référentiels et qu'elles mettent à jour les horodatages même si vous revenez à la même version, alors pensez que tout doit être reconstruit.
Cascabel
La deuxième question que j'ai liée concerne un cas inhabituel - les fusions qui pourraient être rapides, mais que le PO souhaitait fusionner en utilisant l' --no-ffoption, ce qui entraîne tout de même l'enregistrement d'une validation de fusion. Si cela vous intéresse, ma réponse montre comment vous pouvez y parvenir - pas aussi robuste que ma réponse affichée ici, mais les forces des deux pourraient certainement être combinées.
Cascabel

Réponses:

973

La réponse courte

Tant que vous effectuez une fusion à avance rapide , vous pouvez simplement utiliser

git fetch <remote> <sourceBranch>:<destinationBranch>

Exemples:

# Merge local branch foo into local branch master,
# without having to checkout master first.
# Here `.` means to use the local repository as the "remote":
git fetch . foo:master

# Merge remote branch origin/foo into local branch foo,
# without having to checkout foo first:
git fetch origin foo:foo

Bien que la réponse d'Amber fonctionne également dans les cas d'avance rapide, l'utilisation git fetchde cette manière est un peu plus sûre que le simple déplacement forcé de la référence de branche, car git fetchcela empêchera automatiquement les avances non rapides accidentelles tant que vous ne les utilisez pas +dans le refspec.

La réponse longue

Vous ne pouvez pas fusionner une branche B dans la branche A sans vérifier d'abord A si cela entraînerait une fusion sans avance rapide. En effet, une copie de travail est nécessaire pour résoudre tout conflit potentiel.

Cependant, dans le cas des fusions à avance rapide, cela est possible , car de telles fusions ne peuvent jamais entraîner de conflits, par définition. Pour ce faire sans vérifier d'abord une branche, vous pouvez utilisergit fetch avec une refspec.

Voici un exemple de mise à jour master(interdiction des modifications non rapides) si vous avez featureextrait une autre branche :

git fetch upstream master:master

Ce cas d'utilisation est si courant que vous voudrez probablement en faire un alias dans votre fichier de configuration git, comme celui-ci:

[alias]
    sync = !sh -c 'git checkout --quiet HEAD; git fetch upstream master:master; git checkout --quiet -'

Ce que cet alias fait est le suivant:

  1. git checkout HEAD: ceci met votre copie de travail dans un état de tête détachée. Ceci est utile si vous souhaitez mettre à jour masterpendant que vous l'avez récupéré. Je pense que c'était nécessaire parce que sinon la référence de branche pour masterne bougera pas, mais je ne me souviens pas si c'est vraiment tout de suite.

  2. git fetch upstream master:master: cela fait avancer rapidement votre section locale masterau même endroit que upstream/master.

  3. git checkout -vérifie votre branche précédemment extraite (c'est ce que -fait dans ce cas).

La syntaxe de git fetchfor (non-) fast-forward merges

Si vous voulez que la fetchcommande échoue si la mise à jour n'est pas en avance rapide, alors vous utilisez simplement une spécification du formulaire

git fetch <remote> <remoteBranch>:<localBranch>

Si vous souhaitez autoriser les mises à jour sans avance rapide, vous ajoutez alors un +au début de la spécification de référence:

git fetch <remote> +<remoteBranch>:<localBranch>

Notez que vous pouvez passer votre dépôt local en tant que paramètre "distant" en utilisant .:

git fetch . <sourceBranch>:<destinationBranch>

La documentation

De la git fetchdocumentation qui explique cette syntaxe (soulignement le mien):

<refspec>

Le format d'un <refspec>paramètre est un plus facultatif +, suivi de la référence source <src>, suivi de deux points :, suivi de la référence destination <dst>.

La référence distante qui correspond <src>est récupérée, et s'il <dst>ne s'agit pas d'une chaîne vide, la référence locale qui la correspond est transmise rapidement à l'aide<src> . Si le plus facultatif+est utilisé, la référence locale est mise à jour même si elle n'entraîne pas une mise à jour rapide.

Voir également

  1. Git checkout et fusionner sans toucher à l'arbre de travail

  2. Fusion sans changer le répertoire de travail

Communauté
la source
3
git checkout --quiet HEADest à git checkout --quiet --detachpartir de Git 1.7.5.
Rafa
6
J'ai trouvé que je devais faire: git fetch . origin/foo:foomettre à jour mon foo local vers mon origine locale / foo
weston
3
Y a-t-il une raison pour laquelle les parties «git checkout HEAD --quiet» et «git checkout --quiet -» sont incluses dans la réponse longue, mais pas dans la réponse courte? Je suppose que c'est parce que le script peut être exécuté lorsque vous avez extrait master, même si vous pouvez simplement faire un pull git?
Sean
8
pourquoi 'chercher' la commande pour faire 'fusionner' ici ... ça n'a tout simplement pas de sens; Si «pull» est «fetch» ​​suivi de «merge», il doit y avoir un équivalent «merge --ff-only» plus logique qui mettrait à jour «branch» depuis «origin / branch» localement, étant donné qu'un «fetch» ​​a déjà été exécuté.
Ed Randall
1
Merci pour l' git checkout -astuce! Aussi simple que cd -.
Steed
84

Non, il n'y en a pas. Une extraction de la branche cible est nécessaire pour vous permettre de résoudre les conflits, entre autres (si Git n'est pas en mesure de les fusionner automatiquement).

Cependant, si la fusion est rapide, vous n'avez pas besoin de vérifier la branche cible, car vous n'avez en fait pas besoin de fusionner - tout ce que vous avez à faire est de mettre à jour la branche pour pointer vers le nouvelle tête ref. Vous pouvez le faire avec git branch -f:

git branch -f branch-b branch-a

Mettra à jour branch-bpour pointer vers le chef de branch-a.

L' -foption signifie --force, ce qui signifie que vous devez être prudent lorsque vous l'utilisez.

Ne l'utilisez que si vous êtes absolument sûr que la fusion sera rapide.

ambre
la source
46
Faites juste très attention à ne pas le faire à moins que vous n'ayez absolument fait en sorte que la fusion soit une avance rapide! Je ne veux pas réaliser plus tard que vous avez égaré les commits.
Cascabel
@FuadSaud Pas exactement. git resetne fonctionne que sur la branche actuellement extraite.
Amber
9
Le même résultat (avance rapide) est obtenu par git fetch upstream branch-b:branch-b(tiré de cette réponse ).
Oliver
6
Pour développer le commentaire de @ Oliver, vous pouvez également le faire git fetch <remote> B:A, où B et A sont des branches complètement différentes, mais B peut être fusionné rapidement en A. Vous pouvez également passer votre référentiel local en tant que "distant" en utilisant .comme alias distant: git fetch . B:A.
8
La question du PO indique assez clairement que la fusion se fera en effet rapidement. Peu importe, branch -fpourrait être dangereux, comme vous le soulignez. Alors ne l'utilisez pas! Utilisez fetch origin branchB:branchB, qui échouera en toute sécurité si la fusion n'est pas rapide.
Bennett McElwee
30

Comme l'a dit Amber, les fusions rapides sont le seul cas dans lequel vous pourriez imaginer faire cela. Toute autre fusion doit théoriquement passer par la fusion à trois, appliquer des correctifs, résoudre les conflits - et cela signifie qu'il doit y avoir des fichiers.

Il se trouve que j'ai un script que j'utilise exactement pour cela: faire des fusions d'avance rapide sans toucher à l'arbre de travail (sauf si vous fusionnez dans HEAD). Il est un peu long, car il est au moins un peu robuste - il vérifie que la fusion serait une avance rapide, puis l'exécute sans vérifier la branche, mais produisant les mêmes résultats que si vous l'aviez - vous voyez le diff --statrésumé des modifications, et l'entrée dans le reflog est exactement comme une fusion d'avance rapide, au lieu de la «réinitialisation» que vous obtenez si vous utilisez branch -f. Si vous nommez git-merge-ffet déposez - le dans votre répertoire bin, vous pouvez l' appeler comme une commande git: git merge-ff.

#!/bin/bash

_usage() {
    echo "Usage: git merge-ff <branch> <committish-to-merge>" 1>&2
    exit 1
}

_merge_ff() {
    branch="$1"
    commit="$2"

    branch_orig_hash="$(git show-ref -s --verify refs/heads/$branch 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown branch $branch" 1>&2
        _usage
    fi

    commit_orig_hash="$(git rev-parse --verify $commit 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown revision $commit" 1>&2
        _usage
    fi

    if [ "$(git symbolic-ref HEAD)" = "refs/heads/$branch" ]; then
        git merge $quiet --ff-only "$commit"
    else
        if [ "$(git merge-base $branch_orig_hash $commit_orig_hash)" != "$branch_orig_hash" ]; then
            echo "Error: merging $commit into $branch would not be a fast-forward" 1>&2
            exit 1
        fi
        echo "Updating ${branch_orig_hash:0:7}..${commit_orig_hash:0:7}"
        if git update-ref -m "merge $commit: Fast forward" "refs/heads/$branch" "$commit_orig_hash" "$branch_orig_hash"; then
            if [ -z $quiet ]; then
                echo "Fast forward"
                git diff --stat "$branch@{1}" "$branch"
            fi
        else
            echo "Error: fast forward using update-ref failed" 1>&2
        fi
    fi
}

while getopts "q" opt; do
    case $opt in
        q ) quiet="-q";;
        * ) ;;
    esac
done
shift $((OPTIND-1))

case $# in
    2 ) _merge_ff "$1" "$2";;
    * ) _usage
esac

PS Si quelqu'un voit des problèmes avec ce script, veuillez commenter! C'était un travail d'écriture et d'oubli, mais je serais heureux de l'améliorer.

Cascabel
la source
vous pourriez être intéressé par stackoverflow.com/a/5148202/717355 pour comparaison
Philip Oakley
@PhilipOakley D'un coup d'œil, cela au fond fait exactement la même chose que le mien. Mais le mien n'est pas codé en dur pour une seule paire de branches, il a beaucoup plus de gestion des erreurs, il simule la sortie de git-merge, et il fait ce que vous voulez dire si vous l'appelez alors que vous êtes réellement sur la branche.
Cascabel
J'ai le vôtre dans mon répertoire \ bin ;-) Je venais de l'oublier et je regardais autour de moi, j'ai vu ce script et cela m'a incité à me rappeler! Ma copie a ce lien et # or git branch -f localbranch remote/remotebranchpour me rappeler la source et les options. A donné à votre commentaire sur l'autre lien un +1.
Philip Oakley
3
+1, peut également être défini par défaut "$branch@{u}"comme le commit à fusionner pour obtenir la branche en amont (à partir de kernel.org/pub/software/scm/git/docs/gitrevisions.html )
orip
Merci! Y a-t-il une chance que vous puissiez jeter un simple exemple d'utilisation dans la réponse?
nmr
20

Vous ne pouvez le faire que si la fusion est une avance rapide. Si ce n'est pas le cas, alors git doit faire extraire les fichiers pour pouvoir les fusionner!

Pour le faire pour une avance rapide uniquement :

git fetch <branch that would be pulled for branchB>
git update-ref -m "merge <commit>: Fast forward" refs/heads/<branch> <commit>

<commit>est le commit récupéré, celui vers lequel vous souhaitez effectuer une avance rapide. C'est essentiellement comme utiliser git branch -fpour déplacer la branche, sauf qu'elle l'enregistre également dans le reflog comme si vous aviez réellement fait la fusion.

S'il vous plaît, s'il vous plaît, s'il vous plaît ne faites pas cela pour quelque chose qui n'est pas une avance rapide, ou vous allez simplement réinitialiser votre branche à l'autre commit. (Pour vérifier, voyez si git merge-base <branch> <commit>donne le SHA1 de la branche.)

Cascabel
la source
4
Existe-t-il un moyen de le faire échouer s'il ne peut pas avancer rapidement?
gman
2
@gman vous pouvez utiliser git merge-base --is-ancestor <A> <B>. "B" étant la chose qui doit être fusionnée en "A". Par exemple, A = master et B = develop, en veillant à ce que develop soit rapidement transmissible en master. Remarque: il existe avec 0 s'il n'est pas compatible, existe avec 1 s'il l'est.
eddiemoya
1
Non documenté dans git-scm, mais il est sur kernal.org. kernel.org/pub/software/scm/git/docs/git-merge-base.html
eddiemoya
J'ai fait un petit résumé pour résumer ce dont je parle (je ne l'ai pas testé, mais vous devriez y arriver principalement). gist.github.com/eddiemoya/ad4285b2d8a6bdabf432 ---- en guise de note, j'avais la plupart de ces informations à portée de main, j'ai un script qui vérifie ff et sinon vous permet de rebaser la branche souhaitée en premier - puis ff fusionne - le tout sans rien vérifier.
eddiemoya
12

Dans votre cas, vous pouvez utiliser

git fetch origin branchB:branchB

qui fait ce que vous voulez (en supposant que la fusion soit rapide). Si la branche ne peut pas être mise à jour car elle nécessite une fusion sans avance rapide, cela échoue en toute sécurité avec un message.

Cette forme de récupération propose également des options plus utiles:

git fetch <remote> <sourceBranch>:<destinationBranch>

Notez qu'il <remote> peut s'agir d'un référentiel local et <sourceBranch>peut être une branche de suivi. Vous pouvez donc mettre à jour une branche locale, même si elle n'est pas extraite, sans accéder au réseau .

Actuellement, mon accès au serveur en amont se fait via un VPN lent, donc je me connecte périodiquement, git fetchpour mettre à jour toutes les télécommandes, puis me déconnecter. Si, par exemple, le maître distant a changé, je peux le faire

git fetch . remotes/origin/master:master

pour mettre à jour mon maître local en toute sécurité, même si j'ai actuellement une autre succursale vérifiée. Aucun accès réseau requis.

Bennett McElwee
la source
11

Une autre façon, certes assez brute, est de simplement recréer la branche:

git fetch remote
git branch -f localbranch remote/remotebranch

Cela supprime la branche locale obsolète et en recrée une avec le même nom, alors utilisez-la avec précaution ...

kkoehne
la source
Je viens de voir maintenant que la réponse originale mentionne déjà la branche -f ... Pourtant, je ne vois aucun avantage à avoir la fusion dans le reflog pour le cas d'utilisation décrit à l'origine.
kkoehne
7

Vous pouvez cloner le référentiel et faire la fusion dans le nouveau référentiel. Sur le même système de fichiers, cela liera en dur plutôt que de copier la plupart des données. Terminez en tirant les résultats dans le référentiel d'origine.

wnoise
la source
4

Entrez git-forward-merge :

Sans avoir à extraire la destination, git-forward-merge <source> <destination>fusionne la source dans la branche de destination.

https://github.com/schuyler1d/git-forward-merge

Fonctionne uniquement pour les fusions automatiques, s'il y a des conflits, vous devez utiliser la fusion régulière.

lkraider
la source
2
Je pense que c'est mieux que git fetch <remote> <source>: <destination>, car une avance rapide est une opération de fusion et non de récupération et il est plus facile d'écrire. Mauvaise chose cependant, ce n'est pas dans le git par défaut.
Binarian
4

Dans de nombreux cas (comme la fusion), vous pouvez simplement utiliser la branche distante sans avoir à mettre à jour la branche de suivi locale. L'ajout d'un message dans le reflog sonne comme une surpuissance et l'empêchera d'être plus rapide. Pour faciliter la récupération, ajoutez ce qui suit dans votre configuration git

[core]
    logallrefupdates=true

Tapez ensuite

git reflog show mybranch

pour voir l'historique récent de votre succursale

Casebash
la source
Je pense que la section ne doit [core]pas être [user]? (et il est par défaut
activé
3

J'ai écrit une fonction shell pour un cas d'utilisation similaire que je rencontre quotidiennement sur des projets. Il s'agit essentiellement d'un raccourci pour garder les succursales locales à jour avec une branche commune comme développer avant d'ouvrir un PR, etc.

Publier ceci même si vous ne voulez pas l'utiliser checkout, au cas où d'autres ne dérangeraient pas cette contrainte.

glmh("git pull and merge here") sera automatiquement checkout branchB, pullle plus récent, re checkout branchAet merge branchB.

Ne répond pas à la nécessité de conserver une copie locale de branchA, mais pourrait facilement être modifié pour ce faire en ajoutant une étape avant d'extraire branchB. Quelque chose comme...

git branch ${branchA}-no-branchB ${branchA}

Pour les fusions rapides simples, ceci passe à l'invite de message de validation.

Pour les fusions non rapides, cela place votre branche dans l'état de résolution de conflit (vous devrez probablement intervenir).

Pour configurer, ajouter à .bashrcou .zshrc, etc.:

glmh() {
    branchB=$1
    [ $# -eq 0 ] && { branchB="develop" }
    branchA="$(git branch | grep '*' | sed 's/* //g')"
    git checkout ${branchB} && git pull
    git checkout ${branchA} && git merge ${branchB} 
}

Usage:

# No argument given, will assume "develop"
> glmh

# Pass an argument to pull and merge a specific branch
> glmh your-other-branch

Remarque: ce n'est pas assez robuste pour transférer les arguments au-delà du nom de la branche versgit merge

rkd
la source
2

Une autre façon de le faire efficacement est:

git fetch
git branch -d branchB
git branch -t branchB origin/branchB

Comme il s'agit d'un minuscule -d, il ne le supprimera que si les données existent toujours quelque part. C'est similaire à la réponse de @ kkoehne, sauf qu'elle ne force pas. À cause de -tcela, il réinstalle la télécommande.

J'avais un besoin légèrement différent de celui d'OP, qui était de créer une nouvelle branche de fonctionnalité develop(ou master), après avoir fusionné une demande d'extraction. Cela peut être accompli dans une ligne sans force, mais cela ne met pas à jour la developbranche locale . Il s'agit simplement de vérifier une nouvelle branche et de la baser sur origin/develop:

git checkout -b new-feature origin/develop
Benjamin Atkin
la source
1

juste pour tirer le maître sans vérifier le maître que j'utilise

git fetch origin master:master

Sodhi saab
la source
1

Il est absolument possible de faire n'importe quelle fusion, même les fusions non rapides, sans git checkout. La worktreeréponse de @grego est un bon indice. Pour développer cela:

cd local_repo
git worktree add _master_wt master
cd _master_wt
git pull origin master:master
git merge --no-ff -m "merging workbranch" my_work_branch
cd ..
git worktree remove _master_wt

Vous avez maintenant fusionné la branche de travail locale avec la masterbranche locale sans changer de caisse.

zanerock
la source
1

Si vous souhaitez conserver le même arbre que l'une des branches que vous souhaitez fusionner (c'est-à-dire pas une véritable "fusion"), vous pouvez le faire comme ceci.

# Check if you can fast-forward
if git merge-base --is-ancestor a b; then
    git update-ref refs/heads/a refs/heads/b
    exit
fi

# Else, create a "merge" commit
commit="$(git commit-tree -p a -p b -m "merge b into a" "$(git show -s --pretty=format:%T b)")"
# And update the branch to point to that commit
git update-ref refs/heads/a "$commit"
phkb
la source
1
git worktree add [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]

Vous pouvez essayer git worktreed'avoir deux branches ouvertes côte à côte, cela semble être ce que vous voulez, mais très différent de certaines des autres réponses que j'ai vues ici.

De cette façon, vous pouvez avoir deux branches distinctes dans le même référentiel git, vous n'avez donc qu'à récupérer une fois pour obtenir des mises à jour dans les deux arborescences de travail (plutôt que d'avoir à git cloner deux fois et git tirer sur chacune)

Worktree créera un nouveau répertoire de travail pour votre code où vous pouvez faire extraire une branche différente simultanément au lieu de permuter les branches en place.

Lorsque vous souhaitez l'enlever, vous pouvez nettoyer avec

git worktree remove [-f] <worktree>
grego
la source