1

pre-commitコミットの実行を許可する前に、フックでプログラムをコンパイルし、すべての自動テストを実行したいと思います。問題は、通常、コミット中に作業コピーがクリーンでないことです。それらは、コミットしたくないステージングされたファイルでも追跡されていないファイルでもありません。時には、現在ステージングされているものとは関係のないいくつかのファイルのみをコミットするように明示的に指定することさえあります。

もちろん、コミットされる変更のみをコンパイルしてテストし、他の変更は無視したいと思います。それには3つのステップがあります:

  1. コミットされないすべての変更を削除します。
  2. テストを実行します。
  3. すべての変更を、最初のステップの前の状態に正確に復元します。

最初のステップは、 を実行することで達成できますgit stash push --include-untracked --keep-index。stash エントリは、3 番目のステップにも役立ちます。ただし、ステージングされていないファイルの明示的なリストをコミットするときに何をすべきかわかりません。

(2 番目のステップは実際には問題の一部ではありません。)

3 番目のステップは、理論的にはコマンドを使用して実行できますgit stash pop --indexが、このコマンドは、一部のファイルがステージングされ、再度ステージングせずにさらに変更された場合に競合が発生しやすいようです。

このスクリプトは、さまざまなコーナー ケースをカバーするいくつかのファイルと変更を含むリポジトリを作成します。

#!/usr/bin/env sh

set -e -x

git init test-repo
cd test-repo
git config user.email "you@example.com"
git config user.name "Your Name"

echo foo >old-file-unchanged
echo foo >old-file-changed-staged
echo foo >old-file-changed-unstaged
echo foo >old-file-changed-both
git add .
git commit -m 'previous commit'

echo bar >old-file-changed-staged
echo bar >old-file-changed-both
echo bar >new-file-staged
echo bar >new-file-both
git add .
echo baz >old-file-changed-unstaged
echo baz >old-file-changed-both
echo baz >new-file-both
echo baz >untracked-file
4

1 に答える 1

2

あなたは実際には正しい解決策にかなり近かった.

(この回答では、「ステージ」の代わりに「キャッシュ」という単語を使用します。後者は「スタッシュ」にあまりにも似ているためです。)

実際、stash を使用したトリックは、キャッシュされていないファイルをコミットする場合でも機能します。これは、Git がフックの実行中にキャッシュを変更し、常に正しいファイルが含まれているためです。コマンドgit statuspre-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"
于 2021-09-08T21:52:43.600 に答える