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:
+78
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ type CLI struct {
|
|||||||
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"`
|
||||||
|
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"`
|
Version kong.VersionFlag `help:"Show version information"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user