Jak naprawić złe połączenie i odtworzyć swoje dobre zobowiązania na stałe połączenie?

407

filename.origKilka przypadków temu przypadkowo przesłałem niechciany plik ( podczas rozwiązywania scalenia) do mojego repozytorium, do tej pory tego nie zauważając. Chcę całkowicie usunąć plik z historii repozytorium.

Czy możliwe jest przepisanie historii zmian tak, aby filename.orignigdy nie była dodawana do repozytorium?

Grant Limberg
źródło

Odpowiedzi:

297

Nie używaj tego przepisu, jeśli Twoja sytuacja nie jest zgodna z opisem w pytaniu. Ten przepis służy do naprawiania złego scalenia i odtwarzania dobrych zatwierdzeń na ustalonym scaleniu.

Chociaż filter-branchzrobi to, co chcesz, jest to dość złożone polecenie i prawdopodobnie wybrałbym to git rebase. To prawdopodobnie osobiste preferencje. filter-branchmożna to zrobić za pomocą pojedynczej, nieco bardziej złożonej komendy, podczas gdy rebaserozwiązanie wykonuje równoważne operacje logiczne krok po kroku.

Wypróbuj następujący przepis:

# 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

(Zauważ, że tak naprawdę nie potrzebujesz gałęzi tymczasowej, możesz to zrobić za pomocą „odłączonej HEAD”, ale musisz zanotować identyfikator zatwierdzenia wygenerowany przez git commit --amendkrok w celu dostarczenia do git rebasepolecenia zamiast używania gałęzi tymczasowej imię.)

CB Bailey
źródło
6
Czy nie git rebase -ibyłoby szybciej i tak łatwo? $ git rebase -i <sh1-of-merge> Oznacz prawidłowy jako „edytuj” $ git rm somefile.orig $ git commit --amend $ git rebase - continue Jednak z jakiegoś powodu wciąż mam ten plik gdzieś ostatni czas to zrobiłem. Prawdopodobnie czegoś brakuje.
Wernight
12
git rebase -ijest bardzo przydatny, szczególnie gdy masz do wykonania wiele operacji rebase-y, ale dobrze jest dokładnie opisać, kiedy tak naprawdę nie wskazujesz komuś ramienia i widzisz, co robi z jego edytorem. Używam vima, ale nie wszyscy byliby zadowoleni z: „ggjcesquash <Esc> jddjp: wq” i instrukcji takich jak „Przenieś górną linię na bieżącą drugą linię i zmień pierwsze słowo w czwartej linii na„ edytuj ”teraz zapisz i quit ”szybko wydają się bardziej skomplikowane niż w rzeczywistości. Zazwyczaj kończy się z niektórymi --amendi --continuedziałań, jak również.
CB Bailey,
3
Zrobiłem to, ale nowe zatwierdzenie zostało ponownie zastosowane nad poprawionym, z tym samym komunikatem. Najwyraźniej git dokonał trzykrotnego scalenia między starym, niezmienionym zatwierdzeniem zawierającym niechciany plik, a stałym zatwierdzeniem z drugiej gałęzi, i dlatego utworzył nowe zatwierdzenie na starym, aby ponownie zastosować plik.
6
@UncleCJ: Czy Twój plik został dodany w zatwierdzeniu scalania? To jest ważne. Ten przepis został zaprojektowany, aby poradzić sobie ze złym zatwierdzeniem scalania. Nie zadziała, jeśli Twój niechciany plik został dodany w normalnym zatwierdzeniu w historii.
CB Bailey,
1
Dziwi mnie, jak mogłem to wszystko zrobić za pomocą smartgit i żadnego terminalu! Dzięki za przepis!
cregox
209

Wprowadzenie: masz 5 dostępnych rozwiązań

Oryginalny plakat stwierdza:

Przypadkowo wysłałem niechciany plik ... do mojego repozytorium kilka zmian temu ... Chcę całkowicie usunąć plik z historii repozytorium.

Czy możliwe jest przepisanie historii zmian tak, aby filename.orignigdy nie była dodawana do repozytorium?

Istnieje wiele różnych sposobów całkowitego usunięcia historii pliku z git:

  1. Zmienia zobowiązania.
  2. Twarde resety (ewentualnie plus rebase).
  3. Nieinteraktywny rebase.
  4. Interaktywne bazy danych.
  5. Filtrowanie oddziałów.

W przypadku oryginalnego plakatu, zmiana zatwierdzenia nie jest tak naprawdę opcją samą w sobie, ponieważ później dokonał kilku dodatkowych zatwierdzeń, ale dla kompletności wyjaśnię również, jak to zrobić, dla każdego, kto tylko chce zmienić poprzednie zatwierdzenie.

Zauważ, że wszystkie te rozwiązania wymagają zmiany / ponownego zapisywania historii / zatwierdzeń w inny sposób, więc każdy, kto ma stare kopie zatwierdzeń, będzie musiał wykonać dodatkową pracę, aby ponownie zsynchronizować swoją historię z nową historią.


Rozwiązanie 1: Zobowiązania korygujące

Jeśli przypadkowo dokonałeś zmiany (np. Dodając plik) w poprzednim zatwierdzeniu i nie chcesz, aby historia tej zmiany już istniała, możesz po prostu zmienić poprzednie zatwierdzenie, aby usunąć z niego plik:

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

Rozwiązanie 2: Twardy reset (prawdopodobnie plus rease)

Podobnie jak w rozwiązaniu nr 1, jeśli chcesz po prostu pozbyć się poprzedniego zatwierdzenia, masz również opcję twardego resetu do jego rodzica:

git reset --hard HEAD^

Że komenda będzie trudno zresetować oddziału do poprzedniego 1 st rodzica popełnił.

Jeśli jednak , podobnie jak oryginalny plakat, dokonałeś kilku zatwierdzeń po zatwierdzeniu, w którym chcesz cofnąć zmianę, nadal możesz użyć twardego resetu, aby ją zmodyfikować, ale to również wymaga użycia bazy. Oto kroki, których możesz użyć, aby zmienić zatwierdzenie dalej w historii:

# 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}

Rozwiązanie 3: Nieinteraktywne Rebase

Działa to, jeśli chcesz całkowicie usunąć zatwierdzenie z historii:

# 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}

Rozwiązanie 4: Interaktywne zmiany

To rozwiązanie pozwoli ci osiągnąć te same rzeczy, co rozwiązania 2 i 3, tj. Zmodyfikować lub usunąć zatwierdzenia z powrotem w historii niż poprzednie wcześniejsze zatwierdzenie, więc to, które rozwiązanie wybierzesz, zależy od ciebie. Interaktywne rebazy nie nadają się do rebasingu setek commits ze względu na wydajność, dlatego w takich sytuacjach użyłbym nieinteraktywnych rebase'ów lub rozwiązania gałęzi filtrowania (patrz poniżej).

Aby rozpocząć interaktywną bazę, użyj następujących poleceń:

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

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

Spowoduje to, że git przewinie historię zatwierdzeń z powrotem do elementu nadrzędnego zatwierdzenia, które chcesz zmodyfikować lub usunąć. Następnie wyświetli listę zatwierdzonych przewinięć w odwrotnej kolejności w dowolnym ustawionym git edytorze (domyślnie jest to Vim):

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)

Zatwierdzenie, które chcesz zmodyfikować lub usunąć, będzie na górze tej listy. Aby go usunąć, po prostu usuń jego linię z listy. W przeciwnym wypadku, należy wymienić „pick” z „edit” na 1 st linii, tak jak poniżej:

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

Następnie wprowadź git rebase --continue. Jeśli zdecydujesz się całkowicie usunąć zatwierdzenie, to wszystko, co musisz zrobić (oprócz weryfikacji, zobacz ostatni krok tego rozwiązania). Jeśli z drugiej strony chcesz zmodyfikować zatwierdzenie, git ponownie zastosuje zatwierdzenie, a następnie zatrzyma zmianę bazy.

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

W tym momencie możesz usunąć plik i zmienić zatwierdzenie, a następnie kontynuować rebase:

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

Otóż ​​to. Na koniec, niezależnie od tego, czy zmodyfikowałeś zatwierdzenie, czy całkowicie go usunąłeś, zawsze dobrym pomysłem jest sprawdzenie, czy nie dokonano żadnych innych nieoczekiwanych zmian w twojej gałęzi, różnicując go swoim stanem przed zmianą bazy:

git diff master@{1}

Rozwiązanie 5: Filtrowanie oddziałów

Wreszcie, to rozwiązanie jest najlepsze, jeśli chcesz całkowicie usunąć wszystkie ślady istnienia pliku z historii, a żadne z pozostałych rozwiązań nie spełnia tego zadania.

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

To usunie <file>wszystkie zatwierdzenia, poczynając od głównego zatwierdzenia. Jeśli zamiast tego chcesz po prostu przepisać zakres zatwierdzenia HEAD~5..HEAD, możesz przekazać to jako dodatkowy argument filter-branch, jak wskazano w tej odpowiedzi :

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

Ponownie, po filter-branchzakończeniu, zwykle dobrym pomysłem jest sprawdzenie, czy nie ma innych nieoczekiwanych zmian, poprzez różnicowanie gałęzi do poprzedniego stanu przed operacją filtrowania:

git diff master@{1}

Alternatywa dla rozgałęzień: BFG Repo Cleaner

Słyszałem, że narzędzie BFG Repo Cleaner działa szybciej niż git filter-branch, więc możesz również sprawdzić to jako opcję. Jest nawet oficjalnie wspomniany w dokumentacji gałęzi filtrów jako realna alternatywa:

git-filter-branch pozwala tworzyć złożone zapisy historii Gita w skryptach powłoki, ale prawdopodobnie nie potrzebujesz tej elastyczności, jeśli po prostu usuwasz niechciane dane, takie jak duże pliki lub hasła. W przypadku tych operacji warto rozważyć użycie BFG Repo-Cleaner , opartej na maszynie JVM alternatywy dla gałęzi git-filter, zwykle co najmniej 10-50 razy szybszej w tych przypadkach użycia i o całkiem innych właściwościach:

  • Każda konkretna wersja pliku jest czyszczona dokładnie raz . BFG, w przeciwieństwie do git-filter-branch, nie daje ci możliwości obsługi pliku inaczej w zależności od tego, gdzie i kiedy został popełniony w twojej historii. To ograniczenie daje podstawową korzyść BFG w zakresie wydajności i jest dobrze dostosowane do zadania czyszczenia złych danych - nie obchodzi Cię, gdzie są złe dane, po prostu chcesz, aby zniknęły .

  • Domyślnie BFG w pełni wykorzystuje maszyny wielordzeniowe, równolegle czyszcząc drzewa plików zatwierdzeń. czyszczenia np git-filter-branch commity kolejno (czyli w jednowątkowy sposób), choć jest możliwe napisanie filtry, które zawierają własne parallellism w skrypty wykonywane przed każdym popełnić.

  • Te opcje polecenia są znacznie bardziej restrykcyjne niż oddział git-filtrującej, a dedykowany tylko do zadań usuwania niechcianych teleinformatyczny np --strip-blobs-bigger-than 1M.

Dodatkowe zasoby

  1. Pro Git § 6.4 Git Tools - Przepisywanie historii .
  2. git-filter-branch (1) Podręcznik strony .
  3. git-commit (1) Podręcznik strony .
  4. git-reset (1) Podręcznik strony .
  5. git-rebase (1) Podręcznik strony .
  6. BFG Repo Cleaner (zobacz także odpowiedź samego twórcy ).
Społeczność
źródło
Czy filter-branchpowoduje ponowne obliczanie skrótów? Jeśli zespół pracuje z repozytorium, w którym duży plik powinien być filtrowany, jak to zrobić, aby wszyscy mieli ten sam stan repo?
JakowL,
@YakovL. Wszystko ponownie oblicza skróty. W rzeczywistości zatwierdzenia są niezmienne. Tworzy zupełnie nową historię i przenosi do niej wskaźnik gałęzi. Jedynym sposobem na upewnienie się, że wszyscy mają tę samą historię, jest twardy reset.
Szalony fizyk,
118

Jeśli od tego czasu nic nie popełniłeś, po prostu git rmplik i git commit --amend.

Jeśli masz

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

przejdzie każdą zmianę od merge-pointdo HEAD, usunie nazwę pliku.orig i przepisze zmianę. Użycie --ignore-unmatchoznacza, że ​​polecenie nie zawiedzie, jeśli z jakiegoś powodu brakuje pliku filename.orig w zmianie. Jest to zalecany sposób z sekcji Przykłady na stronie man git-filter-branch .

Uwaga dla użytkowników systemu Windows: ścieżka do pliku musi zawierać ukośniki

Schwern
źródło
3
Dzięki! git filter-branch działał dla mnie tam, gdzie podany przykład odpowiedzi nie działał: kroki wydawały się działać, ale potem wypychanie nie powiodło się. Wykonano ściąganie, a następnie wypchnęło pomyślnie, ale plik nadal był w pobliżu. Próbowałem powtórzyć kroki rebase, a potem wszystko poszło nie tak z konfliktami scalania. Użyłem jednak nieco innej komendy filter-branch, podanej tutaj „An Improved Method”: github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index- filtruj „git update-index --remove filename” <introduction-revision-sha1>
..HEAD
1
Nie jestem pewien, która z nich jest ulepszoną metodą. Git oficjalna dokumentacja git-filter-branchwydaje się dawać pierwszą.
Wernight
5
Sprawdź zyxware.com/articles/4027/... Uważam, że jest to najbardziej kompletne i proste rozwiązanie, które obejmujefilter-branch
leontalbot
2
@atomicules, jeśli spróbujesz wypchnąć lokalne repozytorium do zdalnego, git będzie nalegał, aby najpierw pobrać z zdalnego, ponieważ ma zmiany, których nie masz lokalnie. Możesz użyć opcji --force flag do wypchnięcia do pilota - spowoduje to całkowite usunięcie stamtąd plików. Ale bądź ostrożny, upewnij się, że nie wymusisz zastąpienia czegoś innego niż tylko pliki.
sol0mka
1
Pamiętaj, aby używać, "a nie 'podczas korzystania z systemu Windows, w przeciwnym razie pojawi się niepoprawnie sformułowany błąd „zła wersja”.
cz
49

To najlepszy sposób:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Pamiętaj tylko, aby najpierw wykonać kopię zapasową plików.

EDYTOWAĆ

Edycja Neon została niestety odrzucona podczas przeglądu.
Zobacz post Neons poniżej, może zawierać przydatne informacje!


Np. Aby usunąć wszystkie *.gzpliki przypadkowo przypisane do repozytorium 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

To wciąż nie działało dla mnie? (Obecnie jestem w wersji git 1.7.6.1)

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

Nie jestem pewien, dlaczego, ponieważ miałem tylko JEDNĄ gałąź master. Tak czy inaczej, w końcu naprawdę wyczyściłem moje repozytorium git, pchając do nowego pustego repozytorium git, np.

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

(tak!)

Następnie sklonowałem to do nowego katalogu i przeniosłem do niego folder .git. na przykład

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

(tak! w końcu posprzątał!)

Po sprawdzeniu, że wszystko jest w porządku, możesz usunąć katalogi ../large_dot_giti ../tmpdir(może za kilka tygodni lub miesięcy, na wszelki wypadek ...)

Darren
źródło
1
To działało dla mnie przed „To nadal nie działało dla mnie?” komentarz
shadi
Świetna odpowiedź, ale sugeruję dodanie --prune-emptydo polecenia filter-branch.
ideasman42
27

Przepisywanie historii Git wymaga zmiany wszystkich identyfikatorów zatwierdzeń, których dotyczy ten problem, więc każdy, kto pracuje nad projektem, będzie musiał usunąć swoje stare kopie repozytorium i wykonać nowy klon po wyczyszczeniu historii. Im więcej ludzi to niedogodności, tym bardziej trzeba mieć dobry powód, by to zrobić - zbędny plik nie jest tak naprawdę przyczyną problemu, ale jeśli tylko ty pracujesz nad projektem, równie dobrze można oczyścić historię Git jeśli chcesz do!

Aby było to tak proste, jak to możliwe, polecam użycie BFG Repo-Cleaner , prostszej, szybszej alternatywy dla git-filter-branchspecjalnie zaprojektowanej do usuwania plików z historii Git. Jednym ze sposobów, w jaki ułatwia ci to życie, jest fakt, że domyślnie obsługuje wszystkie referencje (wszystkie tagi, gałęzie itp.), Ale jest także 10 - 50 razy szybszy.

Powinieneś uważnie wykonać następujące kroki: http://rtyley.github.com/bfg-repo-cleaner/#usage - ale rdzeń jest po prostu następujący: pobierz słoik BFG (wymaga Java 6 lub nowszej) i uruchom to polecenie :

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

Cała historia repozytorium zostanie przeskanowana, a każdy plik o nazwie filename.orig(nieobecny w ostatnim zatwierdzeniu ) zostanie usunięty. Jest to znacznie łatwiejsze niż używanie git-filter-branchtej samej rzeczy!

Pełne ujawnienie: jestem autorem BFG Repo-Cleaner.

Roberto Tyley
źródło
4
Jest to doskonałe narzędzie: pojedyncze polecenie, generuje bardzo wyraźne dane wyjściowe i zapewnia plik dziennika, który pasuje do każdego starego zatwierdzenia nowego . Nie lubię instalować Javy, ale warto.
mikemaccana
To jedyna rzecz, która działała dla mnie, ale dzieje się tak, ponieważ nie działałem poprawnie 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
paulalexandru
źródło
1
Chociaż wszystkie odpowiedzi wydają się być na ścieżce filtru gałęzi, ta pokazuje, jak wyczyścić WSZYSTKIE gałęzie w twojej historii.
Cameron Lowell Palmer
4

Aby dodać to do rozwiązania Charlesa Baileya, użyłem git rebase -i do usunięcia niechcianych plików z wcześniejszego zatwierdzenia i działało to jak urok. Kroki:

# 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
Sverrir Sigmundarson
źródło
4

Najprostszy sposób, jaki znalazłem, został zasugerowany przez leontalbot(jako komentarz), który jest postem opublikowanym przez Anoopjohna . Myślę, że jest to warte własnej przestrzeni jako odpowiedź:

(Przekształciłem go w skrypt 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."

Wszystkie kredyty trafiają do Annopjohni leontalbotza wskazanie tego.

UWAGA

Pamiętaj, że skrypt nie obejmuje sprawdzania poprawności, więc upewnij się, że nie popełniasz błędów i że masz kopię zapasową na wypadek, gdyby coś poszło nie tak. To działało dla mnie, ale może nie działać w twojej sytuacji. KORZYSTAJ Z niego OSTROŻNIE (kliknij link, jeśli chcesz wiedzieć, co się dzieje).

lepe
źródło
3

Zdecydowanie git filter-branchjest to najlepsza droga.

Niestety, nie wystarczy to całkowicie usunąć filename.origz repozytorium, ponieważ nadal można do nich odwoływać się za pomocą tagów, wpisów na blogu, pilotów itp.

Polecam również usunięcie wszystkich tych referencji, a następnie wywołanie modułu wyrzucania elementów bezużytecznych. Możesz użyć git forget-blobskryptu z tej witryny, aby zrobić to wszystko w jednym kroku.

git forget-blob filename.orig

nachoparker
źródło
1

Jeśli to ostatni zatwierdzenie, które chcesz wyczyścić, próbowałem z git w wersji 2.14.3 (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
clarkttfu
źródło
git reflog expire --expire=now --all; git gc --prune=nowto bardzo zła rzecz do zrobienia. O ile nie zabraknie ci miejsca na dysku, pozwól git śmieciowi zebrać te zatwierdzenia po kilku tygodniach
avmohan
Dzięki za zwrócenie na to uwagi. Moje repozytorium zostało przesłane z wieloma dużymi plikami binarnymi, a kopia repozytorium jest tworzona całkowicie co noc. Więc chciałem po prostu z tego wszystkiego;)
clarkttfu
-1

Możesz także użyć:

git reset HEAD file/path

paolo granada lim
źródło
3
Jeśli plik został dodany do zatwierdzenia, to nawet nie usuwa pliku z indeksu, po prostu resetuje indeks do wersji HEAD pliku.
CB Bailey,