Stai trovando un punto di diramazione con Git?


454

Ho un repository con i rami master e A e molte attività di fusione tra i due. Come posso trovare il commit nel mio repository quando il ramo A è stato creato in base al master?

Il mio repository è sostanzialmente così:

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

Sto cercando la revisione A, che non è ciò che git merge-base (--all)trova.


Risposte:


498

Stavo cercando la stessa cosa e ho trovato questa domanda. Grazie per averlo chiesto!

Tuttavia, ho trovato che le risposte che vedo qui non sembrano del tutto dare la risposta che hai chiesto (o che io cercavo) - sembrano dare il Gcommit, al posto del Acommit.

Quindi, ho creato il seguente albero (lettere assegnate in ordine cronologico), in modo da poter testare le cose:

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

Questo sembra un po 'diverso dal tuo, perché volevo assicurarmi di aver ottenuto (facendo riferimento a questo grafico, non al tuo) B, ma non A (e non D o E). Ecco le lettere allegate ai prefissi SHA e ai messaggi di commit (il mio repository può essere clonato da qui , se questo è interessante per chiunque):

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

Così, l' obiettivo: trovare B . Ecco tre modi che ho trovato, dopo un po 'di armeggiamento:


1. visivamente, con gitk:

Dovresti vedere visivamente un albero come questo (visto dal maestro):

cattura dello schermo gitk dal master

o qui (come visto dall'argomento):

cattura dello schermo gitk dall'argomento

in entrambi i casi, ho selezionato il commit che è Bnel mio grafico. Una volta cliccato su di esso, il suo SHA completo viene presentato in un campo di input di testo appena sotto il grafico.


2. visivamente, ma dal terminale:

git log --graph --oneline --all

(Modifica / nota a margine: l'aggiunta --decoratepuò anche essere interessante; aggiunge un'indicazione di nomi di rami, tag, ecc. Non aggiungendo questo alla riga di comando sopra poiché l'output sotto non riflette il suo uso.)

che mostra (supponendo git config --global color.ui auto):

output di git log --graph --oneline --all

Oppure, in testo semplice:

* a9546a2 unisci dall'argomento al master
| \  
| * 648ca35 fondendo il master sull'argomento
| | \  
| * | 132ee2a primo commit sul ramo argomento
* | | e7c863d esegue il commit sul master dopo che il master è stato unito all'argomento
| | /  
| / |   
* | 37ad159 commit post-branch sul master
| /  
* 6aafd7f secondo commit sul master prima della diramazione
* 4112403 commit iniziale sul master

in entrambi i casi, vediamo il commit 6aafd7f come il punto comune più basso, cioè Bnel mio grafico o Anel tuo.


3. Con la magia delle conchiglie:

Non specifichi nella tua domanda se volevi qualcosa di simile a quello sopra o un singolo comando che ti procurasse l'unica revisione e nient'altro. Bene, ecco il secondo:

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

Che puoi anche inserire nel tuo ~ / .gitconfig come (nota: il trattino finale è importante; grazie Brian per averlo richiamato) :

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

Che potrebbe essere fatto tramite la seguente riga di comando (contorta con virgolette):

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: zshpotrebbe facilmente essere stato bash, ma shlo farà senza il lavoro - la <()sintassi non esiste in vaniglia sh. (Grazie ancora, @conny, per avermelo informato in un commento su un'altra risposta in questa pagina!)

Nota: versione alternativa di quanto sopra:

Grazie a liori per aver sottolineato che quanto sopra potrebbe cadere quando si confrontano rami identici e trovare una forma diff alternativa che rimuove la forma sed dal mix e rende questo "più sicuro" (cioè restituisce un risultato (vale a dire, il commit più recente) anche quando si confronta master con master):

Come linea .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' -

Dalla shell:

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

Quindi, nel mio albero di prova (che non era disponibile per un po ', scusate; è tornato), che ora funziona sia su master che su argomento (dando rispettivamente G e B di commit). Grazie ancora, liori, per la forma alternativa.


Quindi, questo è quello che ho ideato. Sembra funzionare per me. Inoltre, consente un ulteriore paio di alias che potrebbero rivelarsi utili:

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

Buon umore!


5
Grazie a Lindes, l'opzione shell è ottima per le situazioni in cui si desidera trovare il punto di diramazione di un ramo di manutenzione di lunga durata. Quando stai cercando una revisione che potrebbe essere un migliaio di commit in passato, le opzioni visive non lo taglieranno davvero. * 8 ')
Mark Booth

4
Nel tuo terzo metodo dipendi dal fatto che il contesto mostrerà la prima riga invariata. Ciò non accadrà in alcuni casi limite o se ti capitano requisiti leggermente diversi (ad esempio, ho solo bisogno che una delle storie sia - first-parent, e sto usando questo metodo in uno script che a volte potrebbe usare lo stesso rami su entrambi i lati). Ho trovato più sicuro l'uso diff's if-then-else modalità e cancellazione cambiati / cancellato le linee dalla sua uscita, invece di contare su avere abbastanza grande contesto, dai.: 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)~funzionerà anche, credo
Tobias Schulte,

4
@ JakubNarębski: hai un modo per git merge-base --fork-point ...dare il commit B (commit 6aafd7f) per questo albero? Ero impegnato con altre cose quando l'hai postato, e suonava bene, ma alla fine l'ho appena provato, e non riesco a farlo funzionare ... O ottengo qualcosa di più recente, o solo un errore silenzioso (nessun errore messaggio, ma stato di uscita 1), cercando argomenti come 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(sia per cassa), ecc c'è qualcosa che sto facendo male o manca?
termina il

25
@ JakubNarębski @lindes --fork-pointsi basa sul reflog, quindi funzionerà solo se hai apportato le modifiche localmente. Anche allora, le voci di reflog potrebbero essere scadute. È utile ma per nulla affidabile.
Daniel Lubarov,

131

Forse stai cercando git merge-base:

git merge-base trova i migliori antenati comuni tra due commit da usare in un'unione a tre vie. Un antenato comune è migliore di un altro antenato comune se quest'ultimo è un antenato del primo. Un antenato comune che non ha antenati comuni migliori è un antenato comune migliore , cioè una base di unione . Si noti che può esistere più di una base di unione per una coppia di commit.


10
Nota anche l' --allopzione " git merge-base"
Jakub Narębski,

10
Questo non risponde alla domanda originale, ma la maggior parte delle persone pone la domanda molto più semplice per la quale questa è la risposta :)
FelipeC

6
disse che non sarebbe stato il risultato di git merge-base
Tom Tanner il

9
@ TomTanner: ho appena esaminato la cronologia delle domande e la domanda originale è stata modificata per includere quella nota circa git merge-basecinque ore dopo la pubblicazione della mia risposta (probabilmente in risposta alla mia risposta). Tuttavia, lascerò questa risposta così com'è perché potrebbe essere ancora utile per qualcun altro che trova questa domanda tramite la ricerca.
Greg Hewgill,

Credo che potresti usare i risultati di merge-base --all e quindi scorrere l'elenco dei commit di ritorno usando merge-base --is-antenor per trovare il più antico antenato comune dall'elenco, ma questo non è il modo più semplice .
Matt_G

42

Ho usato git rev-listper questo genere di cose. Ad esempio, (notare i 3 punti)

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

sputerà il punto di diramazione. Ora non è perfetto; dal momento che hai unito il master nel ramo A un paio di volte, questo dividerà un paio di possibili punti del ramo (fondamentalmente, il punto del ramo originale e quindi ogni punto in cui hai unito il maestro nel ramo A). Tuttavia, dovrebbe almeno restringere le possibilità.

Ho aggiunto quel comando ai miei alias ~/.gitconfigcome:

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

così posso chiamarlo come:

$ git diverges branch-a master

Nota: questo sembra dare il primo commit sul ramo, piuttosto che l'antenato comune. (cioè dà Ginvece A, per il grafico nella domanda originale.) Ho una risposta che arriva A, che posterò al momento.
termina il

@lindes: dà l'antenato comune in ogni caso in cui l'ho provato. Hai un esempio in cui no?
mipadi,

Sì. Nella mia risposta (che ha un collegamento a un repository è possibile clonare; git checkout topice quindi eseguirlo con topical posto di branch-a), elenca 648ca357b946939654da12aaf2dc072763f3caeee37ad15952db5c065679d7fc31838369712f0b338 - entrambi 37ad159e 648ca35sono nella discendenza degli attuali rami (quest'ultimo è l'attuale HEAD di topic), ma nemmeno il punto prima che succedesse la ramificazione. Hai qualcosa di diverso?
termina il

@lindes: non sono stato in grado di clonare il tuo repository (forse un problema di autorizzazioni?).
mipadi,

Oops scusa! Grazie per avermi fatto sapere. Ho dimenticato di eseguire git update-server-info. Ora dovrebbe andare bene. :)
scade il

31

Se ti piacciono i comandi concatenati,

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

Ecco una spiegazione

Il seguente comando fornisce l'elenco di tutti i commit nel master che si sono verificati dopo la creazione di branch_name

git rev-list - first-parent ^ branch_name master 

Dato che ti interessa solo il primo di questi commit, vuoi l'ultima riga dell'output:

git rev-list ^ branch_name - master first-parent | coda -n1

Il genitore del primo commit che non è un antenato di "branch_name" è, per definizione, in "branch_name" ed è in "master" poiché è antenato di qualcosa in "master". Quindi hai il primo commit in entrambi i rami.

Il comando

git rev-list commit ^^!

è solo un modo per mostrare il riferimento del commit principale. Puoi usare

git log -1 commit ^

o qualunque altra cosa.

PS: Non sono d'accordo con l'argomento secondo cui l'ordine degli antenati è irrilevante. Dipende da cosa vuoi. Ad esempio, in questo caso

_C1___C2_______ master
  \ \ _XXXXX_ ramo A (le X indicano incroci arbitrari tra master e A)
   \ _____ / ramo B

ha perfettamente senso emettere C2 come commit "branching". Questo è quando lo sviluppatore si è ramificato da "master". Quando si ramificava, il ramo "B" non era nemmeno unito al suo ramo! Questo è ciò che offre la soluzione in questo post.

Se quello che vuoi è l'ultimo commit C in modo tale che tutti i percorsi dall'origine all'ultimo commit sul ramo "A" passino attraverso C, allora vuoi ignorare l'ordine degli antenati. È puramente topologico e ti dà un'idea di quando hai due versioni del codice in esecuzione contemporaneamente. Questo è quando andresti con approcci basati su merge-base, e restituirà C1 nel mio esempio.


3
Questa è di gran lunga la risposta più chiara, facciamolo votare al top. Una modifica suggerita: git rev-list commit^^!può essere semplificata comegit rev-parse commit^
Russell Davis

Questa dovrebbe essere la risposta!
trinto,

2
Questa risposta è bello, ho appena sostituito git rev-list --first-parent ^branch_name mastercon git rev-list --first-parent branch_name ^master, perché se il ramo master è 0 commit avanti dell'altro ramo (veloce-inoltrabile ad esso), sarebbe stato creato alcun output. Con la mia soluzione, non viene creato alcun output se master è strettamente in anticipo (ovvero il ramo è stato completamente unito), che è quello che voglio.
Michael Schmeißer,

3
Questo non funzionerà a meno che non mi manchi qualcosa. Ci sono fusioni in entrambe le direzioni nei rami di esempio. Sembra che tu abbia cercato di tenerne conto, ma credo che questo causerà il fallimento della tua risposta. git rev-list --first-parent ^topic masterti riporterà al primo commit dopo l'ultima unione da masterin topic(se esiste).
jerry,

1
@jerry Hai ragione, questa risposta è spazzatura; per esempio, nel caso in cui si sia appena verificato un backmerge (unendo master in topic) e dopo non ci siano nuovi commit, il primo comando git rev-list - first-parent non produce nulla.
TamaMcGlinn,

19

Dato che molte delle risposte in questo thread non danno la risposta che la domanda stava ponendo, ecco un riepilogo dei risultati di ciascuna soluzione, insieme allo script che ho usato per replicare il repository fornito nella domanda.

Il ceppo

Creando un repository con la struttura data, otteniamo il log git di:

$ 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

La mia unica aggiunta è il tag che lo rende esplicito riguardo al punto in cui abbiamo creato il ramo e quindi il commit che desideriamo trovare.

La soluzione che funziona

L'unica soluzione che funziona è quella fornita da Lindes restituisce correttamente A:

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

Come sottolinea Charles Bailey , questa soluzione è molto fragile.

Se branch_Ain mastere quindi unire masterin branch_Asenza intervenire si impegna quindi la soluzione Lindes' ti dà solo il più recente prima Divergenza .

Ciò significa che, per il mio flusso di lavoro, penso che dovrò attenermi all'etichettatura del punto di diramazione dei rami di lunga durata, poiché non posso garantire che possano essere trovati in modo affidabile in un secondo momento.

Tutto ciò si riduce alla gitmancanza di ciò che hgchiama filiali . Il blogger jhw chiama questi lignaggi contro le famiglie nel suo articolo Why I Like Mercurial More Than Git e nel suo articolo di follow-up More su Mercurial vs. Git (con Graphs!) . Consiglierei alle persone di leggerli per capire perché alcuni convertitori di mercurial mancano di non aver chiamato i rami in git.

Le soluzioni che non funzionano

La soluzione fornita da mipadi restituisce due risposte Ie C:

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

La soluzione fornita da Greg Hewgill ritornaI

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

La soluzione fornita da Karl restituisce X:

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

Il copione

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"

Dubito che la versione git faccia molta differenza, ma:

$ git --version
git version 1.7.1

Grazie a Charles Bailey per avermi mostrato un modo più compatto di scrivere il repository di esempio.


2
La soluzione di Karl è facile da risolvere: diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1. Grazie per aver fornito le istruzioni per creare il repository :)
FelipeC

Penso che tu intenda "La variazione ripulita della soluzione fornita da Karl restituisce X". L'originale ha funzionato bene, è stato semplicemente brutto :-)
Karl

No, l'originale non funziona bene. Certo, la variazione funziona anche peggio. Ma aggiungendo l'opzione --topo-order fa funzionare la tua versione :)
FelipeC,

@felipec - Vedi il mio commento finale sulla risposta di Charles Bailey . Purtroppo la nostra chat (e quindi tutti i vecchi commenti) sono stati eliminati. Cercherò di aggiornare la mia risposta quando avrò il tempo.
Mark Booth,

Interessante. Pensavo che la topologia fosse quella predefinita. Silly me :-)
Karl

10

In generale, questo non è possibile. Nella cronologia di un ramo un ramo si fonde prima che un ramo nominato venisse ramificato e un ramo intermedio di due rami nominati avesse lo stesso aspetto.

In effetti, i rami sono solo i nomi attuali dei suggerimenti di sezioni della storia. Non hanno davvero una forte identità.

Questo di solito non è un grosso problema in quanto la base di unione (vedi la risposta di Greg Hewgill) di due commit è di solito molto più utile, dando il commit più recente condiviso dai due rami.

Una soluzione basata sull'ordine dei genitori di un commit ovviamente non funzionerà in situazioni in cui un ramo è stato completamente integrato ad un certo punto della storia del ramo.

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"

Questa tecnica fallisce anche se è stata effettuata un'unione di integrazione con i genitori invertiti (ad esempio un ramo temporaneo è stato usato per eseguire una fusione di prova in master e quindi avanzare rapidamente nel ramo di funzionalità per continuare ulteriormente).

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"


3
Grazie Charles, mi hai convinto, se voglio sapere il punto in cui il ramo originariamente era divergente , dovrò etichettarlo. Vorrei davvero che gitavevano un equivalente a hg's chiamato rami, renderebbe la gestione di sportelli di manutenzione a lungo vissuto in modo molto più facile.
Mark Booth,

1
"In git, i rami sono solo i nomi attuali delle punte di sezioni della storia. Non hanno davvero una forte identità" È una cosa spaventosa da dire e mi ha convinto che ho bisogno di capire meglio i rami di Git - grazie (+ 1)
silente il

Nella cronologia di un ramo un ramo si fonde prima che un ramo nominato venisse ramificato e un ramo intermedio di due rami nominati avesse lo stesso aspetto. Sì. +1.
user1071847

5

Che ne dici di qualcosa del genere

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`^

Questo funziona È davvero ingombrante ma è l'unica cosa che ho trovato che in realtà sembra fare il lavoro.
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)^'(senza file temporanei)
conny

@conny: Oh, wow - non avevo mai visto la sintassi <(pippo) ... è incredibilmente utile, grazie! (Funziona anche in zsh, FYI.)
termina il

questo sembra darmi il primo impegno sul ramo, piuttosto che l'antenato comune. (cioè dà Ginvece A, per il grafico nella domanda originale.) Penso di aver trovato una risposta, che posterò al momento.
termina il

Invece di 'git log --pretty = oneline' puoi semplicemente fare 'git rev-list', quindi puoi anche saltare il taglio, inoltre, questo dà al genitore l'impegno del punto di divergenza, quindi basta coda -2 | testa 1. Quindi:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC

5

sicuramente mi manca qualcosa, ma IMO, tutti i problemi di cui sopra sono causati perché cerchiamo sempre di trovare il punto di partenza che va indietro nella storia e questo causa tutti i tipi di problemi a causa delle combinazioni di fusione disponibili.

Invece, ho seguito un approccio diverso, basato sul fatto che entrambe le filiali condividono molta storia, esattamente tutta la storia prima della ramificazione è la stessa al 100%, quindi invece di tornare indietro, la mia proposta è di andare avanti (dal 1 ° commit), cercando la prima differenza in entrambi i rami. Il punto di diramazione sarà semplicemente il genitore della prima differenza rilevata.

In pratica:

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

E sta risolvendo tutti i miei soliti casi. Certo ci sono quelli di confine non coperti ma ... ciao :-)


diff <(git rev-list "$ {1: -master}" - first-parent) <(git rev-list "$ {2: -HEAD}" - first-parent) -U1 | coda -1
Alexander Bird

l'ho trovato più veloce (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

Dopo molte ricerche e discussioni, è chiaro che non esiste un proiettile magico che funzioni in tutte le situazioni, almeno non nella versione attuale di Git.

Ecco perché ho scritto un paio di patch che aggiungono il concetto di tailramo. Ogni volta che viene creato un ramo, viene creato anche un puntatore al punto originale, iltail rif. Questo riferimento viene aggiornato ogni volta che il ramo viene riformulato.

Per scoprire il punto di diramazione del ramo di sviluppo, tutto quello che devi fare è usare devel@{tail} , tutto qui.

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


1
Potrebbe essere l'unica soluzione stabile. Hai visto se questo potrebbe entrare in git? Non ho visto una richiesta pull.
Alexander Klimetschek,

@AlexanderKlimetschek Non ho inviato le patch e non credo che sarebbero state accettate. Tuttavia, ho provato un metodo diverso: un hook "update-branch" che fa qualcosa di molto simile. In questo modo per impostazione predefinita Git non farebbe nulla, ma potresti abilitare l'hook ad aggiornare il ramo di coda. Tuttavia non avresti sviluppato @ {tail}, ma non sarebbe così male usare invece code / sviluppo.
FelipeC,

3

Ecco una versione migliorata della mia risposta precedente risposta precedente . Si basa sui messaggi di commit delle fusioni per trovare il punto in cui il ramo è stato creato per la prima volta.

Funziona su tutti i repository menzionati qui e ne ho persino risolti alcuni difficili che sono stati generati nella mailing list . Ho anche scritto dei test per questo.

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
}

3

Il comando seguente rivelerà lo SHA1 di Commit A

git merge-base --fork-point A


Questo non succederà se i rami padre e figlio hanno una fusione intermedia tra loro.
nawfal,

2

Mi sembra di provare gioia

git rev-list branch...master

L'ultima riga che ottieni è il primo commit sul ramo, quindi è una questione di ottenere il genitore di quello. Così

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

Sembra funzionare per me e non ha bisogno di diff e così via (il che è utile perché non abbiamo quella versione di diff)

Correzione: questo non funziona se ci si trova nel ramo principale, ma lo sto facendo in uno script, quindi è meno un problema


2

Per trovare commit dal punto di diramazione, puoi usare questo.

git log --ancestry-path master..topicbranch

Questo comando non funziona per me nell'esempio dato. Per favore, cosa forniresti come parametri per l'intervallo di commit?
Jesper Rønn-Jensen,

2

A volte è effettivamente impossibile (con alcune eccezioni di dove potresti essere fortunato ad avere dati aggiuntivi) e le soluzioni qui non funzioneranno.

Git non conserva la cronologia degli ref (che include le filiali). Memorizza solo la posizione corrente per ciascun ramo (la testa). Questo significa che puoi perdere un po 'di storia del ramo in git nel tempo. Ogni volta che si ramifica, ad esempio, si perde immediatamente quale ramo era quello originale. Tutto un ramo fa è:

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

Si potrebbe presumere che il primo a cui è stato eseguito il commit sia il ramo. Questo tende ad essere il caso, ma non è sempre così. Non c'è nulla che ti impedisca di impegnarti in una delle filiali prima dell'operazione sopra descritta. Inoltre, i timestamp di git non sono garantiti come affidabili. Non è fino a quando non ti impegni ad entrambi che diventano veramente rami strutturalmente.

Mentre nei diagrammi tendiamo a numerare i commit concettualmente, git non ha un vero concetto stabile di sequenza quando l'albero di commit si ramifica. In questo caso puoi supporre che i numeri (che indicano l'ordine) siano determinati dal timestamp (potrebbe essere divertente vedere come un'interfaccia utente git gestisce le cose quando imposti tutti i timestamp sullo stesso).

Questo è ciò che un umano si aspetta concettualmente:

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

Questo è ciò che effettivamente ottieni:

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

Supponeresti che B1 sia il ramo originale ma potrebbe essere semplicemente un ramo morto (qualcuno ha fatto il checkout -b ma non si è mai impegnato con esso). Non è fino a quando non ti impegni a ottenere una struttura di ramo legittima all'interno di git:

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

Sai sempre che C1 è venuto prima di C2 e C3 ma non sai mai in modo affidabile se C2 è venuto prima di C3 o C3 prima di C2 (perché puoi impostare l'ora sulla tua workstation su qualsiasi cosa, ad esempio). Anche B1 e B2 sono fuorvianti in quanto non si può sapere quale ramo è arrivato per primo. Puoi fare un'ipotesi molto buona e di solito accurata in molti casi. È un po 'come una pista. A parità di tutte le cose, quindi, si può presumere che un'auto che arriva in un giro dietro abbia iniziato un giro dietro. Abbiamo anche convenzioni molto affidabili, ad esempio il maestro rappresenterà quasi sempre i rami più longevi, anche se purtroppo ho visto casi in cui anche questo non è il caso.

L'esempio fornito qui è un esempio di conservazione della storia:

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)

Il reale qui è anche fuorviante perché noi come umani lo leggiamo da sinistra a destra, da radice a foglia (rif). Git non lo fa. Dove facciamo (A-> B) nella nostra testa git fa (A <-B o B-> A). Lo legge da ref a root. I riferimenti possono essere ovunque ma tendono ad essere foglie, almeno per i rami attivi. Un ref indica un commit e contiene solo un like nei confronti dei loro genitori, non dei loro figli. Quando un commit è un commit di merge avrà più di un genitore. Il primo genitore è sempre il commit originale in cui è stato unito. Gli altri genitori sono sempre commit che sono stati uniti al commit originale.

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

Questa non è una rappresentazione molto efficiente, piuttosto un'espressione di tutti i percorsi che git può prendere da ciascun riferimento (B1 e B2).

La memoria interna di Git è più simile a questa (non che A come genitore appare due volte):

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

Se scarichi un commit git grezzo vedrai zero o più campi padre. Se ci sono zero, significa che nessun genitore e il commit è una radice (puoi effettivamente avere più radici). Se ce n'è uno, significa che non c'era unione e non è un commit root. Se ce n'è più di uno, significa che il commit è il risultato di una fusione e tutti i genitori dopo il primo sono commit di fusione.

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)

Quando entrambi colpiscono A la loro catena sarà la stessa, prima che la loro catena sarà completamente diversa. Il primo commesso che altri due hanno in comune è l'antenato comune e da cui divergevano. potrebbe esserci un po 'di confusione tra i termini commit, branch e ref. Puoi infatti unire un commit. Questo è ciò che fa davvero l'unione. Un ref indica semplicemente un commit e un ramo non è altro che un ref nella cartella .git / refs / heads, la posizione della cartella è ciò che determina che un ref è un ramo piuttosto che qualcos'altro come un tag.

Dove perdi la storia è che l'unione farà una delle due cose a seconda delle circostanze.

Prendere in considerazione:

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

In questo caso, un'unione in entrambe le direzioni creerà un nuovo commit con il primo genitore come il commit indicato dal ramo estratto corrente e il secondo genitore come il commit sulla punta del ramo che hai unito nel ramo corrente. Deve creare un nuovo commit poiché entrambi i rami hanno dei cambiamenti dal loro antenato comune che devono essere combinati.

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

A questo punto D (B1) ora ha entrambe le serie di modifiche da entrambi i rami (se stesso e B2). Tuttavia, il secondo ramo non ha le modifiche da B1. Se unisci le modifiche da B1 a B2 in modo che siano sincronizzate, potresti aspettarti qualcosa che assomigli a questo (puoi forzare git merge a farlo in questo modo comunque con --no-ff):

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

Lo otterrai anche se B1 ha ulteriori commit. Finché non ci sono cambiamenti in B2 che B1 non ha, i due rami verranno uniti. Fa un avanzamento veloce che è come un rebase (anche i rebase mangiano o linearizzano la storia), tranne che a differenza di un rebase poiché solo un ramo ha un set di modifiche che non deve applicare un changeset da un ramo sopra quello da un altro.

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

Se smetti di lavorare su B1, allora le cose andranno benissimo per preservare la storia nel lungo periodo. Solamente B1 (che potrebbe essere il master) avanzerà in genere, quindi la posizione di B2 nella storia di B2 rappresenta con successo il punto in cui è stata fusa in B1. Questo è ciò che git si aspetta che tu faccia, ramificare B da A, quindi puoi unire A in B quanto desideri quando si accumulano le modifiche, tuttavia quando si fonde nuovamente B in A, non si prevede che lavorerai su B e oltre . Se continui a lavorare sul tuo ramo dopo averlo fatto avanzare rapidamente e lo hai reintegrato nel ramo su cui stavi lavorando, cancellerai ogni volta la storia precedente di B. Stai davvero creando un nuovo ramo ogni volta che esegui il commit di avanzamento rapido sull'origine, quindi esegui il commit sul ramo.

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

Da 1 a 3 e da 5 a 8 sono rami strutturali che si presentano se segui la storia per 4 o 9. Non c'è modo di sapere a quale di questi rami strutturali senza nome e senza riferimento appartengono i rami nominati e riferimenti come fine della struttura. Si potrebbe supporre da questo disegno che 0 a 4 appartiene a B1 e da 4 a 9 appartiene a B2 ma a parte 4 e 9 non è stato possibile sapere quale ramo appartiene a quale ramo, l'ho semplicemente disegnato in un modo che dà il illusione di quello. 0 potrebbe appartenere a B2 e 5 potrebbe appartenere a B1. Esistono 16 diverse possibilità in questo caso, di cui potrebbe appartenere ciascuno dei rami nominali.

Ci sono una serie di strategie git che aggirano questo. Puoi forzare git merge a non avanzare mai velocemente e creare sempre un ramo di unione. Un modo orribile per preservare la cronologia dei rami è con tag e / o rami (i tag sono davvero consigliati) secondo alcune convenzioni di tua scelta. Non consiglierei davvero un fittizio commit vuoto nel ramo in cui ti stai unendo. Una convenzione molto comune è quella di non fondersi in un ramo di integrazione fino a quando non si desidera chiudere realmente il proprio ramo. Questa è una pratica a cui le persone dovrebbero tentare di aderire poiché altrimenti si sta lavorando intorno al punto di avere rami. Tuttavia, nel mondo reale l'ideale non è sempre un significato pratico, fare la cosa giusta non è praticabile in ogni situazione. Se quello che tu '


"Git non conserva la cronologia degli ref" Lo fa, ma non per impostazione predefinita e non per molto tempo. Vedere man git-refloge la parte relativa alle date: "master@{one.week.ago} significa" dove il master indicava una settimana fa in questo repository locale "". O la discussione su <refname>@{<date>}in man gitrevisions. E core.reflogExpiredentro man git-config.
Patrick Mevzek,

2

Non è proprio una soluzione alla domanda, ma ho pensato che valesse la pena notare l'approccio che utilizzo quando ho un ramo di lunga durata:

Allo stesso tempo creo il ramo, creo anche un tag con lo stesso nome ma con un -initsuffisso, ad esempio feature-branche feature-branch-init.

(È un po 'strano che questa sia una domanda così difficile a cui rispondere!)


1
Considerando la straordinaria stupidità sbalorditiva di progettare un concetto di "ramo" senza alcuna idea di quando e dove è stato creato ... oltre alle immense complicazioni di altre soluzioni suggerite - da parte di persone che cercano di essere super intelligenti in questo modo troppo intelligente cosa, penso che preferirei la tua soluzione. Solo questo appesantisce la necessità di RICORDARSI di farlo ogni volta che crei un ramo - una cosa che gli utenti git fanno molto spesso. Inoltre, ho letto da qualche parte che "i tag hanno la penalità di essere" pesanti ". Tuttavia, penso che sia quello che penso che farò.
Motti Shneor,

1

Un modo semplice per semplificare la visualizzazione del punto di diramazione git log --graphè utilizzare l'opzione --first-parent.

Ad esempio, prendere il repository dalla risposta accettata :

$ 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

Ora aggiungi --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

Questo lo rende più facile!

Nota se il repository ha molti rami che vorrai specificare i 2 rami che stai confrontando invece di usare --all:

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

0

Il problema sembra essere quello di trovare il taglio di commit singolo più recente tra entrambi i rami da un lato e il primo antenato comune dall'altro (probabilmente il commit iniziale del repository). Ciò corrisponde alla mia intuizione su quale sia il punto di "ramificazione".

Ciò in mente, questo non è affatto facile da calcolare con i normali comandi di git shell, poiché git rev-list- il nostro strumento più potente - non ci consente di limitare il percorso attraverso il quale viene raggiunto un commit. Il più vicino che abbiamo è git rev-list --boundary, che può darci una serie di tutti i commit che "ci hanno bloccato". (Nota:git rev-list --ancestry-path è interessante ma non so come renderlo utile qui.)

Ecco lo script: https://gist.github.com/abortz/d464c88923c520b79e3d . È relativamente semplice, ma a causa di un ciclo è abbastanza complicato da giustificare un senso.

Si noti che la maggior parte delle altre soluzioni proposte qui non può funzionare in tutte le situazioni per un semplice motivo: git rev-list --first-parentnon è affidabile nella linearizzazione della cronologia perché possono esserci fusioni con entrambi gli ordini.

git rev-list --topo-orderd'altra parte, è molto utile - per camminare si impegna in ordine topografico - ma fare differenze è fragile: ci sono più possibili ordini topografici per un dato grafico, quindi si dipende da una certa stabilità degli ordini. Detto questo, la soluzione di strongk7 probabilmente funziona maledettamente bene la maggior parte delle volte. Tuttavia è più lento del mio a causa di dover percorrere l'intera storia del repository ... due volte. :-)


0

Il seguente implementa l'equivalente git di svn log --stop-on-copy e può anche essere usato per trovare l'origine del ramo.

Approccio

  1. Ottieni la testa per tutti i rami
  2. raccogliere mergeBase per il ramo di destinazione ogni altro ramo
  3. git.log e iterare
  4. Arresta al primo commit visualizzato nell'elenco mergeBase

Come tutti i fiumi corrono verso il mare, tutti i rami corrono verso il padrone e quindi troviamo una base di fusione tra rami apparentemente non correlati. Mentre torniamo dalla testa del ramo attraverso gli antenati, possiamo fermarci alla prima potenziale base di unione poiché in teoria dovrebbe essere il punto di origine di questo ramo.

Appunti

  • Non ho provato questo approccio in cui i rami dei fratelli e dei cugini si sono fusi l'uno con l'altro.
  • So che ci deve essere una soluzione migliore.

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


-1

È possibile utilizzare il comando seguente per restituire il commit più vecchio in branch_a, che non è raggiungibile dal master:

git rev-list branch_a ^master | tail -1

Forse con un ulteriore controllo di integrità che il genitore di quel commit è effettivamente raggiungibile dal master ...


1
Questo non funziona Se branch_a viene unito al master una volta e poi continua, i commit su quell'unione sarebbero considerati parte del master, quindi non verrebbero visualizzati in ^ master.
FelipeC,

-2

È possibile esaminare il reflog del ramo A per scoprire da quale commit è stato creato, nonché la cronologia completa di cui ha puntato quel ramo. I reflog sono presenti .git/logs.


2
Non penso che questo funzioni in generale perché il reflog può essere eliminato. E non credo che neanche i reflog (?) Vengano spinti, quindi funzionerebbe solo in una situazione a singolo repository.
GaryO,

-2

Credo di aver trovato un modo per gestire tutti i casi limite menzionati qui:

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 ha perfettamente ragione sul fatto che le soluzioni basate sull'ordine degli antenati hanno solo un valore limitato; alla fine della giornata hai bisogno di una sorta di record di "questo commit proviene dal ramo X", ma tale record esiste già; di default 'git merge' userebbe un messaggio di commit come "Unisci ramo 'branch_A' nel master", questo ti dice che tutti i commit dal secondo genitore (commit ^ 2) provengono da 'branch_A' e sono stati uniti al primo parent (commit ^ 1), che è 'master'.

Grazie a queste informazioni puoi trovare la prima unione di 'branch_A' (che è quando 'branch_A' è realmente esistita), e trovare la base di unione, che sarebbe il punto di diramazione :)

Ho provato con i repository di Mark Booth e Charles Bailey e la soluzione funziona; come è potuto? L'unico modo in cui ciò non funzionerebbe è se hai modificato manualmente il messaggio di commit predefinito per le fusioni, in modo che le informazioni sul ramo vengano veramente perse.

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

Quindi puoi fare ' git branch-point branch_A'.

Godere ;)


4
Affidarsi ai messaggi di unione è più fragile dell'ipotesi sull'ordine dei genitori. Non è nemmeno solo una situazione ipotetica; Spesso uso git merge -mper dire ciò in cui mi sono unito piuttosto che il nome di un ramo potenzialmente effimero (ad esempio "unisci i cambiamenti della linea principale nel rifattore xyz di funzionalità"). Supponiamo che -mnel mio esempio sarei stato meno utile con il mio? Il problema non è semplicemente solubile nella sua piena generalità perché posso fare la stessa storia con uno o due rami temporanei e non c'è modo di dire la differenza.
CB Bailey,

1
@CharlesBailey Questo è il tuo problema allora. Non dovresti rimuovere quelle righe dal messaggio di commit, dovresti aggiungere il resto del messaggio sotto quello originale. Al giorno d'oggi 'git merge' apre automaticamente un editor per aggiungere tutto quello che vuoi, e per le vecchie versioni di git puoi fare 'git merge --edit'. Ad ogni modo, puoi usare un hook di commit per aggiungere un "Commo on branch 'pippo" a ogni commit se è quello che vuoi davvero. Tuttavia, questa soluzione funziona per la maggior parte delle persone .
FelipeC,

2
Non ha funzionato per me. La branch_A è stata biforcuta dal master che aveva già molte fusioni. Questa logica non mi ha dato l'esatto hash di commit in cui è stato creato branch_A.
Venkat Kotra,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.