Comment puis-je réparer facilement un commit passé?

116

Je viens de lire la modification d'un seul fichier dans un commit passé dans git mais malheureusement la solution acceptée «réorganise» les commits, ce qui n'est pas ce que je veux. Alors voici ma question:

De temps en temps, je remarque un bogue dans mon code en travaillant sur une fonctionnalité (non liée). Un rapide git blamerévèle ensuite que le bogue a été introduit il y a quelques commits (je commets beaucoup, donc généralement ce n'est pas le commit le plus récent qui a introduit le bogue). À ce stade, je fais généralement ceci:

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

Cependant, cela arrive si souvent que la séquence ci-dessus devient ennuyeuse. Surtout le «rebase interactif» est ennuyeux. Existe-t-il un raccourci vers la séquence ci-dessus, qui me permet de modifier un commit arbitraire dans le passé avec les modifications par étapes? Je suis parfaitement conscient que cela change l'histoire, mais je fais des erreurs si souvent que j'aimerais vraiment avoir quelque chose comme

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.

Peut-être un script intelligent qui peut réécrire les commits à l'aide d'outils de plomberie ou plus?

Frerich Raabe
la source
Qu'entendez-vous par «réorganiser» les commits? Si vous modifiez l'historique, tous les commits depuis les commits modifiés doivent être différents, mais la réponse acceptée à la question liée ne réorganise pas les commits dans un sens significatif.
CB Bailey
1
@Charles: Je voulais dire réorganiser comme dans: si je remarque que HEAD ~ 5 est le commit cassé, la réponse suivante acceptée dans la question liée fera de HEAD (la pointe de la branche) le commit fixe. Cependant, j'aimerais que HEAD ~ 5 soit le commit fixe - ce que vous obtenez lorsque vous utilisez un rebase interactif et que vous modifiez un seul commit pour le corriger.
Frerich Raabe
Oui, mais la commande rebase revérifiera le master et rebasera tous les commits suivants sur le commit fixe. N'est-ce pas ainsi que vous conduisez le rebase -i?
CB Bailey
En fait, il y a un problème potentiel avec cette réponse, je pense que ça devrait l'être rebase --onto tmp bad-commit master. Tel qu'il est écrit, il essaiera d'appliquer le mauvais commit à l'état de commit fixe.
CB Bailey
Voici un autre outil pour automatiser le processus de correction / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Réponses:

166

RÉPONSE MIS À JOUR

Il y a quelque temps, un nouvel --fixupargument a été ajouté, git commitauquel il peut être utilisé pour construire un commit avec un message de journal approprié pour git rebase --interactive --autosquash. Donc, le moyen le plus simple de réparer un commit passé est maintenant:

$ 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

RÉPONSE ORIGINALE

Voici un petit script Python que j'ai écrit il y a quelque temps qui implémente cette git fixuplogique que j'espérais dans ma question initiale. Le script suppose que vous avez organisé certaines modifications, puis que vous les appliquez à la validation donnée.

REMARQUE : ce script est spécifique à Windows; il recherche git.exeet définit la GIT_EDITORvariable d'environnement en utilisantset . Ajustez-le si nécessaire pour les autres systèmes d'exploitation.

En utilisant ce script, je peux implémenter précisément le workflow `` réparer les sources cassées, mettre en scène les correctifs, exécuter git fixup '' que j'ai demandé:

#!/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
la source
2
vous pouvez utiliser git stashet git stash popautour de votre rebase pour ne plus avoir besoin d'un répertoire de travail propre
Tobias Kienzler
@TobiasKienzler: À propos de l'utilisation de git stashet git stash pop: vous avez raison, mais malheureusement, git stashc'est beaucoup plus lent sous Windows que sous Linux ou OS / X. Étant donné que mon répertoire de travail est généralement propre, j'ai omis cette étape pour ne pas ralentir la commande.
Frerich Raabe
Je peux le confirmer, en particulier lorsque vous travaillez sur un partage réseau: - /
Tobias Kienzler
1
Agréable. Je l'ai fait accidentellement git rebase -i --fixup, et cela a rebasé à partir du commit corrigé comme point de départ, donc l'argument sha n'était pas nécessaire dans mon cas.
fwielstra
1
Pour les personnes qui utilisent souvent --autosquash, il peut être utile de le définir comme comportement par défaut: git config --global rebase.autosquash true
taktak004
31

Ce que je fais c'est:

git add ... # Ajoutez le correctif.
git commit # Committed, mais au mauvais endroit.
git rebase -i HEAD ~ 5 # Examinez les 5 derniers commits pour le rebasage.

Votre éditeur s'ouvrira avec une liste des 5 derniers commits, prêts à être manipulés. Changement:

pick 08e833c Bon changement 1.
choisir 9134ac9 Bon changement 2.
choisissez 5adda55 Mauvais changement!
choisir 400bce4 Bon changement 3.
pick 2bc82n1 Correction d'un mauvais changement.

...à:

pick 08e833c Bon changement 1.
choisir 9134ac9 Bon changement 2.
choisissez 5adda55 Mauvais changement!
f 2bc82n1 Correction d'un mauvais changement. # Montez et changez 'pick' en 'f' pour 'fixup'.
choisir 400bce4 Bon changement 3.

Enregistrez et quittez votre éditeur, et le correctif sera réécrit dans le commit auquel il appartient.

Après avoir fait cela plusieurs fois, vous le ferez en quelques secondes pendant votre sommeil. Le rebasage interactif est la fonctionnalité qui m'a vraiment vendu sur git. C'est incroyablement utile pour cela et plus encore ...

Kris Jenkins
la source
9
Évidemment, vous pouvez changer HEAD ~ 5 en HEAD ~ n pour revenir plus loin. Vous ne voudrez pas vous mêler de l'historique que vous avez poussé en amont, donc je tape généralement 'git rebase -i origin / master' pour m'assurer que je ne change que l'historique non poussé.
Kris Jenkins du
4
Cela ressemble beaucoup à ce que j'ai toujours fait; FWIW, vous pourriez être intéressé par le --autosquashcommutateur pour git rebase, qui réorganise automatiquement les étapes dans l'éditeur pour vous. Voir ma réponse pour un script qui en profite pour implémenter une git fixupcommande.
Frerich Raabe
Je ne savais pas que vous pouviez simplement réorganiser les hachages de validation, bien!
Aaron Franke
C'est super! Une branche de fonctionnalité distincte est uniquement pour s'assurer que tout le travail de rebase est terminé. Et ne pas jouer avec la branche commune comme maître.
Jay Modi
22

Un peu tard à la fête, mais voici une solution qui fonctionne comme l'auteur l'imaginait.

Ajoutez ceci à votre .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' -"

Exemple d'utilisation:

git add -p
git fixup HEAD~5

Cependant, si vous avez des modifications non mises en scène, vous devez les cacher avant le rebase.

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

Vous pouvez modifier l'alias pour qu'il se cache automatiquement, au lieu de donner un avertissement. Cependant, si la correction ne s'applique pas proprement, vous devrez ouvrir la cachette manuellement après avoir résolu les conflits. Faire à la fois la sauvegarde et le popping manuellement semble plus cohérent et moins déroutant.

dschlyter
la source
Ceci est très utile. Pour moi, le cas d'utilisation le plus courant est de corriger les modifications apportées au commit précédent, c'est pourquoi git fixup HEADj'ai créé un alias. Je pourrais également utiliser amender pour cela, je suppose.
sauterelle
Je vous remercie! Je l'utilise aussi le plus souvent lors du dernier commit, mais j'ai un autre alias pour une modification rapide. amend = commit --amend --reuse-message=HEADEnsuite, vous pouvez simplement taper git amendou git amend -aet ignorer l'éditeur pour le message de validation.
dschlyter le
3
Le problème avec amend est que je ne me souviens pas comment l'épeler. Je dois toujours penser, est-il amendé ou amendé et ce n'est pas bon.
grasshopper
12

Pour réparer un commit:

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

où a0b1c2d3 est le commit que vous voulez corriger et où 2 est le nombre de commits +1 collés que vous voulez changer.

Remarque: git rebase --autosquash sans -i ne fonctionne pas mais avec -i fonctionne, ce qui est étrange.

Sérgio
la source
2016 et --autosquashsans -ine fonctionne toujours pas.
Jonathan Cross
1
Comme le dit la page de manuel: Cette option n'est valide que lorsque l'option --interactive est utilisée. Mais il existe un moyen simple de sauter l'éditeur:EDITOR=true git rebase --autosquash -i
joeytwiddle
La deuxième étape ne fonctionne pas pour moi, en disant: Veuillez spécifier sur quelle branche vous voulez rebaser.
djangonaut
git rebase --autosquash -i HEAD ~ 2 (où 2 est le nombre de commits +1 collés que vous voulez changer.
Sérgio
6

MISE À JOUR: Une version plus propre du script peut maintenant être trouvée ici: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Je cherchais quelque chose de similaire. Ce script Python semble cependant trop compliqué, c'est pourquoi j'ai mis au point ma propre solution:

Tout d'abord, mes alias git ressemblent à ça (emprunté à ici ):

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

Maintenant, la fonction bash devient assez simple:

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
}

Ce code met d'abord en scène toutes les modifications actuelles (vous pouvez supprimer cette partie si vous souhaitez mettre en scène les fichiers vous-même). Crée ensuite le correctif (squash peut également être utilisé, si c'est ce dont vous avez besoin) commit. Après cela, il lance un rebase interactif avec l' --autosquashindicateur sur le parent du commit que vous donnez comme argument. Cela ouvrira votre éditeur de texte configuré, vous pourrez donc vérifier que tout est comme prévu et la simple fermeture de l'éditeur terminera le processus.

La if [[ "$1" == HEAD* ]]partie (empruntée ici ) est utilisée, car si vous utilisez, par exemple, HEAD ~ 2 comme référence de commit (le commit avec lequel vous voulez corriger les changements actuels), alors le HEAD sera déplacé après la création du commit de correction et vous devrez utiliser HEAD ~ 3 pour faire référence au même commit.

Deiwin
la source
Alternative intéressante. +1
VonC
4

Vous pouvez éviter l'étape interactive en utilisant un éditeur "nul":

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

Cela utilisera /bin/truecomme éditeur, au lieu de /usr/bin/vim. Il accepte toujours tout ce que git suggère, sans incitation.

joeytwiddle
la source
En effet, c'est exactement ce que j'ai fait dans ma réponse au script Python «réponse originale» du 30 septembre 2010 (notez comment en bas du script, il est dit call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

Ce qui m'a vraiment dérangé dans le workflow de correction, c'est que je devais déterminer moi-même dans quel commit je voulais écraser le changement à chaque fois. J'ai créé une commande "git fixup" qui m'aide.

Cette commande crée des commits de correction, avec la magie supplémentaire qu'elle utilise git-deps pour trouver automatiquement le commit pertinent, de sorte que le flux de travail se résume souvent à:

# 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

Cela ne fonctionne que si les changements par étapes peuvent être attribués sans ambiguïté à un commit particulier sur l'arbre de travail (entre master et HEAD). Je trouve que c'est le cas très souvent pour le type de petits changements pour lesquels j'utilise ceci, par exemple les fautes de frappe dans les commentaires ou les noms de méthodes nouvellement introduites (ou renommées). Si ce n'est pas le cas, il affichera au moins une liste de commits candidats.

J'utilise beaucoup cela dans mon flux de travail quotidien, pour intégrer rapidement de petits changements aux lignes précédemment modifiées dans les commits sur ma branche de travail. Le script n'est pas aussi beau qu'il pourrait l'être, et il est écrit en zsh, mais il fait assez bien le travail pour moi depuis un bon moment maintenant que je n'ai jamais ressenti le besoin de le réécrire:

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

Valodim
la source
2

Vous pouvez créer une correction pour un fichier particulier en utilisant cet 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' -"

Si vous avez apporté des modifications dans myfile.txtmais que vous ne voulez pas les mettre dans un nouveau commit, git fixup-file myfile.txtcréera un fixup!pour le commit où a myfile.txtété modifié pour la dernière fois, puis il le sera rebase --autosquash.

Alvaro
la source
Très intelligent, je préférerais que cela git rebasene soit pas automatiquement appelé.
hurikhan77
2

commit --fixupet rebase --autosquashsont excellents, mais ils n'en font pas assez. Quand j'ai une séquence de commits A-B-Cet que j'écris d'autres changements dans mon arbre de travail qui appartiennent à un ou plusieurs de ces commits existants, je dois regarder manuellement l'historique, décider quels changements appartiennent à quels commits, les mettre en scène et créer le fixup!s'engage. Mais git a déjà accès à suffisamment d'informations pour pouvoir faire tout cela pour moi, j'ai donc écrit un script Perl qui fait exactement cela.

Pour chaque morceau dans git diffle script utilise git blamepour trouver le commit qui a touché en dernier les lignes pertinentes, et appelle git commit --fixupà écrire les fixup!commits appropriés , en faisant essentiellement la même chose que je faisais manuellement auparavant.

Si vous le trouvez utile, n'hésitez pas à l'améliorer et à le répéter et peut-être qu'un jour nous obtiendrons une telle fonctionnalité git. J'aimerais voir un outil capable de comprendre comment un conflit de fusion doit être résolu lorsqu'il a été introduit par un rebase interactif.

Oktaliste
la source
J'avais aussi des rêves d'automatisation: git devrait juste essayer de le replacer le plus loin possible dans l'histoire, sans que le patch ne casse. Mais votre méthode est probablement plus sensée. Ravi de voir que vous l'avez essayé. Je vais l'essayer! (Bien sûr, il y a des moments où le correctif de correction apparaît ailleurs dans le fichier, et seul le développeur sait à quel commit il appartient. Ou peut-être qu'un nouveau test dans la suite de tests pourrait aider la machine à déterminer où le correctif doit aller.)
joeytwiddle
1

J'ai écrit une petite fonction shell appelée gcfpour effectuer le commit de correction et le rebase automatiquement:

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

Par exemple, vous pouvez patcher le deuxième commit avant le dernier avec: gcf HEAD~~

Voici la fonction . Vous pouvez le coller dans votre~/.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'

Il utilise --autostashpour cacher et afficher toutes les modifications non validées si nécessaire.

--autosquashnécessite un --interactiverebase, mais nous évitons l'interaction en utilisant un mannequin EDITOR.

--no-fork-point protège les commits d'être insérés en silence rares situations (lorsque vous avez bifurqué une nouvelle branche et que quelqu'un a déjà rebasé les commits passés).

joeytwiddle
la source
0

Je ne connais pas de méthode automatisée, mais voici une solution qui pourrait être plus facile à botiser par l'homme:

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
la source
Voir ma réponse pour un script qui en profite pour implémenter une git fixupcommande.
Frerich Raabe
@Frerich Raabe: ça sonne bien, je ne sais pas--autosquash
Tobias Kienzler
0

Je recommanderais https://github.com/tummychow/git-absorb :

Résumé en quelques secondes

Vous avez une branche de fonctionnalités avec quelques validations. Votre coéquipier a examiné la branche et a signalé quelques bugs. Vous avez des correctifs pour les bogues, mais vous ne voulez pas les pousser tous dans un commit opaque qui dit des correctifs, parce que vous croyez aux commits atomiques. Au lieu de rechercher manuellement les SHA de validation git commit --fixupou d'exécuter un rebase interactif manuel, procédez comme suit:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • ou: git rebase -i --autosquash master

git absorbidentifiera automatiquement quels commits peuvent être modifiés en toute sécurité et quels changements indexés appartiennent à chacun de ces commits. Il écrira alors fixup! s'engage pour chacun de ces changements. Vous pouvez vérifier sa sortie manuellement si vous ne lui faites pas confiance, puis replier les correctifs dans votre branche de fonctionnalités avec la fonctionnalité autosquash intégrée de git.

Petski
la source