diff --git a/.claude/claude.md b/.claude/claude.md index b8660be..86844c0 100644 --- a/.claude/claude.md +++ b/.claude/claude.md @@ -10,8 +10,7 @@ - `list ` — list all branches in a folder - `increment [folder]` — create and check out the next numbered branch; errors if not on the max branch when folder is inferred - `squash` — increment + squash all commits since trunk divergence into one -- `delete [--force] ` — delete all branches in folder (with confirmation) -- `delete-upto [--force] ` — delete numeric branches below n +- `delete [--force] [--upto ] ` — delete branches in folder (with confirmation); `--upto ` deletes only numeric branches below n - `rename [--force] ` — rename a folder prefix **Plumbing:** diff --git a/README.md b/README.md index fed8ccb..72edf8d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ async/temp I know I can safely get rid of most of those now, so I do: ``` -$ git folder delete-upto async 4 +$ git folder delete --upto 4 async keep: async/4 async/5 diff --git a/cmd/git-folder/USAGE.txt b/cmd/git-folder/USAGE.txt index 13fd937..aa7f707 100644 --- a/cmd/git-folder/USAGE.txt +++ b/cmd/git-folder/USAGE.txt @@ -1,7 +1,6 @@ usage: git folder list or: git folder increment [] - or: git folder delete [--force] - or: git folder delete-upto [--force] + or: git folder delete [--force] [--upto ] [] or: git folder squash or: git folder rename [--force] or: git folder max [branch|number] [] diff --git a/cmd/git-folder/main.go b/cmd/git-folder/main.go index ecacc32..b902c17 100644 --- a/cmd/git-folder/main.go +++ b/cmd/git-folder/main.go @@ -130,8 +130,6 @@ func main() { err = cmdMax(args[1:]) case "delete": err = cmdDelete(args[1:]) - case "delete-upto": - err = cmdDeleteUpto(args[1:]) case "squash": err = cmdSquash() case "rename": @@ -308,7 +306,27 @@ func cmdIncrement(args []string) error { } func cmdDelete(args []string) error { - name, err := resolveFolder(args) + var uptoSet bool + var upto float64 + var rest []string + for i := 0; i < len(args); i++ { + if args[i] == "--upto" { + if i+1 >= len(args) { + return fmt.Errorf("--upto requires a number") + } + n, err := strconv.ParseFloat(args[i+1], 64) + if err != nil { + return fmt.Errorf("invalid --upto number: %s", args[i+1]) + } + upto = n + uptoSet = true + i++ + } else { + rest = append(rest, args[i]) + } + } + + name, err := resolveFolder(rest) if err != nil { return err } @@ -321,66 +339,21 @@ func cmdDelete(args []string) error { return fmt.Errorf("no branches in folder %s/", name) } - detach, err := branchesPreflight(branches) - if err != nil { - return err - } - - fmt.Printf("delete all branches in folder %s/:\n", name) - for _, b := range branches { - fmt.Printf(" %s\n", b) - } - - if !confirm("confirm? [yN] ") { - fmt.Println("aborted") - return nil - } - - if detach != "" { - if err := detachHEAD(detach); err != nil { - return err + var toKeep, toDelete []string + if uptoSet { + for _, b := range branches { + num, ok := folder.NumberFloat(b) + if ok && num < upto { + toDelete = append(toDelete, b) + } else { + toKeep = append(toKeep, b) + } } - } - for _, b := range branches { - if err := gitExec("branch", "-D", b); err != nil { - return fmt.Errorf("failed to delete %s: %w", b, err) + if len(toDelete) == 0 { + return fmt.Errorf("no numbered branches below %v in folder %s/", upto, name) } - } - return nil -} - -func cmdDeleteUpto(args []string) error { - if len(args) != 2 { - return fmt.Errorf("usage: git folder delete-upto ") - } - - folderName := args[0] - if err := validateFolder(folderName); err != nil { - return err - } - n, err := strconv.ParseFloat(args[1], 64) - if err != nil { - return fmt.Errorf("invalid number: %s", args[1]) - } - - branches, err := folder.Enumerate(folderName) - if err != nil { - return err - } - - var toKeep []string - var toDelete []string - for _, b := range branches { - num, ok := folder.NumberFloat(b) - if ok && num < n { - toDelete = append(toDelete, b) - } else { - toKeep = append(toKeep, b) - } - } - - if len(toDelete) == 0 { - return fmt.Errorf("no numbered branches below %v in folder %s/", n, folderName) + } else { + toDelete = branches } detach, err := branchesPreflight(toDelete) @@ -388,14 +361,21 @@ func cmdDeleteUpto(args []string) error { return err } - fmt.Printf("keep:\n") - for _, b := range toKeep { - fmt.Printf(" %s\n", b) - } - fmt.Println() - fmt.Printf("delete:\n") - for _, b := range toDelete { - fmt.Printf(" %s\n", b) + if uptoSet { + fmt.Printf("keep:\n") + for _, b := range toKeep { + fmt.Printf(" %s\n", b) + } + fmt.Println() + fmt.Printf("delete:\n") + for _, b := range toDelete { + fmt.Printf(" %s\n", b) + } + } else { + fmt.Printf("delete all branches in folder %s/:\n", name) + for _, b := range toDelete { + fmt.Printf(" %s\n", b) + } } if !confirm("confirm? [yN] ") { @@ -568,8 +548,7 @@ _git-folder() { 'list:list branches in a folder' 'max:print max branch or number' 'increment:create next numbered branch' - 'delete:delete all branches in a folder' - 'delete-upto:delete numbered branches below n' + 'delete:delete all branches in a folder (use --upto N to keep N+)' 'squash:increment and squash commits' 'rename:rename a folder prefix' 'version:show version' @@ -585,8 +564,12 @@ _git-folder() { _describe 'command' commands ;; args) + # After --upto, expect a numeric threshold; don't suggest folders. + if [[ ${words[CURRENT-1]} == "--upto" ]]; then + return + fi case $words[1] in - list|delete|delete-upto|increment|rename) + list|delete|increment|rename) _git-folder-folders ;; esac diff --git a/cmd/git-folder/main_test.go b/cmd/git-folder/main_test.go index 87d3064..6b060d5 100644 --- a/cmd/git-folder/main_test.go +++ b/cmd/git-folder/main_test.go @@ -418,7 +418,7 @@ func initAsyncRepo(t *testing.T) string { return dir } -// --- cmdDeleteUpto --- +// --- cmdDelete --upto --- func TestCmdDeleteUptoReadmeExample(t *testing.T) { dir := initAsyncRepo(t) @@ -426,11 +426,11 @@ func TestCmdDeleteUptoReadmeExample(t *testing.T) { deleted := withFolder("async", "1", "2", "2.5", "3") assertBranchesExist(t, dir, deleted) - cmd := exec.Command(binaryPath, "delete-upto", "async", "4") + cmd := exec.Command(binaryPath, "delete", "--upto", "4", "async") cmd.Dir = dir cmd.Stdin = strings.NewReader("y\n") if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("delete-upto failed: %v\n%s", err, out) + t.Fatalf("delete --upto failed: %v\n%s", err, out) } kept := branchList(t, dir, "async/*") @@ -454,7 +454,7 @@ func TestCmdDeleteUptoConfirm(t *testing.T) { withStdin(t, "y\n") - err := cmdDeleteUpto([]string{"x", "3"}) + err := cmdDelete([]string{"--upto", "3", "x"}) if err != nil { t.Fatal(err) } @@ -484,7 +484,7 @@ func TestCmdDeleteUptoFloatThreshold(t *testing.T) { withStdin(t, "y\n") - err := cmdDeleteUpto([]string{"x", "2.5"}) + err := cmdDelete([]string{"--upto", "2.5", "x"}) if err != nil { t.Fatal(err) } @@ -510,7 +510,7 @@ func TestCmdDeleteUptoAbort(t *testing.T) { withStdin(t, "n\n") - err := cmdDeleteUpto([]string{"y", "2"}) + err := cmdDelete([]string{"--upto", "2", "y"}) if err != nil { t.Fatal(err) } @@ -527,27 +527,24 @@ func TestCmdDeleteUptoNoneBelow(t *testing.T) { run(t, dir, "git", "branch", "z/5") - err := cmdDeleteUpto([]string{"z", "1"}) + err := cmdDelete([]string{"--upto", "1", "z"}) if err == nil { t.Fatal("expected error when no branches below n") } } func TestCmdDeleteUptoBadArgs(t *testing.T) { - if err := cmdDeleteUpto(nil); err == nil { - t.Fatal("expected error for no args") - } - if err := cmdDeleteUpto([]string{"a"}); err == nil { - t.Fatal("expected error for one arg") + if err := cmdDelete([]string{"--upto"}); err == nil { + t.Fatal("expected error for missing upto value") } - if err := cmdDeleteUpto([]string{"a", "notanumber"}); err == nil { - t.Fatal("expected error for non-numeric arg") + if err := cmdDelete([]string{"--upto", "notanumber", "a"}); err == nil { + t.Fatal("expected error for non-numeric upto") } } func TestCmdDeleteUptoNotARepo(t *testing.T) { inNonRepo(t) - if err := cmdDeleteUpto([]string{"foo", "3"}); err == nil { + if err := cmdDelete([]string{"--upto", "3", "foo"}); err == nil { t.Fatal("expected error outside git repo") } } @@ -869,7 +866,7 @@ func TestForceFlag(t *testing.T) { forceFlag = false }) - t.Run("delete-upto with --force", func(t *testing.T) { + t.Run("delete --upto with --force", func(t *testing.T) { dir := initTestRepo(t) inDir(t, dir) run(t, dir, "git", "checkout", "-b", "upto/1") @@ -878,12 +875,12 @@ func TestForceFlag(t *testing.T) { run(t, dir, "git", "checkout", "main") forceFlag = false - args := parseGlobalFlags([]string{"--force", "delete-upto", "upto", "3"}) + args := parseGlobalFlags([]string{"--force", "delete", "--upto", "3", "upto"}) if !forceFlag { t.Fatal("--force flag not parsed") } - err := cmdDeleteUpto(args[1:]) + err := cmdDelete(args[1:]) if err != nil { t.Fatal(err) } @@ -989,7 +986,7 @@ func TestDeleteUptoCheckedOutBranch(t *testing.T) { run(t, dir, "git", "checkout", "test/1") forceFlag = false - err := cmdDeleteUpto([]string{"test", "3"}) + err := cmdDelete([]string{"--upto", "3", "test"}) if err == nil { t.Fatal("expected error when deleting checked-out branch") } @@ -1014,7 +1011,7 @@ func TestDeleteUptoCheckedOutBranch(t *testing.T) { forceFlag = true defer func() { forceFlag = false }() - err := cmdDeleteUpto([]string{"test", "3"}) + err := cmdDelete([]string{"--upto", "3", "test"}) if err != nil { t.Fatalf("unexpected error with --force: %v", err) } diff --git a/git-folder.1 b/git-folder.1 index c7d1daf..d941102 100644 --- a/git-folder.1 +++ b/git-folder.1 @@ -9,11 +9,9 @@ git\-folder \- manage groups of git branches as folders git folder list git folder increment [] git folder squash -git folder delete [\-\-force] -git folder delete\-upto [\-\-force] +git folder delete [\-\-force] [\-\-upto ] [] git folder rename [\-\-force] -git folder max branch -git folder max number +git folder max [branch|number] [] .EE .SH DESCRIPTION \f[B]git\-folder\f[R] manages groups of related branches under a common @@ -39,14 +37,12 @@ Detects the trunk branch automatically. Errors if the current branch is not a folder branch or not the max branch. .TP -\f[B]delete\f[R] \f[I]\f[R] +\f[B]delete\f[R] [\f[B]\(enupto\f[R] \f[I]\f[R]] \f[I][]\f[R] Delete all branches in the folder. -Prompts for confirmation unless \f[CR]\-\-force\f[R] is set. -.TP -\f[B]delete\-upto\f[R] \f[I]\f[R] \f[I]\f[R] -Delete all numeric branches in the folder with suffix less than -\f[I]n\f[R]. -Non\-numeric branches (e.g.\ \f[CR]async/temp\f[R]) are preserved. +If \f[I]folder\f[R] is omitted, infers from the current branch. +With \f[B]\(enupto\f[R] \f[I]\f[R], delete only numeric branches with +suffix less than \f[I]n\f[R]; non\-numeric branches (e.g. +\f[CR]async/temp\f[R]) are preserved. Prompts for confirmation unless \f[CR]\-\-force\f[R] is set. .TP \f[B]rename\f[R] \f[I]\f[R] \f[I]\f[R] @@ -54,13 +50,12 @@ Rename all branches from prefix \f[I]old\f[R] to \f[I]new\f[R]. Prompts for confirmation unless \f[CR]\-\-force\f[R] is set. .SH LOW\-LEVEL COMMANDS (PLUMBING) .TP -\f[B]max branch\f[R] \f[I]\f[R] -Print the full name of the max branch in the folder -(e.g.\ \f[CR]async/4\f[R]). -.TP -\f[B]max number\f[R] \f[I]\f[R] -Print the numeric suffix of the max branch in the folder +\f[B]max\f[R] [\f[B]branch\f[R]|\f[B]number\f[R]] [\f[I]\f[R]] +Print the max branch in the folder. +With \f[B]branch\f[R], prints the full name (e.g.\ \f[CR]async/4\f[R]). +With \f[B]number\f[R] (the default), prints the numeric suffix (e.g.\ \f[CR]4\f[R]). +If \f[I]folder\f[R] is omitted, infers from the current branch. .SH OPTIONS .TP \f[B]\(enforce\f[R], \f[B]\-f\f[R] @@ -76,8 +71,8 @@ If the current branch contains no \f[CR]/\f[R], the command fails. Numeric branch suffixes may be integers or decimals (e.g.\ \f[CR]2\f[R], \f[CR]2.5\f[R]). Non\-numeric suffixes (e.g.\ \f[CR]temp\f[R], \f[CR]bigbooty\f[R]) are -treated as opaque and ignored by \f[B]increment\f[R] and -\f[B]delete\-upto\f[R]. +treated as opaque and ignored by \f[B]increment\f[R] and \f[B]delete +\(enupto\f[R]. .SH COMPLETION zsh completion is available via: .IP @@ -111,7 +106,7 @@ async/temp Delete old branches, keeping from 4 onward: .IP .EX -$ git folder delete\-upto async 4 +$ git folder delete \-\-upto 4 async keep: async/4 async/bigbooty diff --git a/git-folder.1.md b/git-folder.1.md index 037aa1b..dc12aca 100644 --- a/git-folder.1.md +++ b/git-folder.1.md @@ -12,8 +12,7 @@ git-folder - manage groups of git branches as folders git folder list git folder increment [] git folder squash -git folder delete [--force] -git folder delete-upto [--force] +git folder delete [--force] [--upto ] [] git folder rename [--force] git folder max [branch|number] [] ``` @@ -41,14 +40,12 @@ git folder max [branch|number] [] automatically. Errors if the current branch is not a folder branch or not the max branch. -**delete** *\* -: Delete all branches in the folder. Prompts for confirmation unless - `--force` is set. - -**delete-upto** *\* *\* -: Delete all numeric branches in the folder with suffix less than *n*. - Non-numeric branches (e.g. `async/temp`) are preserved. Prompts for - confirmation unless `--force` is set. +**delete** [**--upto** *\*] *[\]* +: Delete all branches in the folder. If *folder* is omitted, infers from + the current branch. With **--upto** *\*, delete only numeric + branches with suffix less than *n*; non-numeric branches (e.g. + `async/temp`) are preserved. Prompts for confirmation unless `--force` + is set. **rename** *\* *\* : Rename all branches from prefix *old* to *new*. Prompts for confirmation @@ -76,7 +73,7 @@ contains no `/`, the command fails. Numeric branch suffixes may be integers or decimals (e.g. `2`, `2.5`). Non-numeric suffixes (e.g. `temp`, `bigbooty`) are treated as opaque and -ignored by **increment** and **delete-upto**. +ignored by **increment** and **delete --upto**. # COMPLETION @@ -114,7 +111,7 @@ async/temp Delete old branches, keeping from 4 onward: ``` -$ git folder delete-upto async 4 +$ git folder delete --upto 4 async keep: async/4 async/bigbooty