Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ See [Commands Reference](docs/commands.md) for full documentation.
- `stack prune` - Clean up branches with merged PRs
- `stack rename <new-name>` - Rename branch preserving stack relationships
- `stack parent <new-parent>` - Change the parent of the current branch
- `stack worktree <branch-name>` - Create a worktree for a branch
- `stack worktree [branch-name]` - Create a worktree, generating a randomized branch name when omitted

## Configuration

Expand Down
41 changes: 35 additions & 6 deletions cmd/worktree.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
Expand All @@ -18,20 +20,24 @@ var worktreePruneAll bool
var worktreeList bool

var worktreeCmd = &cobra.Command{
Use: "worktree <branch-name> [base-branch]",
Use: "worktree [branch-name] [base-branch]",
Short: "Create a worktree in the configured worktrees directory",
Long: `Create a git worktree in the configured worktrees directory for the specified branch.
Long: `Create a git worktree in the configured worktrees directory.

If the branch exists locally or on the remote, it will be used.
If the branch doesn't exist, a new branch will be created from the current branch
(or from base-branch if specified) and stack tracking will be set up automatically.
If no branch name is specified, a randomized branch name will be generated.
Use --list to show worktrees for this repository, or --list --all for all repos.
Use --prune to clean up worktrees for branches with merged PRs.
Use --prune --all to remove all worktrees for this repository.

By default, worktrees are created under ~/.stack/worktrees/<reponame>.
You can change this with: git config stack.worktreesDir <path> (or use 'stack config set')`,
Example: ` # Create worktree for new branch (from current branch, with stack tracking)
Example: ` # Create a worktree with a randomized branch name
stack worktree

# Create worktree for new branch (from current branch, with stack tracking)
stack worktree my-feature

# Create worktree from a fresh main branch
Expand All @@ -55,6 +61,9 @@ You can change this with: git config stack.worktreesDir <path> (or use 'stack co
# Preview without executing
stack worktree my-feature --dry-run`,
Args: func(cmd *cobra.Command, args []string) error {
if worktreePruneAll && !worktreeList && !worktreePrune {
return fmt.Errorf("--all requires --list or --prune")
}
if worktreeList {
if len(args) > 0 {
return fmt.Errorf("--list does not take arguments")
Expand All @@ -67,8 +76,8 @@ You can change this with: git config stack.worktreesDir <path> (or use 'stack co
}
return nil
}
if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("requires 1 or 2 arguments: branch name [base-branch]")
if len(args) > 2 {
return fmt.Errorf("accepts at most 2 arguments: branch name [base-branch]")
}
return nil
},
Expand All @@ -83,11 +92,23 @@ You can change this with: git config stack.worktreesDir <path> (or use 'stack co
} else if worktreePrune {
err = runWorktreePrune(gitClient, githubClient)
} else {
branchName := ""
if len(args) > 0 {
branchName = args[0]
}
var baseBranch string
if len(args) > 1 {
baseBranch = args[1]
}
err = runWorktree(gitClient, githubClient, args[0], baseBranch)
if branchName == "" {
branchName, err = generateRandomWorktreeName()
if err == nil {
fmt.Printf("Generated branch name %s\n", ui.Branch(branchName))
}
}
if err == nil {
err = runWorktree(gitClient, githubClient, branchName, baseBranch)
}
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
Expand All @@ -96,6 +117,14 @@ You can change this with: git config stack.worktreesDir <path> (or use 'stack co
},
}

func generateRandomWorktreeName() (string, error) {
randomBytes := make([]byte, 8)
if _, err := rand.Read(randomBytes); err != nil {
return "", fmt.Errorf("failed to generate random worktree name: %w", err)
}
return "worktree-" + hex.EncodeToString(randomBytes), nil
}

func init() {
worktreeCmd.Flags().BoolVarP(&worktreeList, "list", "l", false, "List all worktrees for this repository")
worktreeCmd.Flags().BoolVar(&worktreePrune, "prune", false, "Remove worktrees for branches with merged PRs")
Expand Down
43 changes: 43 additions & 0 deletions cmd/worktree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cmd

import (
"regexp"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerateRandomWorktreeName(t *testing.T) {
first, err := generateRandomWorktreeName()
require.NoError(t, err)
second, err := generateRandomWorktreeName()
require.NoError(t, err)

assert.Regexp(t, regexp.MustCompile(`^worktree-[0-9a-f]{16}$`), first)
assert.Regexp(t, regexp.MustCompile(`^worktree-[0-9a-f]{16}$`), second)
assert.NotEqual(t, first, second)
}

func TestWorktreeArgs(t *testing.T) {
originalList := worktreeList
originalPrune := worktreePrune
originalAll := worktreePruneAll
t.Cleanup(func() {
worktreeList = originalList
worktreePrune = originalPrune
worktreePruneAll = originalAll
})

worktreeList = false
worktreePrune = false
worktreePruneAll = false

assert.NoError(t, worktreeCmd.Args(worktreeCmd, nil))
assert.NoError(t, worktreeCmd.Args(worktreeCmd, []string{"feature"}))
assert.NoError(t, worktreeCmd.Args(worktreeCmd, []string{"feature", "main"}))
assert.Error(t, worktreeCmd.Args(worktreeCmd, []string{"feature", "main", "extra"}))

worktreePruneAll = true
assert.EqualError(t, worktreeCmd.Args(worktreeCmd, nil), "--all requires --list or --prune")
}
8 changes: 8 additions & 0 deletions decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Architectural and design decisions for Stackinator.

## 2026-08-17 — Generate a randomized worktree when no branch is provided

**Decision**: Allow `stack worktree` with no arguments to create a new branch and worktree using a randomized `worktree-<16 hex characters>` name.

**Context**: Creating a disposable worktree required inventing and typing a branch name even when the name itself was unimportant.

**Resolution**: Made the branch-name argument optional. No-argument invocation generates the name with the operating system's cryptographic random source, then follows the existing new-branch worktree flow so the current branch is recorded as its stack parent. Explicit branch names and worktree management flags retain their existing behavior.

## 2026-08-12 — Bound and separate sync network operations

**Decision**: Give git fetches a five-minute timeout and GitHub CLI operations a 30-second timeout. Display fetch and PR loading as separate sync progress steps, and propagate PR lookup failures.
Expand Down
7 changes: 5 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ stack rename feature-improved-name
stack rename feature-improved-name --dry-run
```

## `stack worktree <branch-name> [base-branch]`
## `stack worktree [branch-name] [base-branch]`

Create a git worktree in the configured worktrees directory for the specified branch.
Create a git worktree in the configured worktrees directory. With no arguments, Stackinator generates a randomized branch name such as `worktree-a1b2c3d4e5f60718` and creates it from the current branch.

If the branch exists locally or on the remote, it will be used. If the branch doesn't exist, a new branch will be created from the current branch (or from base-branch if specified) and stack tracking will be set up automatically.

Expand All @@ -137,6 +137,9 @@ Interactively choose where worktrees are created for this repo:
- `./.worktrees` (project-local)

```bash
# Create a worktree with a randomized branch name
stack worktree

# Create worktree for new branch (from current branch, with stack tracking)
stack worktree my-feature

Expand Down
Loading