Trouver un point de branchement avec Git?

455

J'ai un référentiel avec les branches master et A et beaucoup d'activités de fusion entre les deux. Comment puis-je trouver la validation dans mon référentiel lorsque la branche A a été créée sur la base du maître?

Mon référentiel ressemble à ceci:

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

Je cherche la révision A, ce qui n'est pas ce qui git merge-base (--all)se trouve.

Jochen
la source

Réponses:

499

Je cherchais la même chose, et j'ai trouvé cette question. Merci de le demander!

Cependant, j'ai trouvé que les réponses que je vois ici ne semblent pas tout à fait donner la réponse que vous avez demandée (ou que je cherchais) - elles semblent donner le Gcommit, au lieu du Acommit.

J'ai donc créé l'arborescence suivante (lettres attribuées dans l'ordre chronologique), afin de pouvoir tester les choses:

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

Cela semble un peu différent du vôtre, car je voulais m'assurer d'avoir (en référence à ce graphique, pas le vôtre) B, mais pas A (et pas D ou E). Voici les lettres attachées aux préfixes SHA et aux messages de commit (mon dépôt peut être cloné à partir d' ici , si cela intéresse quelqu'un):

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

Ainsi, l' objectif: trouver B . Voici trois façons que j'ai trouvées, après un peu de bricolage:


1. visuellement, avec gitk:

Vous devriez voir visuellement un arbre comme celui-ci (vu du maître):

capture d'écran gitk du maître

ou ici (vu du sujet):

capture d'écran gitk à partir du sujet

dans les deux cas, j'ai sélectionné le commit qui se trouve Bdans mon graphique. Une fois que vous avez cliqué dessus, son SHA complet est présenté dans un champ de saisie de texte juste en dessous du graphique.


2. visuellement, mais depuis le terminal:

git log --graph --oneline --all

(Édition / note latérale: l'ajout --decoratepeut également être intéressant; il ajoute une indication des noms de branche, des balises, etc. Ne pas ajouter cela à la ligne de commande ci-dessus car la sortie ci-dessous ne reflète pas son utilisation.)

qui montre (en supposant git config --global color.ui auto):

sortie de git log --graph --oneline --all

Ou, en texte simple:

* a9546a2 fusion du sujet au maître
| \  
| * 648ca35 fusionnant le maître sur le sujet
| | \  
| * | 132ee2a premier commit sur la branche du sujet
* | | e7c863d validation sur le maître après la fusion du maître au sujet
| | /  
| / |   
* | 37ad159 validation post-branchement sur le maître
| /  
* 6aafd7f deuxième validation sur le maître avant la branche
* 4112403 validation initiale sur le maître

dans les deux cas, nous considérons la validation 6aafd7f comme le point commun le plus bas, c'est- Bà- dire dans mon graphique ou Adans le vôtre.


3. Avec la magie des coquillages:

Vous ne spécifiez pas dans votre question si vous vouliez quelque chose comme ce qui précède, ou une seule commande qui ne vous donnera qu'une seule révision, et rien d'autre. Eh bien, voici ce dernier:

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

Que vous pouvez également mettre dans votre ~ / .gitconfig en tant que (note: le tiret arrière est important; merci Brian pour avoir attiré l'attention sur cela) :

[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' -

Ce qui pourrait être fait via la ligne de commande suivante (compliquée par des guillemets):

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'\'' -'

Note: zshpourrait tout aussi bien avoir été bash, mais le shfera pas le travail - la <()syntaxe n'existe pas dans la vanille sh. (Merci encore, @conny, de m'en avoir fait part dans un commentaire sur une autre réponse sur cette page!)

Remarque: autre version de ce qui précède:

Merci à liori pour avoir souligné que ce qui précède pourrait tomber lors de la comparaison de branches identiques, et trouver une autre forme de diff qui supprime la forme sed du mélange et rend cela "plus sûr" (c'est-à-dire qu'il renvoie un résultat (à savoir, le commit le plus récent) même lorsque vous comparez master à master):

En tant que ligne .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 coque:

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'\'' -'

Donc, dans mon arbre de test (qui n'était pas disponible pendant un certain temps, désolé; il est de retour), cela fonctionne maintenant à la fois sur le maître et le sujet (en donnant les commits G et B, respectivement). Merci encore, liori, pour la forme alternative.


C'est donc ce que j'ai [et liori] avec. Il semble fonctionner pour moi. Il autorise également quelques alias supplémentaires qui pourraient s'avérer utiles:

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

Heureux git-ing!

lindes
la source
5
Merci lindes, l'option shell est idéale pour les situations où vous souhaitez trouver le point de branchement d'une branche de maintenance de longue durée. Lorsque vous recherchez une révision qui pourrait être un millier de commits dans le passé, les options visuelles ne vont vraiment pas la couper. * 8 ')
Mark Booth
4
Dans votre troisième méthode, vous dépendez du fait que le contexte affichera la première ligne inchangée. Cela ne se produira pas dans certains cas extrêmes ou si vous avez des exigences légèrement différentes (par exemple, je n'ai besoin que de l'une des histoires --first-parent, et j'utilise cette méthode dans un script qui peut parfois utiliser la même chose). branches des deux côtés). Je l' ai trouvé plus sûr d'utiliser diffest si-then-else et le mode d' effacement a changé / supprimé les lignes de sa sortie au lieu de compter sur le contexte ayant assez grand, par.: 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)~fonctionnera aussi, je pense
Tobias Schulte
4
@ JakubNarębski: Avez-vous un moyen git merge-base --fork-point ...de donner le commit B (commit 6aafd7f) pour cet arbre? J'étais occupé avec d'autres trucs quand tu as posté ça, et ça sonnait bien, mais je l'ai finalement juste essayé, et je ne le fais pas fonctionner ... Je reçois quelque chose de plus récent, ou juste un échec silencieux (pas d'erreur message, mais l' état de la sortie 1), en essayant des arguments tels que 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(soit pour la caisse), etc. y at - il quelque chose que je fais mal ou manquant?
lindes
25
@ JakubNarębski @lindes --fork-pointest basé sur le reflog, donc cela ne fonctionnera que si vous avez fait les changements localement. Même alors, les entrées de reflog pourraient avoir expiré. C'est utile mais pas fiable du tout.
Daniel Lubarov
131

Vous cherchez peut-être git merge-base:

git merge-base trouve les meilleurs ancêtres communs entre deux validations à utiliser dans une fusion à trois voies. Un ancêtre commun vaut mieux qu'un autre ancêtre commun si ce dernier est un ancêtre du premier. Un ancêtre commun qui n'a pas de meilleur ancêtre commun est un meilleur ancêtre commun , c'est-à-dire une base de fusion . Notez qu'il peut y avoir plus d'une base de fusion pour une paire de validations.

Greg Hewgill
la source
10
Notez également l' --alloption " git merge-base"
Jakub Narębski
10
Cela ne répond pas à la question d'origine, mais la plupart des gens posent la question beaucoup plus simple pour laquelle c'est la réponse :)
FelipeC
6
il a dit qu'il ne voulait pas le résultat de la fusion de git
Tom Tanner
9
@TomTanner: Je viens de regarder l'historique des questions et la question d'origine a été modifiée pour inclure cette note environ git merge-basecinq heures après la publication de ma réponse (probablement en réponse à ma réponse). Néanmoins, je laisserai cette réponse telle quelle car elle peut toujours être utile pour quelqu'un d'autre qui trouve cette question via la recherche.
Greg Hewgill
Je pense que vous pouvez utiliser les résultats de merge-base --all, puis parcourir la liste des validations de retour en utilisant merge-base --is-ancestor pour trouver le plus ancien ancêtre commun dans la liste, mais ce n'est pas le moyen le plus simple .
Matt_G
42

Je l'ai utilisé git rev-listpour ce genre de chose. Par exemple, (notez les 3 points)

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

va cracher le point de branchement. Maintenant, ce n'est pas parfait; puisque vous avez fusionné le maître dans la branche A plusieurs fois, cela divisera quelques points de branche possibles (en gros, le point de branche d'origine, puis chaque point auquel vous avez fusionné le maître dans la branche A). Cependant, cela devrait au moins réduire les possibilités.

J'ai ajouté cette commande à mes alias en ~/.gitconfigtant que:

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

donc je peux l'appeler comme:

$ git diverges branch-a master
mipadi
la source
Remarque: cela semble donner le premier commit sur la branche, plutôt que l'ancêtre commun. (c'est-à-dire qu'il donne Gau lieu de A, selon le graphique de la question d'origine.) J'ai une réponse qui obtient A, que je publierai actuellement.
lindes
@lindes: Il donne l'ancêtre commun dans tous les cas où je l'ai essayé. Avez-vous un exemple où ce n'est pas le cas?
mipadi
Oui. Dans ma réponse (qui a un lien vers un référentiel, vous pouvez cloner; git checkout topicpuis l'exécuter topicà la place de branch-a), il répertorie 648ca357b946939654da12aaf2dc072763f3caeeet 37ad15952db5c065679d7fc31838369712f0b338- les deux 37ad159et 648ca35sont dans l'ancêtre des branches actuelles (cette dernière étant la TÊTE actuelle de topic), mais ce n'est pas non plus le point avant que la ramification ne se produise. Obtenez-vous quelque chose de différent?
lindes
@lindes: Je n'ai pas pu cloner votre dépôt (peut-être un problème d'autorisations?).
mipadi
Oops désolé! Merci de me l'avoir dit. J'ai oublié d'exécuter git update-server-info. Ça devrait être bon d'y aller maintenant. :)
lindes
31

Si vous aimez les commandes laconiques,

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

Voici une explication.

La commande suivante vous donne la liste de toutes les validations dans master qui se sont produites après la création de nom_branche

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

Puisque vous ne vous souciez que du premier de ces commits, vous voulez la dernière ligne de la sortie:

git rev-list ^ branch_name --premier parent principal | queue -n1

Le parent du premier commit qui n'est pas un ancêtre de "nom_branche" est, par définition, dans "nom_branche" et est dans "maître" car il est l'ancêtre de quelque chose dans "maître". Vous avez donc le premier commit dans les deux branches.

La commande

git rev-list commit ^^!

est juste un moyen d'afficher la référence de validation parent. Vous pourriez utiliser

git log -1 commit ^

ou peu importe.

PS: Je ne suis pas d'accord avec l'argument selon lequel l'ordre des ancêtres n'est pas pertinent. Cela dépend de ce que vous voulez. Par exemple, dans ce cas

_C1___C2_______ maître
  \ \ _XXXXX_ branche A (les X indiquent des croisements arbitraires entre le maître et A)
   \ _____ / branche B

il est parfaitement logique de sortir C2 en tant que commit de "branchement". C'est à ce moment que le développeur s'est éloigné de "maître". Lorsqu'il s'est ramifié, la branche "B" n'a même pas été fusionnée dans sa branche! C'est ce que donne la solution dans ce post.

Si ce que vous voulez est le dernier commit C tel que tous les chemins depuis l'origine jusqu'au dernier commit sur la branche "A" passent par C, alors vous voulez ignorer l'ordre d'ascendance. C'est purement topologique et vous donne une idée de depuis quand vous avez deux versions du code en même temps. C'est à ce moment que vous opteriez pour des approches basées sur la fusion, et cela retournera C1 dans mon exemple.

Lionel
la source
3
C'est de loin la réponse la plus claire, faisons voter cela en haut. Un montage suggéré: git rev-list commit^^!peut être simplifié commegit rev-parse commit^
Russell Davis
Ça devrait être la réponse!
trinth
2
Cette réponse est sympa, je viens de la remplacer git rev-list --first-parent ^branch_name masterpar git rev-list --first-parent branch_name ^mastercar si la branche master est 0 validée devant l'autre branche (qui peut lui être rapidement transmise), aucune sortie ne sera créée. Avec ma solution, aucune sortie n'est créée si le maître est strictement en avance (c'est-à-dire que la branche a été complètement fusionnée), c'est ce que je veux.
Michael Schmeißer
3
Cela ne fonctionnera que si je manque totalement quelque chose. Il y a des fusions dans les deux sens dans les branches d'exemple. Il semble que vous ayez essayé de prendre cela en compte, mais je pense que cela entraînera l'échec de votre réponse. git rev-list --first-parent ^topic masterne vous ramènera au premier commit qu'après la dernière fusion de masterinto topic(si cela existe même).
jerry
1
@jerry Vous avez raison, cette réponse est des ordures; par exemple, dans le cas où une fusion vient de se produire (fusionné master dans topic), et master n'a plus de nouveau commit après cela, la première commande git rev-list --first-parent ne produit rien du tout.
TamaMcGlinn
19

Étant donné que tant de réponses dans ce fil ne donnent pas la réponse à la question, voici un résumé des résultats de chaque solution, ainsi que le script que j'ai utilisé pour répliquer le référentiel donné dans la question.

Le journal

En créant un dépôt avec la structure donnée, nous obtenons le journal 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

Mon seul ajout, c'est la balise qui rend explicite le point auquel nous avons créé la branche et donc le commit que nous souhaitons trouver.

La solution qui fonctionne

La seule solution qui fonctionne est celle fournie par lindes renvoie correctement A:

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

Comme le souligne Charles Bailey , cette solution est très fragile.

Si vous branch_Aen masterpuis fusionner masteren branch_Asans intervenir commits alors la solution de lindes ne vous donne le plus récent en premier Divergance .

Cela signifie que pour mon flux de travail, je pense que je vais devoir m'en tenir au balisage du point de branchement des branches de longue durée, car je ne peux pas garantir qu'elles pourront être trouvées de manière fiable plus tard.

Tout cela se résume vraiment à l' gitabsence de ce qu'on hgappelle des branches nommées . Le blogueur jhw appelle ces lignées vs familles dans son article Why I Like Mercurial More Than Git et son article de suivi More On Mercurial vs. Git (with Graphs!) . Je recommanderais aux gens de les lire pour voir pourquoi certains convertis mercuriels manquent de ne pas avoir nommé de branches dansgit .

Les solutions qui ne fonctionnent pas

La solution apportée par mipadi renvoie deux réponses, Iet C:

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

La solution apportée par Greg Hewgill retourI

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

La solution apportée par Karl revient X:

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

Le scénario

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"

Je doute que la version git fasse beaucoup de différence, mais:

$ git --version
git version 1.7.1

Merci à Charles Bailey de m'avoir montré une manière plus compacte de scripter le référentiel d'exemples.

Mark Booth
la source
2
La solution par Karl est facile à fixer: diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1. Merci d'avoir fourni des instructions pour créer le
dépôt
Je pense que vous voulez dire "La variante nettoyée de la solution fournie par Karl renvoie X". L'original a bien fonctionné, il était juste moche :-)
Karl
Non, votre original ne fonctionne pas bien. Certes, la variation fonctionne encore pire. Mais l'ajout de l'option --topo-order fait fonctionner votre version :)
FelipeC
@felipec - Voir mon dernier commentaire sur la réponse de Charles Bailey . Hélas, notre chat (et donc tous les anciens commentaires) ont maintenant été supprimés. J'essaierai de mettre à jour ma réponse lorsque j'aurai le temps.
Mark Booth
Intéressant. Je suppose que la topologie est la valeur par défaut. Silly me :-)
Karl
10

En général, ce n'est pas possible. Dans un historique de branche, une branche et fusion avant qu'une branche nommée ait été branchée et une branche intermédiaire de deux branches nommées se ressemblent.

Dans git, les branches ne sont que les noms actuels des conseils de sections de l'histoire. Ils n'ont pas vraiment d'identité forte.

Ce n'est généralement pas un gros problème car la base de fusion (voir la réponse de Greg Hewgill) de deux validations est généralement beaucoup plus utile, donnant la validation la plus récente partagée par les deux branches.

Une solution reposant sur l'ordre des parents d'un commit ne fonctionnera évidemment pas dans les situations où une branche a été complètement intégrée à un moment donné de son histoire.

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"

Cette technique échoue également si une fusion d'intégration a été effectuée avec les parents inversés (par exemple, une branche temporaire a été utilisée pour effectuer une fusion de test dans le maître, puis avancée rapidement dans la branche de fonctionnalité pour continuer).

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
la source
Continuons cette discussion dans le chat
Mark Booth
3
Merci Charles, vous m'avez convaincu, si je veux savoir à quel point la branche a divergé à l'origine , je vais devoir la marquer. Je souhaite vraiment que gitavait un équivalent à hg« s nommé branches, il ferait la gestion des branches d'entretien à long vécu de façon beaucoup plus facile.
Mark Booth
1
"Dans git, les branches ne sont que les noms actuels des bouts de sections de l'histoire. Elles n'ont pas vraiment une identité forte" C'est une chose effrayante à dire et m'a convaincu que j'ai besoin de mieux comprendre les branches Git - merci (+ 1)
Dumbledad
Dans un historique de branche, une branche et fusion avant qu'une branche nommée ait été branchée et une branche intermédiaire de deux branches nommées se ressemblent. Oui. +1.
user1071847
5

Que diriez-vous de quelque chose comme

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
la source
Cela marche. C'est vraiment lourd, mais c'est la seule chose que j'ai trouvée qui semble vraiment faire le travail.
GaryO
1
Équivalent d'alias Git: diverges = !bash -c 'git rev-parse $(diff <(git log --pretty=oneline ${1}) <(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'(sans fichiers temporaires)
conny
@conny: Oh, wow - je n'avais jamais vu la syntaxe <(foo) ... c'est incroyablement utile, merci! (Fonctionne également en zsh, FYI.)
lindes
cela semble me donner le premier commit sur la branche, plutôt que l'ancêtre commun. (c'est-à-dire qu'il donne Gau lieu de A, selon le graphique de la question d'origine.) Je pense que j'ai trouvé une réponse, cependant, que je publierai actuellement.
lindes
Au lieu de 'git log --pretty = oneline', vous pouvez simplement faire 'git rev-list', alors vous pouvez également sauter la coupe, de plus, cela donne au parent la validation du point de divergence, donc juste tail -2 | tête 1. Donc:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC
5

je manque sûrement quelque chose, mais à mon avis, tous les problèmes ci-dessus sont causés parce que nous essayons toujours de trouver le point de branchement remontant dans l'histoire, et cela provoque toutes sortes de problèmes en raison des combinaisons de fusion disponibles.

Au lieu de cela, j'ai suivi une approche différente, basée sur le fait que les deux branches partagent beaucoup d'histoire, exactement toute l'histoire avant la ramification est la même à 100%, donc au lieu de revenir en arrière, ma proposition est d'aller de l'avant (à partir du 1er commit), à la recherche de la 1ère différence dans les deux branches. Le point de branchement sera, simplement, le parent de la première différence trouvée.

En pratique:

#!/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

Et ça résout tous mes cas habituels. Bien sûr, il y a des frontières non couvertes mais ... ciao :-)

stronk7
la source
diff <(git rev-list "$ {1: -master}" --first-parent) <(git rev-list "$ {2: -HEAD}" --first-parent) -U1 | tail -1
Alexander Bird
j'ai trouvé que c'était plus rapide (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

Après beaucoup de recherches et de discussions, il est clair qu'il n'y a pas de solution miracle qui fonctionnerait dans toutes les situations, du moins pas dans la version actuelle de Git.

C'est pourquoi j'ai écrit quelques patchs qui ajoutent le concept d'une tailbranche. Chaque fois qu'une branche est créée, un pointeur vers le point d'origine est également créé, la tailréf. Cette référence est mise à jour chaque fois que la branche est rebasée.

Pour connaître le point de branchement de la branche devel, tout ce que vous avez à faire est d'utiliser devel@{tail}, c'est tout.

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

FelipeC
la source
1
Pourrait être la seule solution stable. Avez-vous vu si cela pouvait entrer dans git? Je n'ai pas vu de demande de retrait.
Alexander Klimetschek
@AlexanderKlimetschek Je n'ai pas envoyé les correctifs, et je ne pense pas que ceux-ci seraient acceptés. Cependant, j'ai essayé une méthode différente: un crochet "update-branch" qui fait quelque chose de très similaire. De cette façon, par défaut, Git ne ferait rien, mais vous pouvez activer le hook pour mettre à jour la branche de queue. Cependant, vous n'auriez pas devel @ {tail}, mais ce ne serait pas si mal d'utiliser à la place tails / devel.
FelipeC
3

Voici une version améliorée de ma réponse précédente réponse précédente . Il s'appuie sur les messages de validation des fusions pour trouver où la branche a été créée pour la première fois.

Il fonctionne sur tous les référentiels mentionnés ici, et j'en ai même traité quelques-uns difficiles qui sont apparus sur la liste de diffusion . J'ai également écrit des tests pour cela.

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
la source
3

La commande suivante révélera le SHA1 du Commit A

git merge-base --fork-point A

Gayan Pathirage
la source
Cela ne se produit pas si les branches parent et enfant ont des fusions intermédiaires entre elles.
nawfal
2

Il me semble que j'obtiens de la joie

git rev-list branch...master

La dernière ligne que vous obtenez est le premier commit sur la branche, il s'agit donc d'obtenir le parent de cela. Donc

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

Semble fonctionner pour moi et n'a pas besoin de diff et ainsi de suite (ce qui est utile car nous n'avons pas cette version de diff)

Correction: cela ne fonctionne pas si vous êtes sur la branche principale, mais je le fais dans un script, donc c'est moins un problème

Tom Tanner
la source
2

Pour trouver des validations à partir du point de branchement, vous pouvez utiliser ceci.

git log --ancestry-path master..topicbranch
Andor
la source
Cette commande ne fonctionne pas pour moi sur l'exemple donné. Veuillez fournir quels paramètres fourniriez-vous pour la plage de validation?
Jesper Rønn-Jensen
2

Parfois, c'est effectivement impossible (à quelques exceptions près où vous pourriez avoir la chance d'avoir des données supplémentaires) et les solutions ici ne fonctionneront pas.

Git ne conserve pas l'historique des références (qui inclut les branches). Il stocke uniquement la position actuelle de chaque branche (la tête). Cela signifie que vous pouvez perdre un peu d'historique de branche dans git au fil du temps. Chaque fois que vous branchez par exemple, vous perdez immédiatement la branche d'origine. Une succursale ne fait que:

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

Vous pouvez supposer que le premier engagé est la branche. Cela a tendance à être le cas, mais ce n'est pas toujours le cas. Rien ne vous empêche de vous engager dans l'une ou l'autre branche en premier après l'opération ci-dessus. De plus, les horodatages git ne sont pas garantis pour être fiables. Ce n'est que lorsque vous vous engagez à la fois qu'ils deviennent vraiment des branches structurellement.

Alors que dans les diagrammes, nous avons tendance à numéroter les validations de manière conceptuelle, git n'a pas de véritable concept stable de séquence lorsque l'arbre de validation se branche. Dans ce cas, vous pouvez supposer que les nombres (indiquant l'ordre) sont déterminés par l'horodatage (il peut être amusant de voir comment une interface utilisateur git gère les choses lorsque vous définissez tous les horodatages de la même manière).

C'est ce qu'un humain attend conceptuellement:

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

Voici ce que vous obtenez réellement:

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

Vous supposeriez que B1 soit la branche d'origine, mais cela pourrait en fait être simplement une branche morte (quelqu'un a vérifié -b mais ne s'y est jamais engagé). Ce n'est que lorsque vous vous engagez à la fois que vous obtenez une structure de branche légitime dans git:

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

Vous savez toujours que C1 est venu avant C2 et C3 mais vous ne savez jamais de manière fiable si C2 est venu avant C3 ou C3 est venu avant C2 (parce que vous pouvez régler l'heure sur votre poste de travail sur n'importe quoi par exemple). B1 et B2 sont également trompeurs car vous ne pouvez pas savoir quelle branche est arrivée en premier. Dans bien des cas, vous pouvez faire une très bonne estimation, généralement précise. C'est un peu comme une piste de course. Toutes choses étant généralement égales aux voitures, vous pouvez supposer qu'une voiture qui arrive dans un tour derrière a commencé un tour derrière. Nous avons également des conventions qui sont très fiables, par exemple, master représentera presque toujours les branches les plus anciennes, bien que j'ai malheureusement vu des cas où même ce n'est pas le cas.

L'exemple donné ici est un exemple préservant l'historique:

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)

Le vrai ici est également trompeur parce que nous, les humains, le lisons de gauche à droite, de la racine à la feuille (réf). Git ne fait pas ça. Où nous faisons (A-> B) dans nos têtes git fait (A <-B ou B-> A). Il le lit de ref à root. Les références peuvent être n'importe où mais ont tendance à être des feuilles, au moins pour les branches actives. Une référence pointe vers un commit et les commits ne contiennent qu'un like pour leurs parents, pas pour leurs enfants. Lorsqu'un commit est un commit de fusion, il aura plusieurs parents. Le premier parent est toujours le commit d'origine qui a été fusionné. Les autres parents sont toujours des validations qui ont été fusionnées dans la validation d'origine.

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

Ce n'est pas une représentation très efficace, mais plutôt une expression de tous les chemins que git peut emprunter à chaque référence (B1 et B2).

Le stockage interne de Git ressemble plus à ceci (pas que A en tant que parent apparaisse deux fois):

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

Si vous sauvegardez un commit git brut, vous verrez zéro ou plusieurs champs parents. S'il y a zéro, cela signifie qu'il n'y a pas de parent et que la validation est une racine (vous pouvez en fait avoir plusieurs racines). S'il y en a un, cela signifie qu'il n'y a pas eu de fusion et que ce n'est pas un commit root. S'il y en a plusieurs, cela signifie que la validation est le résultat d'une fusion et que tous les parents après la première sont des validations de fusion.

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)

Lorsque les deux frappent A, leur chaîne sera la même, avant cela, leur chaîne sera entièrement différente. Le premier commit que deux autres commits ont en commun est l'ancêtre commun et d'où ils ont divergé. il pourrait y avoir une certaine confusion ici entre les termes commit, branch et ref. Vous pouvez en fait fusionner un commit. C'est ce que fait vraiment la fusion. Une référence pointe simplement vers un commit et une branche n'est rien d'autre qu'une référence dans le dossier .git / refs / heads, l'emplacement du dossier est ce qui détermine qu'une référence est une branche plutôt que quelque chose d'autre comme une balise.

Là où vous perdez l'histoire, c'est que la fusion fera l'une des deux choses selon les circonstances.

Considérer:

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

Dans ce cas, une fusion dans l'une ou l'autre direction créera un nouveau commit avec le premier parent comme commit pointé par la branche extraite actuelle et le second parent comme commit à l'extrémité de la branche que vous avez fusionné dans votre branche actuelle. Il doit créer un nouveau commit car les deux branches ont des changements depuis leur ancêtre commun qui doivent être combinés.

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

À ce stade, D (B1) a maintenant les deux ensembles de changements des deux branches (lui-même et B2). Cependant, la deuxième branche n'a pas les changements de B1. Si vous fusionnez les modifications de B1 à B2 afin qu'elles soient synchronisées, vous pouvez vous attendre à quelque chose qui ressemble à ceci (vous pouvez cependant forcer git merge à le faire comme ceci avec --no-ff):

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

Vous obtiendrez cela même si B1 a des commits supplémentaires. Tant qu'il n'y aura pas de changements dans B2 que B1 n'a pas, les deux branches seront fusionnées. Il fait une avance rapide qui ressemble à un rebase (les rebases mangent ou linéarisent également l'historique), sauf contrairement à un rebase car une seule branche a un ensemble de changements, il n'est pas nécessaire d'appliquer un ensemble de changements d'une branche en plus de celui d'une autre.

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

Si vous cessez de travailler sur B1, alors les choses sont très bien pour préserver l'histoire à long terme. Seul B1 (qui pourrait être maître) avancera généralement, donc l'emplacement de B2 dans l'histoire de B2 représente avec succès le point de fusion avec B1. C'est ce que git attend de vous, pour dériver B de A, alors vous pouvez fusionner A en B autant que vous le souhaitez à mesure que les modifications s'accumulent, mais lors de la fusion de B en A, il n'est pas prévu que vous travailliez sur B et plus loin . Si vous continuez à travailler sur votre branche après l'avoir rapidement fusionnée dans la branche sur laquelle vous travailliez, puis effacez l'historique précédent de B à chaque fois. Vous créez vraiment une nouvelle branche à chaque fois après une avance rapide sur la source, puis sur la branche.

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

1 à 3 et 5 à 8 sont des branches structurelles qui s'affichent si vous suivez l'historique pour 4 ou 9. Il n'y a aucun moyen de savoir à laquelle de ces branches structurelles non nommées et non référencées appartiennent les branches nommées et références comme fin de la structure. Vous pourriez supposer à partir de ce dessin que 0 à 4 appartient à B1 et 4 à 9 appartient à B2 mais à part 4 et 9 ne pouvait pas savoir quelle branche appartient à quelle branche, je l'ai simplement dessinée d'une manière qui donne la illusion de cela. 0 pourrait appartenir à B2 et 5 pourrait appartenir à B1. Il y a 16 possibilités différentes dans ce cas dont la branche nommée à laquelle chacune des branches structurelles pourrait appartenir.

Il existe un certain nombre de stratégies Git qui fonctionnent autour de cela. Vous pouvez forcer git merge à ne jamais avancer rapidement et toujours créer une branche de fusion. Une horrible façon de préserver l'historique des branches est d'utiliser des balises et / ou des branches (les balises sont vraiment recommandées) selon une convention de votre choix. Je ne recommanderais vraiment pas un commit vide factice dans la branche dans laquelle vous fusionnez. Une convention très courante consiste à ne pas fusionner dans une branche d'intégration tant que vous ne voulez pas vraiment fermer votre branche. C'est une pratique que les gens devraient essayer d'adhérer, sinon vous travaillez sur le point d'avoir des succursales. Cependant, dans le monde réel, l'idéal n'est pas toujours pratique, ce qui signifie que faire la bonne chose n'est pas viable dans toutes les situations. Si ce que vous '

jgmjgm
la source
"Git ne conserve pas l'historique des références" Il le fait, mais pas par défaut et pas pendant une longue période. Voir man git-refloget la partie sur les dates: "master@{one.week.ago} signifie" où master avait l'habitude de pointer il y a une semaine dans ce dépôt local "". Ou la discussion sur <refname>@{<date>}in man gitrevisions. Et core.reflogExpiredans man git-config.
Patrick Mevzek
2

Pas tout à fait une solution à la question, mais j'ai pensé qu'il valait la peine de noter l'approche que j'utilise lorsque j'ai une branche de longue date:

En même temps je crée la branche, je crée aussi un tag avec le même nom mais avec un -init suffixe, par exemple feature-branchetfeature-branch-init .

(C'est assez bizarre que ce soit une question tellement difficile à répondre!)

Stephen Darlington
la source
1
Compte tenu de la stupidité stupéfiante de concevoir un concept de "branche" sans aucune notion de quand et où il a été créé ... plus les immenses complications des autres solutions suggérées - par des gens qui essaient de devenir trop intelligents de cette façon trop intelligente chose, je pense que je préférerais votre solution. Seulement, cela oblige la personne à se rappeler de le faire chaque fois que vous créez une branche - une chose que les utilisateurs de git font très souvent. De plus - j'ai lu quelque part que les «tags ont une pénalité d'être« lourds ». Pourtant, je pense que c'est ce que je pense que je vais faire.
Motti Shneor
1

Un moyen simple de rendre plus facile la visualisation du point de branchement git log --graphconsiste à utiliser l'option --first-parent.

Par exemple, prenez le dépôt de la réponse acceptée :

$ 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

Ajoutez maintenant --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

C'est plus simple!

Notez que si le dépôt a beaucoup de branches, vous voudrez spécifier les 2 branches que vous comparez au lieu d'utiliser --all:

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

Le problème semble être de trouver la coupe à validation unique la plus récente entre les deux branches d'un côté et la plus ancienne ancêtre commun de l'autre (probablement la validation initiale du dépôt). Cela correspond à mon intuition de ce qu'est le point de "ramification".

Cela à l'esprit, ce n'est pas du tout facile à calculer avec les commandes git shell normales, car git rev-list- notre outil le plus puissant - ne nous permet pas de restreindre le chemin par lequel une validation est atteinte. Le plus proche que nous avons est git rev-list --boundary, ce qui peut nous donner un ensemble de tous les commits qui "ont bloqué notre chemin". (Remarque:git rev-list --ancestry-path c'est intéressant mais je ne sais pas comment le rendre utile ici.)

Voici le script: https://gist.github.com/abortz/d464c88923c520b79e3d . C'est relativement simple, mais en raison d'une boucle, c'est assez compliqué pour justifier un résumé.

Notez que la plupart des autres solutions proposées ici ne peuvent pas fonctionner dans toutes les situations pour une raison simple: git rev-list --first-parent n'est pas fiable dans l'historique de linéarisation car il peut y avoir des fusions avec l'une ou l'autre commande.

git rev-list --topo-order, d'autre part, est très utile - pour les commits de marche dans l'ordre topographique - mais faire des différences est fragile: il existe plusieurs ordres topographiques possibles pour un graphique donné, vous dépendez donc d'une certaine stabilité des ordres. Cela dit, la solution de strongk7 fonctionne probablement très bien la plupart du temps. Cependant, il est plus lent que le mien car il a dû parcourir toute l'histoire du repo ... deux fois. :-)

Andrew Bortz
la source
0

Ce qui suit implémente l'équivalent git de svn log --stop-on-copy et peut également être utilisé pour trouver l'origine de la branche.

Approche

  1. Obtenez la tête pour toutes les branches
  2. collecter mergeBase pour la branche cible l'autre branche
  3. git.log et itérer
  4. Arrêter au premier commit qui apparaît dans la liste mergeBase

Comme toutes les rivières qui coulent vers la mer, toutes les branches se dirigent vers la maîtrise et nous trouvons donc une base de fusion entre des branches apparemment sans rapport. Lorsque nous revenons de la tête de la branche aux ancêtres, nous pouvons nous arrêter à la première base de fusion potentielle, car en théorie, elle devrait être le point d'origine de cette branche.

Remarques

  • Je n'ai pas essayé cette approche où les frères et sœurs et les cousins ​​se sont fusionnés.
  • Je sais qu'il doit y avoir une meilleure solution.

détails: https://stackoverflow.com/a/35353202/9950

Peter Kahn
la source
-1

Vous pouvez utiliser la commande suivante pour renvoyer le commit le plus ancien dans branch_a, qui n'est pas accessible depuis master:

git rev-list branch_a ^master | tail -1

Peut-être avec une vérification de cohérence supplémentaire que le parent de cette validation est réellement accessible à partir du maître ...

Marc Richarme
la source
1
Ça ne marche pas. Si branch_a est fusionné dans master une fois, puis continue, les validations sur cette fusion sont considérées comme faisant partie de master, donc elles n'apparaissent pas dans ^ master.
FelipeC
-2

Vous pouvez examiner le reflog de la branche A pour trouver à partir de quelle validation elle a été créée, ainsi que l'historique complet des validations de cette branche pointée. Les reflogs sont là .git/logs.

Paggas
la source
2
Je ne pense pas que cela fonctionne en général car le reflog peut être élagué. Et je ne pense pas que (?) Les reflogs soient poussés non plus, donc cela ne fonctionnerait que dans une situation de repo unique.
GaryO
-2

Je crois que j'ai trouvé un moyen qui traite tous les cas de coin mentionnés ici:

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 a tout à fait raison de dire que les solutions basées sur l'ordre des ancêtres n'ont qu'une valeur limitée; à la fin de la journée, vous avez besoin d'une sorte d'enregistrement de "ce commit est venu de la branche X", mais cet enregistrement existe déjà; par défaut, 'git merge' utiliserait un message de validation tel que "Merge branch 'branch_A' into master", cela vous indique que toutes les validations du deuxième parent (commit ^ 2) provenaient de 'branch_A' et ont été fusionnées avec la première parent (commit ^ 1), qui est «maître».

Armé de ces informations, vous pouvez trouver la première fusion de 'branch_A' (qui est quand 'branch_A' a vraiment vu le jour), et trouver la base de fusion, qui serait le point de branchement :)

J'ai essayé avec les référentiels de Mark Booth et Charles Bailey et la solution fonctionne; comment ne pouvait-il pas? La seule façon dont cela ne fonctionnerait pas est si vous avez modifié manuellement le message de validation par défaut pour les fusions afin que les informations de branche soient vraiment perdues.

Pour l'utilité:

[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'

Ensuite, vous pouvez le faire ' git branch-point branch_A'.

Prendre plaisir ;)

FelipeC
la source
4
S'appuyer sur les messages de fusion est plus fragile que de faire des hypothèses sur l'ordre des parents. Ce n'est pas seulement une situation hypothétique non plus; J'utilise souvent git merge -mpour dire dans quoi j'ai fusionné plutôt que le nom d'une branche potentiellement éphémère (par exemple "fusionner les modifications de la ligne principale en refactoriser la fonctionnalité xyz"). Supposons que j'aie été moins utile avec mon -mdans mon exemple? Le problème n'est tout simplement pas soluble dans toute sa généralité car je peux faire la même histoire avec une ou deux branches temporaires et il n'y a aucun moyen de faire la différence.
CB Bailey
1
@CharlesBailey C'est donc votre problème. Vous ne devez pas supprimer ces lignes du message de validation, vous devez ajouter le reste du message sous l'original. De nos jours, 'git merge' ouvre automatiquement un éditeur pour que vous puissiez ajouter ce que vous voulez, et pour les anciennes versions de git, vous pouvez faire 'git merge --edit'. Dans les deux cas, vous pouvez utiliser un crochet de validation pour ajouter un "Commited on branch 'foo" "à chaque validation si c'est ce que vous voulez vraiment. Cependant, cette solution fonctionne pour la plupart des gens .
FelipeC
2
N'a pas travaillé pour moi. Le branch_A a été dérivé du maître qui avait déjà beaucoup de fusions. Cette logique ne m'a pas donné le hachage de validation exact où la branche_A a été créée.
Venkat Kotra