Combina, actualiza y extrae ramas de Git sin usar las cajas


629

Trabajo en un proyecto que tiene 2 ramas, A y B. Normalmente trabajo en la rama A, y fusiono cosas de la rama B. Para la fusión, normalmente haría:

git merge origin/branchB

Sin embargo, también me gustaría mantener una copia local de la rama B, ya que ocasionalmente puedo revisar la rama sin fusionarme primero con mi rama A. Para esto, haría:

git checkout branchB
git pull
git checkout branchA

¿Hay alguna manera de hacer lo anterior en un comando, y sin tener que cambiar de rama de un lado a otro? ¿Debo estar usando git update-refpara eso? ¿Cómo?



1
La respuesta de Jakub a la primera pregunta vinculada explica por qué esto es en general imposible. Otra explicación (a posteriori) es que no puede fusionarse en un repositorio simple, por lo que claramente requiere el árbol de trabajo.
Cascabel

3
@Eric: Los motivos comunes son que los pagos son largos para repositorios grandes, y que actualizan las marcas de tiempo incluso si regresa a la misma versión, por lo que cree que todo debe reconstruirse.
Cascabel

La segunda pregunta que vinculé es la de un caso inusual: fusiones que podrían avanzar rápidamente, pero que el OP quería fusionar utilizando la --no-ffopción, lo que hace que se grabe un compromiso de fusión de todos modos. Si está interesado en eso, mi respuesta allí muestra cómo podría hacer eso, no tan robusto como mi respuesta publicada aquí, pero las fortalezas de los dos sin duda podrían combinarse.
Cascabel

Respuestas:


973

La respuesta corta

Siempre y cuando esté haciendo una fusión de avance rápido , simplemente puede usar

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

Ejemplos:

# 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

Si bien la respuesta de Amber también funcionará en casos de avance rápido, el uso git fetchde esta manera en su lugar es un poco más seguro que simplemente mover la referencia de rama a la fuerza, ya que git fetchevitará automáticamente el avance no acelerado accidental siempre que no lo use +en el refspec.

La larga respuesta

No puede fusionar una rama B en la rama A sin desproteger A primero si daría como resultado una fusión que no sea de avance rápido. Esto se debe a que se necesita una copia de trabajo para resolver posibles conflictos.

Sin embargo, en el caso de las fusiones de avance rápido, esto es posible porque tales fusiones nunca pueden generar conflictos, por definición. Para hacer esto sin verificar primero una rama, puede usar git fetchuna refspec.

Aquí hay un ejemplo de actualización master(no permitir cambios que no sean de avance rápido) si tiene otra rama featuredesprotegida:

git fetch upstream master:master

Este caso de uso es tan común, que probablemente desee crear un alias en su archivo de configuración de git, como este:

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

Lo que hace este alias es lo siguiente:

  1. git checkout HEAD: esto pone su copia de trabajo en un estado de cabeza separada. Esto es útil si desea actualizar mastermientras lo tiene desprotegido. Creo que fue necesario hacerlo porque, de lo contrario, la referencia de la rama masterno se moverá, pero no recuerdo si eso realmente está fuera de mi alcance.

  2. git fetch upstream master:master: esto adelanta rápidamente su local masteral mismo lugar que upstream/master.

  3. git checkout -desprotege su rama desprotegida anteriormente (eso es lo que -hace en este caso).

La sintaxis de git fetch fusiones de avance rápido (no)

Si desea que el fetchcomando falle si la actualización no es de avance rápido, simplemente use una especificación de referencia del formulario

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

Si desea permitir actualizaciones que no sean de avance rápido, entonces agregue un +al frente de la especificación de referencia:

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

Tenga en cuenta que puede pasar su repositorio local como el parámetro "remoto" usando .:

git fetch . <sourceBranch>:<destinationBranch>

La documentación

De la git fetchdocumentación que explica esta sintaxis (énfasis mío):

<refspec>

El formato de un <refspec>parámetro es un plus opcional +, seguido de la referencia de origen <src>, seguido de dos puntos :, seguido de la referencia de destino <dst>.

La referencia remota que coincide <src>se obtiene, y si <dst>no es una cadena vacía, la referencia local que coincide se reenvía rápidamente mediante<src> . Si+se usa elplus opcional, la referencia local se actualiza incluso si no da como resultado una actualización de avance rápido.

Ver también

  1. Git checkout y fusionar sin tocar el árbol de trabajo

  2. Fusionar sin cambiar el directorio de trabajo


3
git checkout --quiet HEADes a git checkout --quiet --detachpartir de Git 1.7.5.
Rafa

66
Descubrí que tenía que hacer: git fetch . origin/foo:fooactualizar mi foo local a mi origen / foo local
weston

3
¿Hay alguna razón para que las partes "git checkout HEAD --quiet" y "git checkout --quiet -" se incluyan en la respuesta larga, pero no en la respuesta corta? Supongo que se debe a que el script se puede ejecutar cuando el maestro ha desprotegido, a pesar de que podría hacer un git pull.
Sean

8
¿Por qué es 'buscar' el comando para 'fusionar' aquí? Simplemente no tiene sentido; Si 'pull' es 'fetch' seguido de 'merge', debe haber un equivalente más lógico 'merge --ff-only' que actualizaría 'branch' desde 'origin / branch' localmente, dado que un 'fetch' tiene Ya se ha ejecutado.
Ed Randall

1
Gracias por el git checkout -truco! Tan fácil como cd -.
Steed

84

No no hay. Es necesario realizar un pago de la rama objetivo para permitirle resolver conflictos, entre otras cosas (si Git no puede fusionarlos automáticamente).

Sin embargo, si la fusión es rápida, no necesita verificar la rama de destino, porque en realidad no necesita fusionar nada; todo lo que tiene que hacer es actualizar la rama para señalar nuevo cabezal ref. Puedes hacer esto con git branch -f:

git branch -f branch-b branch-a

Se actualizará branch-bpara apuntar a la cabeza debranch-a .

La -fopción significa --force, lo que significa que debe tener cuidado al usarla.

No lo use a menos que esté absolutamente seguro de que la fusión avanzará rápidamente.


46
¡Solo tenga mucho cuidado de no hacer eso a menos que se haya asegurado absolutamente de que la fusión sea un avance rápido! No quiero darme cuenta más tarde de que has perdido las confirmaciones.
Cascabel

@FuadSaud No exactamente. git resetsolo funciona en la rama actualmente desprotegida.
Ámbar

99
El mismo resultado (avance rápido) se logra mediante git fetch upstream branch-b:branch-b(tomado de esta respuesta ).
Oliver

66
Para ampliar el comentario de @ Oliver, también puede hacerlo git fetch <remote> B:A, donde B y A son ramas completamente diferentes, pero B puede avanzar rápidamente en A. También puede pasar su repositorio local como "remoto" usando .como alias remoto: git fetch . B:A.

8
La pregunta del OP deja bastante claro que la fusión se adelantará. De todos modos, branch -fpodría ser peligroso, como usted señala. ¡Así que no lo uses! Uso fetch origin branchB:branchB, que fallará de forma segura si la fusión no avanza rápidamente.
Bennett McElwee

30

Como dijo Amber, las fusiones de avance rápido son el único caso en el que posiblemente podría hacer esto. Cualquier otra combinación posiblemente deba pasar por la combinación completa de tres vías, aplicar parches, resolver conflictos y eso significa que debe haber archivos.

Resulta que tengo un script que utilizo exactamente para esto: hacer fusiones de avance rápido sin tocar el árbol de trabajo (a menos que se esté fusionando en HEAD). Es un poco largo, porque es al menos un poco robusto: verifica para asegurarse de que la fusión sea un avance rápido, luego lo realiza sin verificar la rama, pero produce los mismos resultados que si lo hubiera hecho. diff --statresumen de los cambios, y la entrada en el registro de registro es exactamente como una combinación de avance rápido, en lugar del "restablecimiento" que obtienes si lo usas branch -f. Si lo que sea git-merge-ffy colocar en el directorio bin, se le puede llamar como 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

PD: si alguien ve algún problema con ese script, ¡comente! Era un trabajo de escribir y olvidar, pero estaría feliz de mejorarlo.


podría estar interesado en stackoverflow.com/a/5148202/717355 para comparar
Philip Oakley

@PhilipOakley De un vistazo rápido, eso en el núcleo hace exactamente lo mismo que el mío. Pero el mío no está codificado en un solo par de ramas, tiene mucho más manejo de errores, simula la salida de git-merge y hace lo que quieres decir si lo llamas mientras estás en la rama.
Cascabel

Tengo el suyo en mi directorio \ bin ;-) ¡Simplemente lo había olvidado y estaba mirando alrededor, vi ese script y provocó mi retiro! Mi copia tiene este enlace y # or git branch -f localbranch remote/remotebranchme recuerda la fuente y las opciones. Dio su comentario en el otro enlace a +1.
Philip Oakley

3
+1, también puede predeterminarse "$branch@{u}"como el compromiso de fusión para obtener la rama ascendente (desde kernel.org/pub/software/scm/git/docs/gitrevisions.html )
orip

¡Gracias! ¿Hay alguna posibilidad de que pueda incluir un simple ejemplo de uso en la respuesta?
RMN

20

Solo puede hacer esto si la fusión es un avance rápido. Si no es así, ¡git necesita que se revisen los archivos para que pueda fusionarlos!

Para hacerlo solo para un avance rápido :

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

dónde <commit>está el commit recuperado, al que desea avanzar rápidamente. Esto es básicamente como usar git branch -fpara mover la rama, excepto que también la registra en el registro como si realmente hubiera hecho la fusión.

Por favor, por favor, no hagas esto por algo que no sea un avance rápido, o simplemente restablecerás tu rama a la otra confirmación. (Para verificar, ver si git merge-base <branch> <commit>da SHA1 de la sucursal).


44
¿Hay alguna manera de hacer que falle si no puede avanzar rápidamente?
gman

2
@gman que puedes usar git merge-base --is-ancestor <A> <B>. "B" es lo que necesita fusionarse en "A". El ejemplo sería A = maestro y B = desarrollar, asegurándose de que el desarrollo sea rápido reenviable en maestro. Nota: existe con 0 si no es ff-able, existe con 1 si lo es.
eddiemoya

1
No está documentado en git-scm, pero está en kernal.org. kernel.org/pub/software/scm/git/docs/git-merge-base.html
eddiemoya

Hice una idea rápida para concluir de lo que estoy hablando (en realidad no lo probé, pero debería llevarte principalmente allí). gist.github.com/eddiemoya/ad4285b2d8a6bdabf432 ---- como nota al margen, tuve la mayor parte de esto a mano, tengo un script que comprueba ff y, si no, le permite volver a crear la rama deseada primero - luego ff se fusiona - todo sin revisar nada.
eddiemoya

12

En tu caso puedes usar

git fetch origin branchB:branchB

que hace lo que quiere (suponiendo que la fusión es un avance rápido). Si la rama no se puede actualizar porque requiere una fusión que no sea de avance rápido, entonces esto falla de manera segura con un mensaje.

Esta forma de búsqueda también tiene algunas opciones más útiles:

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

Tenga en cuenta que <remote> puede ser un repositorio local y <sourceBranch>puede ser una rama de seguimiento. Por lo tanto, puede actualizar una sucursal local, incluso si no está desprotegida, sin acceder a la red .

Actualmente, mi acceso al servidor ascendente es a través de una VPN lenta, por lo que me conecto periódicamente git fetchpara actualizar todos los controles remotos y luego desconectarme. Entonces, si, por ejemplo, el maestro remoto ha cambiado, puedo hacer

git fetch . remotes/origin/master:master

para actualizar de forma segura a mi maestro local, incluso si actualmente tengo alguna otra sucursal desprotegida. No se requiere acceso a la red.


11

Otra forma, sin duda, bastante brutal es simplemente volver a crear la rama:

git fetch remote
git branch -f localbranch remote/remotebranch

Esto desecha la rama local obsoleta y vuelve a crear una con el mismo nombre, así que úsala con cuidado ...


Acabo de ver ahora que la respuesta original ya menciona la rama -f ... Aún así, no veo una ventaja en tener la fusión en el registro para el caso de uso descrito originalmente.
kkoehne

7

Puede clonar el repositorio y fusionarlo en el nuevo repositorio. En el mismo sistema de archivos, esto enlazará en lugar de copiar la mayoría de los datos. Termine colocando los resultados en el repositorio original.


4

Ingrese git-forward-merge :

Sin necesidad de pagar el destino, git-forward-merge <source> <destination>fusiona el origen en la sucursal de destino.

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

Solo funciona para fusiones automáticas, si hay conflictos necesita usar la fusión regular.


2
Creo que esto es mejor que git fetch <remote> <source>: <destination>, porque un avance rápido es una operación de fusión no fetch y es más fácil de escribir. Sin embargo, algo malo no está en el git predeterminado.
Binarian

4

Para muchos casos (como la fusión), puede usar la rama remota sin tener que actualizar la rama de seguimiento local. Agregar un mensaje al reflog suena como una exageración y evitará que sea más rápido. Para facilitar la recuperación, agregue lo siguiente en su configuración de git

[core]
    logallrefupdates=true

Luego escribe

git reflog show mybranch

para ver el historial reciente de tu sucursal


Creo que la sección debe ser [core]no [user]? (y está
activado de

3

Escribí una función de shell para un caso de uso similar que encuentro a diario en proyectos. Esto es básicamente un atajo para mantener actualizadas las sucursales locales con una sucursal común como desarrollar antes de abrir un RP, etc.

Publicar esto aunque no quiera usarlo checkout, en caso de que a otros no les importe esa restricción.

glmh("git pull and merge here") automáticamente checkout branchB, pulllo último, re checkout branchAy merge branchB.

No aborda la necesidad de mantener una copia local de la rama A, pero podría modificarse fácilmente para hacerlo agregando un paso antes de retirar la rama B. Algo como...

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

Para fusiones de avance rápido simples, esto salta al mensaje de confirmación del mensaje.

Para las fusiones no avanzadas, esto coloca a su sucursal en el estado de resolución de conflictos (es probable que deba intervenir).

Para configurar, agregar .bashrco .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} 
}

Uso:

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

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

Nota: Esto no es lo suficientemente robusto como para transferir args más allá del nombre de la rama paragit merge


2

Otra forma de hacer esto de manera efectiva es:

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

Debido a que es una minúscula -d, solo lo eliminará si los datos aún existen en algún lugar. Es similar a la respuesta de @kkoehne, excepto que no obliga. Por el-t esto, configurará el control remoto nuevamente.

Tenía una necesidad ligeramente diferente de OP, que era crear una nueva rama de función develop(o master), después de fusionar una solicitud de extracción. Eso se puede lograr en una sola línea sin fuerza, pero no actualiza la developrama local . Es solo una cuestión de verificar una nueva sucursal y hacer que se base origin/develop:

git checkout -b new-feature origin/develop

1

solo para extraer el maestro sin verificar el maestro que uso

git fetch origin master:master


1

Es absolutamente posible hacer cualquier combinación, incluso las combinaciones de avance no rápido, sin git checkout. La worktreerespuesta de @grego es una buena pista. Para ampliar sobre eso:

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

Ahora ha fusionado la rama de trabajo local a la masterrama local sin cambiar su pago.


1

Si desea mantener el mismo árbol que una de las ramas que desea fusionar (es decir, no una "fusión" real), puede hacerlo así.

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

Puede intentar git worktreetener dos ramas abiertas una al lado de la otra, parece que podría ser lo que desea, pero es muy diferente a algunas de las otras respuestas que he visto aquí.

De esta manera, puede tener dos ramas separadas rastreando en el mismo repositorio de git, por lo que solo tiene que buscar una vez para obtener actualizaciones en ambos árboles de trabajo (en lugar de tener que clonar dos veces y tirar de cada una)

Worktree creará un nuevo directorio de trabajo para su código donde puede tener una rama diferente extraída simultáneamente en lugar de intercambiar ramas en su lugar.

Cuando desee eliminarlo, puede limpiar con

git worktree remove [-f] <worktree>
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.