From 4534eb11bc71f16ba25fca262a4a25b96d553918 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Tue, 23 Dec 2025 22:27:18 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat(cmd/gf):=20=E4=B8=BA`gf=20run`?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=B7=BB=E5=8A=A0=E5=BF=BD=E7=95=A5=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81`-i`?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=BF=87=E6=BB=A4=E4=B8=8D=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E7=9B=91=E5=90=AC=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 245 ++++++++++++++---- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 235 +++++++++++++++++ .../cmd/testdata/fix/fix25_content.go | 3 - 3 files changed, 432 insertions(+), 51 deletions(-) create mode 100644 cmd/gf/internal/cmd/cmd_z_unit_run_test.go diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 224972d8901..c45ccad9670 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -34,11 +34,12 @@ type cRun struct { } type cRunApp struct { - File string // Go run file name. - Path string // Directory storing built binary. - Options string // Extra "go run" options. - Args string // Custom arguments. - WatchPaths []string // Watch paths for live reload. + File string // Go run file name. + Path string // Directory storing built binary. + Options string // Extra "go run" options. + Args string // Custom arguments. + WatchPaths []string // Watch paths for live reload. + IgnorePaths []string // Ignore paths for file watching. } const ( @@ -48,43 +49,50 @@ const ( gf run main.go gf run main.go --args "server -p 8080" gf run main.go -mod=vendor -gf run main.go -w "manifest/config/*.yaml" +gf run main.go -w internal/,api/ +gf run main.go -i vendor/*,*.pb.go,node_modules/* +gf run main.go -w app/,manifest/ -i .git/*,.github/*,dist/* +gf run main.go -p ./bin -w . -i "test/*,tmp/*,*.log" +gf run main.go -w service/,model/ -i "frontend/*,web/*,build/*" ` cRunDc = ` The "run" command is used for running go codes with hot-compiled-like feature, which compiles and runs the go codes asynchronously when codes change. ` - cRunFileBrief = `building file path.` - cRunPathBrief = `output directory path for built binary file. it's "./" in default` - cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` - cRunArgsBrief = `custom arguments for your process` - cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml"` + cRunFileBrief = `building file path.` + cRunPathBrief = `output directory path for built binary file. it's "./" in default` + cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` + cRunArgsBrief = `custom arguments for your process` + cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal/,api/"` + cRunIgnorePathsBrief = `ignore paths for file watching, separated by ",". i.e. "vendor/*,*.pb.go,node_modules/*,.git/*"` ) var process *gproc.Process func init() { gtag.Sets(g.MapStrStr{ - `cRunUsage`: cRunUsage, - `cRunBrief`: cRunBrief, - `cRunEg`: cRunEg, - `cRunDc`: cRunDc, - `cRunFileBrief`: cRunFileBrief, - `cRunPathBrief`: cRunPathBrief, - `cRunExtraBrief`: cRunExtraBrief, - `cRunArgsBrief`: cRunArgsBrief, - `cRunWatchPathsBrief`: cRunWatchPathsBrief, + `cRunUsage`: cRunUsage, + `cRunBrief`: cRunBrief, + `cRunEg`: cRunEg, + `cRunDc`: cRunDc, + `cRunFileBrief`: cRunFileBrief, + `cRunPathBrief`: cRunPathBrief, + `cRunExtraBrief`: cRunExtraBrief, + `cRunArgsBrief`: cRunArgsBrief, + `cRunWatchPathsBrief`: cRunWatchPathsBrief, + `cRunIgnorePathsBrief`: cRunIgnorePathsBrief, }) } type ( cRunInput struct { - g.Meta `name:"run" config:"gfcli.run"` - File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` - Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` - Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` - Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` - WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` + g.Meta `name:"run" config:"gfcli.run"` + File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` + Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` + Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` + Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` + WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` + IgnorePaths []string `name:"ignorePaths" short:"i" brief:"{cRunIgnorePathsBrief}"` } cRunOutput struct{} ) @@ -106,12 +114,18 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err mlog.Printf("watchPaths: %v", in.WatchPaths) } + if len(in.IgnorePaths) == 1 { + in.IgnorePaths = strings.Split(in.IgnorePaths[0], ",") + mlog.Printf("ignorePaths: %v", in.IgnorePaths) + } + app := &cRunApp{ - File: in.File, - Path: filepath.FromSlash(in.Path), - Options: in.Extra, - Args: in.Args, - WatchPaths: in.WatchPaths, + File: in.File, + Path: filepath.FromSlash(in.Path), + Options: in.Extra, + Args: in.Args, + WatchPaths: in.WatchPaths, + IgnorePaths: in.IgnorePaths, } dirty := gtype.NewBool() @@ -121,6 +135,7 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err return } + // Check if the file extension is 'go'. if gfile.ExtName(event.Path) != "go" { return } @@ -138,15 +153,10 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err }) } - if len(app.WatchPaths) > 0 { - for _, path := range app.WatchPaths { - _, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc) - if err != nil { - mlog.Fatal(err) - } - } - } else { - _, err = gfsnotify.Add(gfile.RealPath("."), callbackFunc) + // Get all paths to watch after filtering. + watchPaths := app.getWatchPaths() + for _, path := range watchPaths { + _, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc) if err != nil { mlog.Fatal(err) } @@ -263,21 +273,160 @@ func (app *cRunApp) genOutputPath() (outputPath string) { return filepath.FromSlash(outputPath) } -func matchWatchPaths(watchPaths []string, eventPath string) bool { - for _, path := range watchPaths { - absPath, err := filepath.Abs(path) - if err != nil { - mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) +// getWatchPaths returns all paths to watch after filtering with ignore patterns. +func (app *cRunApp) getWatchPaths() []string { + // Collect all root paths to scan. + roots := []string{"."} + if len(app.WatchPaths) > 0 { + roots = app.WatchPaths + } + + // Scan all paths and filter with ignore patterns. + var watchPaths []string + seen := make(map[string]bool) + + for _, root := range roots { + absRoot := gfile.RealPath(root) + if absRoot == "" { + mlog.Printf("watch path '%s' not found, skipping", root) continue } - matched, err := filepath.Match(absPath, eventPath) + + // Check if the root itself should be ignored. + if app.shouldIgnorePath(absRoot) { + mlog.Printf("ignoring path: %s", absRoot) + continue + } + + // Scan directory recursively with custom filtering to avoid scanning ignored directories. + files, err := app.scanDirWithFilter(absRoot, "*", true) if err != nil { - mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) + mlog.Printf("scan directory '%s' error: %s", absRoot, err.Error()) + continue + } + + // Filter files with ignore patterns. + for _, file := range files { + if seen[file] { + continue + } + seen[file] = true + + if !app.shouldIgnorePath(file) { + watchPaths = append(watchPaths, file) + } else { + mlog.Printf("ignoring path: %s", file) + } + } + } + + if len(watchPaths) == 0 { + mlog.Printf("no paths to watch after filtering, watching current directory") + return []string{"."} + } + + mlog.Printf("watching %d paths", len(watchPaths)) + // for _, v := range watchPaths { + // mlog.Printf("path: %s", v) + // } + return watchPaths +} + +// scanDirWithFilter scans directory recursively but skips ignored directories to improve performance. +func (app *cRunApp) scanDirWithFilter(path string, pattern string, recursive bool) ([]string, error) { + if !recursive { + return gfile.ScanDir(path, pattern, false) + } + + var result []string + files, err := gfile.ScanDir(path, pattern, false) + if err != nil { + return nil, err + } + result = append(result, files...) + + // Get subdirectories + subDirs, err := gfile.ScanDir(path, "*", false) + if err != nil { + return nil, err + } + + for _, subDir := range subDirs { + if !gfile.IsDir(subDir) { continue } - if matched { + + // Check if this directory should be ignored + if app.shouldIgnorePath(subDir) { + mlog.Printf("skipping ignored directory: %s", subDir) + continue + } + + // Recursively scan this directory + subFiles, err := app.scanDirWithFilter(subDir, pattern, true) + if err != nil { + return nil, err + } + result = append(result, subFiles...) + } + + return result, nil +} + +// shouldIgnorePath checks if the given file path should be ignored based on ignore patterns. +func (app *cRunApp) shouldIgnorePath(path string) bool { + if len(app.IgnorePaths) == 0 { + return false + } + + absPath, err := filepath.Abs(path) + if err != nil { + mlog.Printf("convert path to absolute error: %s", err.Error()) + return false + } + + // Get the file name for pattern matching. + fileName := filepath.Base(path) + + for _, pattern := range app.IgnorePaths { + // Normalize the pattern to handle "./" or "../" prefixes. + normalizedPattern := filepath.Clean(pattern) + + // Match against file name. + if matched, _ := filepath.Match(normalizedPattern, fileName); matched { return true } + + // Match against full path. + if matched, _ := filepath.Match(normalizedPattern, absPath); matched { + return true + } + + // Match against relative path from the current working directory. + if cwd, err := os.Getwd(); err == nil { + if rel, err := filepath.Rel(cwd, absPath); err == nil { + // Match against relative path. + if matched, _ := filepath.Match(normalizedPattern, rel); matched { + return true + } + + // Check if the path starts with a directory pattern (e.g., "vendor/", "node_modules/") + if strings.Contains(normalizedPattern, string(filepath.Separator)) { + if strings.HasPrefix(rel, normalizedPattern) { + return true + } + } + + // Check if any part of the path matches the pattern. + pathParts := strings.Split(rel, string(filepath.Separator)) + for _, part := range pathParts { + if matched, _ := filepath.Match(normalizedPattern, part); matched { + return true + } + } + } + } } + return false } diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go new file mode 100644 index 00000000000..f9f4268fe2c --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -0,0 +1,235 @@ +// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package cmd + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +func Test_cRunApp_shouldIgnorePath_CommonPatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{".git", "*.js", "node_modules", "vendor", "*.pb.go"}, + } + + // Test cases for common development patterns + testCases := []struct { + path string + expected bool + desc string + }{ + // Git related files + {".git", true, "git directory"}, + {".git/config", true, "git config file"}, + {".gitignore", false, "gitignore file should not be ignored"}, + + // JavaScript files + {"main.js", true, "js file"}, + {"src/utils.js", true, "js file in subdirectory"}, + {"test.ts", false, "ts file should not be ignored"}, + + // Node modules + {"node_modules", true, "node_modules directory"}, + {"node_modules/react", true, "react in node_modules"}, + {"node_modules/.bin", true, "bin in node_modules"}, + + // Vendor directory + {"vendor", true, "vendor directory"}, + {"vendor/package", true, "package in vendor"}, + + // Proto files + {"api.pb.go", true, "pb.go file"}, + {"test.pb.go", true, "test pb.go file"}, + {"proto.go", false, "proto.go without pb"}, + + // Normal Go files that should not be ignored + {"main.go", false, "main go file"}, + {"internal/utils.go", false, "internal go file"}, + {"cmd/server/main.go", false, "server go file"}, + {"pkg/model/user.go", false, "model go file"}, + } + + for _, tc := range testCases { + result := app.shouldIgnorePath(tc.path) + if result != tc.expected { + t.Errorf("path: %s, desc: %s, expected: %v, got: %v", tc.path, tc.desc, tc.expected, result) + } + } + }) +} + +func Test_cRunApp_shouldIgnorePath_EdgeCases(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{"test_*", "*.tmp", "backup"}, + } + + testCases := []struct { + path string + expected bool + }{ + // Pattern matching with wildcards + {"test_main.go", true}, + {"test_utils.go", true}, + {"main_test.go", false}, // Different pattern + {"test.go", false}, // No underscore + + // Temporary files + {"file.tmp", true}, + {"temp.tmp", true}, + {"tmp", false}, // Directory name, not file extension + + // Backup directory + {"backup", true}, + {"backup/file.go", true}, + {"backup-old", false}, // Different name + } + + for _, tc := range testCases { + result := app.shouldIgnorePath(tc.path) + t.Assert(result, tc.expected) + } + }) +} + +func Test_cRunApp_shouldIgnorePath_EmptyPatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{}, // Empty list + } + + // All paths should return false when no patterns are set + testPaths := []string{ + "main.go", + "node_modules/react", + ".git/config", + "vendor/package", + "test.js", + } + + for _, path := range testPaths { + result := app.shouldIgnorePath(path) + t.Assert(result, false) + } + }) +} + +func Test_cRunApp_shouldIgnorePath_ComplexPatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{"build/*", "dist/*", "*.log", "cache"}, + } + + testCases := []struct { + path string + expected bool + }{ + // Build and dist directories + {"build", true}, + {"build/main", true}, + {"dist", true}, + {"dist/app", true}, + {"builds", false}, // Different name + + // Log files + {"app.log", true}, + {"error.log", true}, + {"log.txt", false}, // Different extension + + // Cache directory + {"cache", true}, + {"cache/file", true}, + {".cache", false}, // Different name + } + + for _, tc := range testCases { + t.Log(tc.path) + result := app.shouldIgnorePath(tc.path) + t.Assert(result, tc.expected) + } + }) +} + +func Test_cRunApp_getWatchPaths_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{"*.js", ".git"}, + WatchPaths: []string{"src", "pkg"}, + } + watchPaths := app.getWatchPaths() + + // Should return at least the current directory and specified watch paths + t.AssertGT(len(watchPaths), 0) + t.Assert(watchPaths[0], ".") + + // Should contain specified watch paths (if they exist) + // Note: These assertions depend on actual directory structure + // For testing purposes, we just verify the function returns valid paths + for _, path := range watchPaths { + if path == "src" || path == "pkg" { + // Found one of the expected paths + break + } + } + }) +} + +func Test_cRunApp_getWatchPaths_NoWatchPaths(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{"*.js", ".git"}, + WatchPaths: []string{}, // Empty watch paths + } + + watchPaths := app.getWatchPaths() + + // Should return at least the current directory + t.AssertGT(len(watchPaths), 0) + }) +} + +func Test_cRunApp_shouldIgnorePath_LogPatternFix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + IgnorePaths: []string{"*.log"}, + } + + testCases := []struct { + path string + expected bool + desc string + }{ + // Log files should be ignored + {"app.log", true, "log file"}, + {"error.log", true, "error log file"}, + {"logs/app.log", true, "log file in logs directory"}, + + // Go files should NOT be ignored (this was the bug) + {"main.go", false, "main go file"}, + {"app/main.go", false, "go file in app directory"}, + {"internal/app/main.go", false, "nested go file"}, + + // Directories should NOT be ignored + {"app", false, "app directory"}, + {"internal", false, "internal directory"}, + {"vendor", false, "vendor directory"}, + + // Other files should NOT be ignored + {"README.md", false, "markdown file"}, + {"config.yaml", false, "yaml file"}, + } + + for _, tc := range testCases { + result := app.shouldIgnorePath(tc.path) + if result != tc.expected { + t.Errorf("path: %s, desc: %s, expected: %v, got: %v", tc.path, tc.desc, tc.expected, result) + } + } + }) +} diff --git a/cmd/gf/internal/cmd/testdata/fix/fix25_content.go b/cmd/gf/internal/cmd/testdata/fix/fix25_content.go index 934ebd2a831..9b555245850 100644 --- a/cmd/gf/internal/cmd/testdata/fix/fix25_content.go +++ b/cmd/gf/internal/cmd/testdata/fix/fix25_content.go @@ -1,13 +1,10 @@ package testdata import ( - "fmt" "testing" - "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/guid" ) From 4c8780f7bd46cdec4467b6135108b79eb5284ef4 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Wed, 24 Dec 2025 16:12:33 +0800 Subject: [PATCH 02/10] =?UTF-8?q?refactor(cmd/gf):=20=E4=BC=98=E5=8C=96`gf?= =?UTF-8?q?=20run`=E5=91=BD=E4=BB=A4=E7=9A=84=E7=9B=91=E5=90=AC=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=92=8C=E5=BF=BD=E7=95=A5=E6=A8=A1=E5=BC=8F=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 268 +++++++++------------ cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 210 ++-------------- 2 files changed, 129 insertions(+), 349 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index c45ccad9670..133b0459d13 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -34,12 +34,12 @@ type cRun struct { } type cRunApp struct { - File string // Go run file name. - Path string // Directory storing built binary. - Options string // Extra "go run" options. - Args string // Custom arguments. - WatchPaths []string // Watch paths for live reload. - IgnorePaths []string // Ignore paths for file watching. + File string // Go run file name. + Path string // Directory storing built binary. + Options string // Extra "go run" options. + Args string // Custom arguments. + WatchPaths []string // Watch paths for live reload. + IgnorePatterns []string // Custom ignore patterns. } const ( @@ -49,50 +49,47 @@ const ( gf run main.go gf run main.go --args "server -p 8080" gf run main.go -mod=vendor -gf run main.go -w internal/,api/ -gf run main.go -i vendor/*,*.pb.go,node_modules/* -gf run main.go -w app/,manifest/ -i .git/*,.github/*,dist/* -gf run main.go -p ./bin -w . -i "test/*,tmp/*,*.log" -gf run main.go -w service/,model/ -i "frontend/*,web/*,build/*" +gf run main.go -w internal,api +gf run main.go -i ".git,node_modules" ` cRunDc = ` The "run" command is used for running go codes with hot-compiled-like feature, which compiles and runs the go codes asynchronously when codes change. ` - cRunFileBrief = `building file path.` - cRunPathBrief = `output directory path for built binary file. it's "./" in default` - cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` - cRunArgsBrief = `custom arguments for your process` - cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal/,api/"` - cRunIgnorePathsBrief = `ignore paths for file watching, separated by ",". i.e. "vendor/*,*.pb.go,node_modules/*,.git/*"` + cRunFileBrief = `building file path.` + cRunPathBrief = `output directory path for built binary file. it's "./" in default` + cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` + cRunArgsBrief = `custom arguments for your process` + cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"` + cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules"` ) var process *gproc.Process func init() { gtag.Sets(g.MapStrStr{ - `cRunUsage`: cRunUsage, - `cRunBrief`: cRunBrief, - `cRunEg`: cRunEg, - `cRunDc`: cRunDc, - `cRunFileBrief`: cRunFileBrief, - `cRunPathBrief`: cRunPathBrief, - `cRunExtraBrief`: cRunExtraBrief, - `cRunArgsBrief`: cRunArgsBrief, - `cRunWatchPathsBrief`: cRunWatchPathsBrief, - `cRunIgnorePathsBrief`: cRunIgnorePathsBrief, + `cRunUsage`: cRunUsage, + `cRunBrief`: cRunBrief, + `cRunEg`: cRunEg, + `cRunDc`: cRunDc, + `cRunFileBrief`: cRunFileBrief, + `cRunPathBrief`: cRunPathBrief, + `cRunExtraBrief`: cRunExtraBrief, + `cRunArgsBrief`: cRunArgsBrief, + `cRunWatchPathsBrief`: cRunWatchPathsBrief, + `cRunIgnorePatternBrief`: cRunIgnorePatternBrief, }) } type ( cRunInput struct { - g.Meta `name:"run" config:"gfcli.run"` - File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` - Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` - Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` - Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` - WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` - IgnorePaths []string `name:"ignorePaths" short:"i" brief:"{cRunIgnorePathsBrief}"` + g.Meta `name:"run" config:"gfcli.run"` + File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` + Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` + Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` + Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` + WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` + IgnorePatterns []string `name:"ignorePatterns" short:"i" brief:"{cRunIgnorePatternBrief}"` } cRunOutput struct{} ) @@ -114,18 +111,18 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err mlog.Printf("watchPaths: %v", in.WatchPaths) } - if len(in.IgnorePaths) == 1 { - in.IgnorePaths = strings.Split(in.IgnorePaths[0], ",") - mlog.Printf("ignorePaths: %v", in.IgnorePaths) + if len(in.IgnorePatterns) == 1 { + in.IgnorePatterns = strings.Split(in.IgnorePatterns[0], ",") + mlog.Printf("ignorePatterns: %v", in.IgnorePatterns) } app := &cRunApp{ - File: in.File, - Path: filepath.FromSlash(in.Path), - Options: in.Extra, - Args: in.Args, - WatchPaths: in.WatchPaths, - IgnorePaths: in.IgnorePaths, + File: in.File, + Path: filepath.FromSlash(in.Path), + Options: in.Extra, + Args: in.Args, + WatchPaths: in.WatchPaths, + IgnorePatterns: in.IgnorePatterns, } dirty := gtype.NewBool() @@ -153,10 +150,10 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err }) } - // Get all paths to watch after filtering. + // Get directories to watch (recursive monitoring). watchPaths := app.getWatchPaths() for _, path := range watchPaths { - _, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc) + _, err = gfsnotify.Add(path, callbackFunc) if err != nil { mlog.Fatal(err) } @@ -273,160 +270,121 @@ func (app *cRunApp) genOutputPath() (outputPath string) { return filepath.FromSlash(outputPath) } -// getWatchPaths returns all paths to watch after filtering with ignore patterns. +// getWatchPaths uses BFS to find the minimal set of directories to watch. +// Rule: if a directory and all its descendants have no ignored subdirectories, watch it; +// otherwise, recurse into valid children. func (app *cRunApp) getWatchPaths() []string { - // Collect all root paths to scan. roots := []string{"."} if len(app.WatchPaths) > 0 { roots = app.WatchPaths } - // Scan all paths and filter with ignore patterns. + // Use custom ignore patterns if provided, otherwise use default. + ignorePatterns := defaultIgnorePatterns + if len(app.IgnorePatterns) > 0 { + ignorePatterns = app.IgnorePatterns + } + var watchPaths []string - seen := make(map[string]bool) + queue := make([]string, 0) + // Initialize queue with valid roots. for _, root := range roots { absRoot := gfile.RealPath(root) if absRoot == "" { mlog.Printf("watch path '%s' not found, skipping", root) continue } - - // Check if the root itself should be ignored. - if app.shouldIgnorePath(absRoot) { - mlog.Printf("ignoring path: %s", absRoot) + if isIgnoredDirName(absRoot, ignorePatterns) { continue } + queue = append(queue, absRoot) + } - // Scan directory recursively with custom filtering to avoid scanning ignored directories. - files, err := app.scanDirWithFilter(absRoot, "*", true) - if err != nil { - mlog.Printf("scan directory '%s' error: %s", absRoot, err.Error()) - continue - } + // BFS traversal. + for len(queue) > 0 { + dir := queue[0] + queue = queue[1:] - // Filter files with ignore patterns. - for _, file := range files { - if seen[file] { + // Check if this directory or any descendant contains ignored directories. + if !hasIgnoredDescendant(dir, ignorePatterns) { + // No ignored descendants, watch this directory (recursive watch covers all). + watchPaths = append(watchPaths, dir) + } else { + // Has ignored descendants, get direct children and add valid ones to queue. + entries, err := gfile.ScanDir(dir, "*", false) + if err != nil { + mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) continue } - seen[file] = true - - if !app.shouldIgnorePath(file) { - watchPaths = append(watchPaths, file) - } else { - mlog.Printf("ignoring path: %s", file) + for _, entry := range entries { + if !gfile.IsDir(entry) { + continue + } + if !isIgnoredDirName(entry, ignorePatterns) { + queue = append(queue, entry) + } } } } if len(watchPaths) == 0 { - mlog.Printf("no paths to watch after filtering, watching current directory") + mlog.Printf("no directories to watch, using current directory") return []string{"."} } - mlog.Printf("watching %d paths", len(watchPaths)) - // for _, v := range watchPaths { - // mlog.Printf("path: %s", v) - // } + mlog.Printf("watching %d directories (recursive)", len(watchPaths)) + for _, path := range watchPaths { + mlog.Debugf(" - %s", path) + } return watchPaths } -// scanDirWithFilter scans directory recursively but skips ignored directories to improve performance. -func (app *cRunApp) scanDirWithFilter(path string, pattern string, recursive bool) ([]string, error) { - if !recursive { - return gfile.ScanDir(path, pattern, false) - } - - var result []string - files, err := gfile.ScanDir(path, pattern, false) +// hasIgnoredDescendant checks if the directory or any of its descendants is ignored. +func hasIgnoredDescendant(dir string, ignorePatterns []string) bool { + entries, err := gfile.ScanDir(dir, "*", false) if err != nil { - return nil, err - } - result = append(result, files...) - - // Get subdirectories - subDirs, err := gfile.ScanDir(path, "*", false) - if err != nil { - return nil, err + return false } - for _, subDir := range subDirs { - if !gfile.IsDir(subDir) { + for _, entry := range entries { + if !gfile.IsDir(entry) { continue } - - // Check if this directory should be ignored - if app.shouldIgnorePath(subDir) { - mlog.Printf("skipping ignored directory: %s", subDir) - continue + if isIgnoredDirName(entry, ignorePatterns) { + return true } - - // Recursively scan this directory - subFiles, err := app.scanDirWithFilter(subDir, pattern, true) - if err != nil { - return nil, err + // Recursively check subdirectories. + if hasIgnoredDescendant(entry, ignorePatterns) { + return true } - result = append(result, subFiles...) } - - return result, nil + return false } -// shouldIgnorePath checks if the given file path should be ignored based on ignore patterns. -func (app *cRunApp) shouldIgnorePath(path string) bool { - if len(app.IgnorePaths) == 0 { - return false - } - - absPath, err := filepath.Abs(path) - if err != nil { - mlog.Printf("convert path to absolute error: %s", err.Error()) - return false - } - - // Get the file name for pattern matching. - fileName := filepath.Base(path) - - for _, pattern := range app.IgnorePaths { - // Normalize the pattern to handle "./" or "../" prefixes. - normalizedPattern := filepath.Clean(pattern) - - // Match against file name. - if matched, _ := filepath.Match(normalizedPattern, fileName); matched { - return true - } +// defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching. +// These directories typically contain third-party code or non-source files. +// Patterns support glob syntax: * matches any sequence of characters, ? matches single character. +var defaultIgnorePatterns = []string{ + ".git", + ".svn", + ".hg", + ".idea", + ".vscode", + "node_modules", + "vendor", + ".*", // All hidden directories + "_*", // Directories starting with underscore +} - // Match against full path. - if matched, _ := filepath.Match(normalizedPattern, absPath); matched { +// isIgnoredDirName checks if a directory name matches any ignored pattern. +// It accepts either a full path or just the directory name. +func isIgnoredDirName(name string, ignorePatterns []string) bool { + baseName := gfile.Basename(name) + for _, pattern := range ignorePatterns { + if matched, _ := filepath.Match(pattern, baseName); matched { return true } - - // Match against relative path from the current working directory. - if cwd, err := os.Getwd(); err == nil { - if rel, err := filepath.Rel(cwd, absPath); err == nil { - // Match against relative path. - if matched, _ := filepath.Match(normalizedPattern, rel); matched { - return true - } - - // Check if the path starts with a directory pattern (e.g., "vendor/", "node_modules/") - if strings.Contains(normalizedPattern, string(filepath.Separator)) { - if strings.HasPrefix(rel, normalizedPattern) { - return true - } - } - - // Check if any part of the path matches the pattern. - pathParts := strings.Split(rel, string(filepath.Separator)) - for _, part := range pathParts { - if matched, _ := filepath.Match(normalizedPattern, part); matched { - return true - } - } - } - } } - return false } diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index f9f4268fe2c..e59bdd2ecf1 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -7,229 +7,51 @@ package cmd import ( + "strings" "testing" "github.com/gogf/gf/v2/test/gtest" ) -func Test_cRunApp_shouldIgnorePath_CommonPatterns(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - app := &cRunApp{ - IgnorePaths: []string{".git", "*.js", "node_modules", "vendor", "*.pb.go"}, - } - - // Test cases for common development patterns - testCases := []struct { - path string - expected bool - desc string - }{ - // Git related files - {".git", true, "git directory"}, - {".git/config", true, "git config file"}, - {".gitignore", false, "gitignore file should not be ignored"}, - - // JavaScript files - {"main.js", true, "js file"}, - {"src/utils.js", true, "js file in subdirectory"}, - {"test.ts", false, "ts file should not be ignored"}, - - // Node modules - {"node_modules", true, "node_modules directory"}, - {"node_modules/react", true, "react in node_modules"}, - {"node_modules/.bin", true, "bin in node_modules"}, - - // Vendor directory - {"vendor", true, "vendor directory"}, - {"vendor/package", true, "package in vendor"}, - - // Proto files - {"api.pb.go", true, "pb.go file"}, - {"test.pb.go", true, "test pb.go file"}, - {"proto.go", false, "proto.go without pb"}, - - // Normal Go files that should not be ignored - {"main.go", false, "main go file"}, - {"internal/utils.go", false, "internal go file"}, - {"cmd/server/main.go", false, "server go file"}, - {"pkg/model/user.go", false, "model go file"}, - } - - for _, tc := range testCases { - result := app.shouldIgnorePath(tc.path) - if result != tc.expected { - t.Errorf("path: %s, desc: %s, expected: %v, got: %v", tc.path, tc.desc, tc.expected, result) - } - } - }) -} - -func Test_cRunApp_shouldIgnorePath_EdgeCases(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - app := &cRunApp{ - IgnorePaths: []string{"test_*", "*.tmp", "backup"}, - } - - testCases := []struct { - path string - expected bool - }{ - // Pattern matching with wildcards - {"test_main.go", true}, - {"test_utils.go", true}, - {"main_test.go", false}, // Different pattern - {"test.go", false}, // No underscore - - // Temporary files - {"file.tmp", true}, - {"temp.tmp", true}, - {"tmp", false}, // Directory name, not file extension - - // Backup directory - {"backup", true}, - {"backup/file.go", true}, - {"backup-old", false}, // Different name - } - - for _, tc := range testCases { - result := app.shouldIgnorePath(tc.path) - t.Assert(result, tc.expected) - } - }) -} - -func Test_cRunApp_shouldIgnorePath_EmptyPatterns(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - app := &cRunApp{ - IgnorePaths: []string{}, // Empty list - } - - // All paths should return false when no patterns are set - testPaths := []string{ - "main.go", - "node_modules/react", - ".git/config", - "vendor/package", - "test.js", - } - - for _, path := range testPaths { - result := app.shouldIgnorePath(path) - t.Assert(result, false) - } - }) -} - -func Test_cRunApp_shouldIgnorePath_ComplexPatterns(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - app := &cRunApp{ - IgnorePaths: []string{"build/*", "dist/*", "*.log", "cache"}, - } - - testCases := []struct { - path string - expected bool - }{ - // Build and dist directories - {"build", true}, - {"build/main", true}, - {"dist", true}, - {"dist/app", true}, - {"builds", false}, // Different name - - // Log files - {"app.log", true}, - {"error.log", true}, - {"log.txt", false}, // Different extension - - // Cache directory - {"cache", true}, - {"cache/file", true}, - {".cache", false}, // Different name - } - - for _, tc := range testCases { - t.Log(tc.path) - result := app.shouldIgnorePath(tc.path) - t.Assert(result, tc.expected) - } - }) -} - func Test_cRunApp_getWatchPaths_Basic(t *testing.T) { gtest.C(t, func(t *gtest.T) { app := &cRunApp{ - IgnorePaths: []string{"*.js", ".git"}, - WatchPaths: []string{"src", "pkg"}, + WatchPaths: []string{"."}, } watchPaths := app.getWatchPaths() - // Should return at least the current directory and specified watch paths t.AssertGT(len(watchPaths), 0) - t.Assert(watchPaths[0], ".") - - // Should contain specified watch paths (if they exist) - // Note: These assertions depend on actual directory structure - // For testing purposes, we just verify the function returns valid paths - for _, path := range watchPaths { - if path == "src" || path == "pkg" { - // Found one of the expected paths - break - } + for _, v := range watchPaths { + t.Log(v) } }) } -func Test_cRunApp_getWatchPaths_NoWatchPaths(t *testing.T) { +func Test_cRunApp_getWatchPaths_EmptyWatchPaths(t *testing.T) { gtest.C(t, func(t *gtest.T) { app := &cRunApp{ - IgnorePaths: []string{"*.js", ".git"}, - WatchPaths: []string{}, // Empty watch paths + WatchPaths: []string{}, } - watchPaths := app.getWatchPaths() - // Should return at least the current directory + // Should default to current directory "." t.AssertGT(len(watchPaths), 0) }) } -func Test_cRunApp_shouldIgnorePath_LogPatternFix(t *testing.T) { +func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) { gtest.C(t, func(t *gtest.T) { app := &cRunApp{ - IgnorePaths: []string{"*.log"}, - } - - testCases := []struct { - path string - expected bool - desc string - }{ - // Log files should be ignored - {"app.log", true, "log file"}, - {"error.log", true, "error log file"}, - {"logs/app.log", true, "log file in logs directory"}, - - // Go files should NOT be ignored (this was the bug) - {"main.go", false, "main go file"}, - {"app/main.go", false, "go file in app directory"}, - {"internal/app/main.go", false, "nested go file"}, - - // Directories should NOT be ignored - {"app", false, "app directory"}, - {"internal", false, "internal directory"}, - {"vendor", false, "vendor directory"}, - - // Other files should NOT be ignored - {"README.md", false, "markdown file"}, - {"config.yaml", false, "yaml file"}, + WatchPaths: []string{"testdata"}, + IgnorePatterns: []string{"2572"}, } + watchPaths := app.getWatchPaths() - for _, tc := range testCases { - result := app.shouldIgnorePath(tc.path) - if result != tc.expected { - t.Errorf("path: %s, desc: %s, expected: %v, got: %v", tc.path, tc.desc, tc.expected, result) - } + // Ensure the "2572" directory is not watched directly. + for _, path := range watchPaths { + t.Log("watch path:", path) + t.AssertNE(true, strings.HasSuffix(path, "2572")) } + t.AssertGT(len(watchPaths), 0) }) } From e08c29ba7abf884afb87cf7c352a43a8586f7cc7 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 25 Dec 2025 22:51:48 +0800 Subject: [PATCH 03/10] =?UTF-8?q?fix(cmd/gf):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=B8=AD`getWatchPaths`?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=9A=84=E6=96=AD=E8=A8=80=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index e59bdd2ecf1..7a2fa60b665 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -50,7 +50,7 @@ func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) { // Ensure the "2572" directory is not watched directly. for _, path := range watchPaths { t.Log("watch path:", path) - t.AssertNE(true, strings.HasSuffix(path, "2572")) + t.Assert(strings.HasSuffix(path, "2572"), false) } t.AssertGT(len(watchPaths), 0) }) From 650de6f92edee76f4025d3eace8887f500034dfc Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 25 Dec 2025 22:52:04 +0800 Subject: [PATCH 04/10] =?UTF-8?q?refactor(cmd/gf):=20=E4=BC=98=E5=8C=96`gf?= =?UTF-8?q?=20run`=E5=91=BD=E4=BB=A4=E7=9A=84=E8=B7=AF=E5=BE=84=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=BF=BD=E7=95=A5=E6=A8=A1=E5=BC=8F=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 133b0459d13..e3e2ef5ed82 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -61,7 +61,7 @@ which compiles and runs the go codes asynchronously when codes change. cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` cRunArgsBrief = `custom arguments for your process` cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"` - cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules"` + cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*` ) var process *gproc.Process @@ -107,12 +107,20 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err } if len(in.WatchPaths) == 1 { - in.WatchPaths = strings.Split(in.WatchPaths[0], ",") + parts := strings.Split(in.WatchPaths[0], ",") + for i, part := range parts { + parts[i] = strings.TrimSpace(part) + } + in.WatchPaths = parts mlog.Printf("watchPaths: %v", in.WatchPaths) } if len(in.IgnorePatterns) == 1 { - in.IgnorePatterns = strings.Split(in.IgnorePatterns[0], ",") + parts := strings.Split(in.IgnorePatterns[0], ",") + for i, part := range parts { + parts[i] = strings.TrimSpace(part) + } + in.IgnorePatterns = parts mlog.Printf("ignorePatterns: %v", in.IgnorePatterns) } @@ -330,6 +338,9 @@ func (app *cRunApp) getWatchPaths() []string { if len(watchPaths) == 0 { mlog.Printf("no directories to watch, using current directory") + if absCur := gfile.RealPath("."); absCur != "" { + return []string{absCur} + } return []string{"."} } @@ -366,14 +377,9 @@ func hasIgnoredDescendant(dir string, ignorePatterns []string) bool { // These directories typically contain third-party code or non-source files. // Patterns support glob syntax: * matches any sequence of characters, ? matches single character. var defaultIgnorePatterns = []string{ - ".git", - ".svn", - ".hg", - ".idea", - ".vscode", "node_modules", "vendor", - ".*", // All hidden directories + ".*", // All hidden directories (covers .git, .svn, .hg, .idea, .vscode, etc.) "_*", // Directories starting with underscore } From 4f008f0d36fd933d5fcedc35ce7a75d3f9b35dc3 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 25 Dec 2025 23:16:33 +0800 Subject: [PATCH 05/10] =?UTF-8?q?refactor(cmd/gf):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/go.mod | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/gf/go.mod b/cmd/gf/go.mod index 1f091cf678e..a46f187466f 100644 --- a/cmd/gf/go.mod +++ b/cmd/gf/go.mod @@ -9,10 +9,6 @@ require ( github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6 github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/tidb/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/oceanbase/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/gaussdb/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/mariadb/v2 v2.9.6 github.com/gogf/gf/v2 v2.9.6 github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f github.com/olekukonko/tablewriter v1.1.0 From db7d9d7b54f0f4b0113b6883c4580e2ad2ee0408 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 25 Dec 2025 23:42:56 +0800 Subject: [PATCH 06/10] =?UTF-8?q?refactor(cmd/gf):=20=E5=B0=86`getWatchPat?= =?UTF-8?q?hs`=E6=96=B9=E6=B3=95=E4=BB=8EBFS=E6=94=B9=E4=B8=BADFS=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E4=BC=98=E5=8C=96=E7=9B=AE=E5=BD=95=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E9=80=BB=E8=BE=91=E5=B9=B6=E6=B7=BB=E5=8A=A0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 66 +++---- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 195 +++++++++++++++++++++ 2 files changed, 222 insertions(+), 39 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index e3e2ef5ed82..35da3f749e1 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -278,7 +278,7 @@ func (app *cRunApp) genOutputPath() (outputPath string) { return filepath.FromSlash(outputPath) } -// getWatchPaths uses BFS to find the minimal set of directories to watch. +// getWatchPaths uses DFS to find the minimal set of directories to watch. // Rule: if a directory and all its descendants have no ignored subdirectories, watch it; // otherwise, recurse into valid children. func (app *cRunApp) getWatchPaths() []string { @@ -294,9 +294,7 @@ func (app *cRunApp) getWatchPaths() []string { } var watchPaths []string - queue := make([]string, 0) - // Initialize queue with valid roots. for _, root := range roots { absRoot := gfile.RealPath(root) if absRoot == "" { @@ -306,34 +304,7 @@ func (app *cRunApp) getWatchPaths() []string { if isIgnoredDirName(absRoot, ignorePatterns) { continue } - queue = append(queue, absRoot) - } - - // BFS traversal. - for len(queue) > 0 { - dir := queue[0] - queue = queue[1:] - - // Check if this directory or any descendant contains ignored directories. - if !hasIgnoredDescendant(dir, ignorePatterns) { - // No ignored descendants, watch this directory (recursive watch covers all). - watchPaths = append(watchPaths, dir) - } else { - // Has ignored descendants, get direct children and add valid ones to queue. - entries, err := gfile.ScanDir(dir, "*", false) - if err != nil { - mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) - continue - } - for _, entry := range entries { - if !gfile.IsDir(entry) { - continue - } - if !isIgnoredDirName(entry, ignorePatterns) { - queue = append(queue, entry) - } - } - } + app.collectWatchPaths(absRoot, ignorePatterns, &watchPaths) } if len(watchPaths) == 0 { @@ -351,26 +322,43 @@ func (app *cRunApp) getWatchPaths() []string { return watchPaths } -// hasIgnoredDescendant checks if the directory or any of its descendants is ignored. -func hasIgnoredDescendant(dir string, ignorePatterns []string) bool { +// collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch. +func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]string) { + // Check if this directory or any immediate child is ignored. + hasIgnoredChild := false entries, err := gfile.ScanDir(dir, "*", false) if err != nil { - return false + mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) + // If we can't scan the directory, add it to watch list as fallback + *watchPaths = append(*watchPaths, dir) + return } + // Check for ignored directories in immediate children for _, entry := range entries { if !gfile.IsDir(entry) { continue } if isIgnoredDirName(entry, ignorePatterns) { - return true + hasIgnoredChild = true + break } - // Recursively check subdirectories. - if hasIgnoredDescendant(entry, ignorePatterns) { - return true + } + + if !hasIgnoredChild { + // No ignored descendants, watch this directory (recursive watch covers all). + *watchPaths = append(*watchPaths, dir) + } else { + // Has ignored immediate children, recurse into valid subdirectories. + for _, entry := range entries { + if !gfile.IsDir(entry) { + continue + } + if !isIgnoredDirName(entry, ignorePatterns) { + app.collectWatchPaths(entry, ignorePatterns, watchPaths) + } } } - return false } // defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching. diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index 7a2fa60b665..fbdceb91d32 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -7,9 +7,12 @@ package cmd import ( + "os" + "path/filepath" "strings" "testing" + "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/test/gtest" ) @@ -55,3 +58,195 @@ func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) { t.AssertGT(len(watchPaths), 0) }) } + +func Test_cRunApp_getWatchPaths_WithIgnoredDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure for testing + tempDir := gfile.Temp("gf_run_test") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + // ├── vendor/ <-- ignored + // └── node_modules/ <-- ignored + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + gfile.Mkdir(filepath.Join(tempDir, "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "node_modules")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch the src directory since parent has ignored children + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0], filepath.Join(tempDir, "src")) + }) +} + +func Test_cRunApp_getWatchPaths_NoIgnoredDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure without ignored directories + tempDir := gfile.Temp("gf_run_test_no_ignore") + defer gfile.Remove(tempDir) + + // Create directory structure without ignored patterns: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch the root directory since no ignored directories exist + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0], tempDir) + }) +} + +func Test_cRunApp_getWatchPaths_CustomIgnorePatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure + tempDir := gfile.Temp("gf_run_test_custom_ignore") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + // ├── build/ <-- ignored + // └── dist/ <-- ignored + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + gfile.Mkdir(filepath.Join(tempDir, "build")) + gfile.Mkdir(filepath.Join(tempDir, "dist")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + IgnorePatterns: []string{"build", "dist"}, + } + watchPaths := app.getWatchPaths() + + // Should watch the src directory since parent has ignored children (build, dist) + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0], filepath.Join(tempDir, "src")) + }) +} + +func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a deep nested directory structure + tempDir := gfile.Temp("gf_run_test_deep") + defer gfile.Remove(tempDir) + + // Create deep directory structure: + // tempDir/ + // ├── a/ + // │ ├── b/ + // │ │ └── c/ + // │ └── vendor/ <-- ignored + // └── d/ + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c")) + gfile.Mkdir(filepath.Join(tempDir, "a", "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "d")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch individual valid directories due to ignored vendor directory + t.AssertGT(len(watchPaths), 0) + + // Verify that vendor directory is not in watch list + for _, path := range watchPaths { + t.Assert(strings.Contains(path, "vendor"), false) + } + }) +} + +func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create multiple temporary directories + tempDir1 := gfile.Temp("gf_run_test_multi1") + tempDir2 := gfile.Temp("gf_run_test_multi2") + defer gfile.Remove(tempDir1) + defer gfile.Remove(tempDir2) + + gfile.Mkdir(filepath.Join(tempDir1, "src")) + gfile.Mkdir(filepath.Join(tempDir2, "api")) + + app := &cRunApp{ + WatchPaths: []string{tempDir1, tempDir2}, + } + watchPaths := app.getWatchPaths() + + // Should watch both root directories + t.Assert(len(watchPaths), 2) + + // Both directories should be in the watch list + foundDir1, foundDir2 := false, false + for _, path := range watchPaths { + if path == tempDir1 { + foundDir1 = true + } + if path == tempDir2 { + foundDir2 = true + } + } + t.Assert(foundDir1, true) + t.Assert(foundDir2, true) + }) +} + +func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + WatchPaths: []string{"/non/existent/path"}, + } + watchPaths := app.getWatchPaths() + + // Should fall back to current directory when no valid paths found + t.AssertGT(len(watchPaths), 0) + + // Should contain current directory + currentDir, _ := os.Getwd() + foundCurrentDir := false + for _, path := range watchPaths { + if path == currentDir { + foundCurrentDir = true + break + } + } + t.Assert(foundCurrentDir, true) + }) +} + +func Test_isIgnoredDirName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test default ignore patterns + t.Assert(isIgnoredDirName("node_modules", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("vendor", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName(".git", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("_private", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("src", defaultIgnorePatterns), false) + t.Assert(isIgnoredDirName("api", defaultIgnorePatterns), false) + + // Test custom ignore patterns + customPatterns := []string{"build", "dist", "*.tmp"} + t.Assert(isIgnoredDirName("build", customPatterns), true) + t.Assert(isIgnoredDirName("dist", customPatterns), true) + t.Assert(isIgnoredDirName("test.tmp", customPatterns), true) + t.Assert(isIgnoredDirName("src", customPatterns), false) + }) +} From e10ec87321375ded3b797a75bebeba133a395402 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 25 Dec 2025 15:43:22 +0000 Subject: [PATCH 07/10] Apply gci import order changes --- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index fbdceb91d32..6c52ac574ff 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -167,7 +167,7 @@ func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) { // Should watch individual valid directories due to ignored vendor directory t.AssertGT(len(watchPaths), 0) - + // Verify that vendor directory is not in watch list for _, path := range watchPaths { t.Assert(strings.Contains(path, "vendor"), false) @@ -193,7 +193,7 @@ func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) { // Should watch both root directories t.Assert(len(watchPaths), 2) - + // Both directories should be in the watch list foundDir1, foundDir2 := false, false for _, path := range watchPaths { @@ -218,7 +218,7 @@ func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) { // Should fall back to current directory when no valid paths found t.AssertGT(len(watchPaths), 0) - + // Should contain current directory currentDir, _ := os.Getwd() foundCurrentDir := false @@ -241,7 +241,7 @@ func Test_isIgnoredDirName(t *testing.T) { t.Assert(isIgnoredDirName("_private", defaultIgnorePatterns), true) t.Assert(isIgnoredDirName("src", defaultIgnorePatterns), false) t.Assert(isIgnoredDirName("api", defaultIgnorePatterns), false) - + // Test custom ignore patterns customPatterns := []string{"build", "dist", "*.tmp"} t.Assert(isIgnoredDirName("build", customPatterns), true) From 6c5d37f2560067d2d5c4484ac4cd598158c62de3 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Thu, 25 Dec 2025 23:43:29 +0800 Subject: [PATCH 08/10] =?UTF-8?q?refactor(cmd/gf):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=9B=91=E5=90=AC=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=80=92=E5=BD=92=E5=92=8C=E9=9D=9E=E9=80=92?= =?UTF-8?q?=E5=BD=92=E7=9B=91=E6=8E=A7=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 44 ++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 35da3f749e1..ba3b76bd01c 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -33,6 +33,11 @@ type cRun struct { g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"` } +type watchPath struct { + Path string + Recursive bool +} + type cRunApp struct { File string // Go run file name. Path string // Directory storing built binary. @@ -158,10 +163,12 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err }) } - // Get directories to watch (recursive monitoring). + // Get directories to watch (recursive or non-recursive monitoring). watchPaths := app.getWatchPaths() - for _, path := range watchPaths { - _, err = gfsnotify.Add(path, callbackFunc) + for _, wp := range watchPaths { + mlog.Printf("watchPaths: %v", wp) + option := gfsnotify.WatchOption{NoRecursive: !wp.Recursive} + _, err = gfsnotify.Add(wp.Path, callbackFunc, option) if err != nil { mlog.Fatal(err) } @@ -280,8 +287,8 @@ func (app *cRunApp) genOutputPath() (outputPath string) { // getWatchPaths uses DFS to find the minimal set of directories to watch. // Rule: if a directory and all its descendants have no ignored subdirectories, watch it; -// otherwise, recurse into valid children. -func (app *cRunApp) getWatchPaths() []string { +// otherwise, recurse into valid children and watch the current directory non-recursively. +func (app *cRunApp) getWatchPaths() []watchPath { roots := []string{"."} if len(app.WatchPaths) > 0 { roots = app.WatchPaths @@ -293,7 +300,7 @@ func (app *cRunApp) getWatchPaths() []string { ignorePatterns = app.IgnorePatterns } - var watchPaths []string + var watchPaths []watchPath for _, root := range roots { absRoot := gfile.RealPath(root) @@ -310,32 +317,37 @@ func (app *cRunApp) getWatchPaths() []string { if len(watchPaths) == 0 { mlog.Printf("no directories to watch, using current directory") if absCur := gfile.RealPath("."); absCur != "" { - return []string{absCur} + return []watchPath{{Path: absCur, Recursive: true}} } - return []string{"."} + return []watchPath{{Path: ".", Recursive: true}} } - mlog.Printf("watching %d directories (recursive)", len(watchPaths)) - for _, path := range watchPaths { - mlog.Debugf(" - %s", path) + mlog.Printf("watching %d paths", len(watchPaths)) + for _, wp := range watchPaths { + recursiveStr := "recursive" + if !wp.Recursive { + recursiveStr = "non-recursive" + } + mlog.Debugf(" - %s (%s)", wp.Path, recursiveStr) } return watchPaths } // collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch. -func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]string) { +func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) { // Check if this directory or any immediate child is ignored. hasIgnoredChild := false entries, err := gfile.ScanDir(dir, "*", false) if err != nil { mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) // If we can't scan the directory, add it to watch list as fallback - *watchPaths = append(*watchPaths, dir) + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) return } // Check for ignored directories in immediate children for _, entry := range entries { + mlog.Printf("entry: %s", entry) if !gfile.IsDir(entry) { continue } @@ -347,9 +359,11 @@ func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watch if !hasIgnoredChild { // No ignored descendants, watch this directory (recursive watch covers all). - *watchPaths = append(*watchPaths, dir) + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) } else { - // Has ignored immediate children, recurse into valid subdirectories. + // Has ignored immediate children, watch current directory non-recursively to catch top-level files, + // and recurse into valid subdirectories recursively. + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) for _, entry := range entries { if !gfile.IsDir(entry) { continue From 0d8cd5f7faabdc93f723d1acdce8dccc2c9040b6 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 26 Dec 2025 12:07:16 +0800 Subject: [PATCH 09/10] =?UTF-8?q?refactor(cmd/gf):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=9B=91=E5=90=AC=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=B7=B1=E5=BA=A6=E4=BC=98=E5=85=88=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=A3=80=E6=B5=8B=E8=A2=AB=E5=BF=BD=E7=95=A5=E7=9A=84?= =?UTF-8?q?=E5=AD=90=E7=9B=AE=E5=BD=95=E5=B9=B6=E6=8F=90=E5=8D=87=E6=80=A7?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 51 ++++--- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 156 ++++++++++++++++++--- 2 files changed, 169 insertions(+), 38 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index ba3b76bd01c..3a4c3e36fba 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -166,7 +166,6 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err // Get directories to watch (recursive or non-recursive monitoring). watchPaths := app.getWatchPaths() for _, wp := range watchPaths { - mlog.Printf("watchPaths: %v", wp) option := gfsnotify.WatchOption{NoRecursive: !wp.Recursive} _, err = gfsnotify.Add(wp.Path, callbackFunc, option) if err != nil { @@ -334,9 +333,9 @@ func (app *cRunApp) getWatchPaths() []watchPath { } // collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch. +// Rule: if a directory has no ignored descendants at any depth, watch it recursively; +// otherwise, watch it non-recursively and recurse into valid children. func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) { - // Check if this directory or any immediate child is ignored. - hasIgnoredChild := false entries, err := gfile.ScanDir(dir, "*", false) if err != nil { mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) @@ -345,24 +344,13 @@ func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watch return } - // Check for ignored directories in immediate children - for _, entry := range entries { - mlog.Printf("entry: %s", entry) - if !gfile.IsDir(entry) { - continue - } - if isIgnoredDirName(entry, ignorePatterns) { - hasIgnoredChild = true - break - } - } - - if !hasIgnoredChild { - // No ignored descendants, watch this directory (recursive watch covers all). + // Check if any descendant (at any depth) is ignored using DFS with early termination. + if !hasIgnoredDescendant(dir, ignorePatterns) { + // No ignored descendants at any depth, watch this directory recursively. *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) } else { - // Has ignored immediate children, watch current directory non-recursively to catch top-level files, - // and recurse into valid subdirectories recursively. + // Has ignored descendants, watch current directory non-recursively to catch top-level files, + // and recurse into valid subdirectories. *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) for _, entry := range entries { if !gfile.IsDir(entry) { @@ -375,6 +363,31 @@ func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watch } } +// hasIgnoredDescendant checks if the directory has any ignored subdirectory at any depth. +// Uses DFS with early termination - stops as soon as an ignored directory is found. +// Key optimization: when an ignored directory is found, we don't enter it (pruning). +func hasIgnoredDescendant(dir string, ignorePatterns []string) bool { + entries, err := gfile.ScanDir(dir, "*", false) + if err != nil { + return false + } + + for _, entry := range entries { + if !gfile.IsDir(entry) { + continue + } + // Check if this child directory is ignored + if isIgnoredDirName(entry, ignorePatterns) { + return true // Found an ignored directory, early termination + } + // Recursively check descendants (DFS with pruning) + if hasIgnoredDescendant(entry, ignorePatterns) { + return true // Found an ignored descendant, early termination + } + } + return false +} + // defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching. // These directories typically contain third-party code or non-source files. // Patterns support glob syntax: * matches any sequence of characters, ? matches single character. diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index 6c52ac574ff..835f3e07d54 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -51,9 +51,9 @@ func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) { watchPaths := app.getWatchPaths() // Ensure the "2572" directory is not watched directly. - for _, path := range watchPaths { - t.Log("watch path:", path) - t.Assert(strings.HasSuffix(path, "2572"), false) + for _, wp := range watchPaths { + t.Log("watch path:", wp) + t.Assert(strings.HasSuffix(wp.Path, "2572"), false) } t.AssertGT(len(watchPaths), 0) }) @@ -82,9 +82,14 @@ func Test_cRunApp_getWatchPaths_WithIgnoredDirectories(t *testing.T) { } watchPaths := app.getWatchPaths() - // Should watch the src directory since parent has ignored children - t.Assert(len(watchPaths), 1) - t.Assert(watchPaths[0], filepath.Join(tempDir, "src")) + // Should watch tempDir non-recursively (to catch top-level files) and src recursively + t.Assert(len(watchPaths), 2) + // First path is tempDir (non-recursive) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, false) + // Second path is src (recursive, since it has no ignored descendants) + t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src")) + t.Assert(watchPaths[1].Recursive, true) }) } @@ -107,9 +112,10 @@ func Test_cRunApp_getWatchPaths_NoIgnoredDirectories(t *testing.T) { } watchPaths := app.getWatchPaths() - // Should watch the root directory since no ignored directories exist + // Should watch the root directory recursively since no ignored directories exist t.Assert(len(watchPaths), 1) - t.Assert(watchPaths[0], tempDir) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, true) }) } @@ -137,9 +143,12 @@ func Test_cRunApp_getWatchPaths_CustomIgnorePatterns(t *testing.T) { } watchPaths := app.getWatchPaths() - // Should watch the src directory since parent has ignored children (build, dist) - t.Assert(len(watchPaths), 1) - t.Assert(watchPaths[0], filepath.Join(tempDir, "src")) + // Should watch tempDir non-recursively and src recursively + t.Assert(len(watchPaths), 2) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, false) + t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src")) + t.Assert(watchPaths[1].Recursive, true) }) } @@ -169,8 +178,8 @@ func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) { t.AssertGT(len(watchPaths), 0) // Verify that vendor directory is not in watch list - for _, path := range watchPaths { - t.Assert(strings.Contains(path, "vendor"), false) + for _, wp := range watchPaths { + t.Assert(strings.Contains(wp.Path, "vendor"), false) } }) } @@ -191,17 +200,19 @@ func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) { } watchPaths := app.getWatchPaths() - // Should watch both root directories + // Should watch both root directories recursively t.Assert(len(watchPaths), 2) // Both directories should be in the watch list foundDir1, foundDir2 := false, false - for _, path := range watchPaths { - if path == tempDir1 { + for _, wp := range watchPaths { + if wp.Path == tempDir1 { foundDir1 = true + t.Assert(wp.Recursive, true) } - if path == tempDir2 { + if wp.Path == tempDir2 { foundDir2 = true + t.Assert(wp.Recursive, true) } } t.Assert(foundDir1, true) @@ -222,8 +233,8 @@ func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) { // Should contain current directory currentDir, _ := os.Getwd() foundCurrentDir := false - for _, path := range watchPaths { - if path == currentDir { + for _, wp := range watchPaths { + if wp.Path == currentDir { foundCurrentDir = true break } @@ -250,3 +261,110 @@ func Test_isIgnoredDirName(t *testing.T) { t.Assert(isIgnoredDirName("src", customPatterns), false) }) } + +func Test_hasIgnoredDescendant(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure + tempDir := gfile.Temp("gf_run_test_has_ignored") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── a/ + // │ ├── b/ + // │ │ └── vendor/ <-- deeply nested ignored + // │ └── c/ + // └── d/ + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "a", "c")) + gfile.Mkdir(filepath.Join(tempDir, "d")) + + // Test: tempDir should have ignored descendant (vendor is 3 levels deep) + t.Assert(hasIgnoredDescendant(tempDir, defaultIgnorePatterns), true) + + // Test: d/ should NOT have ignored descendant + t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "d"), defaultIgnorePatterns), false) + + // Test: a/c/ should NOT have ignored descendant + t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a", "c"), defaultIgnorePatterns), false) + + // Test: a/ should have ignored descendant (vendor in a/b/) + t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a"), defaultIgnorePatterns), true) + + // Test: a/b/ should have ignored descendant (vendor directly inside) + t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a", "b"), defaultIgnorePatterns), true) + }) +} + +func Test_cRunApp_getWatchPaths_DeeplyNestedIgnore(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure with deeply nested ignored directory + tempDir := gfile.Temp("gf_run_test_deeply_nested") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── a/ + // │ ├── b/ + // │ │ ├── c/ + // │ │ │ └── vendor/ <-- deeply nested ignored (4 levels) + // │ │ └── d/ + // │ └── e/ + // └── f/ + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c", "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "d")) + gfile.Mkdir(filepath.Join(tempDir, "a", "e")) + gfile.Mkdir(filepath.Join(tempDir, "f")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Expected watch paths: + // 1. tempDir (non-recursive) - has ignored descendant + // 2. a (non-recursive) - has ignored descendant in b/c/vendor + // 3. b (non-recursive) - has ignored descendant in c/vendor + // 4. c (non-recursive) - has ignored child vendor + // 5. d (recursive) - no ignored descendants + // 6. e (recursive) - no ignored descendants + // 7. f (recursive) - no ignored descendants + + t.AssertGT(len(watchPaths), 0) + + // Verify vendor is not in watch paths + for _, wp := range watchPaths { + t.Assert(strings.Contains(wp.Path, "vendor"), false) + } + + // Find specific paths and verify their recursive flags + foundF := false + for _, wp := range watchPaths { + if wp.Path == filepath.Join(tempDir, "f") { + foundF = true + t.Assert(wp.Recursive, true) // f should be recursive (no ignored descendants) + } + } + t.Assert(foundF, true) + }) +} + +func Test_cRunApp_getWatchPaths_EmptyDirectory(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create an empty temporary directory + tempDir := gfile.Temp("gf_run_test_empty") + defer gfile.Remove(tempDir) + + gfile.Mkdir(tempDir) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Empty directory should be watched recursively (no ignored descendants) + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, true) + }) +} From b25abdcc4cad8af1616173b0d4cf92fdcab77647 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 26 Dec 2025 12:19:06 +0800 Subject: [PATCH 10/10] =?UTF-8?q?refactor(cmd/gf):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=9B=91=E5=90=AC=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BF=BD=E7=95=A5=E6=A8=A1=E5=BC=8F=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E5=B9=B6=E6=8F=90=E5=8D=87=E6=96=87=E4=BB=B6=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmd_run.go | 137 +++++++++++++-------- cmd/gf/internal/cmd/cmd_z_unit_run_test.go | 34 ----- 2 files changed, 85 insertions(+), 86 deletions(-) diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 3a4c3e36fba..7306b7e5e00 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -66,7 +66,7 @@ which compiles and runs the go codes asynchronously when codes change. cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` cRunArgsBrief = `custom arguments for your process` cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"` - cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*` + cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*. Glob syntax: "*" matches any chars, "?" matches single char, "[abc]" matches char class. Note: patterns match directory names only, not paths` ) var process *gproc.Process @@ -111,21 +111,15 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) } - if len(in.WatchPaths) == 1 { - parts := strings.Split(in.WatchPaths[0], ",") - for i, part := range parts { - parts[i] = strings.TrimSpace(part) - } - in.WatchPaths = parts + // Parse comma-separated values in WatchPaths + if len(in.WatchPaths) > 0 { + in.WatchPaths = parseCommaSeparatedArgs(in.WatchPaths) mlog.Printf("watchPaths: %v", in.WatchPaths) } - if len(in.IgnorePatterns) == 1 { - parts := strings.Split(in.IgnorePatterns[0], ",") - for i, part := range parts { - parts[i] = strings.TrimSpace(part) - } - in.IgnorePatterns = parts + // Parse comma-separated values in IgnorePatterns + if len(in.IgnorePatterns) > 0 { + in.IgnorePatterns = parseCommaSeparatedArgs(in.IgnorePatterns) mlog.Printf("ignorePatterns: %v", in.IgnorePatterns) } @@ -270,15 +264,20 @@ func (app *cRunApp) End(ctx context.Context, sig os.Signal, outputPath string) { } func (app *cRunApp) genOutputPath() (outputPath string) { - var renamePath string outputPath = gfile.Join(app.Path, gfile.Name(app.File)) if runtime.GOOS == "windows" { outputPath += ".exe" if gfile.Exists(outputPath) { - renamePath = outputPath + "~" + renamePath := outputPath + "~" if err := gfile.Rename(outputPath, renamePath); err != nil { mlog.Print(err) } + // Clean up the renamed old binary file + defer func() { + if gfile.Exists(renamePath) { + _ = gfile.Remove(renamePath) + } + }() } } return filepath.FromSlash(outputPath) @@ -333,64 +332,81 @@ func (app *cRunApp) getWatchPaths() []watchPath { } // collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch. +// Returns true if the directory or any of its descendants contains ignored directories. // Rule: if a directory has no ignored descendants at any depth, watch it recursively; // otherwise, watch it non-recursively and recurse into valid children. -func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) { +func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) bool { entries, err := gfile.ScanDir(dir, "*", false) if err != nil { mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) // If we can't scan the directory, add it to watch list as fallback *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) - return - } - - // Check if any descendant (at any depth) is ignored using DFS with early termination. - if !hasIgnoredDescendant(dir, ignorePatterns) { - // No ignored descendants at any depth, watch this directory recursively. - *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) - } else { - // Has ignored descendants, watch current directory non-recursively to catch top-level files, - // and recurse into valid subdirectories. - *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) - for _, entry := range entries { - if !gfile.IsDir(entry) { - continue - } - if !isIgnoredDirName(entry, ignorePatterns) { - app.collectWatchPaths(entry, ignorePatterns, watchPaths) - } - } - } -} - -// hasIgnoredDescendant checks if the directory has any ignored subdirectory at any depth. -// Uses DFS with early termination - stops as soon as an ignored directory is found. -// Key optimization: when an ignored directory is found, we don't enter it (pruning). -func hasIgnoredDescendant(dir string, ignorePatterns []string) bool { - entries, err := gfile.ScanDir(dir, "*", false) - if err != nil { return false } + // First pass: identify valid subdirectories and check for directly ignored children + var validSubDirs []string + hasIgnoredChild := false for _, entry := range entries { if !gfile.IsDir(entry) { continue } - // Check if this child directory is ignored if isIgnoredDirName(entry, ignorePatterns) { - return true // Found an ignored directory, early termination + hasIgnoredChild = true + } else { + validSubDirs = append(validSubDirs, entry) } - // Recursively check descendants (DFS with pruning) - if hasIgnoredDescendant(entry, ignorePatterns) { - return true // Found an ignored descendant, early termination + } + + // If already has ignored child, we know this dir needs non-recursive watch + if hasIgnoredChild { + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) + for _, subDir := range validSubDirs { + app.collectWatchPaths(subDir, ignorePatterns, watchPaths) } + return true } - return false + + // No ignored children, but need to check descendants recursively + // Collect results from all subdirectories first + subResults := make([]bool, len(validSubDirs)) + subWatchPaths := make([][]watchPath, len(validSubDirs)) + hasIgnoredDescendant := false + + for i, subDir := range validSubDirs { + var subPaths []watchPath + subResults[i] = app.collectWatchPaths(subDir, ignorePatterns, &subPaths) + subWatchPaths[i] = subPaths + if subResults[i] { + hasIgnoredDescendant = true + } + } + + if !hasIgnoredDescendant { + // No ignored descendants at any depth, watch this directory recursively + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) + return false + } + + // Has ignored descendants, watch current directory non-recursively + // and add all collected subdirectory watch paths + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) + for _, subPaths := range subWatchPaths { + *watchPaths = append(*watchPaths, subPaths...) + } + return true } // defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching. // These directories typically contain third-party code or non-source files. -// Patterns support glob syntax: * matches any sequence of characters, ? matches single character. +// Supported glob syntax (filepath.Match): +// - "*" matches any sequence of non-separator characters +// - "?" matches any single non-separator character +// - "[abc]" matches any character in the bracket +// - "[a-z]" matches any character in the range +// - "[^abc]" or "[!abc]" matches any character not in the bracket +// +// Note: patterns match directory base names only, not full paths (no "/" or path separators allowed). var defaultIgnorePatterns = []string{ "node_modules", "vendor", @@ -399,7 +415,8 @@ var defaultIgnorePatterns = []string{ } // isIgnoredDirName checks if a directory name matches any ignored pattern. -// It accepts either a full path or just the directory name. +// It accepts either a full path or just the directory name, but only matches against the base name. +// Note: patterns should not contain "/" as they only match directory names, not paths. func isIgnoredDirName(name string, ignorePatterns []string) bool { baseName := gfile.Basename(name) for _, pattern := range ignorePatterns { @@ -409,3 +426,19 @@ func isIgnoredDirName(name string, ignorePatterns []string) bool { } return false } + +// parseCommaSeparatedArgs parses command line arguments that may contain comma-separated values. +// It handles both single argument with commas (e.g., "a,b,c") and multiple arguments. +func parseCommaSeparatedArgs(args []string) []string { + var result []string + for _, arg := range args { + parts := strings.Split(arg, ",") + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + } + return result +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go index 835f3e07d54..b5fc4b13ef8 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -262,40 +262,6 @@ func Test_isIgnoredDirName(t *testing.T) { }) } -func Test_hasIgnoredDescendant(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - // Create a temporary directory structure - tempDir := gfile.Temp("gf_run_test_has_ignored") - defer gfile.Remove(tempDir) - - // Create directory structure: - // tempDir/ - // ├── a/ - // │ ├── b/ - // │ │ └── vendor/ <-- deeply nested ignored - // │ └── c/ - // └── d/ - gfile.Mkdir(filepath.Join(tempDir, "a", "b", "vendor")) - gfile.Mkdir(filepath.Join(tempDir, "a", "c")) - gfile.Mkdir(filepath.Join(tempDir, "d")) - - // Test: tempDir should have ignored descendant (vendor is 3 levels deep) - t.Assert(hasIgnoredDescendant(tempDir, defaultIgnorePatterns), true) - - // Test: d/ should NOT have ignored descendant - t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "d"), defaultIgnorePatterns), false) - - // Test: a/c/ should NOT have ignored descendant - t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a", "c"), defaultIgnorePatterns), false) - - // Test: a/ should have ignored descendant (vendor in a/b/) - t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a"), defaultIgnorePatterns), true) - - // Test: a/b/ should have ignored descendant (vendor directly inside) - t.Assert(hasIgnoredDescendant(filepath.Join(tempDir, "a", "b"), defaultIgnorePatterns), true) - }) -} - func Test_cRunApp_getWatchPaths_DeeplyNestedIgnore(t *testing.T) { gtest.C(t, func(t *gtest.T) { // Create a temporary directory structure with deeply nested ignored directory