diff --git a/commands/header.go b/commands/header.go index 60c6f257..062f873a 100644 --- a/commands/header.go +++ b/commands/header.go @@ -25,7 +25,9 @@ var Header = &cobra.Command{ Use: "header", Aliases: []string{"h"}, Short: "License header related commands; e.g. check, fix, etc.", - Long: "`header` command walks the specified paths recursively and checks if the specified files have the license header in the config file.", + Long: "`header` command walks the specified paths and checks if the specified " + + "files have the license header in the config file. " + + "Accepts files, directories, and glob patterns.", } func init() { diff --git a/commands/header_check.go b/commands/header_check.go index 72bd7587..0bcb05d0 100644 --- a/commands/header_check.go +++ b/commands/header_check.go @@ -29,17 +29,28 @@ import ( ) var CheckCommand = &cobra.Command{ - Use: "check", + Use: "check [paths...]", Aliases: []string{"c"}, - Long: "check command walks the specified paths recursively and checks if the specified files have the license header in the config file.", + Long: "check command walks the specified paths recursively and checks if the " + + "specified files have the license header in the config file. " + + "Accepts files, directories, and glob patterns. " + + "If no paths are specified, checks the current directory " + + "recursively as defined in the config file.", RunE: func(_ *cobra.Command, args []string) error { hasErrors := false for _, h := range Config.Headers() { var result header.Result if len(args) > 0 { + // Filter args by the paths in this header config + var filteredArgs []string + for _, arg := range args { + if header.MatchPaths(arg, h.Paths) { + filteredArgs = append(filteredArgs, arg) + } + } logger.Log.Debugln("Overriding paths with command line args.") - h.Paths = args + h.Paths = filteredArgs } if err := header.Check(h, &result); err != nil { diff --git a/commands/header_fix.go b/commands/header_fix.go index 221a3eb0..0ea3d9c1 100644 --- a/commands/header_fix.go +++ b/commands/header_fix.go @@ -28,9 +28,14 @@ import ( ) var FixCommand = &cobra.Command{ - Use: "fix", + Use: "fix [paths...]", Aliases: []string{"f"}, - Long: "fix command walks the specified paths recursively and fix the license header if the specified files don't have the license header.", + Long: "fix command walks the specified paths recursively and fixes the license " + + "header if the specified files don't have the license header. " + + "Accepts individual file paths as arguments, " + + "or directories and glob patterns via the config file. " + + "If no paths are specified, fixes the current directory " + + "recursively as defined in the config file.", RunE: func(_ *cobra.Command, args []string) error { var errors []string for _, h := range Config.Headers() { @@ -38,7 +43,12 @@ var FixCommand = &cobra.Command{ var files []string if len(args) > 0 { - files = args + // Filter args by the paths in this header config + for _, arg := range args { + if header.MatchPaths(arg, h.Paths) { + files = append(files, arg) + } + } } else if err := header.Check(h, &result); err != nil { return err } else { diff --git a/pkg/header/check.go b/pkg/header/check.go index 4981dda0..c5008afb 100644 --- a/pkg/header/check.go +++ b/pkg/header/check.go @@ -40,6 +40,8 @@ import ( "golang.org/x/sync/errgroup" ) +const currentDir = "./" + // Check checks the license headers of the specified paths/globs. func Check(config *ConfigHeader, result *Result) error { fileList, err := listFiles(config) @@ -67,13 +69,13 @@ func Check(config *ConfigHeader, result *Result) error { func listFiles(config *ConfigHeader) ([]string, error) { var fileList []string - repo, err := git.PlainOpen("./") + repo, err := git.PlainOpen(currentDir) if err != nil { // we're not in a Git workspace, fallback to glob paths var localFileList []string for _, pattern := range config.Paths { if pattern == "." { - pattern = "./" + pattern = currentDir } files, err := doublestar.Glob(pattern) if err != nil { @@ -134,12 +136,15 @@ func listFiles(config *ConfigHeader) ([]string, error) { } seen := make(map[string]bool) - for _, file := range candidates { - if !seen[file] { - seen[file] = true - _, err := os.Stat(file) + for _, candidate := range candidates { + if !seen[candidate] { + seen[candidate] = true + _, err := os.Stat(candidate) if err == nil { - fileList = append(fileList, file) + // Filter candidates by the paths/patterns specified in config + if MatchPaths(candidate, config.Paths) { + fileList = append(fileList, candidate) + } } else if !os.IsNotExist(err) { return nil, err } @@ -150,6 +155,39 @@ func listFiles(config *ConfigHeader) ([]string, error) { return fileList, nil } +func MatchPaths(file string, patterns []string) bool { + for _, pattern := range patterns { + if pattern == "." { + pattern = currentDir + } + matched, err := doublestar.Match(pattern, file) + if err == nil && matched { + return true + } + // Fallback for directory patterns (e.g., "pkg/header/" or "pkg/header") + // to behave consistently with tryMatchPatten in config.go. + // Ensure that pattern ends with a path separator so it clearly denotes a directory. + if strings.HasSuffix(pattern, "/") || + strings.HasSuffix(pattern, string(os.PathSeparator)) { + dirPattern := strings.TrimRight(pattern, "/") + // Match if file's directory name equals pattern (exact directory match) + if stat, err := os.Stat(file); err == nil { + if stat.Name() == dirPattern { + return true + } + } + // Match if file is under the directory (prefix match) + // Normalize both paths to use '/' so prefix matching is consistent across platforms. + normalizedDir := strings.ReplaceAll(pattern, "\\", "/") + normalizedFile := strings.ReplaceAll(file, "\\", "/") + if strings.HasPrefix(normalizedFile, normalizedDir) { + return true + } + } + } + return false +} + func addIgnorePatterns(t *git.Worktree) { if ignorePattens, err := gitignore.LoadGlobalPatterns(osfs.New("")); err == nil { t.Excludes = append(t.Excludes, ignorePattens...) diff --git a/pkg/header/check_test.go b/pkg/header/check_test.go index 67436bc4..ab9cc670 100644 --- a/pkg/header/check_test.go +++ b/pkg/header/check_test.go @@ -273,3 +273,76 @@ func TestListFilesWithWorktreeDetachedHEAD(t *testing.T) { t.Error("Expected to find files with valid commit") } } + +func TestMatchPaths(t *testing.T) { + tests := []struct { + name string + file string + patterns []string + expected bool + }{ + { + name: "Exact file match", + file: "test.go", + patterns: []string{"test.go"}, + expected: true, + }, + { + name: "Glob pattern match", + file: "test.go", + patterns: []string{"*.go"}, + expected: true, + }, + { + name: "Double-star glob pattern match", + file: "pkg/header/check.go", + patterns: []string{"**/*.go"}, + expected: true, + }, + { + name: "Multiple patterns with match", + file: "test.go", + patterns: []string{"*.java", "*.go", "*.py"}, + expected: true, + }, + { + name: "Multiple patterns without match", + file: "test.go", + patterns: []string{"*.java", "*.py"}, + expected: false, + }, + { + name: "Directory pattern with trailing slash", + file: "pkg/header/check.go", + patterns: []string{"pkg/header/"}, + expected: true, + }, + { + name: "Directory pattern without trailing slash", + file: "pkg/header/check.go", + patterns: []string{"pkg/header}"}, + expected: false, + }, + { + name: "Dot pattern", + file: "test.go", + patterns: []string{"."}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MatchPaths(tt.file, tt.patterns) + if result != tt.expected { + t.Errorf( + "MatchPaths(%q, %v) = %v, want %v", + tt.file, + tt.patterns, + result, + tt.expected, + ) + } + }) + } +}