Come si corregge un'unione errata e si riproducono i propri impegni positivi in ​​un'unione fissa?


407

Ho impegnato accidentalmente un file indesiderato ( filename.origdurante la risoluzione di un'unione) nel mio repository diversi commit fa, senza che me ne accorgessi fino ad ora. Voglio eliminare completamente il file dalla cronologia del repository.

È possibile riscrivere la cronologia delle modifiche in modo che filename.orignon sia mai stato aggiunto al repository?



Risposte:


297

Non utilizzare questa ricetta se la tua situazione non è quella descritta nella domanda. Questa ricetta serve per correggere una fusione errata e riprodurre nuovamente i tuoi buoni commit su una fusione fissa.

Anche se filter-branchfarà quello che vuoi, è un comando abbastanza complesso e probabilmente sceglierei di farlo git rebase. È probabilmente una preferenza personale. filter-branchpuò farlo in un singolo comando leggermente più complesso, mentre la rebasesoluzione sta eseguendo le operazioni logiche equivalenti un passo alla volta.

Prova la seguente ricetta:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(Si noti che in realtà non è necessario un ramo temporaneo, è possibile farlo con un 'HEAD distaccato', ma è necessario prendere nota dell'ID commit generato dal git commit --amendpassaggio per fornire al git rebasecomando anziché utilizzare il ramo temporaneo nome.)


6
Non git rebase -isarebbe più veloce e facile? $ git rebase -i <sh1-of-merge> Contrassegna quello corretto come "modifica" $ git rm somefile.orig $ git commit - modifica $ git rebase --continua Comunque per qualche motivo ho ancora quel file da qualche parte l'ultimo volta che l'ho fatto. Probabilmente manca qualcosa.
Wernight, il

12
git rebase -iè molto utile, soprattutto quando hai più operazioni rebase-y da eseguire, ma è una pena dolorosa descrivere con precisione quando in realtà non stai puntando sopra la spalla di qualcuno e puoi vedere cosa stanno facendo con il loro editor. Uso vim, ma non tutti sarebbero contenti di: "ggjcesquash <Esc> jddjp: wq" e istruzioni come "Sposta la riga superiore dopo la seconda riga corrente e cambia la prima parola sulla riga quattro in" modifica "ora salva e quit "sembra rapidamente più complesso di quanto lo siano i passaggi reali. Normalmente finisci con alcune --amende anche --continueazioni.
CB Bailey,

3
L'ho fatto ma un nuovo commit è stato riapplicato sopra a quello modificato, con lo stesso messaggio. Apparentemente git ha fatto una fusione a 3 vie tra il vecchio commit non modificato contenente il file indesiderato e il commit fisso dell'altro ramo, e così ha creato un nuovo commit sopra quello vecchio, per riapplicare il file.

6
@UncleCJ: il tuo file è stato aggiunto in un commit di unione? Questo è importante. Questa ricetta è progettata per far fronte a un commit di unione errato. Non funzionerà se il tuo file indesiderato è stato aggiunto in un normale commit nella cronologia.
CB Bailey,

1
Sono stupito di come ho potuto fare tutto questo usando smartgit e nessun terminale! Grazie per la ricetta!
Cregox,

209

Introduzione: sono disponibili 5 soluzioni

Il poster originale afferma:

Ho accidentalmente impegnato un file indesiderato ... nel mio repository diversi commit fa ... Voglio eliminare completamente il file dalla cronologia del repository.

È possibile riscrivere la cronologia delle modifiche in modo che filename.orignon sia mai stato aggiunto al repository?

Esistono molti modi diversi per rimuovere completamente la cronologia di un file da git:

  1. La modifica si impegna.
  2. Hard reset (possibilmente più un rebase).
  3. Reimpostazione non interattiva.
  4. Rebasi interattivi.
  5. Filtraggio dei rami.

Nel caso del poster originale, la modifica del commit non è in realtà un'opzione di per sé, poiché in seguito ha effettuato diversi commit aggiuntivi, ma per completezza, spiegherò anche come farlo, per chiunque altro voglia solo per modificare il loro precedente commit.

Si noti che tutte queste soluzioni comportano l' alterazione / riscrittura della cronologia / commit in un modo un altro, quindi chiunque con vecchie copie dei commit dovrà fare un lavoro extra per risincronizzare la propria cronologia con la nuova cronologia.


Soluzione 1: modifica degli impegni

Se hai apportato una modifica accidentale (come l'aggiunta di un file) nel tuo commit precedente e non vuoi più che la cronologia di quel cambiamento esista, allora puoi semplicemente modificare il commit precedente per rimuovere il file da esso:

git rm <file>
git commit --amend --no-edit

Soluzione 2: Hard Reset (eventualmente Plus a Rebase)

Come la soluzione n. 1, se vuoi semplicemente sbarazzarti del tuo precedente commit, hai anche la possibilità di fare semplicemente un hard reset al suo genitore:

git reset --hard HEAD^

Tale comando reimposterà il tuo ramo al precedente commit del primo genitore.

Tuttavia , se, come il poster originale, dopo aver eseguito il commit in cui hai annullato la modifica, hai effettuato diversi commit, puoi comunque utilizzare i ripristini rigidi per modificarlo, ma ciò comporta anche l'utilizzo di un rebase. Ecco i passaggi che è possibile utilizzare per modificare un commit più indietro nella cronologia:

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

Soluzione 3: Rebase non interattivo

Funzionerà se desideri rimuovere completamente un commit dalla cronologia:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

Soluzione 4: basi interattive

Questa soluzione ti consentirà di realizzare le stesse cose delle soluzioni n. 2 e n. 3, ovvero modificare o rimuovere i commit più indietro nella cronologia rispetto al tuo commit immediatamente precedente, quindi quale soluzione scegli di utilizzare dipende da te. I rebase interattivi non sono adatti al rebasing di centinaia di commit, per motivi di prestazioni, quindi utilizzerei rebases non interattivi o la soluzione di diramazione del filtro (vedi sotto) in quel tipo di situazioni.

Per iniziare il rebase interattivo, utilizzare quanto segue:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

Ciò farà tornare git indietro alla cronologia del commit al genitore del commit che si desidera modificare o rimuovere. Ti presenterà quindi un elenco dei commit riavvolti in ordine inverso in qualunque editor git sia impostato per l'uso (questo è Vim per impostazione predefinita):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

Il commit che si desidera modificare o rimuovere sarà in cima a questo elenco. Per rimuoverlo, è sufficiente eliminare la sua riga nell'elenco. In caso contrario, sostituire "raccogliere" con "Modifica" nella 1 ° linea, in questo modo:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

Quindi, inserisci git rebase --continue. Se si è scelto di rimuovere completamente il commit, è necessario eseguire tutto ciò che è necessario (tranne la verifica, vedere il passaggio finale per questa soluzione). Se, d'altra parte, si desidera modificare il commit, git riapplicherà il commit e quindi metterà in pausa il rebase.

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

A questo punto, è possibile rimuovere il file e modificare il commit, quindi continuare il rebase:

git rm <file>
git commit --amend --no-edit
git rebase --continue

Questo è tutto. Come passaggio finale, sia che tu abbia modificato il commit o sia stato rimosso completamente, è sempre una buona idea verificare che non siano state apportate altre modifiche impreviste al tuo ramo diffondendolo con il suo stato prima del rebase:

git diff master@{1}

Soluzione 5: filtrare i rami

Infine, questa soluzione è la migliore se si desidera cancellare completamente tutte le tracce dell'esistenza di un file dalla cronologia e nessuna delle altre soluzioni è all'altezza del compito.

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

Ciò rimuoverà <file>da tutti i commit, a partire dal commit root. Se invece vuoi solo riscrivere l'intervallo di commit HEAD~5..HEAD, puoi passarlo come argomento aggiuntivo a filter-branch, come sottolineato in questa risposta :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

Ancora una volta, al filter-branchtermine dell'operazione, in genere è consigliabile verificare che non vi siano altre modifiche impreviste diffondendo il ramo con lo stato precedente prima dell'operazione di filtro:

git diff master@{1}

Alternativa filtro-ramo: BFG Repo Cleaner

Ho sentito che lo strumento BFG Repo Cleaner funziona più velocemente di git filter-branch, quindi potresti voler controllare anche questa opzione. È anche menzionato ufficialmente nella documentazione del ramo filtro come alternativa praticabile:

git-filter-branch ti consente di effettuare riscritture complesse con script shell della tua cronologia di Git, ma probabilmente non avrai bisogno di questa flessibilità se stai semplicemente rimuovendo dati indesiderati come file di grandi dimensioni o password. Per quelle operazioni potresti prendere in considerazione The BFG Repo-Cleaner , un'alternativa basata su JVM al ramo git-filter, in genere almeno 10-50x più veloce per quei casi d'uso e con caratteristiche abbastanza diverse:

  • Qualsiasi versione particolare di un file viene pulita esattamente una volta . Il BFG, a differenza di git-filter-branch, non ti dà l'opportunità di gestire un file in modo diverso in base a dove o quando è stato eseguito il commit nella tua cronologia. Questo vincolo offre il principale vantaggio in termini di prestazioni di The BFG ed è adatto al compito di pulire i dati errati: non ti interessa dove si trovano i dati errati, vuoi solo che scompaiano .

  • Per impostazione predefinita, il GGG sfrutta appieno le macchine multi-core, pulendo in parallelo gli alberi dei file di commit. git-filter-branch cleans esegue il commit in modo sequenziale (ovvero in modo a thread singolo), sebbene sia possibile scrivere filtri che includono il proprio parallelismo, negli script eseguiti su ciascun commit.

  • Le opzioni di comando sono molto più restrittive di ramo git-filtro, e dedicato proprio ai compiti di rimozione indesiderata dati- ad esempio: --strip-blobs-bigger-than 1M.

Risorse addizionali

  1. Pro Git § 6.4 Git Tools - Riscrivere la cronologia .
  2. Pagina di manuale git-filter-branch (1) .
  3. Pagina di manuale git-commit (1) .
  4. Pagina di manuale git-reset (1) .
  5. Pagina di manuale git-rebase (1) .
  6. BFG Repo Cleaner (vedi anche questa risposta del creatore stesso ).

Fa filter-branchcausa il ricalcolo di hash? Se una squadra lavora con un repository in cui un file di grandi dimensioni deve essere filtrato, come fanno in modo che tutti finiscano con lo stesso stato del repository?
YakovL

@YakovL. Tutto ricalcola gli hash. In realtà i commit sono immutabili. Crea una storia completamente nuova e sposta il puntatore del ramo su di essa. L'unico modo per garantire a tutti la stessa cronologia è un hard reset.
Fisico pazzo,

118

Se non hai commesso nulla da allora, solo git rmil file e git commit --amend.

Se hai

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

esaminerà ogni modifica da merge-pointa HEAD, eliminerà nomefile.orig e riscriverà la modifica. L'uso --ignore-unmatchsignifica che il comando non fallirà se per qualche motivo il nomefile.orig non è presente in una modifica. Questo è il modo consigliato dalla sezione Esempi nella pagina man git-filter-branch .

Nota per utenti Windows: il percorso del file deve utilizzare le barre


3
Grazie! git filter-branch ha funzionato per me dove l'esempio di rebase fornito come risposta non ha funzionato: i passaggi sembravano funzionare, ma poi la spinta non è riuscita. Ha fatto un pull, quindi ha spinto con successo, ma il file era ancora in giro. Ho provato a ripetere i passaggi di rebase e poi è andato tutto in disordine con i conflitti di unione. Ho usato un comando filtro-ramo leggermente diverso, quello "Un metodo migliorato" dato qui: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- filter 'git update-index --remove filename' <introduction-revision-sha1>
..HEAD

1
Non sono sicuro di quale sia il metodo migliorato . Git documentazione ufficiale di git-filter-branchsembra dare il primo.
Wernight, il

5
Dai un'occhiata a zyxware.com/articles/4027/… la trovo la soluzione più completa e diretta che coinvolgefilter-branch
leontalbot,

2
@atomicules, se proverai a spingere il repository locale su quello remoto, git insisterà prima di estrarre dal telecomando, perché ha delle modifiche che non hai localmente. Puoi usare --force flag per spingere sul telecomando - rimuoverà completamente i file da lì. Ma fai attenzione, assicurati di non forzare a sovrascrivere qualcosa di diverso dai soli file.
sol0mka,

1
Ricorda di usare "e non 'quando usi Windows, altrimenti otterrai un errore di "revisione non corretta".
CZ

49

Questo è il modo migliore:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Assicurati di eseguire prima il backup delle copie dei file.

MODIFICARE

La modifica di Neon è stata purtroppo respinta durante la revisione.
Vedi post Neons di seguito, potrebbe contenere informazioni utili!


Ad esempio per rimuovere tutti i *.gzfile impegnati accidentalmente nel repository git:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

Non ha ancora funzionato per me? (Sono attualmente alla versione 1.7.6.1 di git)

$ du -sh .git ==> e.g. 100M

Non so perché, dato che avevo UN SOLO ramo principale. Ad ogni modo, ho finalmente ripulito il mio repository git spingendolo in un nuovo repository git vuoto e vuoto, ad es.

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(sì!)

Quindi l'ho clonato in una nuova directory e ho spostato la sua cartella .git in questa. per esempio

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(sì! finalmente ripulito!)

Dopo aver verificato che tutto va bene, puoi eliminare le directory ../large_dot_gite ../tmpdir(forse tra un paio di settimane o un mese da ora, per ogni evenienza ...)


1
Questo ha funzionato per me prima del "Ancora non ha funzionato per me?" commento
shadi,

Ottima risposta, ma suggerisci di aggiungere --prune-emptyal comando filtro-ramo.
ideasman42

27

La riscrittura della cronologia di Git richiede la modifica di tutti gli ID di commit interessati, quindi tutti coloro che stanno lavorando al progetto dovranno eliminare le loro vecchie copie del repository e fare un nuovo clone dopo aver pulito la cronologia. Più persone incide, più hai bisogno di una buona ragione per farlo - il tuo file superfluo non sta realmente causando un problema, ma se solo stai lavorando al progetto, potresti anche ripulire la cronologia di Git se vuoi per!

Per renderlo il più semplice possibile, consiglierei di usare BFG Repo-Cleaner , un'alternativa più semplice e veloce a quella git-filter-branchappositamente progettata per rimuovere i file dalla cronologia di Git. Un modo in cui ti semplifica la vita qui è che in realtà gestisce tutti i ref per impostazione predefinita (tutti i tag, rami, ecc.) Ma è anche 10-50 volte più veloce.

Dovresti seguire attentamente i passaggi qui: http://rtyley.github.com/bfg-repo-cleaner/#usage - ma il bit principale è proprio questo: scarica il jar BFG (richiede Java 6 o successivo) ed esegui questo comando :

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

L'intera cronologia del repository verrà scansionata e qualsiasi file denominato filename.orig(che non è nell'ultimo commit ) verrà rimosso. Questo è notevolmente più semplice rispetto all'utilizzo git-filter-branchper fare la stessa cosa!

Informativa completa: sono l'autore del Repo-Cleaner di BFG.


4
Questo è uno strumento eccellente: un singolo comando, produce un output molto chiaro e fornisce un file di registro che abbina ogni vecchio commit a quello nuovo . Non mi piace installare Java ma ne vale la pena.
mikemaccana,

Questa è l'unica cosa che ha funzionato per me, ma è perché non stavo lavorando correttamente a Git Filter Branch. :-)
Kevin LaBranche

14
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all

1
Mentre tutte le risposte sembrano essere sulla traccia filtro-ramo, questa evidenzia come pulire TUTTI i rami nella tua cronologia.
Cameron Lowell Palmer,

4

Solo per aggiungere ciò alla soluzione di Charles Bailey, ho appena usato un git rebase -i per rimuovere i file indesiderati da un commit precedente e ha funzionato come un incantesimo. I passi:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue

4

Il modo più semplice che ho trovato è stato suggerito da leontalbot(come commento), che è un post pubblicato da Anoopjohn . Penso che valga il suo spazio come risposta:

(L'ho convertito in uno script bash)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

Tutti i crediti vanno a Annopjohn, e leontalbotper averlo sottolineato.

NOTA

Tieni presente che lo script non include convalide, quindi assicurati di non commettere errori e di avere un backup nel caso in cui qualcosa vada storto. Ha funzionato per me, ma potrebbe non funzionare nella tua situazione. UTILIZZARLO CON ATTENZIONE (seguire il collegamento se si desidera sapere cosa sta succedendo).


3

Sicuramente, git filter-branchè la strada da percorrere.

Purtroppo, questo non sarà sufficiente per rimuovere completamente filename.origdal tuo repository, in quanto può ancora essere referenziato da tag, voci di reflog, telecomandi e così via.

Consiglio di rimuovere anche tutti questi riferimenti e di chiamare il Garbage Collector. È possibile utilizzare lo git forget-blobscript da questo sito Web per eseguire tutto ciò in un solo passaggio.

git forget-blob filename.orig


1

Se è l'ultimo commit che vuoi ripulire, ho provato con la versione 2.14.3 di Git (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git

git reflog expire --expire=now --all; git gc --prune=nowè una brutta cosa da fare. A meno che tu non stia esaurendo lo spazio su disco, lascia che git garbage raccolga questi commit dopo alcune settimane
avmohan,

Grazie per la segnalazione. Il mio repository è stato inviato con molti file binari di grandi dimensioni e il repository viene eseguito il backup interamente ogni notte. Quindi volevo solo un po 'di tutto;)
clarkttfu il


-1

Puoi anche usare:

git reset HEAD file/path


3
Se il file è stato aggiunto a un commit, questo non rimuove nemmeno il file dall'indice, ma reimposta l'indice sulla versione HEAD del file.
CB Bailey,
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.