2015-03-13 21 views
14

Dato un repository, voglio cancellare tutti i commit che erano prima di un commit particolare, o una data nella storia.Come posso cancellare tutti i commit prima di una determinata data nella cronologia Git?

Ho circa 10000 commit nel mio repository e voglio conservare solo gli ultimi 1000 circa e cancellare il resto. Fondamentalmente quello che voglio fare è dire spostare il primo commit in avanti a X.

All'inizio pensavo di poter rebase e schiacciare tutti questi commit in uno solo, ma questo causa molti conflitti di fusione durante il rebase. Se ci fosse un modo per schiacciare i commit in modo tale che la versione dopo lo squash sia l'ultimo commit, funzionerebbe anche lui.

+2

Può darci maggiori informazioni? Perché se lo fai, finirai per eliminare tutti i primi commit del tuo repository! Che è impossibile Cosa vuoi veramente fare? Magari crea una nuova root nel tuo repository e rebase solo il commit che vuoi su questo commit. – Philippe

+0

Aggiornato con ulteriori informazioni. In pratica voglio solo cancellare una parte importante della cronologia, mantenendo gli ultimi commit. –

+0

Per dirla in altre parole, voglio racchiudere alcune migliaia di commit in uno, ignorando i conflitti di merge e mantenendo solo lo stato del repository all'ultimo commit dell'intervallo. –

risposta

17

Avvertenza: quanto segue è pericoloso, in quanto riscrive la cronologia. Assicurati sempre di avere un backup del tuo repository prima di fare qualsiasi tipo di riscrittura della storia come questa.

Sostituire l'hash di seguito con l'hash del genitore del commit che si desidera avere come nuovo primo commit.

git filter-branch --parent-filter ' 
    read parent 
    if [ "$parent" = "-p 5bdd44e5919cb0a95a9924817529cd7c980f88b5" ] 
    then 
     echo 
    else 
     echo "$parent" 
    fi' 

Questo riscrive i genitori di ciascun commit; per la maggior parte dei commit, li lascia gli stessi, ma quello con il genitore che corrisponde all'hash specificato, sostituisce con un genitore vuoto, il che significa che ora diventerà un commit senza genitore. Questo scollegherà tutta la tua vecchia storia.

Nota: se quello che vuoi essere il tuo primo commit è un commit di merge, dovrai corrispondere a qualcosa come -p parent1 -p parent2 -p parent3 per ciascuno dei genitori del commit di unione, nell'ordine corretto.

Se si desidera applicare questo a tutti i rami e tag anziché solo il ramo corrente, passare in --all alla fine del comando (dopo lo script).

Dopo aver fatto questo, e controllato che ha funzionato correttamente, è possibile eliminare il ramo originale ed eseguire un gc per ripulire i commit ora senza riferimenti:

git update-ref -d refs/original/refs/heads/master 

Nota che, dal momento git tende a cercare di preservare i dati, per liberare effettivamente lo spazio dovrai anche rimuovere i commit dal tuo reflog, quindi eseguire lo gc per pulirlo.

git reflog expire --expire-unreachable=all --all 
git gc --prune=all 

Se non si sta facendo questo per risparmiare spazio o sradicare le vecchie commit, è possibile mantenere la vecchia storia intorno a un ramo, come ad esempio git branch old-master refs/original/refs/heads/master; puoi persino "riattaccarlo virtualmente" usando git replace, a quel punto avresti due storie non connesse (quindi quando premi su un repository remoto, sposterai solo la cronologia troncata) ma quando guardi la cronologia nel repository locale tu vedrà la storia completa.

+0

Il conteggio del commit è diminuito, ma posso ancora vedere la cronologia dei commit su github dopo aver applicato la soluzione. Qualche idea? – ferit

+0

ho provato questo, ho copiato repo prima usando 'cp -a' perché volevo tutti i rami, poi quando ho eseguito questo, tutto ciò che è successo secondo git log, è il commit che volevo essere il primo è ora mancante, ma tutti i precedenti commit sono ancora lì e questo è ciò che volevo andare. Non lo userei finché non avrai una risposta migliore. – blamb

+0

Ciao Brian. Questa sembra essere una soluzione piuttosto elegante. Grazie. Ho clonato stupidamente un repository in cui non avevo bisogno di tutti i commit e ho dimenticato il parametro depth. Questa procedura, anche se richiede un po 'di tempo, ha funzionato. – stubsthewizard

2

Non è possibile ottenere ciò che si desidera, perché non è possibile rimuovere nulla da un repository, è possibile solo aggiungere nuove cose ad esso.

per ribadire, ma con un commit disegno grafico, quello che abbiamo ora è (semplificato):

<jumble of commits> - K - L - M - etc ... <-- master 
         \ /(merges) <-- etc 
         (branches) 

e ciò che si vuole (in modo simile semplificato) è:

K - L - M - etc ... <-- master 
\ /(merges) <-- etc 
(branches) 

in modo che K è ora il commit di root.

Non è possibile ottenere che, ma è possibile ottenere una nuova radice commettere cioè quasi esattamente lo stesso K, con due grandi differenze: un diverso SHA-1, e nessun genitore impegnarsi ID (S). Il commit avrebbe lo stesso albero e tutti gli stessi file come commit K.

aver copiato K a K', allora è possibile copiare L-L' e così via, in modo che quello che si ottiene è un nuovo commit grafico che ha la stessa forma e le stesse file e così via, basta con tutti i nuovi SHA 1 ID.

La cosa giusta che fa questo è filter-branch.

Ci sono almeno due modi per ottenere questo con filter-branch. Uno è quello di avere un filtro commettere che:

  • salta tutti i commit fino a commettere appare K, poi
  • copia tutti i commit (compresi K stesso)

(e quindi aggiungere la solita --tag-name-filter cat e così sopra). Questo è leggermente doloroso in quanto il filtro commit non è eval -ed, quindi è necessario "ricordare" lo stato skip/keep esternamente (ad es. In un file).

Un altro metodo è utilizzare --parent-filteras already described by Brian Campbell.

La differenza tra questi è che il metodo --parent-filter è più semplice ma copia anche tutti gli impegni "pre K", in modo che si finisca con due grafici indipendenti nella copia. Potresti volerlo o no; e se, dopo aver eliminato lo spazio dei nomi refs/original, non ci sono riferimenti ai commit "pre-K'", verranno raccolti come al solito, quindi la differenza scompare.

+0

Qualsiasi metodo di utilizzo di 'git filter-branch' lascerà il vecchio commit in giro, tramite il ramo di backup' refs/original/... '. Poiché il '--parent-filter' che ho scritto tocca solo un commit, sarà un no-op per tutti i commit precedenti a quel punto, quindi saranno esattamente gli stessi commit che hai conservato nel tuo' refs/originale' ramo di backup. –

+0

@BrianCampbell: sì, vero; Stavo pensando principalmente a cosa succede dopo aver rimosso i backup 'refs/original'. Se si omettono le "copie" (che, come si nota, riutilizzano davvero solo gli oggetti originali), il ramo filtro fa anche la cosa "rimappa in antenato". Supponendo che alcuni commit precedenti (ad esempio 'E') abbiano un ramo o un tag che puntano ad esso, se si" copia ", punta ancora a' E'. Non sono sicuro di cosa si debba fare rimappare ad antenato se E-and-before se ne sono andati ... – torek

6

Il più semplice per me è quello di utilizzare git replace (modifica: testato con successo!).

Prima di squash tutto il commit che si desidera in un unico: (che chiameremo la sha dell'ultimo commit si vuole schiacciare e la sha del primo commit, in modo che le radici commettere)

git checkout -b big_squash <LastSha> 
git reset --soft <RootSha> 
git commit --amend -m "My new root" 

Ora, devi avere il tuo ramo big_squash puntato verso una nuova radice (chiamato qui <NewRootSha>. Siamo qui interessati solo da sha1 e il ramo potrebbe essere eliminato alla fine una volta completata l'operazione).

Poi ci sono 2 possibilità:

  • Fare un git rebase --onto dei commit più tardi se è presto fatto (che è la soluzione preferita del libro git ma dopo un test di successo del l'altra soluzione, non il mio è;))
  • Usa git replace a nascondere la vecchia storia (la storia è ancora nel repository! Ma noi renderà permanente con un git filter-branch)

Per sostituire l'ultimo commit si vuole schiacciare con la nuova creazione commettere:

git replace <RootSha> <NewRootSha> 

Ora, si potrebbe fare un git filter-branch dopo la git replace per renderlo permanente!

Dopo la sostituzione, fare:

git filter-branch master, <put here the name of all your branches> 

Se l 'risultato che, poi eliminare la cartella .git/refs/original (che contiene tutti gli arbitri salvati prima del git filter-branch) e la cartella .git/refs/replace (che contiene il sostituzione di cui non hai più bisogno).

Questa soluzione ha il vantaggio di essere semplice e reversibili (tranne l'ultimo passo una volta che hai cancellato le cartelle;))

Ecco fatto!

Si potrebbe trovare la documentazione qui:

+1

Cosa fai con il ramo big_squash? Nella soluzione corrente il tuo ramo principale sarà sempre lo stesso. – volpato

+0

non funziona .. il ramo big_squash finisce con un solo commit "My new root". Anche dopo aver eseguito 'git filter-branch master' –

+0

@vinaywadwa sicuramente hai dimenticato di fare' git replace' prima dello – Philippe

Problemi correlati