Cosa fa esattamente (rebase --preserve-merges) di git (e perché?)


355

La documentazionerebase di Git per il comando è piuttosto breve:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).

Quindi cosa succede realmente quando lo usi --preserve-merges? In cosa differisce dal comportamento predefinito (senza quella bandiera)? Cosa significa "ricreare" un'unione, ecc.


20
Attenzione: a partire da Git 2.18 (Q2 2018, 5 anni dopo), git --rebase-mergesalla fine sostituirà il vecchio git --preserve-merges. Vedi la mia risposta di seguito
VonC,

Risposte:


464

Come nel caso di un normale gase rebase, git --preserve-mergesidentifica innanzitutto un elenco di commit eseguiti in una parte del grafico di commit, quindi riproduce tali commit in cima a un'altra parte. Le differenze con --preserve-mergespreoccupazione quali commit vengono selezionate per il replay e come funziona quel replay per merge commit.

Per essere più espliciti sulle principali differenze tra rebase normale e merge preserving:

  • Il rebase che preserva l'unione è disposto a ripetere (alcuni) i commit di unione, mentre il rebase normale ignora completamente i commit di unione.
  • Poiché è disposto a riprodurre i commit di fusione, rebase che preserva l'unione deve definire cosa significa riprodurre un commit di fusione e gestire alcune rughe extra
    • La parte più interessante, concettualmente, è forse nel scegliere quale dovrebbero essere i genitori merge del nuovo commit.
    • La riproduzione di commit merge richiede anche il check-out esplicito di particolari commit ( git checkout <desired first parent>), mentre il rebase normale non deve preoccuparsene.
  • Il rebase che preserva l'unione considera una serie di commit meno profondi per la riproduzione:
    • In particolare, prenderà in considerazione solo la ripetizione degli commit effettuati dalla più recente base (e) di unione - ovvero l' ultima volta in cui i due rami sono divergenti -, mentre il rebase normale potrebbe ripetere i commit che risalgono alla prima volta in cui i due rami sono divergenti.
    • Per essere provvisorio e poco chiaro, credo che questo sia in definitiva un mezzo per escludere la riproduzione di "vecchi commit" che sono già stati "incorporati" in un commit di merge.

In primo luogo cercherò di descrivere "sufficientemente esattamente" cosa --preserve-mergesfa rebase , e poi ci saranno alcuni esempi. Ovviamente si può iniziare con gli esempi, se questo sembra più utile.

L'algoritmo in "Breve"

Se vuoi davvero entrare nelle erbacce, scarica la fonte git ed esplora il file git-rebase--interactive.sh. (Rebase non fa parte del core C di Git, ma è scritto in bash. E, dietro le quinte, condivide il codice con "rebase interattivo".)

Ma qui traccerò quello che penso ne sia l'essenza. Per ridurre il numero di cose a cui pensare, ho preso alcune libertà. (ad esempio, non provo a catturare con la precisione del 100% l'ordine preciso in cui avvengono i calcoli, e ignoro alcuni argomenti meno apparentemente centrali, ad esempio cosa fare per i commit che sono già stati scelti tra i rami).

Innanzitutto, si noti che un rebase che non preserva l'unione è piuttosto semplice. È più o meno:

Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A") 
Replay all those commits onto B one at a time in order.

Rebase --preserve-mergesè relativamente complicato. Ecco quanto sono stato in grado di farlo senza perdere cose che sembrano piuttosto importanti:

Find the commits to replay:
  First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
    This (these) merge base(s) will serve as a root/boundary for the rebase.
    In particular, we'll take its (their) descendants and replay them on top of new parents
  Now we can define C, the set of commits to replay. In particular, it's those commits:
    1) reachable from B but not A (as in a normal rebase), and ALSO
    2) descendants of the merge base(s)
  If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less:
    git log A..B --not $(git merge-base --all A B)
Replay the commits:
  Create a branch B_new, on which to replay our commits.
  Switch to B_new (i.e. "git checkout B_new")
  Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
    If it's a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
    Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c':
      To create a merge commit, its parents must exist and we must know what they are.
      So first, figure out which parents to use for c', by reference to the parents of c:
        For each parent p_i in parents_of(c):
          If p_i is one of the merge bases mentioned above:
            # p_i is one of the "boundary commits" that we no longer want to use as parents
            For the new commit's ith parent (p_i'), use the HEAD of B_new.
          Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
            # Note: Because we're moving parents-before-children, a rewritten version
            # of p_i must already exist. So reuse it:
            For the new commit's ith parent (p_i'), use the rewritten version of p_i.
          Otherwise:
            # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it
            For the new commit's ith parent (p_i'), use p_i, i.e. the old commit's ith parent.
      Second, actually create the new commit c':
        Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
        Merge in the other parent(s):
          For a typical two-parent merge, it's just "git merge p_2'".
          For an octopus merge, it's "git merge p_2' p_3' p_4' ...".
        Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
  Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")

Rebase con un --onto Cargomento dovrebbe essere molto simile. Invece di avviare la riproduzione in commit su HEAD of B, si avvia invece la riproduzione in commit su HEAD of C. (E usa C_new invece di B_new.)

Esempio 1

Ad esempio, prendi il grafico di commit

  B---C <-- master
 /                     
A-------D------E----m----H <-- topic
         \         /
          F-------G

m è un commit di unione con i genitori E e G.

Supponiamo di aver riformulato l'argomento (H) sopra il master (C) usando un rebase normale, che non preserva l'unione. (Ad esempio, argomento di checkout; rebase master .) In tal caso, git selezionerebbe i seguenti commit per la riproduzione:

  • scegli D
  • scegli E
  • scegli F
  • scegli G
  • scegli H

e quindi aggiorna il grafico di commit in questo modo:

  B---C <-- master
 /     \                
A       D'---E'---F'---G'---H' <-- topic

(D 'è l'equivalente riprodotto di D, ecc.)

Si noti che unisci commit m non è selezionato per la riproduzione.

Se invece facessimo un --preserve-mergesrebase di H sopra C. (Ad esempio, argomento di checkout; rebase --preserve-merges master .) In questo nuovo caso, git selezionerebbe i seguenti commit per la riproduzione:

  • scegli D
  • scegli E
  • seleziona F (su D 'nel ramo' argomento secondario ')
  • scegli G (su F 'nel ramo' argomento secondario ')
  • seleziona Unisci argomento "argomento secondario" nell'argomento
  • scegli H

Ora m è stato scelto per la riproduzione. Si noti inoltre che i genitori di unione E e G sono stati scelti per l'inclusione prima della fusione unione m.

Ecco il grafico di commit risultante:

 B---C <-- master
/     \                
A      D'-----E'----m'----H' <-- topic
        \          / 
         F'-------G'

Ancora una volta, D 'è una versione prescelta (cioè ricreata) di D. Lo stesso per E', ecc. Ogni commit non sul master è stato riprodotto. Sia E che G (i genitori unificati di m) sono stati ricreati come E 'e G' per servire da genitori di m '(dopo il rebase, la storia dell'albero rimane sempre la stessa).

Esempio 2

A differenza del rebase normale, il rebase che preserva l'unione può creare più elementi secondari della testa a monte.

Ad esempio, considera:

  B---C <-- master
 /                     
A-------D------E---m----H <-- topic
 \                 |
  ------- F-----G--/ 

Se rifasiamo H (argomento) sopra C (master), i commit scelti per rebase sono:

  • scegli D
  • scegli E
  • scegli F
  • scegli G
  • scegli m
  • scegli H

E il risultato è così:

  B---C  <-- master
 /    | \                
A     |  D'----E'---m'----H' <-- topic
       \            |
         F'----G'---/

Esempio 3

Negli esempi precedenti, sia il commit di unione che i suoi due genitori sono commit di riproduzione, piuttosto che i genitori originali che hanno il commit di unione originale. Tuttavia, in altri rebase un commit di unione riprodotto può finire con i genitori che erano già nel grafico di commit prima dell'unione.

Ad esempio, considera:

  B--C---D <-- master
 /    \                
A---E--m------F <-- topic

Se ridisegniamo l'argomento su master (preservando le fusioni), gli impegni per la riproduzione saranno

  • pick merge commit m
  • scegli F

Il grafico di commit riscritto sarà simile al seguente:

                     B--C--D <-- master
                    /       \             
                   A-----E---m'--F'; <-- topic

Qui viene riprodotto il commit di unione m 'ottiene i genitori preesistenti nel grafico di commit, ovvero D (la TESTA del master) ed E (uno dei genitori del commit di unione originale m).

Esempio 4

Il rebase che preserva l'unione può essere confuso in alcuni casi di "commit vuoto". Almeno questo è vero solo alcune versioni precedenti di git (ad es. 1.7.8.)

Prendi questo grafico di commit:

                   A--------B-----C-----m2---D <-- master
                    \        \         /
                      E--- F--\--G----/
                            \  \
                             ---m1--H <--topic

Si noti che entrambi i commit m1 e m2 dovrebbero aver incorporato tutte le modifiche da B e F.

Se proviamo a fare git rebase --preserve-mergesH (topic) su D (master), per la riproduzione vengono scelti i seguenti commit:

  • scegli m1
  • scegli H

Nota che le modifiche (B, F) unite in m1 dovrebbero già essere incorporate in D. (Tali modifiche dovrebbero già essere incorporate in m2, perché m2 unisce i figli di B e F.) Pertanto, concettualmente, riproducendo m1 sopra D dovrebbe probabilmente essere una no-op o creare un commit vuoto (cioè uno in cui la differenza tra le revisioni successive è vuota).

Invece, git potrebbe rifiutare il tentativo di riprodurre m1 sopra D. Puoi ottenere un errore del genere:

error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed

Sembra che uno abbia dimenticato di passare una bandiera a git, ma il problema di fondo è che a git non piace creare commit vuoti.


6
Ho notato che git rebase --preserve-mergesè molto più lento di un rebasesenza --preserve-merges. È un effetto collaterale di trovare gli impegni giusti? C'è qualcosa che si può fare per accelerarlo? (A proposito ... grazie per la risposta molto dettagliata!)
David Alan Hjelle il

7
Sembra che dovresti sempre usare --preserve-merges. Altrimenti esiste il potenziale per perdere la storia, ovvero la fusione si impegna.
DarVar

19
@DarVar Perdi sempre la cronologia su un rebase, perché affermi che le modifiche sono state apportate a una base di codice diversa da quella in cui effettivamente.
Cronologico

5
È ancora una "risposta provvisoria"?
Andrew Grimm,

5
@Chronial Ovviamente hai ragione, che il rebasing include sempre la perdita della storia, ma forse DarVar allude al fatto che non solo perdi la storia, ma cambi anche alla base di codice. La risoluzione dei conflitti contiene informazioni che si perdono in tutti i modi possibili in cui un rebase può essere fatto. Devi sempre rifarlo. Non c'è davvero alcun modo, per consentire a Git di ripetere la risoluzione del conflitto? Perché Git Cherry non può scegliere il commit di unione?
Nils_M,

94

Git 2.18 (Q2 2018) migliorerà considerevolmente l' --preserve-mergeopzione aggiungendo una nuova opzione.

" git rebase" imparato " --rebase-merges" per trapiantare l'intera topologia del grafico di commit altrove .

(Nota: Git 2.22, Q2 2019, in realtà si deprezza --preserve-merge , e Git 2.25, Q1 2020, smette di pubblicizzarlo git rebase --helpnell'output " " )

Vedere commettere 25cff9f , commettere 7543f6f , commettere 1131ec9 , commettere 7ccdf65 , commettere 537e7d6 , commettere a9be29c , commettere 8f6aed7 , commettere 1644c73 , commettere d1e8b01 , commettere 4c68e7d , commettere 9055e40 , commettere cb5206e , commettere a01c2a5 , commettere 2f6b1d1 , commettere bf5c057 (25 Apr 2018) di Johannes Schindelin ( dscho) .
Vedi commit f431d73 (25 apr 2018) di Stefan Beller ( stefanbeller) .
Vedi commit 2429335 (25 aprile 2018) di Phillip Wood ( phillipwood) .
(Unita da Junio ​​C Hamano - gitster- in commit 2c18e6a , 23 maggio 2018)

pull: accetta --rebase-mergesdi ricreare la topologia del ramo

Simile alla preservemodalità che passa semplicemente l' --preserve-merges opzione al rebasecomando, la mergesmodalità passa semplicemente l' --rebase-mergesopzione.

Ciò consentirà agli utenti di riformulare convenientemente topologie di commit non banali quando eseguono il pull di nuove commit, senza appiattirle.


git rebaseLa pagina man ora ha una sezione completa dedicata alla riassegnazione della storia con le fusioni .

Estratto:

Esistono motivi legittimi per cui uno sviluppatore potrebbe voler ricreare i commit di merge: mantenere la struttura del ramo (o "commettere topologia") quando si lavora su più rami collegati.

Nell'esempio seguente, lo sviluppatore lavora su un ramo di argomento che riformula la modalità di definizione dei pulsanti e su un altro ramo di argomento che utilizza tale refactoring per implementare un pulsante "Segnala un bug".
L'output di git log --graph --format=%s -5potrebbe essere simile al seguente:

*   Merge branch 'report-a-bug'
|\
| * Add the feedback button
* | Merge branch 'refactor-button'
|\ \
| |/
| * Use the Button class for all buttons
| * Extract a generic Button class from the DownloadButton one

Lo sviluppatore potrebbe voler riassegnare questi commit a un più nuovo master mantenendo la topologia del ramo, ad esempio quando si prevede che il primo ramo dell'argomento sarà integrato in mastermolto prima del secondo, per esempio, per risolvere i conflitti di unione con le modifiche alla DownloadButtonclasse che ha reso in master.

Questo rebase può essere eseguito usando l' --rebase-mergesopzione.


Vedi commit 1644c73 per un piccolo esempio:

rebase-helper --make-script: introduce una bandiera per rifare le fusioni

Il sequencer ha appena appreso nuovi comandi destinati a ricreare la struttura del ramo ( simile nello spirito a --preserve-merges, ma con un design sostanzialmente meno rotto ).

Consentiamo di rebase--helpergenerare liste di todo facendo uso di questi comandi, attivati ​​dalla nuova --rebase-mergesopzione.
Per una topologia di commit come questa (dove HEAD punta a C):

- A - B - C (HEAD)
    \   /
      D

l'elenco di todo generato sarebbe simile al seguente:

# branch D
pick 0123 A
label branch-point
pick 1234 D
label D

reset branch-point
pick 2345 B
merge -C 3456 D # C

Qual è la differenza con --preserve-merge?
Commit 8f6aed7 spiega:

C'era una volta questo sviluppatore che pensava: non sarebbe bello se, diciamo, le patch di Git per Windows sulla parte superiore del core Git potessero essere rappresentate come un boschetto di rami ed essere ridisegnate sulla parte superiore del core Git per mantenere una serie selezionabile di serie di patch?

Il tentativo originale di rispondere a questa era: git rebase --preserve-merges.

Tuttavia, quell'esperimento non è mai stato inteso come un'opzione interattiva e si è solo appoggiato git rebase --interactiveperché l'implementazione di quel comando sembrava già molto, molto familiare: è stata progettata dalla stessa persona che ha progettato --preserve-merges: il tuo veramente.

E per "il tuo vero", l'autore si riferisce a se stesso: Johannes Schindelin ( dscho) , che è la ragione principale (con alcuni altri eroi - Hannes, Steffen, Sebastian, ...) che abbiamo Git For Windows (anche se indietro nel tempo - 2009 - non è stato facile ).
Lavora in Microsoft da settembre 2015 , il che ha senso considerando che Microsoft ora utilizza pesantemente Git e ha bisogno dei suoi servizi.
Quella tendenza è iniziata nel 2013 in realtà, con TFS . Da allora, Microsoft gestisce il più grande repository Git del pianeta ! E, dall'ottobre 2018, Microsoft ha acquisito GitHub .

Puoi vedere Johannes parlare in questo video per Git Merge 2018 nell'aprile 2018.

Qualche tempo dopo, qualche altro sviluppatore (ti sto guardando, Andreas! ;-)) ha deciso che sarebbe una buona idea consentire --preserve-mergesdi combinare --interactive(con avvertenze!) E il manutentore Git (beh, il manutentore temporaneo di Git durante l'assenza di Junio, cioè) d'accordo, e fu allora che il glamour del --preserve-mergesdesign iniziò a sfaldarsi piuttosto rapidamente e senza afflizione.

Qui Jonathan parla di Andreas Schwab di Suse.
Puoi vedere alcune delle loro discussioni nel 2012 .

La ragione? Nella --preserve-mergesmodalità, i genitori di un commit di unione (o del resto, di qualsiasi commit) non venivano dichiarati esplicitamente, ma erano impliciti dal nome del commit passato al pickcomando .

Ciò ha reso impossibile, ad esempio, riordinare i commit .
Per non parlare di spostare gli impegni tra le filiali o, vietare la divinità, di dividere in due le sezioni dell'argomento.

Purtroppo, queste carenze hanno anche impedito a quella modalità (il cui scopo originale era soddisfare le esigenze di Git per Windows, con l'ulteriore speranza che potesse essere utile anche ad altri) dal soddisfare le esigenze di Git per Windows.

Cinque anni dopo, quando divenne davvero insostenibile avere una serie di patch hodge-podge ingombranti e ingombranti di patch parzialmente correlate, parzialmente non correlate in Git per Windows che di tanto in tanto venivano ridisegnate sui tag core di Git (guadagnando l'ira immeritata dello sviluppatore della git-remote-hgserie sfortunata che per prima ha obsoleto l'approccio concorrenziale di Git per Windows, per poi essere abbandonato senza un manutentore in seguito) era davvero insostenibile, nacquero le " cesoie da giardino Git " : una sceneggiatura, appoggiata sulle spalle del rebase interattivo, che determinerebbe dapprima la topologia del ramo delle patch da rielaborare, creerebbe un elenco di pseudo todo per ulteriori modifiche, trasformerebbe il risultato in un vero elenco di todo (facendo un uso pesante delexec per "implementare" i comandi mancanti dell'elenco todo) e infine ricreare le serie di patch in cima al nuovo commit di base.

(Lo script di cesoie Git Garden fa riferimento a questa patch in commit 9055e40 )

Era il 2013.
E ci sono volute circa tre settimane per elaborare il progetto e implementarlo come uno script fuori dall'albero. Inutile dire che l'implementazione ha richiesto alcuni anni per stabilizzarsi, mentre il design stesso si è dimostrato solido.

Con questa patch, la bontà delle cesoie da giardino Git arriva a git rebase -ise stessa .
Passare l' --rebase-mergesopzione genererà un elenco di cose da fare che può essere compreso facilmente e dove è ovvio come riordinare i commit .
Nuovi rami possono essere introdotti inserendo labelcomandi e chiamando merge <label>.
E una volta che questa modalità sarà diventata stabile e universalmente accettata, possiamo deprecare l'errore di progettazione che è stato--preserve-merges .


Git 2.19 (Q3 2018) migliora la nuova --rebase-mergesopzione facendola funzionare --exec.

L '" --exec" opzione per " git rebase --rebase-merges" ha posto i comandi exec in punti sbagliati, che è stato corretto.

Vedi commit 1ace63b (09 agosto 2018) e commit f0880f7 (06 agosto 2018) di Johannes Schindelin ( dscho) .
(Unito da Junio ​​C Hamano - gitster- in commit 750eb11 , 20 ago 2018)

rebase --exec: fallo funzionare con --rebase-merges

L'idea --execè di aggiungere una execchiamata dopo ciascuna pick.

Dall'introduzione di fixup!/ s quash!commit, questa idea è stata estesa per applicarsi a "pick, eventualmente seguito da una catena fixup / squash", cioè un exec non verrebbe inserito tra una picke nessuna delle sue corrispondenti fixupo squashlinee.

L'implementazione corrente usa un trucco sporco per raggiungere questo obiettivo: presuppone che ci siano solo comandi pick / fixup / squash, quindi inserisce le execrighe prima di qualsiasi picktranne la prima e ne aggiunge una finale.

Con i todo list generati da git rebase --rebase-mergesquesta semplice spettacoli di implementazione i suoi problemi: produce la cosa esatta sbagliato quando ci sono label, resete mergecomandi.

Modifichiamo l'implementazione per fare esattamente ciò che vogliamo: cercare le picklinee, saltare le catene di correzione / squash e quindi inserire la exec linea . Raccogliere, sciacquare, ripetere.

Nota: facciamo tutto il possibile per inserire prima delle righe di commento ogni volta che è possibile, poiché i commit vuoti sono rappresentati dalle righe di pick commentate (e vogliamo inserire una riga exec di una pick precedente prima di tale riga, non successivamente).

Mentre ci sei , aggiungi anche execrighe dopo i mergecomandi, perché sono simili nello spirito ai pickcomandi: aggiungono nuovi commit.


Git 2.22 (Q2 2019) corregge l'uso della refs / rewritten / gerarchy per memorizzare uno stato intermedio rebase, il che rende intrinsecamente la gerarchia per worktree.

Vedi commit b9317d5 , commit 90d31ff , commit 09e6564 (07 mar 2019) di Nguyễn Thái Ngọc Duy ( pclouds) .
(Unita da Junio ​​C Hamano - gitster- in commit 917f2cd , 09 apr 2019)

Assicurati che refs / rewritten / sia per-worktree

a9be29c (sequencer: make refs generato dal labelcomando worktree-local, 25-04-2018, Git 2.19) aggiunge refs/rewritten/come spazio di riferimento per ogni worktree.
Sfortunatamente (il mio male) ci sono un paio di posti che devono essere aggiornati per assicurarsi che sia davvero per-worktree.

- add_per_worktree_entries_to_dir()viene aggiornato per accertarsi che l'elenco di riferimenti guardi per-worktree refs/rewritten/anziché per-repo one.

  • common_list[]viene aggiornato in modo da git_path()restituire la posizione corretta. Questo include " rev-parse --git-path".

Questo pasticcio è stato creato da me.
Ho iniziato a provare a risolverlo con l'introduzione di refs/worktree,dove tutti gli arbitri saranno per-worktree senza trattamenti speciali.
I ref sfavorevoli / riscritti sono venuti prima dei ref / worktree quindi questo è tutto ciò che possiamo fare.


Con Git 2.24 (Q4 2019), " git rebase --rebase-merges" ha imparato a guidare diverse strategie di unione e passare loro opzioni specifiche di strategia.

Vedi commit 476998d (04 set 2019) di Elijah Newren ( newren) .
Vedere commettere e1fac53 , commettere a63f990 , commettere 5dcdd74 , commettere e145d99 , commettere 4e6023b , commettere f67336d , commettere a9c7107 , commettere b8c6f24 , commettere d51b771 , commettere c248d32 , commettere 8c1e240 , commettere 5efed0e , commettere 68b54f6 , commettere 2e7bbac , commettere 6180b20 , commettere d5b581f (31 Lug 2019) diJohannes Schindelin ( dscho) .
(Unita da Junio ​​C Hamano - gitster- in commit 917a319 , 18 set 2019)


Con Git 2.25 (Q1 2020), la logica utilizzata per distinguere i riferimenti globali locali e di repository di worktree è fissa, per facilitare l'unione di preservazione.

Vedi commit f45f88b , commit c72fc40 , commit 8a64881 , commit 7cb8c92 , commit e536b1f (21 ott 2019) di SZEDER Gábor ( szeder) .
(Unita da Junio ​​C Hamano - gitster- in commit db806d7 , 10 nov 2019)

path.c: non chiamare la matchfunzione senza valore intrie_find()

Firmato-fuori-da: SZEDER Gábor

'logs / refs' non è un percorso specifico dell'albero funzionante, ma poiché commit b9317d55a3 (Assicurarsi che refs / riscritto / sia per-worktree, 2019-03-07, v2.22.0-rc0) ' git rev-parse --git-path' ha restituito un percorso fasullo se /è presente un " " finale :

$ git -C WT/ rev-parse --git-path logs/refs --git-path logs/refs/
/home/szeder/src/git/.git/logs/refs
/home/szeder/src/git/.git/worktrees/WT/logs/refs/

Utilizziamo una triestruttura di dati per decidere in modo efficiente se un percorso appartiene alla directory comune o funziona in modo specifico per l'albero.

Come accade, b9317d55a3 ha attivato un bug vecchio quanto l' trieimplementazione stessa, aggiunto in 4e09cf2acf (" path: ottimizzare il controllo della directory comune", 2015-08-31, Git v2.7.0-rc0 - unione elencata nel batch # 2 ).

  • Secondo il commento che descrive trie_find(), dovrebbe chiamare la funzione di corrispondenza "fn" solo per un prefisso "/ -or- \ 0-terminato della chiave per la quale il trie contiene un valore".
    Questo non è vero: ci sono tre posti in cui trie_find () chiama la funzione match, ma in uno di questi manca il controllo dell'esistenza del valore.

  • b9317d55a3 ha aggiunto due nuove chiavi a trie:

    • ' logs/refs/rewritten' e
    • ' logs/refs/worktree', accanto al già esistente ' logs/refs/bisect'.
      Ciò ha comportato un trienodo con il percorso ' logs/refs/', che non esisteva prima e che non ha un valore associato.
      Una query per " logs/refs/" trova questo nodo e quindi colpisce quel sito di chiamata della matchfunzione che non verifica l'esistenza del valore, e quindi invoca la matchfunzione NULLcome valore.
  • Quando la matchfunzione check_common()viene invocata con un NULLvalore, restituisce 0, che indica che il percorso richiesto non appartiene alla directory comune, risultando in definitiva il percorso fasullo mostrato sopra.

Aggiungi la condizione mancante a in trie_find()modo che non invocherà mai la funzione di corrispondenza con un valore inesistente.

check_common() non sarà più necessario verificare che abbia ottenuto un valore diverso da NULL, quindi rimuovere tale condizione.

Credo che non ci siano altri percorsi che potrebbero causare risultati fasulli simili.

AFAICT l'unica altra chiave che porta alla chiamata della funzione match con un NULLvalore è ' co' (a causa dei tasti ' common' e ' config').

Tuttavia, poiché non si trovano in una directory appartenente alla directory comune, è previsto il percorso specifico dell'albero di lavoro risultante.


3
Penso che questa dovrebbe essere la risposta migliore, in --preserve-mergesrealtà non "preserva" le fusioni come vuoi, è molto ingenuo. Ciò consente di preservare i commit di unione e le relazioni di commit dei genitori, offrendo al contempo la flessibilità di un rebase interattivo. Questa nuova funzionalità è fantastica e se non fosse per questa risposta SO ben scritta, non l'avrei saputo!
egucciar

@egucciar Grazie. E non è l'unica funzionalità di Git 2.18 ( stackoverflow.com/search?q=user%3A6309+%22git+2.18%22 ) e Git 2.19 ( stackoverflow.com/search?q=user%3A6309+%22git+2.19% 22 )
VonC

1
Estremamente utile se si sta cercando di spostare grossi pezzi di commit in giro come in questo Q / A, stackoverflow.com/questions/45059039/...
okovko

1
Oh, è proprio quello che cercavo da un po '! Ho avuto una soluzione manuale per casi come quello su cui si dovrebbe creare un impegno fittizio che unisce tutte le fusioni.
carnevale

Git tipico. Abbiate il coraggio di porre una domanda semplice e molto probabilmente dovrete imparare la storia di Git, gli algoritmi interni, tutti i dettagli dell'implementazione disordinati e inoltre avrete anche bisogno di una maggiore teoria dei grafi per capire cosa sta succedendo.
Dimitris
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.