Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 156 additions & 49 deletions cmd/gf/internal/cmd/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
IgnorePatterns []string // Custom ignore patterns.
}

const (
Expand All @@ -48,43 +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 "manifest/config/*.yaml"
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. "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"`
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,
`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}"`
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{}
)
Expand All @@ -106,12 +111,18 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err
mlog.Printf("watchPaths: %v", in.WatchPaths)
}

if len(in.IgnorePatterns) == 1 {
in.IgnorePatterns = strings.Split(in.IgnorePatterns[0], ",")
mlog.Printf("ignorePatterns: %v", in.IgnorePatterns)
}
Comment thread
hailaz marked this conversation as resolved.
Outdated

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,
IgnorePatterns: in.IgnorePatterns,
}
dirty := gtype.NewBool()

Expand All @@ -121,6 +132,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
}
Expand All @@ -138,15 +150,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 directories to watch (recursive monitoring).
watchPaths := app.getWatchPaths()
for _, path := range watchPaths {
_, err = gfsnotify.Add(path, callbackFunc)
if err != nil {
mlog.Fatal(err)
}
Expand Down Expand Up @@ -263,19 +270,119 @@ 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 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 {
roots := []string{"."}
if len(app.WatchPaths) > 0 {
roots = app.WatchPaths
}

// Use custom ignore patterns if provided, otherwise use default.
ignorePatterns := defaultIgnorePatterns
if len(app.IgnorePatterns) > 0 {
ignorePatterns = app.IgnorePatterns
}

var watchPaths []string
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
}
matched, err := filepath.Match(absPath, eventPath)
if err != nil {
mlog.Printf("match watchPath '%s' error: %s", path, err.Error())
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)
}
}
}
Comment thread
hailaz marked this conversation as resolved.
Outdated
}

if len(watchPaths) == 0 {
mlog.Printf("no directories to watch, using current directory")
Comment thread
hailaz marked this conversation as resolved.
return []string{"."}
}

mlog.Printf("watching %d directories (recursive)", len(watchPaths))
for _, path := range watchPaths {
mlog.Debugf(" - %s", path)
}
return watchPaths
}

// 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 false
}

for _, entry := range entries {
if !gfile.IsDir(entry) {
continue
}
if matched {
if isIgnoredDirName(entry, ignorePatterns) {
return true
}
// Recursively check subdirectories.
if hasIgnoredDescendant(entry, ignorePatterns) {
return true
}
}
return false
}
Comment thread
hailaz marked this conversation as resolved.
Outdated

// 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
}
Comment thread
hailaz marked this conversation as resolved.

// 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
}
}
Expand Down
57 changes: 57 additions & 0 deletions cmd/gf/internal/cmd/cmd_z_unit_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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 (
"strings"
"testing"

"github.com/gogf/gf/v2/test/gtest"
)

func Test_cRunApp_getWatchPaths_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{"."},
}
watchPaths := app.getWatchPaths()

t.AssertGT(len(watchPaths), 0)
for _, v := range watchPaths {
t.Log(v)
}
})
}

func Test_cRunApp_getWatchPaths_EmptyWatchPaths(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{},
}
watchPaths := app.getWatchPaths()

// Should default to current directory "."
t.AssertGT(len(watchPaths), 0)
})
}

func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
app := &cRunApp{
WatchPaths: []string{"testdata"},
IgnorePatterns: []string{"2572"},
}
watchPaths := app.getWatchPaths()

// Ensure the "2572" directory is not watched directly.
for _, path := range watchPaths {
t.Log("watch path:", path)
t.AssertNE(true, strings.HasSuffix(path, "2572"))
Comment thread
hailaz marked this conversation as resolved.
Outdated
}
t.AssertGT(len(watchPaths), 0)
})
}
3 changes: 0 additions & 3 deletions cmd/gf/internal/cmd/testdata/fix/fix25_content.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down
Loading