¿Encontrar un punto de ramificación con Git?

455

Tengo un repositorio con ramas master y A y mucha actividad de fusión entre los dos. ¿Cómo puedo encontrar el commit en mi repositorio cuando se creó la rama A basada en master?

Mi repositorio básicamente se ve así:

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)

Estoy buscando la revisión A, que no es lo que git merge-base (--all)encuentra.

Jochen
fuente

Respuestas:

499

Estaba buscando lo mismo, y encontré esta pregunta. Gracias por preguntar!

Sin embargo, encontré que las respuestas que vemos aquí no parecen bastante dar la respuesta que solicitó (o que yo estaba buscando) - que parecen dar la Gconfirmación, en lugar de la Aconfirmación.

Entonces, he creado el siguiente árbol (letras asignadas en orden cronológico), para poder probar las cosas:

A - B - D - F - G   <- "master" branch (at G)
     \   \     /
      C - E --'     <- "topic" branch (still at E)

Esto se ve un poco diferente al tuyo, porque quería asegurarme de que obtuve (refiriéndome a este gráfico, no el tuyo) B, pero no A (y no D o E). Aquí están las letras adjuntas a los prefijos SHA y mensajes de confirmación (mi repositorio se puede clonar desde aquí , si eso es interesante para alguien):

G: a9546a2 merge from topic back to master
F: e7c863d commit on master after master was merged to topic
E: 648ca35 merging master onto topic
D: 37ad159 post-branch commit on master
C: 132ee2a first commit on topic branch
B: 6aafd7f second commit on master before branching
A: 4112403 initial commit on master

Por lo tanto, el objetivo: Encontrar B . Aquí hay tres formas que encontré, después de un poco de retoques:


1. visualmente, con gitk:

Debería ver visualmente un árbol como este (como se ve desde el maestro):

gitk screen capture from master

o aquí (como se ve desde el tema):

gitk screen capture from topic

En ambos casos, he seleccionado la confirmación que está Ben mi gráfico. Una vez que hace clic en él, su SHA completo se presenta en un campo de entrada de texto justo debajo del gráfico.


2. visualmente, pero desde la terminal:

git log --graph --oneline --all

(Editar / nota al margen: agregar --decoratetambién puede ser interesante; agrega una indicación de nombres de ramas, etiquetas, etc. No agrega esto a la línea de comandos anterior ya que el resultado a continuación no refleja su uso).

que muestra (suponiendo git config --global color.ui auto):

output of git log --graph --oneline --all

O, en texto directo:

* a9546a2 fusionar de tema a maestro
| \  
El | * 648ca35 fusionando master sobre tema
El | | \  
El | * | 132ee2a primer compromiso en la rama de tema
* | El | e7c863d commit en master después de master se fusionó con el tema
El | | /  
| / |   
* | 37ad159 post-branch commit en master
| /  
* 6aafd7f segundo commit en master antes de bifurcar
* 4112403 confirmación inicial en maestro

En cualquier caso, vemos el 6aafd7f commit como el punto común más bajo, es decir, Ben mi gráfico o Aen el suyo.


3. Con magia de concha:

No especifique en su pregunta si desea algo como lo anterior, o un solo comando que solo le dará una revisión, y nada más. Bueno, aquí está lo último:

diff -u <(git rev-list --first-parent topic) \
             <(git rev-list --first-parent master) | \
     sed -ne 's/^ //p' | head -1
6aafd7ff98017c816033df18395c5c1e7829960d

Lo que también puede poner en su ~ / .gitconfig como (nota: el guión final es importante; gracias Brian por llamar la atención sobre eso) :

[alias]
    oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -

Lo que podría hacerse a través de la siguiente línea de comandos (complicada con comillas):

git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'

Nota: zshpodría fácilmente haber sido bash, pero shlo hará sin trabajo - la <()sintaxis no existe en la vainilla sh. (¡Gracias de nuevo, @conny, por informarme en un comentario sobre otra respuesta en esta página!)

Nota: Versión alternativa de lo anterior:

Gracias a liori por señalar que lo anterior podría caerse al comparar ramas idénticas y crear una forma de diferencia alternativa que elimine la forma sed de la mezcla y la haga "más segura" (es decir, devuelve un resultado (es decir, el confirmación más reciente) incluso cuando compara maestro con maestro):

Como una línea .git-config:

[alias]
    oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -

De la cáscara:

git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'

Entonces, en mi árbol de prueba (que no estuvo disponible por un tiempo, lo siento; está de regreso), eso ahora funciona tanto en el maestro como en el tema (dando confirmaciones G y B, respectivamente). Gracias de nuevo, liori, por la forma alternativa.


Entonces, eso es lo que yo [y liori] inventamos. Parece funcionar para mí También permite un par adicional de alias que pueden resultar útiles:

git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'

¡Feliz git-ing!

lindes
fuente
55
Gracias Lindes, la opción de shell es ideal para situaciones en las que desea encontrar el punto de bifurcación de una bifurcación de mantenimiento de larga ejecución. Cuando está buscando una revisión que podría ser un millar de confirmaciones en el pasado, las opciones visuales realmente no van a ser suficientes. * 8 ')
Mark Booth
44
En su tercer método, depende de que el contexto muestre la primera línea sin cambios. Esto no sucederá en ciertos casos extremos o si tiene requisitos ligeramente diferentes (por ejemplo, solo necesito que uno de los historiales sea: first-parent, y estoy usando este método en un script que a veces puede usar el mismo ramas en ambos lados). He encontrado que es más seguro de usar diffque está if-then-else modo de borrado y cambiar / borrar las líneas de su salida en lugar de contar en tener contexto lo suficientemente grande, por.: diff --old-line-format='' --new-line-format='' <(git rev-list …) <(git rev-list …)|head -1.
liori
3
git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~también funcionará, creo
Tobias Schulte
44
@ JakubNarębski: ¿Tiene alguna forma de asignar git merge-base --fork-point ...commit B (commit 6aafd7f) para este árbol? Estaba ocupado con otras cosas cuando publicaste eso, y sonaba bien, pero finalmente lo probé y no estoy logrando que funcione ... O recibo algo más reciente o simplemente una falla silenciosa (sin error mensaje, pero el código de salida 1), tratando argumentos como git co master; git merge-base --fork-point topic, git co topic; git merge-base --fork-point master, git merge-base --fork-point topic master(ya sea para la salida), etc. ¿hay algo que estoy haciendo mal o que falta?
lindes
25
@ JakubNarębski @lindes --fork-pointse basa en el registro, por lo que solo funcionará si realizó los cambios localmente. Incluso entonces, las entradas de reflog podrían haber expirado. Es útil pero no confiable en absoluto.
Daniel Lubarov
131

Quizás estés buscando git merge-base:

git merge-base encuentra los mejores ancestros comunes entre dos commits para usar en una fusión de tres vías. Un antepasado común es mejor que otro antepasado común si este último es un antepasado del primero. Un antepasado común que no tiene ningún antepasado común mejor es el mejor antepasado común , es decir, una base de fusión . Tenga en cuenta que puede haber más de una base de fusión para un par de confirmaciones.

Greg Hewgill
fuente
10
Tenga en cuenta también la --allopción de " git merge-base"
Jakub Narębski
10
Esto no responde la pregunta original, pero la mayoría de las personas hacen la pregunta mucho más simple para la cual esta es la respuesta :)
FelipeC
66
dijo que no quería el resultado de git merge-base
Tom Tanner
99
@TomTanner: Acabo de mirar el historial de preguntas y la pregunta original fue editada para incluir esa nota aproximadamente git merge-basecinco horas después de que se publicara mi respuesta (probablemente en respuesta a mi respuesta). Sin embargo, dejaré esta respuesta tal como está porque aún puede ser útil para alguien más que encuentre esta pregunta a través de la búsqueda.
Greg Hewgill
Creo que podría usar los resultados de merge-base --todos y luego examinar la lista de confirmaciones de retorno usando merge-base --is-ancestor para encontrar el ancestro común más antiguo de la lista, pero esta no es la forma más simple .
Matt_G
42

Lo he usado git rev-listpara este tipo de cosas. Por ejemplo, (tenga en cuenta los 3 puntos)

$ git rev-list --boundary branch-a...master | grep "^-" | cut -c2-

escupirá el punto de ramificación. Ahora, no es perfecto; ya que has fusionado maestro en rama A un par de veces, eso dividirá un par de posibles puntos de ramificación (básicamente, el punto de ramificación original y luego cada punto en el que fusionó el maestro en la rama A). Sin embargo, al menos debería reducir las posibilidades.

He agregado ese comando a mis alias ~/.gitconfigcomo:

[alias]
    diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-'

entonces puedo llamarlo como:

$ git diverges branch-a master
mipadi
fuente
Nota: esto parece dar el primer compromiso en la rama, en lugar del ancestro común. (es decir, da en Glugar de A, según el gráfico de la pregunta original). Tengo una respuesta que recibo A, que publicaré en este momento.
lindes
@lindes: Da al ancestro común en todos los casos que lo he probado. ¿Tienes un ejemplo donde no lo hace?
mipadi
Si. En mi respuesta (que tiene un enlace a un repositorio que puede clonar; git checkout topicy luego ejecutar esto con topicen lugar de branch-a), enumera 648ca357b946939654da12aaf2dc072763f3caeey 37ad15952db5c065679d7fc31838369712f0b338- ambos 37ad159y 648ca35están en el ancestro de las ramas actuales (esta última es la CABEZA actual de topic) pero tampoco es el punto antes de que ocurriera la ramificación. ¿Obtienes algo diferente?
lindes
@lindes: No pude clonar tu repositorio (¿posiblemente un problema de permisos?).
mipadi
¡Ups, lo siento! Gracias por hacérmelo saber. Olvidé ejecutar git update-server-info. Debería ser bueno ir ahora. :)
lindes
31

Si te gustan los comandos concisos,

git rev-list $ (git rev-list --first-parent ^ branch_name master | tail -n1) ^^! 

Aquí hay una explicación.

El siguiente comando le proporciona la lista de todas las confirmaciones en el maestro que ocurrieron después de que se creó branch_name

git rev-list --first-parent ^ branch_name master 

Como solo te importa la primera de esas confirmaciones, quieres la última línea de la salida:

git rev-list ^ branch_name --primero padre principal | cola -n1

El padre de la primera confirmación que no es un antepasado de "branch_name" está, por definición, en "branch_name", y está en "master" ya que es un antepasado de algo en "master". Entonces tienes la primera confirmación que está en ambas ramas.

El comando

git rev-list commit ^^!

es solo una forma de mostrar la referencia de confirmación principal. Podrías usar

git log -1 commit ^

o lo que sea.

PD: No estoy de acuerdo con el argumento de que el orden de los antepasados ​​es irrelevante. Depende de lo que quieras. Por ejemplo, en este caso

_C1___C2_______ maestro
  \ \ _XXXXX_ rama A (las X denotan cruces arbitrarios entre maestro y A)
   \ _____ / rama B

tiene mucho sentido generar C2 como la confirmación de "ramificación". Esto es cuando el desarrollador se ramificó de "maestro". Cuando se bifurcó, ¡la rama "B" ni siquiera se fusionó en su rama! Esto es lo que da la solución en esta publicación.

Si lo que desea es la última confirmación C de modo que todas las rutas desde el origen hasta la última confirmación en la rama "A" pasen por C, entonces desea ignorar el orden de ascendencia. Eso es puramente topológico y te da una idea de cuándo tienes dos versiones del código funcionando al mismo tiempo. Fue entonces cuando usaría enfoques basados ​​en la combinación de bases, y devolverá C1 en mi ejemplo.

Lionel
fuente
3
Esta es, con mucho, la respuesta más clara, hagamos que se vote a la cima. Una edición sugerida: git rev-list commit^^!se puede simplificar comogit rev-parse commit^
Russell Davis
Esta debería ser la respuesta!
noveno
2
Esta respuesta es agradable, me acaba de reemplazar git rev-list --first-parent ^branch_name mastercon git rev-list --first-parent branch_name ^masterporque si la rama principal es 0 compromete por delante de la otra rama (rápido-remitible a ella), se crearía ninguna salida. Con mi solución, no se crea ningún resultado si el maestro está estrictamente adelantado (es decir, la rama se ha fusionado por completo), que es lo que quiero.
Michael Schmeißer
3
Esto no funcionará a menos que me falte algo por completo. Hay fusiones en ambas direcciones en las ramas de ejemplo. Parece que trató de tomar eso en cuenta, pero creo que esto hará que su respuesta falle. git rev-list --first-parent ^topic mastersolo lo llevará de regreso a la primera confirmación después de la última combinación de masteren topic(si eso incluso existe).
Jerry
1
@jerry Tienes razón, esta respuesta es basura; por ejemplo, en el caso de que acabe de producirse un backmerge (master fusionado en el tema), y master no tiene nuevos commits después de eso, el primer comando git rev-list --first-parent no produce nada en absoluto.
TamaMcGlinn
19

Dado que muchas de las respuestas en este hilo no dan la respuesta que pedía la pregunta, aquí hay un resumen de los resultados de cada solución, junto con el script que utilicé para replicar el repositorio dado en la pregunta.

El registro

Al crear un repositorio con la estructura dada, obtenemos el registro git de:

$ git --no-pager log --graph --oneline --all --decorate
* b80b645 (HEAD, branch_A) J - Work in branch_A branch
| *   3bd4054 (master) F - Merge branch_A into branch master
| |\  
| |/  
|/|   
* |   a06711b I - Merge master into branch_A
|\ \  
* | | bcad6a3 H - Work in branch_A
| | * b46632a D - Work in branch master
| |/  
| *   413851d C - Merge branch_A into branch master
| |\  
| |/  
|/|   
* | 6e343aa G - Work in branch_A
| * 89655bb B - Work in branch master
|/  
* 74c6405 (tag: branch_A_tag) A - Work in branch master
* 7a1c939 X - Work in branch master

Mi única adición es la etiqueta que lo hace explícito sobre el punto en el que creamos la rama y, por lo tanto, la confirmación que deseamos encontrar.

La solución que funciona.

La única solución que funciona es la que proporciona lindes correctamente A:

$ diff -u <(git rev-list --first-parent branch_A) \
          <(git rev-list --first-parent master) | \
      sed -ne 's/^ //p' | head -1
74c6405d17e319bd0c07c690ed876d65d89618d5

Sin embargo, como señala Charles Bailey , esta solución es muy frágil.

Si branch_Aen mastery luego fusionarse masteren branch_Asin intervenir compromete a continuación, la solución lindes' sólo le da la más reciente primero divergance .

Eso significa que para mi flujo de trabajo, creo que tendré que seguir etiquetando el punto de ramificación de las sucursales de larga ejecución, ya que no puedo garantizar que se puedan encontrar de manera confiable más adelante.

Esto realmente se reduce a gitla falta de lo que se hgllama ramas denominadas . El blogger jhw llama a estos linajes vs. familias en su artículo Why I Like Mercurial More Than Git y su artículo de seguimiento More On Mercurial vs. Git (con Graphs!) . Os recomiendo que lean ellos para ver qué algunos conversos no mercuriales de menos tener el nombre ramas en git.

Las soluciones que no funcionan

La solución proporcionada por mipadi devuelve dos respuestas Iy C:

$ git rev-list --boundary branch_A...master | grep ^- | cut -c2-
a06711b55cf7275e8c3c843748daaa0aa75aef54
413851dfecab2718a3692a4bba13b50b81e36afc

La solución provista por Greg Hewgill regresaI

$ git merge-base master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
$ git merge-base --all master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54

La solución proporcionada por Karl devuelve X:

$ diff -u <(git log --pretty=oneline branch_A) \
          <(git log --pretty=oneline master) | \
       tail -1 | cut -c 2-42
7a1c939ec325515acfccb79040b2e4e1c3e7bbe5

La secuencia de comandos

mkdir $1
cd $1
git init
git commit --allow-empty -m "X - Work in branch master"
git commit --allow-empty -m "A - Work in branch master"
git branch branch_A
git tag branch_A_tag     -m "Tag branch point of branch_A"
git commit --allow-empty -m "B - Work in branch master"
git checkout branch_A
git commit --allow-empty -m "G - Work in branch_A"
git checkout master
git merge branch_A       -m "C - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "H - Work in branch_A"
git merge master         -m "I - Merge master into branch_A"
git checkout master
git commit --allow-empty -m "D - Work in branch master"
git merge branch_A       -m "F - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "J - Work in branch_A branch"

Dudo que la versión git haga mucha diferencia en esto, pero:

$ git --version
git version 1.7.1

Gracias a Charles Bailey por mostrarme una forma más compacta de guiar el repositorio de ejemplos.

Mark Booth
fuente
2
La solución de Karl es fácil de solucionar: diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1. Gracias por proporcionar instrucciones para crear el repositorio :)
FelipeC
Creo que quiere decir "La variación limpia de la solución proporcionada por Karl devuelve X". El original funcionó bien, era feo :-)
Karl
No, tu original no funciona bien. Por supuesto, la variación funciona aún peor. Pero agregar la opción --topo-order hace que su versión funcione :)
FelipeC
@felipec - Vea mi comentario final sobre la respuesta de Charles Bailey . Por desgracia, nuestro chat (y, por lo tanto, todos los comentarios anteriores) ahora se han eliminado. Intentaré actualizar mi respuesta cuando tenga el tiempo.
Mark Booth
Interesante. Asumí que topológico era el valor predeterminado. Yo tonto :-)
Karl
10

En general, esto no es posible. En la historia de una rama, una rama y fusión antes de que una rama con nombre se bifurcara y una rama intermedia de dos ramas con nombre se vea igual.

En git, las ramas son solo los nombres actuales de los consejos de las secciones de la historia. Realmente no tienen una identidad fuerte.

Esto no suele ser un gran problema, ya que la base de fusión (ver la respuesta de Greg Hewgill) de dos confirmaciones suele ser mucho más útil, dado la confirmación más reciente que las dos ramas compartieron.

Obviamente, una solución basada en el orden de los padres de un commit no funcionará en situaciones en las que una sucursal se haya integrado completamente en algún momento de la historia de la sucursal.

git commit --allow-empty -m root # actual branch commit
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git merge --ff-only master
git commit --allow-empty -m "More work on branch_A"
git checkout master
git commit --allow-empty -m "More work on master"

Esta técnica también se cae si se ha realizado una fusión de integración con los padres invertidos (p. Ej., Se usó una rama temporal para realizar una fusión de prueba en maestro y luego se reenvió rápidamente en la rama de características para seguir avanzando).

git commit --allow-empty -m root # actual branch point
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git commit --allow-empty -m "More work on branch_A"

git checkout -b tmp-branch master
git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
git checkout branch_A
git merge --ff-only tmp-branch
git branch -d tmp-branch

git checkout master
git commit --allow-empty -m "More work on master"
CB Bailey
fuente
3
Gracias Charles, me has convencido, si quiero saber el punto en el que la rama originalmente divergió , tendré que etiquetarlo. Realmente deseo que gittenía un equivalente a la hgque está llamado ramas, sería hacer la gestión de las ramas de mantenimiento de larga vida de manera mucho más fácil.
Mark Booth
1
"En git, las ramas son solo los nombres actuales de los consejos de secciones de la historia. Realmente no tienen una identidad fuerte". Es algo aterrador decirlo y me ha convencido de que necesito entender mejor las ramas de Git. Gracias (+ 1)
dumbledad el
En la historia de una rama, una rama y fusión antes de que una rama con nombre se bifurcara y una rama intermedia de dos ramas con nombre se vea igual. Sí. +1.
user1071847
5

¿Qué tal algo como

git log --pretty=oneline master > 1
git log --pretty=oneline branch_A > 2

git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^
Karl
fuente
Esto funciona. Es realmente engorroso, pero es lo único que he encontrado que realmente parece hacer el trabajo.
GaryO
1
Git alias equivalente: diverges = !bash -c 'git rev-parse $(diff <(git log --pretty=oneline ${1}) <(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'(sin archivos temporales)
conny
@conny: Oh, wow, nunca había visto la sintaxis <(foo) ... eso es increíblemente útil, ¡gracias! (Funciona también en zsh, para su información)
lindes
Esto parece darme el primer compromiso en la rama, en lugar del antepasado común. (es decir, da en Glugar de A, según el gráfico de la pregunta original). Sin embargo, creo que he encontrado una respuesta, que publicaré en el presente.
lindes
En lugar de 'git log --pretty = oneline' puede simplemente hacer 'git rev-list', luego puede omitir el corte también, además, esto le da al padre el compromiso del punto de divergencia, así que solo cola -2 | cabeza 1. Entonces:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC
5

seguramente me estoy perdiendo algo, pero en mi opinión, todos los problemas anteriores están causados ​​porque siempre estamos tratando de encontrar el punto de ramificación en la historia, y eso causa todo tipo de problemas debido a las combinaciones de fusión disponibles.

En cambio, he seguido un enfoque diferente, basado en el hecho de que ambas ramas comparten una gran cantidad de historia, exactamente toda la historia antes de la ramificación es 100% igual, por lo que en lugar de retroceder, mi propuesta es seguir adelante (desde el 1 commit), buscando la primera diferencia en ambas ramas. El punto de ramificación será, simplemente, el padre de la primera diferencia encontrada.

En la práctica:

#!/bin/bash
diff <( git rev-list "${1:-master}" --reverse --topo-order ) \
     <( git rev-list "${2:-HEAD}" --reverse --topo-order) \
--unified=1 | sed -ne 's/^ //p' | head -1

Y está resolviendo todos mis casos habituales. Claro que hay bordes que no están cubiertos, pero ... ciao :-)

stronk7
fuente
diff <(git rev-list "$ {1: -master}" - first-parent) <(git rev-list "$ {2: -HEAD}" - first-parent) -U1 | cola -1
Alexander Bird
Encontré que esto es más rápido (2-100x): comm --nocheck-order -1 -2 <(git rev-list --reverse --topo-order topic) <(git rev-list --reverse --topo-order master) | head -1
Andrei Neculau
4

Después de mucha investigación y discusiones, está claro que no hay una bala mágica que funcione en todas las situaciones, al menos no en la versión actual de Git.

Es por eso que escribí un par de parches que agregan el concepto de una tailrama. Cada vez que se crea una rama, también se crea un puntero al punto original, la tailreferencia. Esta referencia se actualiza cada vez que se vuelve a crear la rama.

Para averiguar el punto de ramificación de la ramificación de desarrollo, todo lo que tiene que hacer es usar devel@{tail}, eso es todo.

https://github.com/felipec/git/commits/fc/tail

FelipeC
fuente
1
Podría ser la única solución estable. ¿Viste si esto podría entrar en git? No vi una solicitud de extracción.
Alexander Klimetschek
@AlexanderKlimetschek No envié los parches, y no creo que sean aceptados. Sin embargo, probé un método diferente: un gancho "update-branch" que hace algo muy similar. De esta manera, Git no haría nada por defecto, pero podría habilitar el gancho para actualizar la rama de cola. Sin embargo, no habría desarrollado @ {tail}, pero no sería tan malo usar tails / devel en su lugar.
FelipeC
3

Aquí hay una versión mejorada de mi respuesta anterior respuesta anterior . Se basa en los mensajes de confirmación de las fusiones para encontrar dónde se creó la rama por primera vez.

Funciona en todos los repositorios mencionados aquí, e incluso he abordado algunos complicados que se generaron en la lista de correo . También escribí pruebas para esto.

find_merge ()
{
    local selection extra
    test "$2" && extra=" into $2"
    git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1
}

branch_point ()
{
    local first_merge second_merge merge
    first_merge=$(find_merge $1 "" "$1 $2")
    second_merge=$(find_merge $2 $1 $first_merge)
    merge=${second_merge:-$first_merge}

    if [ "$merge" ]; then
        git merge-base $merge^1 $merge^2
    else
        git merge-base $1 $2
    fi
}
FelipeC
fuente
3

El siguiente comando revelará el SHA1 de Commit A

git merge-base --fork-point A

Gayan Pathirage
fuente
Esto no ocurrirá si las ramas padre e hijo tienen fusiones intermedias entre sí.
nawfal
2

Parece que estoy disfrutando un poco con

git rev-list branch...master

La última línea que obtienes es la primera confirmación en la rama, por lo que se trata de obtener el padre de eso. Entonces

git rev-list -1 `git rev-list branch...master | tail -1`^

Parece funcionar para mí y no necesita diffs, etc. (lo cual es útil ya que no tenemos esa versión de diff)

Corrección: esto no funciona si está en la rama maestra, pero lo estoy haciendo en un script, así que eso no es un problema

Tom Tanner
fuente
2

Para encontrar confirmaciones desde el punto de ramificación, puede usar esto.

git log --ancestry-path master..topicbranch
Andor
fuente
Este comando no funciona para mí en el ejemplo dado. Por favor, ¿qué proporcionaría como parámetros para el rango de confirmación?
Jesper Rønn-Jensen
2

A veces es efectivamente imposible (con algunas excepciones de dónde podría tener la suerte de tener datos adicionales) y las soluciones aquí no funcionarán.

Git no conserva el historial de referencia (que incluye ramas). Solo almacena la posición actual de cada rama (la cabeza). Esto significa que puede perder parte del historial de sucursales en git con el tiempo. Cada vez que se bifurca, por ejemplo, se pierde de inmediato qué rama era la original. Todo lo que hace una rama es:

git checkout branch1    # refs/branch1 -> commit1
git checkout -b branch2 # branch2 -> commit1

Puede suponer que el primero comprometido es la rama. Este suele ser el caso, pero no siempre es así. No hay nada que le impida comprometerse con cualquiera de las ramas primero después de la operación anterior. Además, no se garantiza que las marcas de tiempo de git sean confiables. No es hasta que te comprometes con ambos que realmente se convierten en ramas estructuralmente.

Mientras que en los diagramas tendemos a numerar los commits conceptualmente, git no tiene un concepto estable real de secuencia cuando el commit se ramifica. En este caso, puede suponer que los números (que indican el orden) están determinados por la marca de tiempo (puede ser divertido ver cómo una interfaz de usuario de git maneja las cosas cuando establece todas las marcas de tiempo en el mismo).

Esto es lo que un humano espera conceptualmente:

After branch:
       C1 (B1)
      /
    -
      \
       C1 (B2)
After first commit:
       C1 (B1)
      /
    - 
      \
       C1 - C2 (B2)

Esto es lo que realmente obtienes:

After branch:
    - C1 (B1) (B2)
After first commit (human):
    - C1 (B1)
        \
         C2 (B2)
After first commit (real):
    - C1 (B1) - C2 (B2)

Asumirías que B1 es la rama original, pero podría ser simplemente una rama muerta (alguien realizó el pago -b pero nunca se comprometió). No es hasta que te comprometes con ambos que obtienes una estructura de rama legítima dentro de git:

Either:
      / - C2 (B1)
    -- C1
      \ - C3 (B2)
Or:
      / - C3 (B1)
    -- C1
      \ - C2 (B2)

Siempre sabe que C1 vino antes que C2 y C3, pero nunca sabe de manera confiable si C2 vino antes que C3 o C3 vino antes que C2 (porque puede configurar la hora en su estación de trabajo a cualquier cosa, por ejemplo). B1 y B2 también son engañosos ya que no puedes saber qué rama vino primero. Puede hacer una suposición muy buena y generalmente precisa en muchos casos. Es un poco como una pista de carreras. En general, en igualdad de condiciones con los autos, puede suponerse que un auto que viene en una vuelta atrás comenzó una vuelta atrás. También tenemos convenciones que son muy confiables, por ejemplo, master casi siempre representará las ramas más longevas, aunque lamentablemente he visto casos en los que incluso este no es el caso.

El ejemplo dado aquí es un ejemplo de preservación de la historia:

Human:
    - X - A - B - C - D - F (B1)
           \     / \     /
            G - H ----- I - J (B2)
Real:
            B ----- C - D - F (B1)
           /       / \     /
    - X - A       /   \   /
           \     /     \ /
            G - H ----- I - J (B2)

Real aquí también es engañoso porque nosotros, como humanos, lo leemos de izquierda a derecha, de raíz a hoja (ref). Git no hace eso. Donde hacemos (A-> B) en nuestras cabezas git hace (A <-B o B-> A). Lo lee de ref a root. Las referencias pueden estar en cualquier lugar, pero tienden a ser hojas, al menos para ramas activas. Una referencia apunta a un compromiso y los compromisos solo contienen un me gusta para sus padres, no para sus hijos. Cuando un commit es un commit de fusión, tendrá más de un padre. El primer padre es siempre el commit original en el que se fusionó. Los otros padres siempre son commits que se fusionaron en el commit original.

Paths:
    F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
    J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))

Esta no es una representación muy eficiente, sino una expresión de todos los caminos que git puede tomar de cada referencia (B1 y B2).

El almacenamiento interno de Git se parece más a esto (no es que A como padre aparezca dos veces):

    F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A

Si vuelcas un commit de git sin procesar, verás cero o más campos principales. Si hay cero, significa que no hay padre y la confirmación es una raíz (en realidad puede tener múltiples raíces). Si hay uno, significa que no hubo fusión y no es una confirmación de raíz. Si hay más de uno, significa que el commit es el resultado de una fusión y todos los padres después del primero son commits de fusión.

Paths simplified:
    F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X
Paths first parents only:
    F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X
    J->(I->(H->(G->(A->X))) | J->I->H->G->A->X
Or:
    F->D->C | J->I | I->H | C->B->A | H->G->A | A->X
Paths first parents only simplified:
    F->D->C->B->A | J->I->->G->A | A->X
Topological:
    - X - A - B - C - D - F (B1)
           \
            G - H - I - J (B2)

Cuando ambos golpeen A, su cadena será la misma, antes de eso su cadena será completamente diferente. La primera confirmación que otras dos confirmaciones tienen en común es el antepasado común y de dónde divergieron. Puede haber alguna confusión aquí entre los términos commit, branch y ref. De hecho, puede fusionar un commit. Esto es lo que realmente hace la fusión. Una referencia simplemente apunta a una confirmación y una rama no es más que una referencia en la carpeta .git / refs / heads, la ubicación de la carpeta es lo que determina que una referencia es una rama en lugar de algo más como una etiqueta.

Donde pierde la historia es que la fusión hará una de dos cosas dependiendo de las circunstancias.

Considerar:

      / - B (B1)
    - A
      \ - C (B2)

En este caso, una combinación en cualquier dirección creará un nuevo commit con el primer padre como el commit señalado por la rama desprotegida actual y el segundo padre como el commit en la punta de la rama que fusionó en su rama actual. Tiene que crear una nueva confirmación ya que ambas ramas tienen cambios desde su antepasado común que deben combinarse.

      / - B - D (B1)
    - A      /
      \ --- C (B2)

En este punto, D (B1) ahora tiene ambos conjuntos de cambios de ambas ramas (en sí y B2). Sin embargo, la segunda rama no tiene los cambios de B1. Si combina los cambios de B1 a B2 para que estén sincronizados, entonces puede esperar algo que se vea así (puede forzar git merge para que lo haga así sin embargo con --no-ff):

Expected:
      / - B - D (B1)
    - A      / \
      \ --- C - E (B2)
Reality:
      / - B - D (B1) (B2)
    - A      /
      \ --- C

Obtendrá eso incluso si B1 tiene confirmaciones adicionales. Mientras no haya cambios en B2 que B1 no tenga, las dos ramas se fusionarán. Hace un avance rápido que es como un rebase (los rebases también comen o linealizan el historial), excepto que, a diferencia de un rebase, ya que solo una rama tiene un conjunto de cambios, no tiene que aplicar un conjunto de cambios de una rama encima de otra.

From:
      / - B - D - E (B1)
    - A      /
      \ --- C (B2)
To:
      / - B - D - E (B1) (B2)
    - A      /
      \ --- C

Si deja de trabajar en B1, las cosas están muy bien para preservar la historia a largo plazo. Solo B1 (que podría ser maestro) avanzará normalmente, por lo que la ubicación de B2 en el historial de B2 representa con éxito el punto en que se fusionó con B1. Esto es lo que git espera que hagas, para bifurcar B de A, luego puedes fusionar A en B tanto como quieras a medida que se acumulan los cambios, sin embargo, al fusionar B de nuevo en A, no se espera que trabajes en B y más . Si continúa trabajando en su sucursal después de avanzar rápidamente fusionándola nuevamente en la sucursal en la que estaba trabajando, borre el historial anterior de B cada vez. Realmente estás creando una nueva bifurcación cada vez después de un avance rápido, compromete a la fuente y luego confirma a la bifurcación.

         0   1   2   3   4 (B1)
        /-\ /-\ /-\ /-\ /
    ----   -   -   -   -
        \-/ \-/ \-/ \-/ \
         5   6   7   8   9 (B2)

1 a 3 y 5 a 8 son ramas estructurales que aparecen si sigue el historial de 4 o 9. No hay forma en git de saber a cuál de estas ramas estructurales sin nombre y sin referencia pertenecen las ramas con nombre y de referencia como Fin de la estructura. Puede suponer de este dibujo que 0 a 4 pertenece a B1 y 4 a 9 pertenece a B2, pero aparte de 4 y 9 no se sabe qué rama pertenece a qué rama, simplemente lo he dibujado de una manera que da ilusión de eso. 0 podría pertenecer a B2 y 5 podrían pertenecer a B1. Hay 16 posibilidades diferentes en este caso de qué rama nombrada podría pertenecer cada una de las ramas estructurales.

Hay una serie de estrategias git que funcionan alrededor de esto. Puede forzar git merge para que nunca avance rápido y siempre cree una rama de fusión. Una forma horrible de preservar el historial de sucursales es con etiquetas y / o ramas (se recomiendan realmente las etiquetas) de acuerdo con alguna convención de su elección. Realmente no recomendaría una confirmación vacía ficticia en la rama en la que se está fusionando. Una convención muy común es no fusionarse en una rama de integración hasta que realmente desee cerrar su rama. Esta es una práctica a la que las personas deberían intentar adherirse, ya que de lo contrario estás trabajando en el punto de tener sucursales. Sin embargo, en el mundo real, el ideal no siempre es práctico, lo que significa que hacer lo correcto no es viable para cada situación. Si lo que tu

jgmjgm
fuente
"Git no conserva el historial de referencia" Lo hace, pero no por defecto y no por mucho tiempo. Ver man git-reflogy la parte sobre las fechas: "master@{one.week.ago} significa" donde el maestro solía señalar hace una semana en este repositorio local "". O la discusión sobre <refname>@{<date>}en man gitrevisions. Y core.reflogExpireen man git-config.
Patrick Mevzek
2

No es una solución para la pregunta, pero pensé que valía la pena señalar el enfoque que uso cuando tengo una rama de larga vida:

Al mismo tiempo que creo la rama, también creo una etiqueta con el mismo nombre pero con un -initsufijo, por ejemplo feature-branchyfeature-branch-init .

(¡Es un poco extraño que sea una pregunta tan difícil de responder!)

Stephen Darlington
fuente
1
Teniendo en cuenta la absoluta estupidez alucinante de diseñar un concepto de "rama" sin ninguna noción de cuándo y dónde se creó ... más las inmensas complicaciones de otras soluciones sugeridas, por personas que intentan ser inteligentes de esta manera demasiado inteligente Creo que preferiría tu solución. Solo es una carga la necesidad de RECORDAR hacer esto cada vez que crea una rama, algo que los usuarios de git están haciendo muy a menudo. Además, leí en alguna parte que las 'etiquetas tienen la pena de ser' pesadas '. Aún así, creo que eso es lo que creo que haré.
Motti Shneor
1

Una manera simple de hacer que sea más fácil ver el punto de ramificación git log --graphes usar la opción --first-parent.

Por ejemplo, tome el repositorio de la respuesta aceptada :

$ git log --all --oneline --decorate --graph

*   a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
|\  
| *   648ca35 (origin/topic) merging master onto topic
| |\  
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/  
|/|   
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

Ahora agregue --first-parent:

$ git log --all --oneline --decorate --graph --first-parent

* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
| * 648ca35 (origin/topic) merging master onto topic
| * 132ee2a first commit on topic branch
* | e7c863d commit on master after master was merged to topic
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

¡Eso lo hace más fácil!

Tenga en cuenta que si el repositorio tiene muchas ramas, querrá especificar las 2 ramas que está comparando en lugar de usar --all:

$ git log --decorate --oneline --graph --first-parent master origin/topic
binaryfunt
fuente
0

El problema parece ser encontrar el corte de compromiso único más reciente entre ambas ramas en un lado, y el antepasado común más antiguo en el otro (probablemente el compromiso inicial del repositorio). Esto coincide con mi intuición de cuál es el punto de "ramificación".

Eso en mente, esto no es nada fácil de calcular con los comandos normales de git shell, ya que git rev-list, nuestra herramienta más poderosa, no nos permite restringir la ruta por la cual se alcanza un commit. Lo más cercano que tenemos es git rev-list --boundary, lo que nos puede dar un conjunto de todos los commits que "bloquearon nuestro camino". (Nota:git rev-list --ancestry-path es interesante, pero no sé cómo hacerlo útil aquí).

Aquí está el script: https://gist.github.com/abortz/d464c88923c520b79e3d . Es relativamente simple, pero debido a un bucle es lo suficientemente complicado como para justificar una esencia.

Tenga en cuenta que la mayoría de las otras soluciones propuestas aquí no pueden funcionar en todas las situaciones por una simple razón: git rev-list --first-parentno es confiable en la linealización del historial porque puede haber fusiones con cualquier pedido.

git rev-list --topo-order, por otro lado, es muy útil, para realizar commits de caminata en orden topográfico, pero hacer diffs es frágil: existen múltiples posibles ordenamientos topográficos para un gráfico dado, por lo que dependerá de una cierta estabilidad de los ordenamientos. Dicho esto, la solución de strongk7 probablemente funciona muy bien la mayor parte del tiempo. Sin embargo, es más lento que el mío como resultado de tener que recorrer toda la historia del repositorio ... dos veces. :-)

Andrew Bortz
fuente
0

Lo siguiente implementa git equivalente de svn log --stop-on-copy y también se puede usar para encontrar el origen de la rama.

Acercarse

  1. Consigue cabeza para todas las ramas
  2. recopilar mergeBase para la rama de destino entre sí
  3. git.log e iterar
  4. Deténgase en la primera confirmación que aparece en la lista mergeBase

Como todos los ríos corren hacia el mar, todas las ramas corren para dominar y, por lo tanto, encontramos una base de fusión entre ramas aparentemente no relacionadas. A medida que retrocedemos desde la cabeza de la rama a través de los antepasados, podemos detenernos en la primera base de fusión potencial, ya que en teoría debería ser el punto de origen de esta rama.

Notas

  • No he probado este enfoque donde las ramas de hermanos y primos se fusionaron entre sí.
  • Sé que debe haber una mejor solución.

detalles: https://stackoverflow.com/a/35353202/9950

Peter Kahn
fuente
-1

Puede usar el siguiente comando para devolver la confirmación más antigua en branch_a, a la que no se puede acceder desde el maestro:

git rev-list branch_a ^master | tail -1

Tal vez con una comprobación de cordura adicional de que el padre de ese compromiso es realmente accesible desde el maestro ...

Marc Richarme
fuente
1
Esto no funciona Si branch_a se fusiona en master una vez, y luego continúa, los commits en esa fusión se considerarían parte de master, por lo que no aparecerían en ^ master.
FelipeC
-2

Puede examinar el registro de la rama A para encontrar desde qué confirmación se creó, así como el historial completo de las confirmaciones a las que apuntó esa rama. Los registros están en .git/logs.

Paggas
fuente
2
No creo que esto funcione en general porque el reflog puede ser eliminado. Y tampoco creo que los reflogs (?) Se presionen, por lo que esto solo funcionaría en una situación de repositorio único.
GaryO
-2

Creo que he encontrado una manera de lidiar con todos los casos de esquina mencionados aquí:

branch=branch_A
merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1)
git merge-base $merge^1 $merge^2

Charles Bailey tiene toda la razón en que las soluciones basadas en el orden de los antepasados ​​tienen un valor limitado; al final del día, necesita algún tipo de registro de "esta confirmación vino de la rama X", pero dicho registro ya existe; de forma predeterminada, 'git merge' usaría un mensaje de confirmación como "Combinar rama 'branch_A' en maestro", esto le indica que todas las confirmaciones del segundo padre (commit ^ 2) provienen de 'branch_A' y se fusionaron con la primera padre (commit ^ 1), que es 'maestro'.

Armado con esta información, puede encontrar la primera combinación de 'branch_A' (que es cuando 'branch_A' realmente surgió), y encontrar la base de combinación, que sería el punto de ramificación :)

He intentado con los repositorios de Mark Booth y Charles Bailey y la solución funciona; como no pudo La única forma en que esto no funcionaría es si ha cambiado manualmente el mensaje de confirmación predeterminado para las fusiones, de modo que la información de la rama se pierda realmente.

Por utilidad:

[alias]
    branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2'

Entonces puedes hacer ' git branch-point branch_A'.

Disfrutar;)

FelipeC
fuente
44
Confiar en los mensajes de fusión es más frágil que formular hipótesis sobre el orden principal. No es solo una situación hipotética tampoco; Frecuentemente uso git merge -mpara decir lo que he fusionado en lugar del nombre de una rama potencialmente efímera (por ejemplo, "fusionar los cambios de la línea principal en la función xyz refactor"). ¿Y si hubiera sido menos útil con mi -men mi ejemplo? El problema simplemente no es soluble en su generalidad completa porque puedo hacer la misma historia con una o dos ramas temporales y no hay forma de notar la diferencia.
CB Bailey
1
@CharlesBailey Ese es tu problema entonces. No debe eliminar esas líneas del mensaje de confirmación, debe agregar el resto del mensaje debajo del mensaje original. Hoy en día, 'git merge' abre automáticamente un editor para que pueda agregar lo que desee, y para versiones antiguas de git puede hacer 'git merge --edit'. De cualquier manera, puede usar un enlace de confirmación para agregar un "Compromiso en la rama 'foo'" a cada confirmación si eso es lo que realmente quiere. Sin embargo, esta solución funciona para la mayoría de las personas .
FelipeC
2
No funciono para mi. La branch_A se bifurcó fuera de master que ya tenía muchas fusiones. Esta lógica no me dio el hash de confirmación exacto donde se creó branch_A.
Venkat Kotra