feat(cli): add git-aware CLI flags and validation

- Add ChangesOnly, Staged, Unstaged flags to CLI struct
- Implement git flag validation with mutual exclusion rules
- Add comprehensive flag compatibility checks
- Update LLM guide with git workflow examples and documentation
- Include test suite for CLI validation logic
This commit is contained in:
carraes
2025-07-05 22:28:13 -03:00
parent 36c10aefdf
commit 161b5e1862
3 changed files with 222 additions and 13 deletions
+80 -3
View File
@@ -13,7 +13,7 @@ var version = "0.1.2"
func main() { func main() {
var cli types.CLI var cli types.CLI
kong.Parse(&cli, kong.Parse(&cli,
kong.Description("Remove comments from source code files"), kong.Description("Remove comments from source code files"),
kong.Vars{"version": version}) kong.Vars{"version": version})
@@ -22,7 +22,32 @@ func main() {
return return
} }
if cli.Path == "" {
gitFlags := []bool{cli.ChangesOnly, cli.Staged, cli.Unstaged}
gitFlagCount := 0
for _, flag := range gitFlags {
if flag {
gitFlagCount++
}
}
if gitFlagCount > 1 {
fmt.Fprintf(os.Stderr, "Error: git flags (--changes-only, --staged, --unstaged) are mutually exclusive\n")
os.Exit(1)
}
if gitFlagCount > 0 && cli.Path != "" {
fmt.Fprintf(os.Stderr, "Error: cannot use git flags with explicit path argument\n")
os.Exit(1)
}
if gitFlagCount > 0 && cli.Recursive {
fmt.Fprintf(os.Stderr, "Error: cannot use git flags with --recursive (git handles repository scope)\n")
os.Exit(1)
}
if gitFlagCount == 0 && cli.Path == "" {
fmt.Fprintf(os.Stderr, "Error: path argument is required\n") fmt.Fprintf(os.Stderr, "Error: path argument is required\n")
os.Exit(1) os.Exit(1)
} }
@@ -150,6 +175,58 @@ shush src/ -r --inline --backup # Apply line comment removal
- **Original preservation**: Backup contains exact original content - **Original preservation**: Backup contains exact original content
- **Per-file basis**: Each processed file gets individual backup - **Per-file basis**: Each processed file gets individual backup
## Git-Aware Processing
### Git Mode Commands
` + "```bash" + `
# Process all changes (staged + unstaged + untracked)
shush --changes-only # Remove comments from all changed files
shush --changes-only --dry-run # Preview changes across entire repository
# Process only staged changes
shush --staged # Clean comments from staged files
shush --staged --dry-run --verbose # Preview staged changes with details
# Process only unstaged changes
shush --unstaged # Clean comments from unstaged work
shush --unstaged --inline # Remove only line comments from unstaged files
` + "```" + `
### Git Workflow Examples
` + "```bash" + `
# Pre-commit cleanup workflow
shush --staged --dry-run # 1. Review what will be cleaned
shush --staged --backup # 2. Clean staged changes with backup
git commit -m "Clean implementation" # 3. Commit cleaned code
# Feature development cleanup
shush --unstaged --dry-run # 1. Preview unstaged work cleanup
shush --unstaged --inline # 2. Remove only debug comments
shush --changes-only # 3. Clean all changes before review
# Safe exploration workflow
shush --changes-only --dry-run --verbose # See all changes that would be made
shush --staged --backup --verbose # Process with maximum safety
` + "```" + `
### Git Mode Behavior
- **Surgical precision**: Only processes lines that have been changed
- **Repository scope**: Automatically processes relevant files across the repo
- **Change detection**: Uses git diff to identify modified line ranges
- **Untracked files**: Processes entirely (no previous version to compare)
- **Preserves existing code**: Comments in unchanged lines remain untouched
### Git Flag Rules
- **Mutually exclusive**: Cannot combine ` + "`--staged`" + `, ` + "`--unstaged`" + `, ` + "`--changes-only`" + `
- **No explicit paths**: Git flags work on repository scope, not individual files
- **No recursive flag**: Git mode handles repository traversal automatically
- **Compatible with**: ` + "`--inline`" + `, ` + "`--block`" + `, ` + "`--dry-run`" + `, ` + "`--backup`" + `, ` + "`--verbose`" + `
### Git Error Scenarios
- **Not in repository**: Clear error message when git flags used outside git repo
- **No changes found**: Informative message when no staged/unstaged changes exist
- **Git command failures**: Graceful handling of git command errors
## Best Practices for LLM Integration ## Best Practices for LLM Integration
1. **Always start with dry-run** for unknown codebases 1. **Always start with dry-run** for unknown codebases
@@ -173,4 +250,4 @@ shush src/ -r --inline --backup # Apply line comment removal
Shush excels at safe, fast comment removal with excellent preview capabilities for confident code processing. Shush excels at safe, fast comment removal with excellent preview capabilities for confident code processing.
`) `)
} }
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"testing"
"github.com/carlosarraes/shush/internal/types"
)
func TestGitFlagValidation(t *testing.T) {
tests := []struct {
name string
cli types.CLI
shouldPass bool
expectedError string
}{
{
name: "single git flag should pass",
cli: types.CLI{Staged: true},
shouldPass: true,
},
{
name: "changes-only flag should pass",
cli: types.CLI{ChangesOnly: true},
shouldPass: true,
},
{
name: "unstaged flag should pass",
cli: types.CLI{Unstaged: true},
shouldPass: true,
},
{
name: "multiple git flags should fail",
cli: types.CLI{Staged: true, Unstaged: true},
shouldPass: false,
expectedError: "git flags (--changes-only, --staged, --unstaged) are mutually exclusive",
},
{
name: "all git flags should fail",
cli: types.CLI{ChangesOnly: true, Staged: true, Unstaged: true},
shouldPass: false,
expectedError: "git flags (--changes-only, --staged, --unstaged) are mutually exclusive",
},
{
name: "git flag with path should fail",
cli: types.CLI{Staged: true, Path: "src/"},
shouldPass: false,
expectedError: "cannot use git flags with explicit path argument",
},
{
name: "git flag with recursive should fail",
cli: types.CLI{Unstaged: true, Recursive: true},
shouldPass: false,
expectedError: "cannot use git flags with --recursive (git handles repository scope)",
},
{
name: "git flag with compatible flags should pass",
cli: types.CLI{Staged: true, DryRun: true, Verbose: true, Backup: true},
shouldPass: true,
},
{
name: "git flag with inline should pass",
cli: types.CLI{ChangesOnly: true, Inline: true},
shouldPass: true,
},
{
name: "git flag with block should pass",
cli: types.CLI{Unstaged: true, Block: true},
shouldPass: true,
},
{
name: "non-git mode with path should pass",
cli: types.CLI{Path: "src/", Recursive: true},
shouldPass: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitFlags(tt.cli)
if tt.shouldPass {
if err != nil {
t.Errorf("validateGitFlags() should pass but got error: %v", err)
}
} else {
if err == nil {
t.Errorf("validateGitFlags() should fail but passed")
} else if err.Error() != tt.expectedError {
t.Errorf("validateGitFlags() error = %v, want %v", err.Error(), tt.expectedError)
}
}
})
}
}
func validateGitFlags(cli types.CLI) error {
gitFlags := []bool{cli.ChangesOnly, cli.Staged, cli.Unstaged}
gitFlagCount := 0
for _, flag := range gitFlags {
if flag {
gitFlagCount++
}
}
if gitFlagCount > 1 {
return &GitFlagError{"git flags (--changes-only, --staged, --unstaged) are mutually exclusive"}
}
if gitFlagCount > 0 && cli.Path != "" {
return &GitFlagError{"cannot use git flags with explicit path argument"}
}
if gitFlagCount > 0 && cli.Recursive {
return &GitFlagError{"cannot use git flags with --recursive (git handles repository scope)"}
}
return nil
}
type GitFlagError struct {
message string
}
func (e *GitFlagError) Error() string {
return e.message
}
+13 -10
View File
@@ -3,15 +3,18 @@ package types
import "github.com/alecthomas/kong" import "github.com/alecthomas/kong"
type CLI struct { type CLI struct {
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""` Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
Inline bool `help:"Remove only line comments"` Inline bool `help:"Remove only line comments"`
Block bool `help:"Remove only block comments"` Block bool `help:"Remove only block comments"`
Recursive bool `short:"r" help:"Process directories recursively"` Recursive bool `short:"r" help:"Process directories recursively"`
DryRun bool `help:"Show what would be removed without making changes"` DryRun bool `help:"Show what would be removed without making changes"`
Backup bool `help:"Create backup files before modification"` Backup bool `help:"Create backup files before modification"`
Verbose bool `help:"Show detailed output"` Verbose bool `help:"Show detailed output"`
LLM bool `help:"Show LLM-friendly usage guide"` LLM bool `help:"Show LLM-friendly usage guide"`
Version kong.VersionFlag `help:"Show version information"` ChangesOnly bool `help:"Remove comments only from git changes (staged + unstaged + untracked)"`
Staged bool `help:"Remove comments only from staged git changes"`
Unstaged bool `help:"Remove comments only from unstaged git changes"`
Version kong.VersionFlag `help:"Show version information"`
} }
type Language struct { type Language struct {
@@ -22,4 +25,4 @@ type Language struct {
type BlockComment struct { type BlockComment struct {
Start string Start string
End string End string
} }