Ho intenzione di spiegare alcuni sfondi Git per comprendere perché le forcelle longeve sono così difficili.
In Git, una versione / commit rappresenta uno snapshot / stato del codice sorgente. Non rappresenta un insieme di modifiche. Pertanto, non esiste un modo elegante per tracciare diverse "edizioni" o "varianti" del tuo software.
Un ramo e un fork / repository separati sono più o meno la stessa cosa per quanto riguarda Git. All'interno della storia di un repository Git, nessuno di questi concetti esiste: ci sono solo commit. In particolare, un ramo è solo un'etichetta mobile che punta a un commit. Pertanto, mentre i rami vengono spesso utilizzati per organizzare diversi thread di sviluppo, non rappresentano tali cambiamenti. Invece, il commit HEAD di un ramo rappresenta solo lo stato attuale di sviluppo e il commit contiene qualsiasi cronologia che ha portato a quello stato.
L'unione tenta di unificare lo stato di due (o più) commit / snapshot del codice sorgente. Innanzitutto, Git cerca l'ultimo antenato comune di tutti i commit principali, quindi calcola i differenziali tra la testa di ciascun genitore e l'antenato comune. Quindi cerca di unire entrambe le differenze. Se non c'è conflitto, Git applica le modifiche e crea un nuovo commit con questo stato.
Più modifiche ci sono, più conflitti di fusione ottieni e più diventa difficile l'unione. Pertanto, la fusione anticipata è fondamentale per mantenere il carico di lavoro gestibile. Ma questo significa anche che dovresti unire due rami in entrambe le direzioni. Altrimenti, le modifiche non raggruppate continuano ad accumularsi su un ramo e dovrai risolvere gli stessi conflitti più e più volte su ogni unione:
1---2---3---4---5 fork
/ / /
---a---b---c---d---e master
Nell'esempio precedente, c'è un ramo master e un fork longevo.
-
Commit 3
è un'unione con merge-base a
. Questo unisce le modifiche a..c
con a..2
e registra un nuovo commit 3
. Relativo al fork, questa unione rappresenta il modo in cui sono state applicate le modifiche a..c
. Poiché i conflitti di unione potrebbero essere stati risolti, potrebbe non essere effettivamente uguale alle modifiche a..c
.
-
Commit 5
è un'altra unione, questa volta con la base merge c
. Questo unisce le modifiche c..e
con c..4
. Si noti che il commit 3
(che rappresenta il modo in cui sono stati risolti i conflitti precedenti tra i rami) fa parte di uno solo di questi rami. Laddove i due rami differivano durante l'unione 3
, probabilmente si differenziano ancora durante l'unione 5
, quindi dovrai risolvere quelle differenze / conflitti di nuovo .
In effetti, ogni unione viene trasferita alla successiva unione e lo sforzo aumenta. A meno che non si fondano in entrambe le direzioni. Ma ciò va contro il tuo obiettivo di avere Fancy
stuff solo nel fork.
(Esistono approcci alternativi come rebasing e cherry picking che applicano un insieme di modifiche in cima a un commit diverso. Perché questo guarda solo alle modifiche introdotte da quei commit e non all'istantanea del codice sorgente che rappresentano, questo soffre meno dai conflitti.Tuttavia, questo non unisce le cronologie Git ma registra commit completamente nuovi, non correlati per rappresentare il nuovo stato del codice sorgente (riscrittura della cronologia). E se la forcella si discosta troppo, il diff potrebbe non "adattarsi" più al codice sorgente , che richiede di applicare ogni modifica manualmente.)
Quindi Git non è in grado di aiutare qui. Ci sono due approcci:
Uno è quello di visualizzare una variante del software come una serie di patch l'una sull'altra. La forcella non mantiene il risultato finale di tali modifiche, ma solo le differenze che rappresentano queste modifiche. Il repository originale potrebbe quindi essere un sottomodulo. Ogni volta che l'originale cambia, si modificano le patch in modo che continuino a essere adattate. All'interno di Git, potrebbero anche esserci alcuni commit che rebase continuamente in cima al master originale, ma che perde la cronologia delle tue modifiche. Questo è appropriato solo per modifiche chirurgiche molto piccole che non possono essere unite nel repository originale. Mantenere le patch sarebbe opportuno per piccole estensioni o correzioni di bug che non è possibile restituire.
L'approccio migliore è quello di risolvere il problema a livello di architettura software, non a livello Git. Invece di apportare modifiche dirette al software, rendere il software così configurabile che le modifiche possano essere implementate come estensioni. Parole chiave pertinenti: Principio aperto / chiuso, Alterna funzionalità, Iniezione di dipendenza, Modelli di progettazione, Plugin. Quindi, il progetto principale espone i punti di estensione in modo da poter inserire le modifiche di fantasia, oppure hai letteralmente una singola base di codice in cui la modalità fantasia / non-fantasia può essere selezionata al momento della compilazione o in fase di esecuzione.