Como posso facilmente corrigir um commit anterior?

115

Acabei de ler a alteração de um único arquivo em um commit anterior no git, mas infelizmente a solução aceita 'reordena' os commits, que não é o que eu quero. Então aqui está minha pergunta:

De vez em quando, percebo um bug no meu código enquanto trabalho em um recurso (não relacionado). Um rápido git blameentão revela que o bug foi introduzido há alguns commits atrás (eu faço vários commits, então geralmente não é o commit mais recente que introduziu o bug). Nesse ponto, geralmente faço o seguinte:

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

No entanto, isso acontece com tanta frequência que a sequência acima está ficando irritante. Especialmente o 'rebase interativo' é enfadonho. Existe algum atalho para a sequência acima, o que me permite corrigir um commit arbitrário no passado com as mudanças encenadas? Estou perfeitamente ciente de que isso muda a história, mas estou cometendo erros com tanta frequência que adoraria ter algo como

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.

Talvez um script inteligente que pode reescrever commits usando ferramentas de encanamento ou algo assim?

Frerich Raabe
fonte
O que você quer dizer com "reordenar" os commits? Se você está mudando o histórico, então todos os commits desde os commits alterados têm que ser diferentes, mas a resposta aceita para a questão vinculada não reordena os commits em nenhum sentido significativo.
CB Bailey
1
@Charles: Eu quis dizer reordenar como em: se eu perceber que HEAD ~ 5 é o commit quebrado, a seguinte resposta aceita na pergunta vinculada fará com que HEAD (a ponta do branch) seja o commit corrigido. No entanto, eu gostaria que HEAD ~ 5 fosse o commit corrigido - que é o que você obtém ao usar um rebase interativo e editar um único commit para correção.
Frerich Raabe
Sim, mas então o comando rebase fará o checkout do master novamente e rebase todos os commits subsequentes no commit corrigido. Não é assim que você está dirigindo rebase -i?
CB Bailey
Na verdade, há um problema potencial com essa resposta, acho que deveria ser rebase --onto tmp bad-commit master. Conforme está escrito, ele tentará aplicar o commit incorreto ao estado de commit fixo.
CB Bailey
Aqui está outra ferramenta para automatizar o processo de correção / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Respostas:

164

RESPOSTA ATUALIZADA

Um tempo atrás, um novo --fixupargumento foi adicionado ao git commitqual pode ser usado para construir um commit com uma mensagem de log adequada para git rebase --interactive --autosquash. Portanto, a maneira mais simples de corrigir um commit anterior é agora:

$ 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

RESPOSTA ORIGINAL

Aqui está um pequeno script Python que escrevi há algum tempo, que implementa essa git fixuplógica que esperava na minha pergunta original. O script assume que você testou algumas mudanças e depois as aplica ao commit fornecido.

NOTA : Este script é específico do Windows; ele procura git.exee define a GIT_EDITORvariável de ambiente usando set. Ajuste isso conforme necessário para outros sistemas operacionais.

Usando este script, posso implementar precisamente o fluxo de trabalho 'consertar fontes quebradas, preparar correções, executar git fixup' que solicitei:

#!/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
fonte
2
você poderia usar git stashe em git stash poptorno de seu rebase para não precisar mais de um diretório de trabalho limpo
Tobias Kienzler
@TobiasKienzler: Sobre como usar git stashe git stash pop: você está certo, mas infelizmente git stashé muito mais lento no Windows do que no Linux ou OS / X. Como meu diretório de trabalho geralmente está limpo, omiti esta etapa para não desacelerar o comando.
Frerich Raabe
Posso confirmar isso, especialmente ao trabalhar em um compartilhamento de rede: - /
Tobias Kienzler
1
Agradável. Eu acidentalmente fiz git rebase -i --fixup, e ele foi baseado no commit corrigido como ponto de partida, então o argumento sha não foi necessário no meu caso.
fwielstra
1
Para pessoas que usam o --autosquash com frequência, pode ser útil defini-lo como o comportamento padrão: git config --global rebase.autosquash true
taktak004
31

O que eu faço é:

git add ... # Adicione a correção.
git commit # Committed, mas no lugar errado.
git rebase -i HEAD ~ 5 # Examina os últimos 5 commits para rebasing.

Seu editor abrirá com uma lista dos últimos 5 commits, prontos para serem mexidos. Mudança:

pick 08e833c Boa mudança 1.
escolha 9134ac9 Boa mudança 2.
pick 5adda55 Mudança ruim!
pick 400bce4 Boa mudança 3.
pick 2bc82n1 Correção de mudança ruim.

...para:

pick 08e833c Boa mudança 1.
escolha 9134ac9 Boa mudança 2.
pick 5adda55 Mudança ruim!
f 2bc82n1 Correção de alteração ruim. # Mova para cima e altere 'pick' para 'f' para 'fixup'.
pick 400bce4 Boa mudança 3.

Salve e saia do seu editor, e a correção será comprimida de volta no commit a que pertence.

Depois de fazer isso algumas vezes, você fará em segundos enquanto dorme. Rebasing interativo é o recurso que realmente me vendeu no git. É incrivelmente útil para isso e muito mais ...

Kris Jenkins
fonte
9
Obviamente, você pode alterar HEAD ~ 5 para HEAD ~ n para voltar mais longe. Você não vai querer se intrometer com nenhum histórico que tenha enviado para cima, então geralmente digito 'git rebase -i origin / master' para garantir que estou apenas alterando o histórico não compactado.
Kris Jenkins
4
Isso é muito parecido com o que eu sempre fiz; FWIW, você pode estar interessado na --autosquashopção para git rebase, que reordena automaticamente as etapas no editor para você. Veja minha resposta para um script que aproveita isso para implementar um git fixupcomando.
Frerich Raabe
Eu não sabia que você poderia simplesmente reordenar os hashes de commit, legal!
Aaron Franke
Isso é ótimo! Apenas para garantir que todo o trabalho de rebase seja feito é um ramo de recurso separado. E não mexer com ramo comum como mestre.
Jay Modi
22

Um pouco tarde para a festa, mas eis uma solução que funciona como o autor imaginou.

Adicione ao seu .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' -"

Exemplo de uso:

git add -p
git fixup HEAD~5

No entanto, se você tiver mudanças não planejadas, deve armazená-las antes do rebase.

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

Você pode modificar o alias para esconder automaticamente, em vez de dar um aviso. No entanto, se a correção não se aplicar corretamente, você precisará abrir o estoque manualmente após corrigir os conflitos. Salvar e abrir manualmente parece mais consistente e menos confuso.

dschlyter
fonte
Isso é muito útil. Para mim, o caso de uso mais comum é corrigir as alterações no commit anterior, então git fixup HEADé para isso que criei um alias. Eu também poderia usar emendar para isso, suponho.
gafanhoto
Obrigado! Também o uso com mais frequência no último commit, mas tenho outro apelido para uma correção rápida. amend = commit --amend --reuse-message=HEADEntão você pode simplesmente digitar git amendou git amend -ae pular o editor da mensagem de confirmação.
dschlyter
3
O problema de corrigir é que não me lembro como se escreve. Eu sempre tenho que pensar, isso é emendar ou emendar e isso não é bom.
gafanhoto,
11

Para consertar um commit:

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

onde a0b1c2d3 é o commit que você deseja consertar e onde 2 é o número de commits +1 colados que você deseja alterar.

Nota: git rebase --autosquash sem -i não funcionou, mas com -i funcionou, o que é estranho.

Sérgio
fonte
2016 e --autosquashsem -iainda não funciona.
Jonathan Cross,
1
Como diz a página de manual: Esta opção só é válida quando a opção --interactive é usada. Mas há uma maneira fácil de pular o editor:EDITOR=true git rebase --autosquash -i
joeytwiddle
A segunda etapa não funciona para mim, dizendo: Por favor, especifique contra qual branch você deseja realocar.
djangonaut
git rebase --autosquash -i HEAD ~ 2 (onde 2 é o número de commits +1 colados que você deseja alterar.
Sérgio
6

ATUALIZAÇÃO: uma versão mais limpa do script agora pode ser encontrada aqui: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Tenho procurado por algo semelhante. No entanto, este script Python parece muito complicado, portanto, montei minha própria solução:

Primeiro, meus aliases git são assim (emprestados daqui ):

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

Agora a função bash se torna bastante simples:

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
}

Este código primeiro prepara todas as alterações atuais (você pode remover esta parte, se desejar preparar os arquivos você mesmo). Em seguida, cria o commit de correção (squash também pode ser usado, se for necessário). Depois disso, ele inicia um rebase interativo com o --autosquashsinalizador no pai do commit que você fornece como argumento. Isso abrirá seu editor de texto configurado, para que você possa verificar se tudo está conforme o esperado e simplesmente fechar o editor encerrará o processo.

A if [[ "$1" == HEAD* ]]parte (emprestada daqui ) é usada, porque se você usar, por exemplo, HEAD ~ 2 como sua referência de commit (o commit que você deseja corrigir as alterações atuais), então o HEAD será deslocado após o commit de correção ter sido criado e você precisaria usar HEAD ~ 3 para se referir ao mesmo commit.

Deiwin
fonte
Alternativa interessante. +1
VonC
4

Você pode evitar o estágio interativo usando um editor "nulo":

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

Isso será usado /bin/truecomo editor, em vez de /usr/bin/vim. Ele sempre aceita qualquer sugestão do git, sem avisar.

joeytwiddle
fonte
Na verdade, isso é exatamente o que eu fiz na minha resposta do script Python 'resposta original' de 30 de setembro de 2010 (observe como no final do script, diz call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

O que realmente me incomodou sobre o fluxo de trabalho de correção foi que eu tinha que descobrir sozinho em qual commit eu queria esmagar a mudança todas as vezes. Eu criei um comando "git fixup" que ajuda com isso.

Este comando cria commits de correção, com a mágica adicional de usar git-deps para encontrar automaticamente o commit relevante, então o fluxo de trabalho geralmente se resume a:

# 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

Isso só funciona se as mudanças preparadas puderem ser atribuídas sem ambigüidade a um commit particular na árvore de trabalho (entre o master e o HEAD). Acho que esse é o caso com muita frequência para o tipo de pequenas alterações para as quais uso isso, por exemplo, erros de digitação em comentários ou nomes de métodos recém-introduzidos (ou renomeados). Se este não for o caso, pelo menos exibirá uma lista de commits de candidatos.

Eu uso muito isso em meu fluxo de trabalho diário, para integrar rapidamente pequenas alterações em linhas alteradas anteriormente em commits em meu branch de trabalho. O roteiro não é tão bonito quanto poderia ser e é escrito em zsh, mas tem feito o trabalho para mim bem o suficiente por um bom tempo que nunca senti a necessidade de reescrevê-lo:

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

Valodim
fonte
2

Você pode criar uma correção para um arquivo específico usando este alias.

[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' -"

Se você fez algumas alterações, myfile.txtmas não deseja colocá-las em um novo commit, git fixup-file myfile.txtirá criar um fixup!para o commit onde myfile.txtfoi modificado pela última vez, e então irá rebase --autosquash.

Alvaro
fonte
Muito inteligente, embora eu prefira que git rebasenão seja chamado automaticamente.
hurikhan77
2

commit --fixupe rebase --autosquashsão ótimos, mas não fazem o suficiente. Quando eu tenho uma sequência de commits A-B-Ce escrevo mais algumas mudanças em minha árvore de trabalho que pertencem a um ou mais desses commits existentes, eu tenho que olhar manualmente o histórico, decidir quais mudanças pertencem a quais commits, encená-los e criar o fixup!compromete. Mas git já tem acesso a informações suficientes para fazer tudo isso por mim, então escrevi um script Perl que faz exatamente isso.

Para cada pedaço no git diffscript, use git blamepara encontrar o commit que tocou por último nas linhas relevantes e chamadas git commit --fixuppara escrever os fixup!commits apropriados , essencialmente fazendo a mesma coisa que fazia manualmente antes.

Se você achar que é útil, sinta-se à vontade para aprimorá-lo e iterar nele e talvez um dia possamos obter esse recurso de maneira gitadequada. Eu adoraria ver uma ferramenta que pode entender como um conflito de mesclagem deve ser resolvido quando ele foi introduzido por um rebase interativo.

Oktalista
fonte
Eu também sonhava com automação: o git deveria apenas tentar colocá-lo o mais longe possível na história, sem quebrar o patch. Mas seu método provavelmente é mais lógico. Ótimo ver que você tentou. Eu vou experimentar! (Claro que há momentos em que o patch de correção aparece em outro lugar no arquivo, e apenas o desenvolvedor sabe a qual commit ele pertence. Ou talvez um novo teste no conjunto de testes possa ajudar a máquina a descobrir onde a correção deve ir.)
joeytwiddle
1

Eu escrevi uma pequena função shell chamada gcfpara executar o commit de correção e o rebase automaticamente:

$ 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.

Por exemplo, você pode corrigir o segundo commit antes do último com: gcf HEAD~~

Aqui está a função . Você pode colá-lo em seu~/.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'

Ele usa --autostashpara esconder e exibir quaisquer alterações não confirmadas, se necessário.

--autosquashrequer um --interactiverebase, mas evitamos a interação usando um manequim EDITOR.

--no-fork-pointprotege os commits de serem silenciosamente descartados em raras situações (quando você bifurcou um novo branch, e alguém já rebasou commits anteriores).

joeytwiddle
fonte
0

Não conheço uma forma automatizada, mas aqui está uma solução que pode ser mais fácil de ser botada por humanos:

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
fonte
Veja minha resposta para um script que tira vantagem disso para implementar um git fixupcomando.
Frerich Raabe
@Frerich Raabe: parece bom, não sei sobre--autosquash
Tobias Kienzler
0

Eu recomendaria https://github.com/tummychow/git-absorb :

Elevator Pitch

Você tem um branch de recursos com alguns commits. Seu colega de equipe revisou o branch e apontou alguns bugs. Você tem correções para os bugs, mas não quer colocá-los todos em um commit opaco que diz correções, porque você acredita em commits atômicos. Em vez de localizar manualmente SHAs de confirmação git commit --fixupou executar um rebase interativo manual, faça o seguinte:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • ou: git rebase -i --autosquash master

git absorbidentificará automaticamente quais commits são seguros para modificar e quais mudanças indexadas pertencem a cada um desses commits. Em seguida, ele escreverá a correção! confirma para cada uma dessas mudanças. Você pode verificar sua saída manualmente se não confiar nela, e então incluir os ajustes em seu branch de recursos com a funcionalidade autosquash embutida do git.

Petski
fonte