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
+78 -1
View File
@@ -22,7 +22,32 @@ func main() {
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")
os.Exit(1)
}
@@ -150,6 +175,58 @@ shush src/ -r --inline --backup # Apply line comment removal
- **Original preservation**: Backup contains exact original content
- **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
1. **Always start with dry-run** for unknown codebases
+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
}
+3
View File
@@ -11,6 +11,9 @@ type CLI struct {
Backup bool `help:"Create backup files before modification"`
Verbose bool `help:"Show detailed output"`
LLM bool `help:"Show LLM-friendly usage guide"`
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"`
}