web-dev-qa-db-fra.com

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

Je viens de lire modifier 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 lorsque je travaille sur une fonctionnalité (non liée). Un rapide git blame révèle ensuite que le bogue a été introduit il y a quelques commits (je commets beaucoup, donc ce n'est généralement pas la plus récente validation qui a introduit le bogue). À ce stade, je fais habituellement 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 se produit si souvent que la séquence ci-dessus devient agaçante. Surtout le «rebase interactif» est ennuyeux. Existe-t-il un raccourci vers la séquence ci-dessus, qui me permet de modifier une validation arbitraire dans le passé avec les modifications mises en scène? Je suis parfaitement conscient du fait 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 commet en utilisant des outils de plomberie ou alors?

88
Frerich Raabe

RÉPONSE MISE À JOUR

Il y a quelque temps, un nouvel argument --fixup a été ajouté à git commit et peut être utilisé pour construire un commit avec un message de journal adapté à 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 et qui implémente cette logique git fixup que j'espérais dans ma question initiale. Le script suppose que vous avez mis en place des modifications, puis les applique à la validation donnée.

REMARQUE: Ce script est spécifique à Windows; il recherche git.exe et définit la variable d'environnement GIT_EDITOR à l'aide de set. Ajustez-le si nécessaire pour d'autres systèmes d'exploitation.

En utilisant ce script, je peux implémenter avec précision le flux de travaux 'réparer les sources endommagées, les correctifs d'étape, 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)
136
Frerich Raabe

Ce que je fais c'est:

 git add ... # Ajoute le correctif .
 git commit # Engagé, mais au mauvais endroit .
 git rebase -i HEAD ~ 5 # Examinez les 5 derniers commits pour republier .

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

 pick 08e833c Bon changement 1 .
 pick 9134ac9 Bon changement 2 ..
 pick 5adda55 Mauvais changement! 
 pick 400bce4 Bon changement 3 ..

...à:

 pick 08e833c Bon changement 1 .
 pick 9134ac9 Bon changement 2 .
 pick 5adda55 Mauvais changement! 
f 2bc82n1 Correction du mauvais changement.  # Montez et changez 'pick' en 'f' pour 'réparation' .
 Pick 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 dans votre sommeil. Le rebasement interactif est la fonctionnalité qui m'a vraiment vendu sur git. C'est incroyablement utile pour cela et plus encore ...

26
Kris Jenkins

Un peu tard pour 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

Toutefois, si vous avez des modifications non mises en scène, vous devez les stocker avant la nouvelle base.

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

Vous pouvez modifier l'alias pour le masquer automatiquement, au lieu de donner un avertissement. Toutefois, si le correctif ne s’applique pas correctement, vous devrez ouvrir le cache manuellement après avoir résolu les conflits. Faire à la fois les sauvegardes et les sauts manuellement semble plus cohérent et moins déroutant.

17
dschlyter

UPDATE: Une version plus propre du script est maintenant disponible à l’adresse: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

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

Premièrement, mes pseudonymes 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 commence par mettre en place toutes les modifications actuelles (vous pouvez supprimer cette partie si vous souhaitez mettre en scène les fichiers vous-même). Puis crée le correctif (squash peut également être utilisé, si c'est ce dont vous avez besoin) commit. Après cela, il commence une réplication interactive avec l'indicateur --autosquash sur le parent du commit que vous donnez comme argument. Cela ouvrira votre éditeur de texte configuré, vous pourrez ainsi vérifier que tout se déroule comme vous le souhaitez et simplement fermer l'éditeur finira le processus. 

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

6
Deiwin

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

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

Ceci utilisera /bin/true comme éditeur au lieu de /usr/bin/vim. Il accepte toujours tout ce que git suggère, sans y être invité.

4
joeytwiddle

Pour réparer un commit:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i

où 0b1c2d3 est la validation que vous souhaitez réparer.

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

4
Sérgio

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

Cette commande crée des commits de correction, avec la magie supplémentaire qu’elle utilise git-deps pour trouver automatiquement le commit correspondant. Le flux de travail se résume donc 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 modifications étalonnées peuvent être attribuées sans ambiguïté à un commit particulier sur l'arbre de travail (entre maître et HEAD). Je trouve que c'est très souvent le cas pour le type de petites modifications pour lesquelles je l'utilise, par exemple. fautes de frappe dans les commentaires ou noms de méthodes nouvellement introduites (ou renommées). Si ce n'est pas le cas, il affichera au moins une liste des commits de candidats.

J'utilise beaucoup beaucoup dans mon flux de travail quotidien pour intégrer rapidement de petites modifications aux lignes précédemment modifiées dans les commits de ma branche en cours 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 moment maintenant que je n’ai jamais ressenti le besoin de le réécrire:

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

3
Valodim

J'ai écrit une petite fonction Shell appelée gcf pour effectuer automatiquement la validation de la correction et la base de synthèse:

$ 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 corriger 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
  fi

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

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

  echo ">> Performing rebase"
  # --autosquash requires -i, but we can avoid interaction with a dummy EDITOR
  EDITOR=true git rebase --interactive --autosquash --autostash \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

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

1
joeytwiddle

commit --fixup et rebase --autosquash sont excellents, mais ils ne font pas assez. Quand j'ai une séquence de commits A-B-C et que j'écris quelques modifications supplémentaires dans mon arbre de travail qui appartiennent à un ou plusieurs de ces commits existants, je dois consulter manuellement l'historique, décider quelles modifications appartiennent à quelles commissions, les organiser et créer le fixup! commet. 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 ne fait que ça.

Pour chaque bloc dans git diff, le script utilise git blame pour rechercher le dernier engagement passé sur les lignes appropriées et appelle git commit --fixup pour écrire les validations fixup! appropriées, en effectuant essentiellement la même chose que je faisais manuellement auparavant.

Si vous le trouvez utile, n'hésitez pas à l'améliorer et à le parcourir. Peut-être qu'un jour nous aurons une telle fonctionnalité dans git convenable. J'aimerais voir un outil capable de comprendre comment un conflit de fusion devrait être résolu lorsqu'il a été introduit par une base interactive.

1
Oktalist

Je ne suis pas au courant d'une manière automatisée, mais voici une solution qui pourrait être plus facile à humainiser:

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
0
Tobias Kienzler

Vous pouvez créer un fixup 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.txt mais que vous ne voulez pas les mettre dans un nouveau commit, git fixup-file myfile.txt créera un fixup! pour le commit où myfile.txt a été modifié pour la dernière fois, puis rebase --autosquash.

0
Alvaro