🥊

lefthook で特定フォルダの変更を検知して pre-push フックを実行する際にハマった件

2025/03/04に公開

はじめに

lefthook の小ネタです。
所属しているチームで、lefthook で pre-commit 時に静的解析や単体テストを動かしていました。
同じノリで、push 時にも動かすフックを定義したのですが、少しハマったので、記事にしようと思います。

概略

lefthook で、あるディレクトリ配下を変更したコミットを push する際に動かす hook を定義したかった。
そのために、glob にて指定したディレクトリパスに設定したが、うまく動かなかった。
files に files: git diff --name-only HEAD origin/main 設定するとうまくいった。

実践

以下のディレクトリ構成を例に考えます。
※ モノレポでマイクロサービスを運用していて、特定のサービスに対して lefthook を動かしたいイメージ

.
├── lefthook.yml
├── serviceA
│   └── A
│       └── hoge.txt
└── serviceB
    └── B
        └── fuga.txt

lefthook は以下の通りです。

pre-push:
  commands:
    testA:
      root: serviceA/A
      glob: serviceA/A/*.txt
      run: echo "run testA!"
    testB:
      root: serviceB/B
      glob: serviceB/B/*.txt
      run: echo "run testB!"

glob
You can set a glob to filter files for your command. This is only used if you use a file template in run option or provide your custom files command.
https://lefthook.dev/configuration/glob.html#glob

glob は上記の通り hook を動作させるファイルをフィルタリングできます。
各サービスに対応したパスを glob に設定すれば serviceA 配下の変更には、serviceA で指定したコマンドが走ると思っていました。

実際に serviceA/A/hoge.txt のみ編集したコミットを push してみます。

$ echo -e "hoge" >> serviceA/A/hoge.txt
$ git diff                                                                   
diff --git a/serviceA/A/hoge.txt b/serviceA/A/hoge.txt
index e69de29..2262de0 100644
--- a/serviceA/A/hoge.txt
+++ b/serviceA/A/hoge.txt
@@ -0,0 +1 @@
+hoge
$ git add .                                                                  $ git commit -m "testA"                                                      
[main 6c0c72e] testA
 1 file changed, 1 insertion(+)
$ git push origin main                                                       
╭──────────────────────────────────────╮
│ 🥊 lefthook v1.10.11  hook: pre-push │
╰──────────────────────────────────────╯
┃  testA ❯ 

run testA!

┃  testB ❯ 

run testB!                          
  ────────────────────────────────────
summary: (done in 0.07 seconds)       
✔️ testA
✔️ testB

testA, testB 両方フックが動いていますね。本来は testA だけ実行されて欲しかったのに何故だ。という話です。

原因

実際に lefthook がどのように動いているか確認してみます。

$ lefthook run pre-push --verbose[lefthook] cmd:    [git version][lefthook] stdout: git version 2.39.3 (Apple Git-146)

~~省略~~
                                                           
╭──────────────────────────────────────╮
│ 🥊 lefthook v1.10.11  hook: pre-push │
╰──────────────────────────────────────╯
│ [lefthook] cmd:    [git diff --name-only HEAD @{push}][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] error:  exit status 128[lefthook] stdout: 
│ [lefthook] stderr: fatal: no upstream configured for branch 'main'[lefthook] cmd:    [git branch --remotes][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout:   origin/feat/hoge
  origin/main                        
                                     
│ [lefthook] cmd:    [git diff --name-only HEAD 4b825dc642cb6eb9a060e54bf8d69288fbee4904][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout: lefthook.yml
serviceA/A/hoge.txt            
serviceB/B/fuga.txt            
                               
│ [lefthook] files before filters:                      
[lefthook.yml serviceA/A/hoge.txt serviceB/B/fuga.txt][lefthook] files after filters:
[.//hoge.txt]                  
┃  testA ❯ 

run testA![lefthook] cmd:    [git diff --name-only HEAD @{push}][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] error:  exit status 128[lefthook] stdout: 
│ [lefthook] stderr: fatal: no upstream configured for branch 'main'[lefthook] cmd:    [git diff --name-only HEAD 4b825dc642cb6eb9a060e54bf8d69288fbee4904][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout: lefthook.yml
serviceA/A/hoge.txt            
serviceB/B/fuga.txt            
                               
│ [lefthook] files before filters:                      
[lefthook.yml serviceA/A/hoge.txt serviceB/B/fuga.txt][lefthook] files after filters:
[.//fuga.txt]                  
┃  testB ❯ 

run testB!

                                      
  ────────────────────────────────────
summary: (done in 0.10 seconds)       
✔️ testA
✔️ testB

│ [lefthook] cmd: [git diff --name-only HEAD @{push}]
│ [lefthook] dir: /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] error: exit status 128
│ [lefthook] stdout:
│ [lefthook] stderr: fatal: no upstream configured for branch 'main'

プッシュするファイルの差分を取得するために、打っているコマンドが失敗しているようです。
これは、リモート追跡ブランチが設定されていない場合にでるエラーで、git push --set-upstream origin main を実行すれば解決するやつです。
ただ、私は普段から push origin main のようにブランチを指定して push しているので、リモート追跡ブランチは設定されないことがよくあります。

│ [lefthook] cmd: [git diff --name-only HEAD 4b825dc642cb6eb9a060e54bf8d69288fbee4904]
│ [lefthook] dir: /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout: lefthook.yml
serviceA/A/hoge.txt
serviceB/B/fuga.txt

先ほどのコマンドが失敗したため、lefthook は 4b825dc642cb6eb9a060e54bf8d69288fbee4904 との差分を見ているようです。
このハッシュ値は Git の空のツリーを表しているので、当然今あるすべてのファイルが差分として表示されます。

はい。hoge.txt しか変更していないのに、すべてのファイルが差分として表示されてしまっているのが原因でした。

│ [lefthook] files before filters:
[lefthook.yml serviceA/A/hoge.txt serviceB/B/fuga.txt]
│ [lefthook] files after filters:
[.//hoge.txt]
┃ testA ❯

このあとに glob の設定によってフィルターをしていますが、すべてのファイルが対象なので、すべてのフックでヒットすることは自明です。

解決策

[lefthook] cmd: [git diff --name-only HEAD @{push}]

このコマンドが実行されて失敗しているのを回避すれば解決できそうです。

files (global)
A custom git command for files to be referenced in {files} template. See run and files.
If the result of this command is empty, the execution of commands will be skipped.
https://lefthook.dev/configuration/files-global.html

そのために、files を定義して、pre-push が参照するファイルを指定します。

git diff                                                                     
diff --git a/lefthook.yml b/lefthook.yml
index b73731f..768fcfe 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -1,5 +1,7 @@
 
 pre-push:
+  files: git diff --name-only HEAD origin/main
+
   commands:
     testA:
       root: serviceA/A

main ブランチが作られている前提として、origin/main ブランチと比較した差分を対象とするように設定しました。
もう一度、hoge.txt のみ変更した commit を作って、以下のコマンドを実行します。

$ lefthook run pre-push --verbose[lefthook] cmd:    [git version][lefthook] stdout: git version 2.39.3 (Apple Git-146)

~~省略~~
                                                           
╭──────────────────────────────────────╮
│ 🥊 lefthook v1.10.11  hook: pre-push │
╰──────────────────────────────────────╯
│ [lefthook] cmd:    [sh -c git diff --name-only HEAD origin/main][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks/serviceA/A
│ [lefthook] stdout: serviceA/A/hoge.txt 
                               
│ [lefthook] files before filters:  
[serviceA/A/hoge.txt][lefthook] files after filters:
[.//hoge.txt]                  
┃  testA ❯ 

run testA![lefthook] cmd:    [sh -c git diff --name-only HEAD origin/main][lefthook] dir:    /Users/g_kawano/Sandbox/lefthooks/serviceB/B
│ [lefthook] stdout: serviceA/A/hoge.txt            
                               
│ [lefthook] files before filters:  
[serviceA/A/hoge.txt][lefthook] files after filters:
[]                             
│  testB (skip) no files for inspection
                                      
  ────────────────────────────────────
summary: (done in 0.12 seconds)       
✔️ testA

ただしくフィルターされて serviceA で定義したフックのみ動くことが確認できました!

後記

│ [lefthook] cmd: [git diff --name-only HEAD @{push}]
│ [lefthook] dir: /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] error: exit status 128
│ [lefthook] stdout:
│ [lefthook] stderr: fatal: no upstream configured for branch 'main'
│ [lefthook] cmd: [git branch --remotes]
│ [lefthook] dir: /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout: origin/feat/hoge
origin/main
│ [lefthook] cmd: [git diff --name-only HEAD 4b825dc642cb6eb9a060e54bf8d69288fbee4904]
│ [lefthook] dir: /Users/g_kawano/Sandbox/lefthooks
│ [lefthook] stdout: lefthook.yml
serviceA/A/hoge.txt
serviceB/B/fuga.txt
│ [lefthook] files before filters:
[lefthook.yml serviceA/A/hoge.txt serviceB/B/fuga.txt]
│ [lefthook] files after filters:
[.//hoge.txt]

あとから分かったのですが、以前として、files を設定しても上記の実行ログが出力されていました。
おそらくですが、pre-push を設定すると必ず実行されるが、現状うまく動いているため、files を定義するとそちらを優先した結果になるのかなーと推測しています。

Discussion