Domanda

Esiste un modo per utilizzare filter-branch in modo incrementale su un ramo?

più o meno parlando così (ma in realtà non funziona):

git checkout -b branchA origin/branchA  
git branch headBranchA  
# inital rewrite   
git filter-branch ... -- branchA  
git fetch origin  
# incremental rewrite  
git filter-branch ... -- headBranchA..origin/branchA  
git merge origin/branchA  
È stato utile?

Soluzione

Non sono sicuro di cosa tu stia veramente cercando di ottenere, quindi quello che dirò qui è "sì, più o meno, ma probabilmente non quello che stai pensando e potrebbe non aiutarti a raggiungere il tuo obiettivo, qualunque esso sia".

È importante capire qui non solo Che cosa filter-branch lo fa, ma anche, in una certa misura, Come lo fa.


Background (per rendere questa risposta utile ad altri)

Un repository git contiene alcuni grafici di commit.Questi vengono trovati prendendo alcuni nodi di commit iniziali, trovati tramite riferimenti esterni, principalmente nomi di rami e tag, ma anche tag annotati che in un certo senso sorvolerò in quanto non particolarmente importanti in questo caso, e quindi utilizzando quei nodi iniziali per trovare più nodi, finché non sono stati trovati tutti i nodi "raggiungibili".

Ogni commit ha zero o più "commit principali".La maggior parte dei commit ordinari hanno un genitore;le fusioni hanno due o più elementi padre.Un commit root (come il commit iniziale in un repository) non ha genitori.

I nomi dei rami puntano a un commit particolare, che rimanda ai suoi genitori e così via.

  B-C-D
 /     \
A---E---F   <-- master
 \
  G     J   <-- branch1
   \   /
    H-I-K   <-- branch2

Nome della filiale master punti da impegnare F (che è un commit di unione).I nomi branch1 E branch2 punta ai commit J E K rispettivamente.

Notiamo anche che, poiché i commit puntano ai loro genitori, il "set raggiungibile" da name master È A B C D E F, il set per branch1 È A G H I J, e il set per branch2 È A G H I K.

Il "vero nome" di ciascun nodo di commit è il suo SHA-1, che è un checksum crittografico del contenuto del commit.I contenuti includono checksum SHA-1 dei contenuti corrispondenti dell'albero di lavoro e gli SHA-1 dei commit principali.Pertanto, se vai a copiare un commit e modificarlo Niente (non un singolo bit) ottieni lo stesso SHA-1 e quindi finisci con il Stesso commettere;ma se modifichi anche un solo bit (incluso, ad esempio, la modifica dell'ortografia del nome del committente, qualsiasi timestamp o qualsiasi parte dell'albero di lavoro associato), otterrai un commit nuovo e diverso.

git rev-parse E git rev-list

Questi due comandi sono fondamentali per la maggior parte delle operazioni git.

IL rev-parse il comando gira qualsiasi specificatore di revisione git valido in un ID commit.(Ha anche molte di quelle che potremmo chiamare "modalità di assistenza", che consentono di scrivere la maggior parte dei comandi git come script di shell e git filter-branch è in effetti uno script di shell.)

IL rev-list il comando trasforma una revisione allineare (anche in gitrevisions) in un elenco di ID commit.Dato Appena un nome di ramo, trova l'insieme di Tutto revisioni raggiungibili da quel ramo, quindi con l'esempio del grafico di commit sopra, fornito branch2, elenca i valori SHA-1 per i commit A, G, H, I, E K.(Per impostazione predefinita li elenca in ordine cronologico inverso, ma può essere detto di elencarli in "ordine topografico", che è importante per filter-branch, non che intenda entrare così in profondità nei dettagli qui.)

In questo caso, però, ti consigliamo di utilizzare la "limitazione dei commit":data una revisione allineare, come il A..B sintassi o date cose come B ^A, git rev-list limita i suoi set di giri di output ai commit da cui sono raggiungibili B, Ma non raggiungibile da A.Quindi, dato branch2~3..branch2—o equivalentemente, branch2 ^branch2~3—elenca i valori SHA-1 per H, I, E K.Questo è perché branch2~3 i nomi si impegnano G, quindi si impegna A E G vengono potati lontano dall'insieme raggiungibile.


git filter-branch

Lo script filter-branch è piuttosto complesso ma riassumere la sua azione sui "nomi di riferimento forniti sulla riga di comando" non è troppo difficile.

Innanzitutto, utilizza git rev-parse per trovare le attuali revisioni di testa del ramo o dei rami da filtrare.Lo usa due volte, infatti:una volta per ottenere i valori SHA-1 e una volta per ottenere i nomi.Dato, ad esempio, headBranchA..origin/branchA, è necessario ottenere il "vero nome completo" refs/remotes/origin/branchA:

git rev-parse --revs-only --symbolic-full-name headBranchA..origin/branchA

stamperà:

refs/remotes/origin/branchA
^refs/heads/headBranchA

Lo script filter-branch scarta qualsiasi ^-risultati con prefisso per ottenere un elenco di "nomi di riferimento positivi";questi sono ciò che intende riscrivere, alla fine.

Questi sono i "riferimenti positivi" descritti nel manuale di git-filter-branch.

Quindi utilizza git rev-list per ottenere un elenco completo dei commit SHA-1 su cui applicare i filtri.Questo è dove il headBranchA..origin/branchA la sintassi limitante arriva:lo script ora sa funzionare solo sui commit raggiungibili da origin/branchA, ma non da headBranchA.

Una volta ottenuto l'elenco degli ID di commit, git filter-branch applica effettivamente i filtri.Questi creano nuovi commit.

Come sempre, se i nuovi commit sono esattamente identici ai commit originali, gli ID commit rimangono invariati.Se filter-branch deve essere utile, però, presumibilmente ad un certo punto, alcuni commit vengono modificati, dando loro nuovi SHA-1.Tutti i figli immediati di questi commit devono acquisire nuovi ID genitore, quindi anche questi commit vengono modificati e tali modifiche si propagano fino ai suggerimenti dei rami finali.

Infine, dopo aver applicato i filtri a tutti i commit elencati, il filter-branch lo script aggiorna i "riferimenti positivi".


La parte successiva dipende dai filtri effettivi.Supponiamo solo a titolo illustrativo che il tuo filtro cambi l'ortografia del nome di un autore su ogni commit, o cambi il timestamp su ogni commit, o qualcosa del genere, in modo che ogni commit venga riscritto, tranne che per qualche motivo lascia invariato il commit root , per cui il nuovo ramo e il vecchio hanno un antenato comune.

Iniziamo con questo:

git checkout -b branchA origin/branchA

(ora sei attivo branchA, cioè., HEAD contiene ref: refs/heads/branchA)

git branch headBranchA

(questo fa sì che un'altra etichetta di ramo punti al corrente HEAD impegna ma non altera HEAD)

# inital rewrite
git filter-branch ... -- branchA

Il "rif positivo" in questo caso è branchA.I commit da riscrivere sono tutti i commit da cui è raggiungibile branchA, cioè tutto il o nodi seguenti (grafico del commit iniziale creato per l'illustrazione qui), ad eccezione del commit root R:

R-o-o-x-x-x   <-- master
     \
      o-o-o   <-- headBranchA, HEAD=branchA, origin/branchA

Ogni o commit viene copiato e branchA viene spostato per puntare all'ultimo nuovo:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA, origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

Più tardi, vai a prendere nuove cose dal telecomando origin:

git fetch origin

Diciamo che questo aggiunge commit etichettati n (e ne aggiungo solo uno):

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|          \
|           n <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

Ecco dove le cose vanno storte:

git filter-branch ... -- headBranchA..origin/branchA

Il "rif. positivo" eccolo origin/branchA, COSÌ questo è ciò che verrà spostato.I commit selezionati dal elenco rev sono solo quelli segnati n, che è quello che vuoi.Scriviamo il commit riscritto N (maiuscolo) questa volta:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|         |\
|         | n [semi-abandoned - filter-branch writes refs/original/...]
|          \
|           N <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

E ora ci provi git merge origin/branchA, che significa git merge commettere N, che richiede di trovare la base di fusione tra il * catena e impegnarsi N ...e questo è impegno R.

Presumo che questo non sia affatto ciò che volevi fare.

Sospetto che quello che vuoi fare sia, invece, scegliere il commit N sul * catena.Disegniamolo:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- headBranchA
|         |\
|         | n [semi-abandoned - filter-branch writes refs/original/...]
|          \
|           N <-- origin/branchA
 \
  *-*-*-*-*-N'<-- HEAD=branchA

Questa parte va bene, ma lascia un pasticcio per il futuro.Si scopre che in realtà non vuoi impegnarti N affatto, e non vuoi muoverti origin/branchA, perché (presumo) ti piacerebbe poter ripetere il git fetch origin passo dopo.Quindi "annulliamo" questo e proviamo qualcosa di diverso.Lasciamo perdere il headBranchA etichetta interamente e inizia con questo:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

Aggiungiamo un indicatore temporaneo per il commit a cui origin/branchA punti e corri git fetch origin, in modo da ottenere il commit n:

R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*     <-- HEAD=branchA

Ora copiamo commit n A branchA, e mentre lo copiamo, modificalo anche tu (eseguendo qualunque modifica tu voglia fare). git filter-branch) per ottenere un commit chiameremo semplicemente N:

R-o-o-x-x-x     <-- master
|    \     .--------temp
|     o-o-o-n   <-- origin/branchA
 \
  *-*-*-*-*-N    <-- HEAD=branchA

Fatto ciò, cancelliamo temp e siamo pronti a ripetere il ciclo.


Farlo funzionare

Ciò lascia diversi problemi.Il più ovvio è:come copiamo n (o diversi/molti ns) e poi modificarli?Bene, nel modo più semplice, supponendo che tu abbia il tuo filter-branch già funzionante, è da usare git cherry-pick copiarli, quindi git filter-branch per filtrarli.

Funziona solo se il cherry-pick il passaggio non incontrerà problemi di differenza tra gli alberi, quindi dipende da cosa fa il filtro:

# all of this to be done while on branchA
git tag temp origin/branchA
git fetch origin # pick up `n` commit(s)

git tag temp2    # mark the point for filtering
git cherry-pick temp..origin/branchA
git filter-branch ... -- temp2..branchA

# remove temporary markers
git tag -d temp temp2

Cosa succede se il tuo ramo filtro altera l'albero, in modo che questo metodo non funzioni sempre?Bene, possiamo ricorrere all'applicazione del filtro direttamente al file n impegna, dà n' commit, quindi copia il file n' commette.Quelli (n'') i commit sono quelli che vivranno in locale (filtrati) branchA.IL n' i commit non sono necessari una volta copiati, quindi li scartiamo.

# lay down temporary marker as before, and fetch
git tag temp origin/branchA
git fetch origin

# now make a new branch, just for filtering
git checkout -b temp2 origin/branchA
git filter-branch ... -- temp..temp2
# the now-altered new branch, temp..temp2, has filtered commits n'

# copy n' commits to n'' commits on branchA
git checkout branchA
git cherry-pick temp..temp2

# and finally, delete the temporary marker and the temporary branch
git tag -d temp
git branch -D temp2 # temp2 requires a force-delete

Altri problemi

Abbiamo spiegato (nei disegni grafici) come nuovo i commit vengono copiati e modificati nel tuo "filtrato incrementale" branchA.Ma cosa succede se, quando vai a consultare origin, scopri che i commit erano RIMOSSO?

Cioè, iniziamo da questo:

R-o-o-x-x-x   <-- master
|    \
|     o-o-o   <-- origin/branchA
 \
  *-*-*-*-*   <-- HEAD=branchA

Posiamo il nostro segnalino temporaneo come al solito e lo facciamo git fetch origin.Ma quello che hanno fatto è stato rimuovere l'ultimo o impegnarsi, con una spinta forzata da parte loro.Ora abbiamo:

R-o-o-x-x-x   <-- master
|    \
|     o-o     <-- origin/branchA
|        `o.......temp
 \
  *-*-*-*-*   <-- HEAD=branchA

L’implicazione qui è che probabilmente dovremmo tornare indietro branchA anche una revisione.

Dipende da te se vuoi gestire questa situazione.Noterò qui che il risultato di git rev-list temp..origin/branchA sarà vuoto in questo caso particolare (non ci sono commit nel file revisionato origin/branchA da cui non sono raggiungibili temp), Ma origin/branchA..temp Volere non essere vuoto:elencherà l'unico commit "rimosso".Se venissero rimossi due commit, verranno elencati i due commit e così via.

E' possibile per chi controlla origin aver rimosso diversi commit E aggiunti altri nuovi commit (in effetti, questo è esattamente ciò che accade con un "rebase upstream").In questo caso, entrambi git rev-list i comandi non saranno vuoti: origin/branchA..temp ti mostrerà cosa è stato rimosso e temp..origin/branchA ti mostrerà cosa è stato aggiunto.

Infine, è possibile per chi controlla origin per rovinare completamente tutto per te.Loro possono:

  • rimuovere il loro branchA interamente, o
  • creare la loro etichetta branchA indicare un non correlato ramo.

Ancora una volta, spetta a te decidere se, e in caso affermativo, come gestire questi casi.

Altri suggerimenti

Git 2.18 (Q2 2018) propone ora un filtro incrementale.

"git filter-branch"Ho appreso a utilizzare un codice di uscita diverso per consentire ai chiamanti di dire al caso in cui non vi erano nuovi commit da riscrivere da altri casi di errore.

Vedere commettere 0a0eb2e (15 marzo 2018) di Michele Locati (mlocati).
(Uniti da Junio ​​C Hamano-- gitster -- In commettere cb3e97d, 9 aprile 2018)

ramo filtro:restituisce 2 quando non c'è nulla da riscrivere

Usando il --state-branch l'opzione ci consente di eseguire il filtraggio incrementale.Ciò può non avere nulla da riscrivere nei successivi filtrazioni, quindi abbiamo bisogno di un modo per riconoscere questo caso.
Quindi, usciamo con 2 invece di 1 quando si verifica questo "errore".

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top