Add wildcard (*) support for include_ext to watch all file extensions (#833)

* Initial plan

* Add wildcard (*) support for include_ext to watch all file extensions

Co-authored-by: xiantang <34479567+xiantang@users.noreply.github.com>

* Add test coverage for isBinPath empty binPath case

Co-authored-by: xiantang <34479567+xiantang@users.noreply.github.com>

* Extract wildcard string literal to extWildcard constant

Co-authored-by: xiantang <34479567+xiantang@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: xiantang <34479567+xiantang@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-04 20:02:40 +08:00
committed by GitHub
parent a8f644ca65
commit 64a47a2839
3 changed files with 112 additions and 1 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ entrypoint = ["./tmp/main"]
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]
# Watch these filename extensions.
# Watch these filename extensions. Use ["*"] to watch all file extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
+24
View File
@@ -21,6 +21,8 @@ import (
const (
sliceCmdArgSeparator = ","
// extWildcard is used in include_ext to match all file extensions
extWildcard = "*"
)
func (e *Engine) mainLog(format string, v ...interface{}) {
@@ -172,6 +174,10 @@ func (e *Engine) checkIncludeFile(path string) bool {
func (e *Engine) isIncludeExt(path string) bool {
ext := filepath.Ext(path)
for _, v := range e.config.Build.IncludeExt {
if strings.TrimSpace(v) == extWildcard {
// Wildcard matches all files, but exclude the binary file
return !e.isBinPath(path)
}
if ext == "."+strings.TrimSpace(v) {
return true
}
@@ -179,6 +185,24 @@ func (e *Engine) isIncludeExt(path string) bool {
return false
}
// isBinPath checks if the given path is the binary file path
func (e *Engine) isBinPath(path string) bool {
binPath := e.config.binPath()
if binPath == "" {
return false
}
// Normalize the path for comparison
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
absBinPath, err := filepath.Abs(binPath)
if err != nil {
return false
}
return absPath == absBinPath
}
func (e *Engine) isExcludeRegex(path string) (bool, error) {
regexes, err := e.config.Build.RegexCompiled()
if err != nil {
+87
View File
@@ -398,6 +398,93 @@ func TestCheckIncludeFile(t *testing.T) {
assert.False(t, e.checkIncludeFile("."))
}
func TestIsIncludeExt(t *testing.T) {
e := Engine{
config: &Config{
Build: cfgBuild{
IncludeExt: []string{"go", "html"},
},
},
}
assert.True(t, e.isIncludeExt("main.go"))
assert.True(t, e.isIncludeExt("/path/to/file.html"))
assert.False(t, e.isIncludeExt("main.js"))
assert.False(t, e.isIncludeExt("file"))
}
func TestIsIncludeExtWildcard(t *testing.T) {
tmpDir := t.TempDir()
binPath := filepath.Join(tmpDir, "tmp", "main")
e := Engine{
config: &Config{
Root: tmpDir,
Build: cfgBuild{
IncludeExt: []string{"*"},
Entrypoint: entrypoint{binPath},
},
},
}
// Wildcard should match all file extensions
assert.True(t, e.isIncludeExt("main.go"))
assert.True(t, e.isIncludeExt("/path/to/file.html"))
assert.True(t, e.isIncludeExt("main.js"))
assert.True(t, e.isIncludeExt("file.css"))
assert.True(t, e.isIncludeExt("file")) // files without extension
assert.True(t, e.isIncludeExt("/path/noext")) // files without extension
assert.False(t, e.isIncludeExt(binPath)) // binary file should be excluded
assert.True(t, e.isIncludeExt("some/other/bin")) // other files without extension are ok
}
func TestIsIncludeExtWildcardWithSpaces(t *testing.T) {
e := Engine{
config: &Config{
Build: cfgBuild{
IncludeExt: []string{" * "},
Entrypoint: entrypoint{"/tmp/main"},
},
},
}
// Wildcard with spaces should still work
assert.True(t, e.isIncludeExt("main.go"))
assert.True(t, e.isIncludeExt("file.html"))
}
func TestIsBinPath(t *testing.T) {
tmpDir := t.TempDir()
binPath := filepath.Join(tmpDir, "tmp", "main")
e := Engine{
config: &Config{
Root: tmpDir,
Build: cfgBuild{
Entrypoint: entrypoint{binPath},
},
},
}
// Test matching path returns true
assert.True(t, e.isBinPath(binPath))
// Test non-matching paths return false
assert.False(t, e.isBinPath(filepath.Join(tmpDir, "other", "file")))
assert.False(t, e.isBinPath("unrelated.go"))
}
func TestIsBinPathEmptyBinPath(t *testing.T) {
// Test when binPath is empty (no entrypoint configured)
e := Engine{
config: &Config{
Build: cfgBuild{
Entrypoint: entrypoint{}, // empty entrypoint
},
},
}
// Should return false when binPath is empty
assert.False(t, e.isBinPath("/some/path"))
assert.False(t, e.isBinPath("main.go"))
}
func TestJoinPathRelative(t *testing.T) {
root, err := filepath.Abs("test")