あなたは実際には正しい解決策にかなり近かった.
(この回答では、「ステージ」の代わりに「キャッシュ」という単語を使用します。後者は「スタッシュ」にあまりにも似ているためです。)
実際、stash を使用したトリックは、キャッシュされていないファイルをコミットする場合でも機能します。これは、Git がフックの実行中にキャッシュを変更し、常に正しいファイルが含まれているためです。コマンドgit status
をpre-commit
フックに追加することで確認できます。
だから、あなたが使用することができますgit stash push --include-untracked --keep-index
。
スタッシュを復元するときの競合の問題も、非常に簡単に解決できます。すべての変更は既に stash にバックアップされているため、何も失うリスクはありません。現在の変更をすべて削除し、スタッシュを白紙の状態に適用するだけです。
これは 2 つのステップで実行できます。このコマンドgit reset --hard
は、追跡されたすべてのファイルを削除します。このコマンドgit clean -d --force
は、追跡されていないすべてのファイルを削除します。
git stash pop --index
その後、競合のリスクなしで実行できます。
単純なフックは次のようになります。
#!/bin/sh
set -e
git stash push --include-untracked --keep-index --quiet --message='Backed up state for the pre-commit hook (if you can see it, something went wrong)'
#TODO Tests go here
git reset --hard --quiet
git clean -d --force --quiet
git stash pop --index --quiet
exit $tests_result
分解してみましょう。
set -e
エラーが発生した場合にスクリプトがすぐに停止するようにして、それ以上の損害を与えないようにします。すべての変更のバックアップを含む stash エントリは最初に行われるため、エラーが発生した場合は手動で制御してすべてを復元できます。
git stash push --include-untracked --keep-index --quiet --message='...'
2つの目的を果たします。現在のすべての変更からバックアップを作成し、ステージングされていないすべての変更を作業ディレクトリから削除します。このフラグ--include-untracked
により、追跡されていないファイルも確実にバックアップおよび削除されます。このフラグ--keep-index
は、作業ディレクトリからのキャッシュされた変更の削除をキャンセルします (ただし、それらはまだ stash に含まれています)。
#TODO Tests go here
テストを行う場所です。ここでスクリプトを終了しないでください。それを行う前に、隠した変更を復元する必要があります。エラー コードで終了する代わりに、その値を変数に設定しますtests_result
。
git reset --hard --quiet
追跡されたすべての変更を作業ディレクトリから削除します。このフラグ--hard
は、キャッシュに何も残らず、すべてのファイルが削除されるようにします。
git clean -d --force --quiet
追跡されていないすべてのファイルを作業ディレクトリから削除します。このフラグ-d
は、ディレクトリを再帰的に削除するように Git に指示します。フラグ--force
は Git に、自分が何をしているのかを知っていることを伝え、実際にはこれらのファイルをすべて削除する必要があります。
git stash pop --index --quiet
最新のスタッシュに保存されたすべての変更を復元し、それを削除します。フラグ--index
は、どのファイルがキャッシュされ、どのファイルがキャッシュされていないかを混同していないことを確認するように指示します。
この方法の欠点
この方法は半堅牢であり、単純なユースケースには十分です。ただし、実際の使用中に何かが壊れる可能性のある非常に多くのまれなケースです。
git stash push
フラグでのみ追加されたファイルの操作を拒否します--intent-to-add
。それがなぜなのかはわかりませんし、それを修正する方法も見つかりませんでした。フラグなしでファイルを追加するか、少なくとも空のファイルとして追加し、キャッシュされていないコンテンツのみを残すことで、問題を回避できます。
Git はファイルのみを追跡し、ディレクトリは追跡しません。ただし、このコマンドgit clean
はディレクトリを削除できます。その結果、スクリプトは空のディレクトリを削除します (無視されない限り)。
.gitignore
最後のコミット以降に追加されたファイルは削除されます。これは機能だと思いますが、防止したい場合はgit reset
との順序を逆にすることで可能git clean
です。.gitignore
これは、現在のコミットに含まれている場合にのみ機能することに注意してください。
git stash push
変更がない場合は新しい stash を作成しませんが、それでも 0 を返します。メッセージを変更するなどの変更なしでコミットを処理するに--amend
は、stash が実際に作成されたかどうかを確認し、作成された場合にのみポップするコードを追加する必要があります。
Git stash は現在のマージに関する情報を削除するように見えるため、マージ コミットでこのコードを使用すると、それが壊れます。.git/MERGE_*
それを防ぐには、ファイルをバックアップして、スタッシュをポップした後に復元する必要があります。
堅牢なソリューション
私は、この方法の問題点のほとんどを解決することができました (プロセスでコードがずっと長くなります)。
残っている唯一の問題は、空のディレクトリと無視されたファイルを削除することです (上記のとおり)。これらは、時間をかけてバイパスしようとするほど深刻な問題ではないと思います。(でも、それは実行可能です。)
#!/bin/sh
backup_dir='./pre-commit-hook-backup'
if [ -e "$backup_dir" ]
then
printf '"%s" already exists!\n' "$backup_dir" 1>&2
exit 1
fi
intent_to_add_list_file="$backup_dir/intent-to-add"
remove_intent_to_add() {
git diff --name-only --diff-filter=A | tr '\n' '\0' >"$intent_to_add_list_file"
xargs -0 -r -- git reset --quiet -- <"$intent_to_add_list_file"
}
readd_intent_to_add() {
xargs -0 -r -- git add --intent-to-add --force -- <"$intent_to_add_list_file"
}
backup_merge_info() {
echo 'If you can see this, tests in the `pre-commit` hook went wrong. You need to fix this manually.' >"$backup_dir/README"
find ./.git -name 'MERGE_*' -exec cp {} "$backup_dir" \;
}
restore_merge_info() {
find "$backup_dir" -name 'MERGE_*' -exec mv {} ./.git \;
}
create_stash() {
git stash push --include-untracked --keep-index --quiet --message='Backed up state for the pre-commit hook (if you can see it, something went wrong)'
}
restore_stash() {
git reset --hard --quiet
git clean -d --force --quiet
git stash pop --index --quiet
}
run_tests() (
set +e
printf 'TODO: Put your tests here.\n' 1>&2
echo $?
)
set -e
mkdir "$backup_dir"
remove_intent_to_add
backup_merge_info
create_stash
tests_result=$(run_tests)
restore_stash
restore_merge_info
readd_intent_to_add
rm -r "$backup_dir"
exit "$tests_result"