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:
+80
-3
@@ -13,7 +13,7 @@ var version = "0.1.2"
|
||||
|
||||
func main() {
|
||||
var cli types.CLI
|
||||
kong.Parse(&cli,
|
||||
kong.Parse(&cli,
|
||||
kong.Description("Remove comments from source code files"),
|
||||
kong.Vars{"version": version})
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -3,15 +3,18 @@ package types
|
||||
import "github.com/alecthomas/kong"
|
||||
|
||||
type CLI struct {
|
||||
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
|
||||
Inline bool `help:"Remove only line comments"`
|
||||
Block bool `help:"Remove only block comments"`
|
||||
Recursive bool `short:"r" help:"Process directories recursively"`
|
||||
DryRun bool `help:"Show what would be removed without making changes"`
|
||||
Backup bool `help:"Create backup files before modification"`
|
||||
Verbose bool `help:"Show detailed output"`
|
||||
LLM bool `help:"Show LLM-friendly usage guide"`
|
||||
Version kong.VersionFlag `help:"Show version information"`
|
||||
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
|
||||
Inline bool `help:"Remove only line comments"`
|
||||
Block bool `help:"Remove only block comments"`
|
||||
Recursive bool `short:"r" help:"Process directories recursively"`
|
||||
DryRun bool `help:"Show what would be removed without making changes"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
@@ -22,4 +25,4 @@ type Language struct {
|
||||
type BlockComment struct {
|
||||
Start string
|
||||
End string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user