Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 29 additions & 1 deletion cmd/git-ai-commit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type options struct {
amend bool
addAll bool
includeFiles []string
excludeFiles []string
debugPrompt bool
debugCommand bool
}
Expand Down Expand Up @@ -51,6 +52,7 @@ func main() {
opts.amend,
opts.addAll,
opts.includeFiles,
opts.excludeFiles,
opts.debugPrompt,
opts.debugCommand,
); err != nil {
Expand All @@ -76,7 +78,7 @@ func parseArgs(args []string) (options, error) {
return opts, errHelp
case "version":
return opts, errVersion
case "context", "context-file", "prompt", "prompt-file", "engine", "include":
case "context", "context-file", "prompt", "prompt-file", "engine", "include", "exclude":
if !hasValue {
if i+1 >= len(args) {
return opts, fmt.Errorf("missing value for --%s", name)
Expand Down Expand Up @@ -132,6 +134,11 @@ func applyLongOption(opts *options, name, value string) error {
return fmt.Errorf("missing value for --include")
}
opts.includeFiles = append(opts.includeFiles, value)
case "exclude":
if value == "" {
return fmt.Errorf("missing value for --exclude")
}
opts.excludeFiles = append(opts.excludeFiles, value)
default:
return fmt.Errorf("unknown option --%s", name)
}
Expand Down Expand Up @@ -167,6 +174,26 @@ func parseShortOptions(opts *options, arg string, args []string, index *int) err
return fmt.Errorf("missing value for -i")
}
opts.includeFiles = append(opts.includeFiles, value)
case 'x':
value := ""
if i+1 < len(cluster) {
if cluster[i+1] == '=' {
value = cluster[i+2:]
} else {
value = cluster[i+1:]
}
i = len(cluster)
} else {
if *index+1 >= len(args) {
return fmt.Errorf("missing value for -x")
}
*index++
value = args[*index]
}
if value == "" {
return fmt.Errorf("missing value for -x")
}
opts.excludeFiles = append(opts.excludeFiles, value)
case 'h':
return errHelp
default:
Expand All @@ -189,6 +216,7 @@ func printUsage(out *os.File) {
fmt.Fprintln(out, " --amend Amend the previous commit")
fmt.Fprintln(out, " -a, --all Stage modified and deleted files before generating the message")
fmt.Fprintln(out, " -i, --include VALUE Stage specific files before generating the message")
fmt.Fprintln(out, " -x, --exclude VALUE Hide specific files from the diff for message generation")
fmt.Fprintln(out, " --debug-prompt Print the prompt before executing the engine")
fmt.Fprintln(out, " --debug-command Print the engine command before execution")
fmt.Fprintln(out, " -h, --help Show help")
Expand Down
75 changes: 75 additions & 0 deletions cmd/git-ai-commit/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"testing"
)

func TestParseArgs_ExcludeLong(t *testing.T) {
opts, err := parseArgs([]string{"--exclude", "go.sum"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(opts.excludeFiles) != 1 || opts.excludeFiles[0] != "go.sum" {
t.Errorf("expected excludeFiles=[go.sum], got %v", opts.excludeFiles)
}
}

func TestParseArgs_ExcludeLongEquals(t *testing.T) {
opts, err := parseArgs([]string{"--exclude=go.sum"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(opts.excludeFiles) != 1 || opts.excludeFiles[0] != "go.sum" {
t.Errorf("expected excludeFiles=[go.sum], got %v", opts.excludeFiles)
}
}

func TestParseArgs_ExcludeShort(t *testing.T) {
opts, err := parseArgs([]string{"-x", "go.sum"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(opts.excludeFiles) != 1 || opts.excludeFiles[0] != "go.sum" {
t.Errorf("expected excludeFiles=[go.sum], got %v", opts.excludeFiles)
}
}

func TestParseArgs_ExcludeMultiple(t *testing.T) {
opts, err := parseArgs([]string{"--exclude", "go.sum", "-x", "vendor/deps.go"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(opts.excludeFiles) != 2 {
t.Fatalf("expected 2 excludeFiles, got %v", opts.excludeFiles)
}
if opts.excludeFiles[0] != "go.sum" || opts.excludeFiles[1] != "vendor/deps.go" {
t.Errorf("expected [go.sum, vendor/deps.go], got %v", opts.excludeFiles)
}
}

func TestParseArgs_ExcludeMissingValue(t *testing.T) {
_, err := parseArgs([]string{"--exclude"})
if err == nil {
t.Fatal("expected error for missing value")
}
}

func TestParseArgs_ExcludeShortMissingValue(t *testing.T) {
_, err := parseArgs([]string{"-x"})
if err == nil {
t.Fatal("expected error for missing value")
}
}

func TestParseArgs_ExcludeShortCluster(t *testing.T) {
opts, err := parseArgs([]string{"-ax", "go.sum"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !opts.addAll {
t.Error("expected addAll to be true")
}
if len(opts.excludeFiles) != 1 || opts.excludeFiles[0] != "go.sum" {
t.Errorf("expected excludeFiles=[go.sum], got %v", opts.excludeFiles)
}
}
7 changes: 4 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"git-ai-commit/internal/prompt"
)

func Run(context, contextFile, promptName, promptFile, engineName string, amend, addAll bool, includeFiles []string, debugPrompt, debugCommand bool) (err error) {
func Run(context, contextFile, promptName, promptFile, engineName string, amend, addAll bool, includeFiles, excludeFiles []string, debugPrompt, debugCommand bool) (err error) {
cfg, err := config.Load()
if err != nil {
return err
Expand Down Expand Up @@ -45,7 +45,7 @@ func Run(context, contextFile, promptName, promptFile, engineName string, amend,
}
}

diff, err := commitDiff(amend, cfg)
diff, err := commitDiff(amend, cfg, excludeFiles)
if err != nil {
return err
}
Expand Down Expand Up @@ -141,7 +141,7 @@ func sanitizeMessage(message string) string {
return clean
}

func commitDiff(amend bool, cfg config.Config) (string, error) {
func commitDiff(amend bool, cfg config.Config, excludeFiles []string) (string, error) {
var diff string
var err error
if amend {
Expand Down Expand Up @@ -169,6 +169,7 @@ func commitDiff(amend bool, cfg config.Config) (string, error) {
opts := git.Options{
MaxFileLines: maxLines,
ExcludePatterns: patterns,
ExcludeFiles: excludeFiles,
}
result := git.Filter(diff, opts)

Expand Down
17 changes: 17 additions & 0 deletions internal/git/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func DefaultExcludePatterns() []string {
type Options struct {
MaxFileLines int // Maximum lines per file (0 = no limit)
ExcludePatterns []string // Glob patterns for files to exclude
ExcludeFiles []string // Exact file paths to exclude
}

// Result holds the filtering outcome.
Expand Down Expand Up @@ -63,6 +64,12 @@ func Filter(diff string, opts Options) Result {
for _, fileName := range fileNames {
content := files[fileName]

// Check exact file exclusions
if containsFile(fileName, opts.ExcludeFiles) {
result.ExcludedFiles = append(result.ExcludedFiles, fileName)
continue
}

// Check exclusion patterns
if matchesAnyPattern(fileName, opts.ExcludePatterns) {
result.ExcludedFiles = append(result.ExcludedFiles, fileName)
Expand Down Expand Up @@ -212,6 +219,16 @@ func truncateFileDiff(content string, maxLines int, fileName string) (bool, stri
return true, result.String()
}

// containsFile checks if filePath is in the given list of exact paths.
func containsFile(filePath string, files []string) bool {
for _, f := range files {
if f == filePath {
return true
}
}
return false
}

// matchesAnyPattern checks if the file path matches any of the glob patterns.
func matchesAnyPattern(filePath string, patterns []string) bool {
for _, pattern := range patterns {
Expand Down
147 changes: 147 additions & 0 deletions internal/git/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,153 @@ func TestMatchPattern_DoubleStarGlob(t *testing.T) {
}
}

func TestFilter_ExcludeFilesByExactPath(t *testing.T) {
diff := `diff --git a/go.sum b/go.sum
index abc123..def456 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+module v1.0.0 h1:abc=
diff --git a/main.go b/main.go
index abc123..def456 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,4 @@
+code
`
result := Filter(diff, Options{
ExcludeFiles: []string{"go.sum"},
})

if len(result.ExcludedFiles) != 1 || result.ExcludedFiles[0] != "go.sum" {
t.Errorf("expected excluded file 'go.sum', got %v", result.ExcludedFiles)
}
if strings.Contains(result.Diff, "go.sum") {
t.Error("expected go.sum to be excluded from diff")
}
if !strings.Contains(result.Diff, "main.go") {
t.Error("expected main.go to be in diff")
}
}

func TestFilter_ExcludeFilesMultiple(t *testing.T) {
diff := `diff --git a/go.sum b/go.sum
index abc123..def456 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+sum content
diff --git a/main.go b/main.go
index abc123..def456 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,4 @@
+code
diff --git a/vendor/deps.go b/vendor/deps.go
index abc123..def456 100644
--- a/vendor/deps.go
+++ b/vendor/deps.go
@@ -1,3 +1,4 @@
+deps
`
result := Filter(diff, Options{
ExcludeFiles: []string{"go.sum", "vendor/deps.go"},
})

if len(result.ExcludedFiles) != 2 {
t.Errorf("expected 2 excluded files, got %v", result.ExcludedFiles)
}
if strings.Contains(result.Diff, "go.sum") {
t.Error("expected go.sum to be excluded from diff")
}
if strings.Contains(result.Diff, "vendor/deps.go") {
t.Error("expected vendor/deps.go to be excluded from diff")
}
if !strings.Contains(result.Diff, "main.go") {
t.Error("expected main.go to be in diff")
}
}

func TestFilter_ExcludeFilesNoMatch(t *testing.T) {
diff := `diff --git a/main.go b/main.go
index abc123..def456 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,4 @@
+code
`
result := Filter(diff, Options{
ExcludeFiles: []string{"nonexistent.go"},
})

if len(result.ExcludedFiles) != 0 {
t.Errorf("expected no excluded files, got %v", result.ExcludedFiles)
}
if !strings.Contains(result.Diff, "main.go") {
t.Error("expected main.go to be in diff")
}
}

func TestFilter_ExcludeFilesCombinedWithPatterns(t *testing.T) {
diff := `diff --git a/go.sum b/go.sum
index abc123..def456 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+sum content
diff --git a/package-lock.json b/package-lock.json
index abc123..def456 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,3 +1,4 @@
+lock content
diff --git a/main.go b/main.go
index abc123..def456 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,4 @@
+code
`
result := Filter(diff, Options{
ExcludeFiles: []string{"go.sum"},
ExcludePatterns: []string{"**/*-lock.json"},
})

if len(result.ExcludedFiles) != 2 {
t.Errorf("expected 2 excluded files, got %v", result.ExcludedFiles)
}
if strings.Contains(result.Diff, "go.sum") {
t.Error("expected go.sum to be excluded from diff")
}
if strings.Contains(result.Diff, "package-lock.json") {
t.Error("expected package-lock.json to be excluded from diff")
}
if !strings.Contains(result.Diff, "main.go") {
t.Error("expected main.go to be in diff")
}
}

func TestContainsFile(t *testing.T) {
tests := []struct {
filePath string
files []string
want bool
}{
{"go.sum", []string{"go.sum"}, true},
{"vendor/deps.go", []string{"vendor/deps.go"}, true},
{"main.go", []string{"go.sum", "vendor/deps.go"}, false},
{"go.sum", []string{}, false},
{"go.sum", nil, false},
}

for _, tt := range tests {
got := containsFile(tt.filePath, tt.files)
if got != tt.want {
t.Errorf("containsFile(%q, %v) = %v, want %v", tt.filePath, tt.files, got, tt.want)
}
}
}

func TestExtractFilePath(t *testing.T) {
tests := []struct {
line string
Expand Down