web-dev-qa-db-ja.com

過去のコミットを簡単に修正するにはどうすればよいですか?

gitの過去のコミットで1つのファイルを修正する と読みましたが、残念ながら受け入れられた解決策はコミットを「再配列」しますが、これは私が望んでいるものではありません。だからここに私の質問があります:

時々、(無関係な)機能の作業中にコードのバグに気付きます。クイック git blameは、そのバグが数回前にコミットされたことを明らかにします(私はかなりコミットしているので、通常、バグを導入したのは最新のコミットではありません)。この時点で、私は通常これを行います:

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

ただし、これは非常に頻繁に発生するため、上記のシーケンスは煩わしくなります。特に「インタラクティブリベース」は退屈です。上記のシーケンスへのショートカットはありますか?これにより、過去の任意のコミットを段階的な変更で修正できますか?私はこれが歴史を変えることを完全に承知していますが、私は間違いを頻繁に犯しているので、

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.

配管ツールなどを使用してコミットを書き換えることができるスマートなスクリプトでしょうか?

98
Frerich Raabe

更新された回答

しばらく前に、--fixupに適切なログメッセージでコミットを構築するために使用できる新しいgit commit引数がgit rebase --interactive --autosquashに追加されました。したがって、過去のコミットを修正する最も簡単な方法は次のとおりです。

$ 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

元の回答

ここに少しのPython私が元の質問でこのgit fixupロジックを実装するスクリプトを書いたスクリプトがあります。指定されたコミット。

注意:このスクリプトはWindows固有です。 git.exeを探し、setを使用してGIT_EDITOR環境変数を設定します。他のオペレーティングシステムの必要に応じて、これを調整します。

このスクリプトを使用して、「壊れたソースの修正、ステージの修正、git fixupの実行」ワークフローを正確に実装できます。

#!/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)
146
Frerich Raabe

私がやることは:

 git add ...#修正を追加します。
 git commit#コミットされましたが、間違った場所にあります。
 git rebase -i HEAD〜5#最後の5つのコミットを調べますリベース。

エディターが開き、直近の5つのコミットのリストが表示されます。変化する:

 pick 08e833c適切な変更1。
 pick 9134ac9適切な変更2。
 pick 5adda55悪い変更!
 pick 400bce4適切な変更3。
 pick 2bc82n1悪い変更の修正。

...に:

ピック08e833c良い変更1。
ピック9134ac9良い変更2。
ピック5adda55悪い変更!
f 2bc82n1悪い変更の修正。 #上に移動し、「fixup」の「pick」を「f」に変更します。
 pick 400bce4 Good change 3。

エディターを保存して終了すると、修正は所属するコミットに戻されます。

それを数回行った後、あなたはあなたの睡眠中に数秒でそれをします。インタラクティブなリベースは、gitで私を本当に売り込んだ機能です。これなどに非常に便利です...

29
Kris Jenkins

パーティーに少し遅れましたが、著者が想像したとおりに機能するソリューションを以下に示します。

これを.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' -"

使用例:

git add -p
git fixup HEAD~5

ただし、ステージングされていない変更がある場合は、リベースの前にそれらを隠しておく必要があります。

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

警告を出す代わりに、エイリアスを自動的にstashに変更できます。ただし、修正がきれいに適用されない場合は、競合を修正した後に手動でスタッシュをポップする必要があります。保存とポップの両方を手動で行うと、一貫性が増し、混乱が少なくなります。

18
dschlyter

1つのコミットを修正するには:

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

ここで、a0b1c2d3は修正するコミットです。

注:git rebase --autosquashは-iなしでは機能しませんが、-iを使用すると機能しますが、これは奇妙です。

8
Sérgio

UPDATE:スクリプトのよりクリーンなバージョンがここにあります: https://github.com/deiwin/git-dotfiles/ blob/docs/bin/git-fixup

似たようなものを探していました。このPythonスクリプトはあまりにも複雑に思えますが、そのため私は自分の解決策をまとめました:

まず、私のgitエイリアスはそのように見えます( here から借用):

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

これで、bash関数は非常に簡単になります。

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
}

このコードは、最初に現在のすべての変更をステージングします(ファイルを自分でステージングする場合は、この部分を削除できます)。次に、フィックスアップ(必要に応じてスカッシュも使用できます)コミットを作成します。その後、引数として指定したコミットの親に--autosquashフラグを指定して、インタラクティブなリベースを開始します。設定されたテキストエディタが開きますので、すべてが期待どおりであることを確認でき、エディタを閉じるだけでプロセスが終了します。

if [[ "$1" == HEAD* ]]部分( here から借用)が使用されます。たとえば、コミット(現在の変更を修正するコミット)参照としてHEAD〜2を使用すると、 HEADは修正コミットが作成された後に置き換えられます。同じコミットを参照するにはHEAD〜3を使用する必要があります。

6
Deiwin

「null」エディターを使用して、インタラクティブなステージを回避できます。

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

これは、/bin/trueの代わりに/usr/bin/vimをエディターとして使用します。プロンプトが表示されることなく、常にgitが提案するものはすべて受け入れます。

4
joeytwiddle

修正ワークフローについて本当に気になったのは、変更を毎回押しつぶしたいコミットを自分で見つけなければならなかったことです。これに役立つ「git fixup」コマンドを作成しました。

このコマンドは、関連するコミットを自動的に見つけるために git-deps を使用するという追加のマジックを使用して修正コミットを作成します。そのため、ワークフローは次のようになります。

# 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

これは、ステージングされた変更が作業ツリー(マスターとHEADの間)の特定のコミットに明確に起因している場合にのみ機能します。私がこれを使用するタイプの小さな変更の場合、それは非常に頻繁に当てはまることがわかります。コメントのタイプミスまたは新しく導入された(または名前が変更された)メソッドの名前。そうでない場合は、少なくともコミット候補のリストが表示されます。

毎日のワークフローでこれをalot使用して、以前に変更した行に対する小さな変更を作業ブランチのコミットにすばやく統合します。スクリプトは可能な限り美しくなく、zshで書かれていますが、それを書き換える必要性を感じなかったので、しばらくの間十分に仕事をしてきました。

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

3
Valodim

修正コミットとリベースを自動的に実行するために、gcfという小さなシェル関数を作成しました。

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

たとえば、gcf HEAD~~を使用して、最新のコミットの前に2番目のコミットにパッチを適用できます。

関数 です。 ~/.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 \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

--autostashを使用して、必要に応じてコミットされていない変更を隠してポップします。

--autosquashには--interactiveリベースが必要ですが、ダミーのEDITORを使用して相互作用を回避しています。

1
joeytwiddle

commit --fixuprebase --autosquashは優れていますが、十分ではありません。一連のコミットA-B-Cがあり、1つ以上の既存のコミットに属する変更を作業ツリーにさらに書き込むと、履歴を手動で確認し、どの変更がどのコミットに属するかを判断する必要があります。それらをステージングし、fixup!コミットを作成します。しかし、gitはすでに十分な情報にアクセスできるので、すべてのことができます。そのために Perlスクリプト を作成しました。

git diffの各ハンクに対して、スクリプトはgit blameを使用して、関連する行に最後に触れたコミットを見つけ、git commit --fixupを呼び出して適切なfixup!コミットを書き込み、本質的に同じことを行います前に手動でやっていたこと。

便利だと思ったら、お気軽に改善して反復してください。いつか、適切なgitでそのような機能を利用できるようになるでしょう。インタラクティブなリベースによって導入されたマージの競合をどのように解決すべきかを理解できるツールが欲しいです。

1
Oktalist

私は自動化された方法を知っていませんが、人間がボット化する方が簡単かもしれないソリューションがあります:

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

このエイリアスを使用して、特定のファイルに対してfixupを作成できます。

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

myfile.txtにいくつかの変更を加えたが、新しいコミットに入れたくない場合、git fixup-file myfile.txtfixup!が最後にあったコミットのmyfile.txtを作成します変更すると、rebase --autosquashになります。

0
Alvaro