Dove inizia un ramo Git e qual è la sua lunghezza?

Ogni tanto mi viene chiesto, su quale commit un certo ramo su git parte o se un certo commit è stato creato su un ramo specifico. Il punto finale di un ramo è piuttosto chiaro: è lì che si trova l’etichetta del ramo. Ma – da dove è iniziato? La risposta banale sarebbe: su quel commit dove abbiamo creato quel ramo. Ma questa informazione è, per quanto ne so ora, ed è per questo che sto facendo la domanda, persa dopo i primi commit.

Finché conosciamo il commit in cui ci siamo ramificati, possiamo disegnare il grafico per chiarire:

A - B - C - - - - J [master] \ D - E - F - G [branch-A] \ H - - I [branch-B] 

Ho creato branch-B su commit E quindi questo è “start”. Lo so, perché l’ho fatto. Ma gli altri possono riconoscerlo allo stesso modo? Potremmo disegnare lo stesso grafico in questo modo:

 A - B - C - - - - J [master] \ \ F - G [branch-A] \ / D - E \ H - I [branch-B] 

Quindi, guardando il grafico ora, quale ramo ha iniziato a E , quale a B ? Il commit D un membro di entrambe le filiali o possiamo decidere chiaramente se appartiene al ramo A o al ramo B?

Questo suona un po ‘filosofico, ma in realtà non lo è. I supervisori a volte vogliono sapere, quando un ramo è stato avviato (di solito segna l’inizio di un’attività) e a quale ramo appartengono alcune modifiche (per ottenere lo scopo di qualche cambiamento – era necessario per il lavoro) e io piacerebbe sapere se git offre informazioni (strumenti, comandi) o definizioni per rispondere correttamente a queste domande.

In Git, si potrebbe dire che ogni ramo inizia con il commit di root, e questo sarebbe letteralmente vero. Ma immagino che non sia molto utile per te. Quello che potresti fare invece è definire “l’inizio di un ramo” in relazione ad altri rami. Un modo per farlo è usare

 git show-branch branch1 branch2 ... branchN 

e questo mostrerà il commit comune tra tutti i rami specificati nella parte inferiore dell’output (se esiste, in effetti, un commit comune).

Ecco un esempio della documentazione di Linux Kernel Git per show-branch

 $ git show-branch master fixes mhf * [master] Add 'git show-branch'. ! [fixes] Introduce "reset type" flag to "git reset" ! [mhf] Allow "+remote:local" refspec to cause --force when fetching. --- + [mhf] Allow "+remote:local" refspec to cause --force when fetching. + [mhf~1] Use git-octopus when pulling more than one heads. + [fixes] Introduce "reset type" flag to "git reset" + [mhf~2] "git fetch --force". + [mhf~3] Use .git/remote/origin, not .git/branches/origin. + [mhf~4] Make "git pull" and "git fetch" default to origin + [mhf~5] Infamous 'octopus merge' + [mhf~6] Retire git-parse-remote. + [mhf~7] Multi-head fetch. + [mhf~8] Start adding the $GIT_DIR/remotes/ support. *++ [master] Add 'git show-branch'. 

In questo esempio, il master viene confrontato con le fixes e i rami mhf . Pensa a questo risultato come a una tabella, con ogni ramo rappresentato da una propria colonna e ogni commit riceve la propria riga. I rami che contengono un commit avranno + o - nella loro colonna nella riga per quel commit.

Nella parte inferiore dell’output, vedrete che tutti e 3 i rami condividono un commit dell’antenato comune, e che è in effetti il head dominio del master :

 *++ [master] Add 'git show-branch'. 

Ciò significa che entrambe le fixes e mhf erano ramificate da quel commit in master .

Soluzioni alternative

Ovviamente questo è solo un modo ansible per determinare un commit di base comune in Git. Altri modi includono git merge-base per trovare antenati comuni, e git log --all --decorate --graph --oneline o gitk --all per visualizzare i rami e vedere dove divergono (anche se ci sono molti commit diventa difficile molto rapidamente).

Altre domande dal poster originale

Per quanto riguarda queste domande hai avuto:

Il commit D un membro di entrambe le filiali o possiamo decidere chiaramente se appartiene al branch-A o al branch-B ?

D è un membro di entrambi i rami, è un impegno ancestrale per entrambi.

I supervisori a volte vogliono sapere, quando un ramo è stato avviato (di solito segna l’inizio di un’attività)

In Git, puoi riscrivere la cronologia di tutti gli alberi di commit e delle loro diramazioni, quindi quando un ramo “inizia” non è così impostato in pietra come in qualcosa come TFS o SVN. Puoi rebase rami in qualsiasi punto nel tempo in un albero Git, anche mettendolo prima del commit di root! Pertanto, è ansible utilizzarlo per “avviare” un’attività in qualsiasi momento nella struttura che si desidera.

Questo è un caso d’uso comune per git rebase , per sincronizzare le ramificazioni con le ultime modifiche da un ramo upstream, per spingerle “avanti” nel tempo lungo il grafico del commit, come se si fosse “appena iniziato” a lavorare sul ramo, anche anche se in realtà ci hai lavorato per un po ‘. Potresti anche spingere i rami indietro nel tempo lungo il grafico del commit, se lo volessi (anche se potresti dover risolvere molti conflitti, a seconda del contenuto del ramo … o forse non lo farai). Potresti anche inserire o eliminare un ramo da destra nel mezzo della cronologia di sviluppo (anche se così facendo probabilmente cambierai i commit di molti commit). La cronologia di riscrittura è una delle caratteristiche principali di Git che lo rende così potente e flessibile.

Questo è il motivo per cui i commit arrivano sia con una data autenticata (quando il commit è stato originariamente creato), sia con una data di commit (quando il commit è stato commesso l’ultimo sull’albero di commit). Puoi pensare a loro come analoghi per creare data-ora e data-ora dell’ultima modifica.

I supervisori a volte amano sapere … a quale ramo appartengono alcune modifiche (per ottenere lo scopo di qualche cambiamento – era necessario per il lavoro).

Ancora una volta, poiché Git ti consente di riscrivere la cronologia, puoi (ri) basare un insieme di modifiche su praticamente qualsiasi ramo / commit nel grafico del commit che desideri. git rebase ti consente letteralmente di muovere liberamente l’intero ramo (anche se potresti dover risolvere i conflitti man mano che procedi, a seconda di dove si sposta il ramo e di cosa contiene).

Detto questo, uno degli strumenti che puoi usare in Git per determinare quali rami o tag contengono un insieme di modifiche è il --contains :

 # Which branches contains commit X? git branch --all --contains X # Which tags contains commit X? git tag --contains X 

L’avviso di generosità su questa domanda chiede,

Sarei interessato a sapere se pensare o meno ai rami di Git come se avesse un commit “iniziale” definito diverso dal commit di root ha anche senso?

In qualche modo, tranne:

  • il commit di root è “il primo commit accessibile dal ramo HEAD” (e non dimenticare che può esistere più commit di root con rami orfani , usato ad esempio in GitHub per gh-pages )
  • Preferisco considerare l’inizio di un ramo come il commit di un altro ramo da cui è stato creato il suddetto ramo (la risposta di Tobib senza il ~1 ), o (più semplice) l’ antenato comune .
    (anche in ” Trovare un punto di diramazione con Git? “, anche se l’ OP ha menzionato di non essere interessato ai comuni antenati ):

     git merge-base A master 

Questo significa:

  • la prima definizione ti dà un commit fisso (che potrebbe non cambiare mai tranne che in caso di un grosso filter-branch )
  • la seconda definizione ti dà un commit relativo (relativo ad un altro ramo) che può cambiare in qualsiasi momento (l’altro ramo può essere cancellato)

Il secondo ha più senso per git, che riguarda l’unione e il rebase tra i rami.

I supervisori a volte vogliono sapere, quando un ramo è stato avviato (di solito segna l’inizio di un’attività) e a quale ramo appartengono alcune modifiche (per ottenere lo scopo di qualche cambiamento – era necessario per il lavoro)

I rami sono semplicemente l’indicatore sbagliato per questo: a causa della natura transitoria dei rami (che possono essere rinominati / spostati / ribattuti / cancellati / …), non è ansible imitare un “set di modifiche” o una “attività” con un ramo, rappresentare un “compito”.

Questo è un problema XY : l’OP sta chiedendo una soluzione tentata (dove inizia un ramo) piuttosto che il problema reale (cosa potrebbe essere considerata un’attività in Git).

Per fare ciò (rappresentando un compito), puoi usare:

  • tags : sono immutabili (una volta associato a un commit, il commit non è più necessario spostare / essere ridefinito) e qualsiasi commit tra due tag ben denominati può rappresentare un’attività.
  • alcune git notes a un commit per memorizzare a quale “elemento di lavoro” è stato creato il commit (contrariamente ai tag, le note possono essere riscritte se il commit è modificato o ridefinito).
  • hook (per associare un commit ad un object “esterno” come un “object di lavoro”, basato sul messaggio di commit). Questo è ciò che il bridge Git-RTC – IBM Rational Team Concert – fa con un hook di pre-ricezione . Il punto è: l’inizio di un ramo non riflette sempre l’inizio di un’attività, ma semplicemente la continuazione di una storia che può cambiare, e quale sequenza dovrebbe rappresentare un insieme logico di cambiamenti.

Forse stai facendo la domanda sbagliata. IMO, non ha senso chiedere dove inizia un ramo poiché un dato ramo include tutte le modifiche apportate a ogni file in assoluto (cioè dal commit iniziale).

D’altra parte, chiedere dove due divergenze divergono è sicuramente una domanda valida. In effetti, questo sembra essere esattamente quello che vuoi sapere. In altre parole, non vuoi veramente sapere le informazioni su un singolo ramo. Invece, vuoi sapere alcune informazioni sul confronto di due rami.

Un po ‘di ricerca ha trovato la pagina man di gitrevisions che descrive i dettagli di riferimento a specifici commit e gamme di commit. In particolare,

Per escludere i commit raggiungibili da un commit, viene utilizzato un prefisso ^ notazione. Ad esempio ^ r1 r2 significa commit raggiungibile da r2 ma esclude quelli raggiungibili da r1.

Questa operazione impostata appare così spesso che esiste una scorciatoia per questo. Quando si hanno due commit r1 e r2 (denominati in base alla syntax spiegata in SPECIFICARE LE REVISIONI sopra), è ansible chiedere commit che sono raggiungibili da r2 escludendo quelli che sono raggiungibili da r1 per ^ r1 r2 e può essere scritto come r1. .R2.

Quindi, usando l’esempio della tua domanda, puoi ottenere i commit dove branch-A diverge dal master con

 git log master..branch-A 

Penso che questa sia probabilmente una buona opportunità per l’educazione. git realtà non registra il punto di partenza di nessun ramo. A meno che il reflog di quel ramo contenga ancora il record di creazione, non c’è modo di determinare in modo definitivo dove è stato avviato, e se il branch si è fuso ovunque, potrebbe infatti avere più di un commit di root, oltre a molti diversi possibili punti dove potrebbe essere stato creato e iniziato a divergere dalla sua fonte originale.

Potrebbe essere una buona idea porre una domanda contraria in questi casi – perché hai bisogno di sapere da dove si diramano, o importa in qualche modo utile da dove è derivata? Potrebbero esserci o meno validi motivi per cui questo è importante – molte delle ragioni sono probabilmente legate allo specifico stream di lavoro che il tuo team ha adottato e sta cercando di applicare e potrebbero indicare aree in cui il tuo stream di lavoro potrebbe essere migliorato in qualche modo. Forse un miglioramento sarebbe capire quali sono le domande “giuste” da porsi – per esempio, piuttosto che “da dove è uscito branch-B “, forse “quali rami fanno o non contengono le correzioni / nuove funzionalità introdotte dalla branch-B “…

Non sono sicuro che una risposta completamente soddisfacente a questa domanda esista davvero …

Ci sono due preoccupazioni separate qui. A partire dal tuo esempio,

 A - B - C - - - - J [master] \ \ F - G [branch-A] \ / D - E \ H - I [branch-B] 

[…] A volte i supervisori amano sapere, quando un ramo è stato avviato (di solito segna l’inizio di un’attività) e a quale ramo appartengono alcune modifiche (per ottenere lo scopo di qualche cambiamento – era necessario per il lavoro)

due osservazioni concrete prima di arrivare alla carne:

Prima osservazione: ciò che il vostro supervisore vuole sapere è la mapping tra commit e qualche record di workhors esterno: cosa si intende per bug-43289 o featureB? Perché stiamo cambiando l’uso di strcat in longmsg.c ? Chi pagherà per le venti ore tra la tua spinta precedente e questa? I nomi delle filiali non contano qui, ciò che conta sono le relazioni dei commit con i record amministrativi esterni.

Seconda osservazione: se la branch-A o la branch-B vengono pubblicate per prime (tramite say merge o rebase o push), il lavoro in commit D ed E deve andare con esso in quel momento e non essere duplicato da nessuna operazione successiva. Non fa alcuna differenza quello che era corrente quando sono stati fatti quei commit. Anche i nomi delle filiali non contano. Ciò che conta sono le relazioni di commit l’una con l’altra attraverso il grafico di ascendenza.


Quindi la mia risposta è, per quanto riguarda la storia, i nomi dei rami non contano affatto. Sono tag di convenienza che mostrano quale commit è corrente per qualche scopo specifico per quel repository, niente di più. Se vuoi qualche utile moniker nella riga dell’object del messaggio di commit di merge predefinito, git branch some-useful-name tip git branch some-useful-name prima di fonderlo e uniscilo. Sono gli stessi commit in entrambi i casi.

Legare qualsiasi nome di branca che lo sviluppatore abbia verificato al momento del commit con qualche record esterno – o qualcosa del genere – è profondamente inserito nel territorio “tutto bene fintanto che tutto funziona”. Non farlo. Anche con l’uso limitato della maggior parte dei VCS, il tuo DE-{FG,HI} verrà eseguito prima piuttosto che dopo, e quindi le convenzioni di denominazione delle filiali dovranno essere adattate per gestirle, e quindi verrà mostrato qualcosa di più complicato. . .

Perché preoccuparsi? Inserisci il / i numero / i del report che richiede il lavoro in una tagline nella parte inferiore dei messaggi di commit e fallo con esso. git log --grep (e git in generale) è incredibilmente veloce per una buona ragione.

Anche un hook di preparazione abbastanza flessibile per inserire tagline come questo è banale:

 branch=`git symbolic-ref -q --short HEAD` # branch name if any workorder=`git config branch.${branch:+$branch.}x-workorder` # specific or default config tagline="Acme-workorder-id: ${workorder:-***no workorder supplied***}" sed -i "/^ *Acme-workorder-id:/d; \$a$tagline" "$1" 

ed ecco il ciclo di hook di pre-ricezione di base per quando è necessario ispezionare ogni commit:

 while read old new ref; do # for each pushed ref while read commit junk; do # check for bad commits # test here, eg git show -s $commit | grep -q '^ *Acme-workorder-id: ' \ || { rc=1; echo commit $commit has no workorder associated; } # end of this test done < 

Il progetto del kernel utilizza taglines come questo per la registrazione del codice d'autore e la registrazione del codice. Non potrebbe davvero diventare molto più semplice o più robusto.

Nota che ho fatto un po 'di manomissione dopo c & p per de-specializzare gli script reali. Avviso da tastiera a modifica

Da un punto di vista filosofico, la questione della storia di un ramo non può essere risolta in senso globale. Tuttavia, il reflog tiene traccia della cronologia di ogni ramo in quel particolare repository .

Quindi, se hai un singolo repository centrale a cui tutti spingono, puoi usare il suo reflog per tenere traccia di queste informazioni (alcuni dettagli in questa domanda ). Innanzitutto, su quel repository centrale, assicurati che il reflog sia registrato e non venga mai ripulito:

 $ git config core.logAllRefUpdates true $ git config gc.reflogExpire never 

Quindi puoi eseguire git reflog per ispezionare la cronologia del ramo.

Esempio

Ho riprodotto il tuo grafico di commit di esempio con alcuni push in un repository di test. Ora posso fare questo genere di cose:

 $ git log --graph --all --oneline --decorate * 64c393b (branch-b) commit I * feebd2f commit H | * 3b9dbb6 (branch-a) commit G | * 18835df commit F |/ * d3840ca commit E * b25fd0b commit D | * 8648b54 (master) commit J | * 676e263 commit C |/ * 17af0d2 commit B * bdbfd6a commit A $ git reflog --date=local master branch-a branch-b 64c393b [email protected]{Sun Oct 11 21:45:03 2015}: push 3b9dbb6 [email protected]{Sun Oct 11 21:45:17 2015}: push 18835df [email protected]{Sun Oct 11 21:43:32 2015}: push 8648b54 [email protected]{Sun Oct 11 21:42:09 2015}: push 17af0d2 [email protected]{Sun Oct 11 21:41:29 2015}: push bdbfd6a [email protected]{Sun Oct 11 21:40:58 2015}: push 

Quindi puoi vedere che nel mio esempio, quando branch-a entrato in esistenza, è stato puntato su commit F , e che una seconda spinta al server centrale l’ha spostata in avanti per commettere G Considerando che quando il branch-b venuto alla luce per la prima volta, è stato indicato con commit I , e non ha ancora visto alcun aggiornamento.

Avvertenze

Questo mostra solo la storia come è stata spinta al repository centrale . Se per esempio un collaboratore ha avviato il branch-A nel commit A , ma poi lo ha rebasato sul commit B prima di inviarlo, quelle informazioni non si rifletteranno nel reflog del repository centrale.

Questo inoltre non fornisce una registrazione definitiva di dove un ramo ha avuto inizio . Non possiamo davvero dire con certezza quale ramo “proprietario” impegna D ed E inizialmente biforcato dal master. Sono stati creati su branch-a e poi prelevati da branch-b , o viceversa?

Entrambe le filiali apparivano inizialmente nel repository centrale che conteneva quei commit, e il reflog ci dice quale ramo apparve sul repository centrale per primo. Tuttavia, questi commit potrebbero essere stati “passati in giro” tra diversi repository di utenti finali, tramite format-patch , ecc. Quindi anche se sappiamo quale puntatore di ramo era responsabile per portarli prima al server centrale, non sappiamo origine ultima

Come spiegato da @cupcake, non esiste un punto di partenza di un ramo. Puoi solo controllare dove un ramo ha toccato prima un altro. Questo è probabilmente ciò che vuoi nella maggior parte dei casi. @ code-guru ha già spiegato la syntax per fare riferimento a intervalli di commit.

Mettere tutto insieme: questo comando mostra il primo commit prima del primo commit che era nel branch-A ma non nel master :

git show `git rev-list branch-A ^master --topo-order | tail -n 1`~1

Alcuni dettagli architettonici

Git memorizza le revisioni in un repository come una serie di commit. Questi commit contengono un collegamento alle informazioni sulle modifiche ai file dall’ultimo commit e, soprattutto, un link al commit precedente. In termini generali, la cronologia del commit di un ramo è un elenco collegato singolarmente dalla revisione più recente fino alla radice del repository. Lo stato del repository in ogni commit è che il commit combinato con tutti i commit prima di tutto il ritorno alla radice.

Allora, qual è il TESTA? E cos’è un ramo?

L’HEAD è un puntatore speciale per il nuovo commit nel ramo attualmente attivo. Ogni ramo, incluso il master 1 , è anche un puntatore all’ultima revisione nella sua storia.

Chiaro come fango? Diamo un’occhiata ad un esempio usando un’immagine del libro Pro Git , che si spera possa chiarire un po ‘le cose. 2

Semplice albero di Git

In questo diagramma abbiamo un repository relativamente semplice con 4 commit. 98ca9 è la radice. Ci sono due rami, master e testing. Il ramo principale si trova su commit f30ab mentre il ramo di test si trova a 87ab2 . Attualmente stiamo lavorando nel ramo principale, quindi HEAD punta al ramo principale. La cronologia delle filiali nel nostro repository di esempio sono (dal più recente al più vecchio):

 testing: 87ab2 -> f30ab -> 34ac2 -> 98ca9 master: f30ab -> 34ac2 -> 98ca9 

Da questo possiamo vedere che i due rami sono gli stessi a partire da f30ab , quindi possiamo anche dire che il test è stato ramificato in quel commit.

Il libro Pro Git è molto più dettagliato e vale sicuramente la pena leggerlo.

Ora possiamo indirizzare–

La domanda specifica

Fancifying il diagramma otteniamo:

Fancy come un mignolo a bere il tè.

Il commit D è un membro di entrambe le filiali o possiamo decidere chiaramente se appartiene al ramo A o al ramo B?

Sapendo ciò che ora sappiamo, possiamo vedere che commit D è un membro di entrambe le catene che vanno dai puntatori del ramo alla radice. Quindi possiamo dire che D è un membro di entrambi i rami.

Quale ramo è iniziato a E, quale a B?

Sia branch-A che branch-B hanno origine dal ramo master in B, e divergono l’uno dall’altro a E. Git stesso non distingue quale ramo possiede E. Nel loro nucleo, i rami sono solo la catena di commit da più recente a il più vecchio finisce alla radice.


1 Curiosità: il ramo principale è solo un ramo ordinario. Non è diverso da qualsiasi altro ramo.

2 Il libro Pro Git è concesso in licenza con licenza Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported.