Unisci, aggiorna e tira i rami Git senza usare i checkout


629

Lavoro su un progetto che ha 2 rami, A e B. In genere lavoro sul ramo A e unisco elementi dal ramo B. Per la fusione, in genere farei:

git merge origin/branchB

Tuttavia, vorrei anche conservare una copia locale della filiale B, poiché occasionalmente potrei controllare la filiale senza prima fondermi con la mia filiale A. Per questo, farei:

git checkout branchB
git pull
git checkout branchA

C'è un modo per eseguire quanto sopra in un comando e senza dover cambiare il ramo avanti e indietro? Dovrei usarlo git update-refper quello? Come?



1
La risposta di Jakub alla prima domanda collegata spiega perché questo è generalmente impossibile. Un'altra spiegazione (a posteriori) è che non è possibile unire in un repository nudo, quindi chiaramente richiede l'albero di lavoro.
Cascabel,

3
@Eric: I motivi comuni sono che i checkout richiedono molto tempo per repository di grandi dimensioni e che aggiornano i timestamp anche se si ritorna alla stessa versione, quindi pensa che tutto debba essere ricostruito.
Cascabel,

La seconda domanda che ho sollevato riguarda un caso insolito: fusioni che potrebbero essere anticipate, ma che l'OP ha voluto unire usando l' --no-ffopzione, che fa comunque registrare un commit di unione. Se dovessi essere interessato a ciò, la mia risposta lì mostra come potresti farlo - non abbastanza robusto come la mia risposta postata qui, ma i punti di forza dei due potrebbero certamente essere combinati.
Cascabel,

Risposte:


973

La risposta breve

Finché stai eseguendo un'unione con avanzamento rapido , puoi semplicemente utilizzare

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

Esempi:

# 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

Mentre la risposta di Amber funzionerà anche in casi di avanzamento rapido, l'utilizzo git fetchin questo modo invece è un po 'più sicuro del semplice spostamento forzato del riferimento di diramazione, poiché git fetchpreviene automaticamente l'accelerazione accidentale non veloce finché non si utilizza +nel refspec.

La lunga risposta

Non è possibile unire un ramo B nel ramo A senza estrarre A per primo se si tradurrebbe in un'unione senza avanzamento rapido. Questo perché è necessaria una copia di lavoro per risolvere eventuali conflitti.

Tuttavia, nel caso di fusioni ad avanzamento rapido, ciò è possibile , poiché tali fusioni non possono mai tradursi in conflitti, per definizione. Per fare ciò senza prima verificare un ramo, puoi usarlo git fetchcon un refspec.

Ecco un esempio di aggiornamento master(che non consente modifiche non avanzate) se si dispone di un altro ramo featureestratto:

git fetch upstream master:master

Questo caso d'uso è così comune, che probabilmente vorrai crearne uno nel tuo file di configurazione git, come questo:

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

Ciò che fa questo alias è il seguente:

  1. git checkout HEAD: questo mette la tua copia di lavoro in uno stato distaccato. Questo è utile se vuoi aggiornare mastermentre ti capita di averlo estratto. Penso che fosse necessario farlo perché altrimenti il ​​riferimento al ramo per masternon si muoverà, ma non ricordo se è davvero giusto fuori dalla cima della mia testa.

  2. git fetch upstream master:master: questo fa avanzare rapidamente il tuo locale masternello stesso posto di upstream/master.

  3. git checkout -controlla il tuo ramo precedentemente estratto (ecco cosa -fa in questo caso).

La sintassi di git fetchfor (non) fast-forward si fonde

Se si desidera che il fetchcomando non riesca se l'aggiornamento non è un avanzamento rapido, è sufficiente utilizzare un refspec del modulo

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

Se si desidera consentire aggiornamenti non rapidi, si aggiunge a +alla parte anteriore del refspec:

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

Si noti che è possibile passare il repository locale come parametro "remoto" utilizzando .:

git fetch . <sourceBranch>:<destinationBranch>

La documentazione

Dalla git fetchdocumentazione che spiega questa sintassi (sottolineatura mia):

<refspec>

Il formato di un <refspec>parametro è un plus facoltativo +, seguito dal riferimento di origine <src>, seguito da due punti :, seguito dal riferimento di destinazione <dst>.

Il riferimento remoto corrispondente <src>corrisponde al recupero e, se <dst>non è una stringa vuota, il riferimento locale corrispondente corrisponde al passaggio rapido utilizzando<src> . Se+si utilizza ilplus opzionale, il riferimento locale viene aggiornato anche se non comporta un aggiornamento rapido.

Guarda anche

  1. Esegui il checkout e uniscilo senza toccare l'albero di lavoro

  2. Fusione senza modificare la directory di lavoro


3
git checkout --quiet HEADè a git checkout --quiet --detachpartire da Git 1.7.5.
Rafa,

6
Ho scoperto che dovevo fare: git fetch . origin/foo:fooaggiornare il mio foo locale alla mia origine / foo locale
Weston,

3
C'è un motivo per cui le parti "git checkout HEAD --quiet" e "git checkout --quiet -" sono incluse nella risposta lunga, ma non nella risposta breve? Immagino sia perché lo script potrebbe essere eseguito quando hai eseguito il check out del master, anche se potresti semplicemente fare un pull git?
Sean,

8
perché "recupera" il comando per "unire" qui ... semplicemente non ha senso; Se 'pull' è 'fetch' seguito da 'merge', ci deve essere un equivalente più logico 'merge --ff-only' che aggiorni 'branch' da 'origin / branch' localmente, dato che un 'fetch' ha già stato eseguito.
Ed Randall,

1
Grazie per il git checkout -trucco! Semplice come cd -.
Steed

84

No non c'è. È necessario un controllo del ramo di destinazione per consentire di risolvere i conflitti, tra le altre cose (se Git non è in grado di unirli automaticamente).

Tuttavia, se l'unione è un avanzamento rapido, non è necessario controllare il ramo di destinazione, poiché in realtà non è necessario unire nulla: è sufficiente aggiornare il ramo per puntare al nuovo capo rif. Puoi farlo con git branch -f:

git branch -f branch-b branch-a

Si aggiornerà branch-bper indicare il capo di branch-a.

L' -fopzione sta per --force, il che significa che devi stare attento quando lo usi.

Non utilizzarlo a meno che non si sia assolutamente sicuri che l'unione procederà rapidamente.


46
Fai solo attenzione a non farlo a meno che non ti sia assicurato che l'unione sia un avanzamento veloce! Non voglio realizzare più tardi che hai commesso degli errori.
Cascabel,

@FuadSaud Non esattamente. git resetfunziona solo sul ramo attualmente estratto.
Ambra

9
Lo stesso risultato (avanzamento rapido) è ottenuto da git fetch upstream branch-b:branch-b(tratto da questa risposta ).
Oliver,

6
Per espandere il commento di @Oliver, puoi anche fare in git fetch <remote> B:Acui B e A sono rami completamente diversi, ma B può essere unito rapidamente in A. Puoi anche passare il tuo repository locale come "remoto" usando .come alias remoto: git fetch . B:A.

8
La domanda del PO rende abbastanza chiaro che la fusione sarà davvero rapida. Indipendentemente da ciò, branch -fpotrebbe essere pericoloso, come fai notare. Quindi non usarlo! Utilizzare fetch origin branchB:branchB, che fallirà in modo sicuro se l'unione non avanza rapidamente.
Bennett McElwee,

30

Come ha detto Amber, le fusioni con avanzamento rapido sono l'unico caso in cui è possibile farlo. Qualunque altra fusione è plausibilmente necessaria per passare attraverso l'intera fusione a tre vie, applicare patch, risolvere l'affare dei conflitti - e ciò significa che devono esserci dei file.

Mi capita di avere una sceneggiatura che uso esattamente per questo: fare fusioni veloci senza toccare l'albero di lavoro (a meno che non ti unisca a HEAD). È un po 'lungo, perché è almeno un po' robusto - controlla per assicurarsi che l'unione sia un avanzamento veloce, quindi lo esegue senza estrarre il ramo, ma producendo gli stessi risultati che avresti avuto - vedi il diff --statriepilogo delle modifiche e la voce nel reflog è esattamente come un'unione di avanzamento rapido, anziché quella di "ripristino" che si ottiene se si utilizza branch -f. Se è il nome git-merge-ffe rilasciarlo nella directory bin, si può chiamare come un comando 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 Se qualcuno vede problemi con quello script, per favore commenta! Era un lavoro da scrivere e dimenticare, ma sarei felice di migliorarlo.


potresti essere interessato a stackoverflow.com/a/5148202/717355 per il confronto
Philip Oakley,

@PhilipOakley Da una rapida occhiata, che al centro fa esattamente la stessa cosa della mia. Ma il mio non è codificato su una singola coppia di rami, ha molta più gestione degli errori, simula l'output di git-merge e fa quello che vuoi dire se lo chiami mentre sei effettivamente sul ramo.
Cascabel,

Ho il tuo nella mia directory \ bin ;-) Me ne ero appena dimenticato e mi guardavo intorno, ho visto quello script e mi ha spinto a ricordare! La mia copia ha questo link e # or git branch -f localbranch remote/remotebranchmi ricorda la fonte e le opzioni. Ha dato il tuo commento sull'altro collegamento a +1.
Philip Oakley,

3
+1, può anche essere predefinito "$branch@{u}"come committente da unire per ottenere il ramo upstream (da kernel.org/pub/software/scm/git/docs/gitrevisions.html )
orip

Grazie! Qualche possibilità che potresti dare un semplice esempio-uso nella risposta?
nmr

20

Puoi farlo solo se l'unione è un avanzamento veloce. In caso contrario, git deve avere i file estratti per poterli unire!

Per farlo solo per l'avanzamento veloce :

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

dov'è <commit>il commit recuperato, quello a cui vuoi avanzare rapidamente. Questo è fondamentalmente come usare git branch -fper spostare il ramo, tranne per il fatto che lo registra anche nel reflog come se davvero avessi fatto l'unione.

Per favore, per favore, per favore non farlo per qualcosa che non è un avanzamento veloce, o dovrai semplicemente reimpostare il tuo ramo sull'altro commit. (Per verificare, vedere se git merge-base <branch> <commit>fornisce SHA1 del ramo.)


4
C'è un modo per farlo fallire se non può avanzare velocemente?
Gman,

2
@gman puoi usare git merge-base --is-ancestor <A> <B>. "B" è la cosa che deve essere fusa in "A". L'esempio sarebbe A = master e B = sviluppo, assicurandosi che lo sviluppo sia rapido in master. Nota: esiste con 0 se non è in grado di esistere, esiste con 1 se lo è.
eddiemoya,

1
Non documentato in git-scm, ma è su kernal.org. kernel.org/pub/software/scm/git/docs/git-merge-base.html
eddiemoya

Ho fatto una rapida idea per concludere ciò di cui sto parlando (in realtà non lo ha testato, ma dovrebbe portarti principalmente lì). gist.github.com/eddiemoya/ad4285b2d8a6bdabf432 ---- come nota a margine , ho avuto la maggior parte di questo a portata di mano, ho uno script che controlla ff e se non ti consente di modificare prima il ramo desiderato - quindi ff si fonde - tutto senza controllare nulla.
eddiemoya,

12

Nel tuo caso puoi usare

git fetch origin branchB:branchB

che fa quello che vuoi (supponendo che l'unione sia un avanzamento rapido). Se il ramo non può essere aggiornato perché richiede un'unione senza avanzamento rapido, questo non riesce in modo sicuro con un messaggio.

Questa forma di recupero ha anche alcune opzioni più utili:

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

Si noti che <remote> può essere un repository locale e <sourceBranch>può essere un ramo di tracciamento. Quindi puoi aggiornare una filiale locale, anche se non è stata estratta, senza accedere alla rete .

Attualmente, il mio accesso al server upstream avviene tramite una VPN lenta, quindi mi collego periodicamente, git fetchper aggiornare tutti i telecomandi e quindi disconnettermi. Quindi se, per esempio, il master remoto è cambiato, posso farlo

git fetch . remotes/origin/master:master

per aggiornare il mio padrone di casa in modo sicuro, anche se attualmente ho qualche altra filiale verificata. Nessun accesso alla rete richiesto.


11

Un altro modo, certo, piuttosto brutale è semplicemente ricreare il ramo:

git fetch remote
git branch -f localbranch remote/remotebranch

Questo elimina il ramo locale obsoleto e ricrea uno con lo stesso nome, quindi usalo con cura ...


Ho appena visto che la risposta originale menziona già il ramo -f ... Tuttavia, non vedo alcun vantaggio nell'avere l'unione nel reflog per il caso d'uso originariamente descritto.
kkoehne,

7

È possibile clonare il repository ed eseguire l'unione nel nuovo repository. Sullo stesso filesystem, questo sarà hardlink piuttosto che copiare la maggior parte dei dati. Termina tirando i risultati nel repository originale.


4

Inserisci git-forward-merge :

Senza dover effettuare il checkout della destinazione, git-forward-merge <source> <destination>unisce l'origine al ramo di destinazione.

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

Funziona solo per le fusioni automatiche, in caso di conflitti è necessario utilizzare l'unione regolare.


2
Penso che sia meglio di git fetch <remote> <source>: <destinazione>, perché un avanzamento veloce è un'operazione di unione non recupero ed è più facile da scrivere. La cosa brutta però, non è nel git predefinito.
Binarian,

4

Per molti casi (come l'unione), puoi semplicemente utilizzare il ramo remoto senza dover aggiornare il ramo di tracciamento locale. L'aggiunta di un messaggio nel reflog sembra eccessivo e lo impedirà di essere più veloce. Per facilitare il recupero, aggiungi quanto segue nella tua configurazione di git

[core]
    logallrefupdates=true

Quindi digitare

git reflog show mybranch

per vedere la cronologia recente della tua filiale


Penso che la sezione deve essere [core]non [user]? (ed è
attivo

3

Ho scritto una funzione shell per un caso d'uso simile che incontro quotidianamente su progetti. Questa è sostanzialmente una scorciatoia per mantenere aggiornate le filiali locali con una filiale comune come sviluppare prima di aprire un PR, ecc.

Pubblicare questo anche se non si desidera utilizzare checkout, nel caso in cui altri non si preoccupino di quel vincolo.

glmh("git pull and merge here") verrà automaticamente checkout branchB, pulll'ultimo, ri checkout branchAe merge branchB.

Non risponde alla necessità di conservare una copia locale di branchA, ma potrebbe essere facilmente modificato per farlo aggiungendo un passaggio prima di estrarre branchB. Qualcosa di simile a...

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

Per semplici fusioni di avanzamento rapido, si salta al prompt del messaggio di commit.

In caso di fusioni non a avanzamento rapido, la filiale si trova nello stato di risoluzione del conflitto (probabilmente è necessario intervenire).

Per impostare, aggiungere a .bashrco .zshrc, ecc.:

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} 
}

Uso:

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

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

Nota: questo non è abbastanza robusto per distribuire args oltre il nome del ramogit merge


2

Un altro modo per farlo efficacemente è:

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

Poiché è in minuscolo -d, lo eliminerà solo se i dati saranno ancora presenti da qualche parte. È simile alla risposta di @ kkoehne, tranne per il fatto che non forza. Per -tquesto motivo imposterà nuovamente il telecomando.

Avevo un'esigenza leggermente diversa rispetto a OP, che consisteva nel creare una nuova diramazione della funzione develop(o master), dopo aver unito una richiesta pull. Ciò può essere realizzato in un one-liner senza forzatura, ma non aggiorna il developramo locale . Si tratta solo di verificare una nuova filiale e di averla basata origin/develop:

git checkout -b new-feature origin/develop

1

solo per estrarre il master senza controllare il master che uso

git fetch origin master:master


1

È assolutamente possibile fare qualsiasi fusione, anche fusioni a termine non veloci, senza git checkout. La worktreerisposta di @grego è un buon suggerimento. Per espandere su questo:

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

Ora hai unito il ramo di lavoro locale al masterramo locale senza cambiare la cassa.


1

Se vuoi mantenere lo stesso albero di uno dei rami che vuoi unire (cioè non una vera "unione"), puoi farlo in questo modo.

# 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"

1
git worktree add [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]

Puoi provare git worktreead avere due rami aperti fianco a fianco, sembra che potrebbe essere quello che vuoi ma molto diverso da alcune delle altre risposte che ho visto qui.

In questo modo è possibile tenere traccia di due rami separati nello stesso repository git, quindi è necessario eseguire il recupero solo una volta per ottenere gli aggiornamenti in entrambi gli alberi di lavoro (piuttosto che dover gitare il clone due volte e git pull su ciascuno)

Worktree creerà una nuova directory di lavoro per il codice in cui è possibile eseguire il check-out simultaneo di un ramo diverso invece di scambiare i rami in posizione.

Quando vuoi rimuoverlo puoi ripulire

git worktree remove [-f] <worktree>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.