Jak mogę łatwo naprawić poprzednie zatwierdzenie?

117

Właśnie przeczytałem poprawianie pojedynczego pliku w poprzednim zatwierdzeniu w git, ale niestety zaakceptowane rozwiązanie „zmienia kolejność” zatwierdzeń, co nie jest tym, czego chcę. Oto moje pytanie:

Od czasu do czasu zauważam błąd w moim kodzie podczas pracy nad (niepowiązaną) funkcją. Szybko git blameujawnia, że ​​błąd został wprowadzony kilka zatwierdzeń temu (robię dość dużo, więc zwykle nie jest to ostatnie zatwierdzenie, które wprowadziło błąd). W tym momencie zwykle robię to:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
                               # mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

Jednak zdarza się to na tyle często, że powyższa sekwencja staje się irytująca. Zwłaszcza „interaktywna rebase” jest nudna. Czy jest jakiś skrót do powyższej sekwencji, który pozwala mi zmienić dowolne zatwierdzenie w przeszłości za pomocą wprowadzonych zmian? Doskonale zdaję sobie sprawę, że to zmienia historię, ale popełniam błędy tak często, że naprawdę chciałbym mieć coś takiego

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
                                   # rebase any successors of bad commit on rewritten 
                                   # commit.

Może inteligentny skrypt, który może przepisać zmiany przy użyciu narzędzi hydraulicznych?

Frerich Raabe
źródło
Co masz na myśli, mówiąc o „zmianie kolejności” zatwierdzeń? Jeśli zmieniasz historię, wszystkie zatwierdzenia od zmienionych zatwierdzeń muszą być różne, ale zaakceptowana odpowiedź na połączone pytanie nie zmienia kolejności zatwierdzeń w żadnym sensownym sensie.
CB Bailey
1
@Charles: Miałem na myśli zmianę kolejności, tak jak w: jeśli zauważę, że HEAD ~ 5 jest zepsutym zatwierdzeniem, podążanie za zaakceptowaną odpowiedzią w połączonym pytaniu sprawi, że HEAD (końcówka gałęzi) będzie stałym zatwierdzeniem. Jednak chciałbym, aby HEAD ~ 5 był ustalonym zatwierdzeniem - co otrzymujesz podczas korzystania z interaktywnej rebase i edycji pojedynczego zatwierdzenia do naprawy.
Frerich Raabe
Tak, ale wtedy komenda rebase ponownie wyewidencjonuje wzorzec i przełoży wszystkie kolejne zatwierdzenia na ustalone zatwierdzenie. Czy nie tak jeździsz rebase -i?
CB Bailey,
Właściwie jest potencjalny problem z tą odpowiedzią, myślę, że powinien rebase --onto tmp bad-commit master. Jak napisano, będzie próbował zastosować złe zatwierdzenie do stanu ustalonego zatwierdzenia.
CB Bailey,
Oto kolejne narzędzie do automatyzacji procesu naprawy / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Odpowiedzi:

166

ZAKTUALIZOWANA ODPOWIEDŹ

Jakiś czas temu dodano nowy --fixupargument, do git commitktórego można użyć do skonstruowania zatwierdzenia z komunikatem dziennika odpowiednim dla git rebase --interactive --autosquash. Więc najprostszym sposobem naprawienia przeszłego zatwierdzenia jest teraz:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

ORYGINALNA ODPOWIEDŹ

Oto mały skrypt w Pythonie, który napisałem jakiś czas temu, który implementuje git fixuplogikę, na którą liczyłem w moim pierwotnym pytaniu. Skrypt zakłada, że ​​wprowadziłeś pewne zmiany, a następnie stosuje je do danego zatwierdzenia.

UWAGA : Ten skrypt jest specyficzny dla systemu Windows; wyszukuje git.exei ustawia GIT_EDITORzmienną środowiskową za pomocą set. Dostosuj to w razie potrzeby do innych systemów operacyjnych.

Za pomocą tego skryptu mogę precyzyjnie zaimplementować przepływ pracy `` napraw uszkodzone źródła, poprawki scen, uruchom git fixup '', o który prosiłem:

#!/usr/bin/env python
from subprocess import call
import sys

# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

if len(sys.argv) != 2:
    print "Usage: git fixup <commit>"
    sys.exit(1)

git = which("git.exe")
if not git:
    print "git-fixup: failed to locate git executable"
    sys.exit(2)

broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
    print "git-fixup: %s is not a valid commit" % broken_commit
    sys.exit(3)

if call([git, "diff", "--staged", "--quiet"]) == 0:
    print "git-fixup: cannot fixup past commit; no fix staged."
    sys.exit(4)

if call([git, "diff", "--quiet"]) != 0:
    print "git-fixup: cannot fixup past commit; working directory must be clean."
    sys.exit(5)

call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)
Frerich Raabe
źródło
2
możesz użyć git stashi git stash popwokół swojej rebase, aby nie wymagać już czystego katalogu roboczego
Tobias Kienzler
@TobiasKienzler: O używaniu git stashi git stash pop: masz rację, ale niestety git stashjest znacznie wolniejszy w systemie Windows niż w systemie Linux lub OS / X. Ponieważ mój katalog roboczy jest zwykle czysty, pominąłem ten krok, aby nie spowalniać polecenia.
Frerich Raabe
Mogę to potwierdzić, zwłaszcza podczas pracy na udziale sieciowym: - /
Tobias Kienzler
1
Miły. Przypadkowo to zrobiłem git rebase -i --fixupi zostało zmienione na podstawie ustalonego zatwierdzenia jako punktu wyjścia, więc argument sha nie był potrzebny w moim przypadku.
fwielstra
1
Dla osób często używających --autosquash może być przydatne ustawienie domyślnego zachowania: git config --global rebase.autosquash true
taktak004
31

Co robię to:

git add ... # Dodaj poprawkę.
git commit # Committed, ale w niewłaściwym miejscu.
git rebase -i HEAD ~ 5 # Sprawdź ostatnie 5 zatwierdzeń do zmiany bazy.

Twój edytor otworzy się z listą ostatnich 5 zatwierdzeń, gotowych do wtrącenia. Zmiana:

wybierz 08e833c Dobra zmiana 1.
pick 9134ac9 Dobra zmiana 2.
pick 5adda55 Zła zmiana!
wybierz 400bce4 Dobra zmiana 3.
pick 2bc82n1 Poprawka złej zmiany.

...do:

wybierz 08e833c Dobra zmiana 1.
pick 9134ac9 Dobra zmiana 2.
pick 5adda55 Zła zmiana!
f 2bc82n1 Poprawka złej zmiany.# Przejdź w górę i zmień „pick” na „f” dla „fixup”.
wybierz 400bce4 Dobra zmiana 3.

Zapisz i zamknij edytor, a poprawka zostanie umieszczona z powrotem w zatwierdzeniu, do którego należy.

Gdy zrobisz to kilka razy, we śnie zrobisz to w kilka sekund. Interaktywne ponowne bazowanie to funkcja, która naprawdę sprzedała mnie na git. Jest to niezwykle przydatne w tym i nie tylko ...

Kris Jenkins
źródło
9
Oczywiście możesz zmienić HEAD ~ 5 na HEAD ~ n, aby cofnąć się dalej. Nie będziesz chciał wtrącać się w jakąkolwiek historię, którą wypchnąłeś w górę, więc zwykle piszę „git rebase -i origin / master”, aby upewnić się, że zmieniam tylko nieumieszczoną historię.
Kris Jenkins,
4
To jest bardzo podobne do tego, co zawsze robiłem; FWIW, możesz być zainteresowany --autosquashprzełącznikiem dla git rebase, który automatycznie zmienia kolejność kroków w edytorze. Zobacz moją odpowiedź na skrypt, który wykorzystuje to do zaimplementowania git fixuppolecenia.
Frerich Raabe
Nie wiedziałem, że możesz po prostu ponownie zamówić skróty zatwierdzania, świetnie!
Aaron Franke
To wspaniale! Jedynie w celu upewnienia się, że cała rebase została wykonana, jest oddzielna gałąź funkcji. I nie zadzierać ze zwykłą gałęzią jak mistrz.
Jay Modi,
22

Trochę za późno na imprezę, ale tutaj jest rozwiązanie, które działa tak, jak sobie wyobrażał autor.

Dodaj to do swojego .gitconfig:

[alias]
    fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

Przykładowe użycie:

git add -p
git fixup HEAD~5

Jeśli jednak masz niestabilne zmiany, musisz je schować przed rebase.

git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop

Możesz zmodyfikować alias, aby automatycznie ukrywał się, zamiast dawać ostrzeżenie. Jeśli jednak korekta nie zostanie poprawnie zastosowana, po naprawieniu konfliktów będziesz musiał ręcznie otworzyć schowek. Ręczne zapisywanie i wyskakiwanie wydaje się bardziej spójne i mniej zagmatwane.

dschlyter
źródło
To jest bardzo pomocne. Dla mnie najczęstszym przypadkiem jest naprawianie zmian w poprzednim zatwierdzeniu, więc git fixup HEADwłaśnie dla tego utworzyłem alias. Przypuszczam, że mógłbym również użyć do tego poprawki.
konik polny
Dziękuję Ci! Używam go też najczęściej przy ostatnim zatwierdzeniu, ale mam inny alias do szybkiej zmiany. amend = commit --amend --reuse-message=HEADNastępnie możesz po prostu wpisać git amendlub git amend -ai pominąć edytor dla komunikatu o zatwierdzeniu.
dschlyter
3
Problem z poprawką polega na tym, że nie pamiętam, jak to przeliterować. Zawsze muszę pomyśleć, czy to poprawić, czy poprawić, a to nie jest dobre.
konik polny
12

Aby naprawić jedno zatwierdzenie:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2

gdzie a0b1c2d3 to zatwierdzenie, które chcesz naprawić, a 2 to liczba zatwierdzeń +1 wklejonych, które chcesz zmienić.

Uwaga: git rebase --autosquash bez -i nie działało, ale z -i działało, co jest dziwne.

Sérgio
źródło
2016 i --autosquashbez -inadal nie działa.
Jonathan Cross
1
Jak mówi strona podręcznika: Ta opcja jest poprawna tylko wtedy, gdy używana jest opcja --interactive. Ale jest łatwy sposób na pominięcie edytora:EDITOR=true git rebase --autosquash -i
joeytwiddle
Drugi krok nie działa dla mnie, mówiąc: Proszę określić, dla którego oddziału chcesz dokonać ponownego rozliczenia.
djangonaut
git rebase --autosquash -i HEAD ~ 2 (gdzie 2 to liczba zatwierdzeń +1 wklejonych, które chcesz zmienić.
Sérgio
6

AKTUALIZACJA: Czystszą wersję skryptu można teraz znaleźć tutaj: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Szukałem czegoś podobnego. Ten skrypt w Pythonie wydaje się jednak zbyt skomplikowany, dlatego opracowałem własne rozwiązanie:

Po pierwsze, moje aliasy git wyglądają tak (zapożyczone stąd ):

[alias]
  fixup = !sh -c 'git commit --fixup=$1' -
  squash = !sh -c 'git commit --squash=$1' -
  ri = rebase --interactive --autosquash

Teraz funkcja bash staje się całkiem prosta:

function gf {
  if [ $# -eq 1 ]
  then
    if [[ "$1" == HEAD* ]]
    then
      git add -A; git fixup $1; git ri $1~2
    else
      git add -A; git fixup $1; git ri $1~1
    fi
  else
    echo "Usage: gf <commit-ref> "
  fi
}

Ten kod najpierw przygotowuje wszystkie bieżące zmiany (możesz usunąć tę część, jeśli chcesz samodzielnie przygotować pliki). Następnie tworzy fixup (można również użyć squasha, jeśli tego potrzebujesz) commit. Następnie rozpoczyna interaktywne ponowne bazowanie z --autosquashflagą na rodzicu zatwierdzenia, które podajesz jako argument. Spowoduje to otwarcie skonfigurowanego edytora tekstu, dzięki czemu możesz sprawdzić, czy wszystko jest zgodne z oczekiwaniami, a po prostu zamknięcie edytora zakończy proces.

if [[ "$1" == HEAD* ]]Część (zapożyczone z tutaj ) jest używany, ponieważ jeśli używasz na przykład HEAD ~ 2 jak wasz commit (commit chcesz naprawić bieżących zmian z) odniesienie to HEAD zostanie przesunięty po fixup commit została stworzona i musiałbyś użyć HEAD ~ 3, aby odnieść się do tego samego zatwierdzenia.

Deiwin
źródło
Ciekawa alternatywa. +1
VonC
4

Możesz uniknąć etapu interaktywnego, używając edytora „zerowego”:

$ EDITOR=true git rebase --autosquash -i ...

Spowoduje to użycie /bin/truejako edytora zamiast /usr/bin/vim. Zawsze akceptuje wszystko, co sugeruje git, bez podpowiedzi.

joeytwiddle
źródło
Rzeczywiście, to jest dokładnie to, co zrobiłem w mojej odpowiedzi skryptu Python z „oryginalną odpowiedzią” z 30 września 2010 r. (Zwróć uwagę, że na dole skryptu jest napisane call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

To, co naprawdę przeszkadzało mi w przepływie pracy związanym z poprawkami, to fakt, że musiałem sam wymyślić, w którym zatwierdzeniu chciałem za każdym razem zmiażdżyć zmianę. Stworzyłem polecenie „git fixup”, które pomaga w tym.

To polecenie tworzy poprawki poprawek , z dodatkową magią polegającą na tym, że używa git-deps do automatycznego znalezienia odpowiedniego zatwierdzenia, więc przepływ pracy często sprowadza się do:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup

# at some later point squash all the fixup commits that came up
git rebase --autosquash master

Działa to tylko wtedy, gdy etapowe zmiany można jednoznacznie przypisać do konkretnego zatwierdzenia w drzewie roboczym (między głównym a HEAD). Uważam, że jest tak bardzo często w przypadku typu drobnych zmian, których używam, np. Literówek w komentarzach lub nazw nowo wprowadzonych (lub zmienionych nazw) metod. Jeśli tak nie jest, wyświetli się przynajmniej lista zatwierdzeń kandydatów.

Używam tego często w moim codziennym przepływie pracy, aby szybko zintegrować małe zmiany w poprzednio zmienionych wierszach w zatwierdzeniach w mojej gałęzi roboczej. Skrypt nie jest tak piękny, jak mógłby być i jest napisany w zsh, ale już od dłuższego czasu wykonuje swoją pracę wystarczająco dobrze, kiedy nigdy nie czułem potrzeby przepisywania go:

https://github.com/Valodim/git-fixup

Valodim
źródło
2

Korzystając z tego aliasu, można utworzyć korektę dla określonego pliku.

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
        [ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
        [ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
        COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
            git commit --fixup=$COMMIT && \
            git rebase -i --autosquash $COMMIT~1' -"

Jeśli wprowadziłeś jakieś zmiany, myfile.txtale nie chcesz umieszczać ich w nowym zatwierdzeniu, git fixup-file myfile.txtutworzy fixup!dla zatwierdzenia miejsce, w którym myfile.txtbył ostatnio modyfikowany, a następnie tak się stanie rebase --autosquash.

Alvaro
źródło
Bardzo sprytne, wolałbym, żeby git rebasenie wywołano tego automatycznie.
hurikhan77
2

commit --fixup i rebase --autosquash są świetne, ale nie wystarczają. Kiedy mam sekwencję zatwierdzeń A-B-Ci piszę więcej zmian w moim drzewie roboczym, które należą do jednego lub więcej istniejących zatwierdzeń, muszę ręcznie przejrzeć historię, zdecydować, które zmiany należą do których zatwierdzeń, ustawić je i utworzyć fixup!zatwierdza. Ale git ma już dostęp do wystarczającej ilości informacji, aby móc zrobić to wszystko za mnie, więc napisałem skrypt Perla, który właśnie to robi.

Za każdą porcję git diff fragmentu w skrypcie używa się git blamedo znalezienia zatwierdzenia, który ostatnio dotykał odpowiednich wierszy, i wywołuje git commit --fixupnapisanie odpowiednich fixup!zatwierdzeń, zasadniczo robiąc to samo, co wcześniej robiłem ręcznie.

Jeśli uznasz to za przydatne, nie krępuj się, aby go ulepszyć i powtórzyć, a być może pewnego dnia dostaniemy taką funkcję we gitwłaściwy sposób. Bardzo chciałbym zobaczyć narzędzie, które może zrozumieć, jak należy rozwiązać konflikt scalania, gdy został wprowadzony przez interaktywną bazę danych.

Oktalist
źródło
Miałem też marzenia o automatyzacji: git powinien po prostu spróbować umieścić ją jak najdalej w historii, bez zrywania łatki. Ale twoja metoda jest prawdopodobnie bardziej rozsądna. Wspaniale widzieć, że próbowałeś. Spróbuję to! (Oczywiście zdarza się, że łatka naprawcza pojawia się gdzie indziej w pliku i tylko programista wie, do którego zatwierdzenia należy. Lub może nowy test w zestawie testów mógłby pomóc maszynie ustalić, gdzie powinna się znaleźć poprawka.)
joeytwiddle
1

Napisałem małą funkcję powłoki wywoływaną gcfdo automatycznego zatwierdzania poprawek i rebase:

$ git add -p

  ... select hunks for the patch with y/n ...

$ gcf <earlier_commit_id>

  That commits the fixup and does the rebase.  Done!  You can get back to coding.

Na przykład możesz załatać drugie zatwierdzenie przed najnowszym za pomocą: gcf HEAD~~

Oto funkcja . Możesz wkleić go do swojego~/.bashrc

git_commit_immediate_fixup() {
  local commit_to_amend="$1"
  if [ -z "$commit_to_amend" ]; then
    echo "You must provide a commit to fixup!"; return 1
  fi

  # Get a static commit ref in case the commit is something relative like HEAD~
  commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2

  #echo ">> Committing"
  git commit --no-verify --fixup "${commit_to_amend}" || return 3

  #echo ">> Performing rebase"
  EDITOR=true git rebase --interactive --autosquash --autostash \
                --rebase-merges --no-fork-point "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

To używa --autostash do przechowywania i pobierania wszelkich niezatwierdzonych zmian, jeśli to konieczne.

--autosquashwymaga --interactiverebase, ale unikamy interakcji, używając atrapy EDITOR.

--no-fork-pointchroni zatwierdzenia przed dyskretnym porzuceniem w rzadkich sytuacjach (gdy rozwidliłeś nową gałąź, a ktoś już zmienił bazę poprzednich zatwierdzeń).

joeytwiddle
źródło
0

Nie znam sposobu zautomatyzowanego, ale oto rozwiązanie, które może być łatwiejsze do zrobienia przez człowieka:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop
Tobias Kienzler
źródło
Zobacz moją odpowiedź na skrypt, który wykorzystuje to do zaimplementowania git fixuppolecenia.
Frerich Raabe
@Frerich Raabe: brzmi dobrze, nie wiem o czym--autosquash
Tobias Kienzler
0

Polecam https://github.com/tummychow/git-absorb :

Prezentacja w windzie

Masz gałąź funkcji z kilkoma zatwierdzeniami. Twój kolega z drużyny przejrzał gałąź i wskazał kilka błędów. Masz poprawki dla błędów, ale nie chcesz umieszczać ich wszystkich w nieprzezroczystym zatwierdzeniu z napisem poprawki, ponieważ wierzysz w atomowe zatwierdzenia. Zamiast ręcznie wyszukiwać SHA zatwierdzeń dla git commit --fixuplub uruchamiać ręczne interaktywne rebase, wykonaj następujące czynności:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • lub: git rebase -i --autosquash master

git absorbautomatycznie określi, które zatwierdzenia można bezpiecznie modyfikować i które indeksowane zmiany należą do każdego z tych zatwierdzeń. Następnie zapisze poprawkę! zatwierdza dla każdej z tych zmian. Możesz sprawdzić jego wyjście ręcznie, jeśli mu nie ufasz, a następnie złożyć poprawki do gałęzi funkcji za pomocą wbudowanej funkcji autosquash git.

Petski
źródło