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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ See [Commands Reference](docs/commands.md) for full documentation.
- `stack reparent <new-parent>` - Change the parent of the current branch
- `stack worktree <branch-name>` - Create a worktree for a branch

## Configuration

Configure the base branch and worktrees directory with git config:

```bash
git config stack.baseBranch develop # Default is "main"
git config stack.worktreesDir ~/worktrees
```

Or use the interactive helper:

```bash
stack config set
```

## Documentation

- [How It Works](docs/how-it-works.md) - Stack tracking and sync algorithm
Expand Down
131 changes: 131 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package cmd

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/javoire/stackinator/internal/git"
"github.com/javoire/stackinator/internal/ui"
"github.com/spf13/cobra"
)

var configStdin io.Reader = os.Stdin

var configCmd = &cobra.Command{
Use: "config",
Short: "Show stackinator configuration for this repository",
Long: `Show stackinator configuration for this repository.

Use 'stack config set' to update settings.`,
Run: func(cmd *cobra.Command, args []string) {
gitClient := git.NewGitClient()
if err := runConfigShow(gitClient); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}

var configGetCmd = &cobra.Command{
Use: "get",
Short: "Show stackinator configuration for this repository",
Run: func(cmd *cobra.Command, args []string) {
gitClient := git.NewGitClient()
if err := runConfigShow(gitClient); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}

var configSetCmd = &cobra.Command{
Use: "set",
Short: "Interactively configure stackinator settings",
Long: `Interactively configure stackinator settings.

Currently supports the worktrees directory location.`,
Run: func(cmd *cobra.Command, args []string) {
gitClient := git.NewGitClient()
if err := runConfigSet(gitClient); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}

func init() {
configCmd.AddCommand(configGetCmd)
configCmd.AddCommand(configSetCmd)
}

func runConfigShow(gitClient git.GitClient) error {
configured := strings.TrimSpace(gitClient.GetConfig("stack.worktreesDir"))
if configured == "" {
configured = "(default)"
}

effective, err := getWorktreesBaseDir(gitClient)
if err != nil {
return err
}

fmt.Println("Worktrees directory:")
fmt.Printf(" Configured: %s\n", configured)
fmt.Printf(" Effective: %s\n", effective)

return nil
}

func runConfigSet(gitClient git.GitClient) error {
defaultDir, err := getDefaultWorktreesBaseDir()
if err != nil {
return err
}

repoRoot, err := gitClient.GetRepoRoot()
if err != nil {
return err
}

projectDir := filepath.Join(repoRoot, ".worktrees")

fmt.Println("Choose worktrees directory:")
fmt.Printf(" 1) %s (default)\n", defaultDir)
fmt.Printf(" 2) %s (this repo)\n", projectDir)
fmt.Printf("Select [1/2] (default 1): ")

reader := bufio.NewReader(configStdin)
input, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}

choice := strings.TrimSpace(input)
if choice == "" {
choice = "1"
}

var configValue string
switch choice {
case "1":
configValue = "~/.stack/worktrees"
case "2":
configValue = ".worktrees"
default:
return fmt.Errorf("invalid selection: %s", choice)
}

if err := gitClient.SetConfig("stack.worktreesDir", configValue); err != nil {
return fmt.Errorf("failed to set worktrees directory: %w", err)
}

if !dryRun {
fmt.Println(ui.Success(fmt.Sprintf("Worktrees directory set to %s", configValue)))
}

return nil
}
123 changes: 123 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"errors"
"strings"
"testing"

"github.com/javoire/stackinator/internal/testutil"
"github.com/stretchr/testify/assert"
)

func TestGetWorktreesBaseDir(t *testing.T) {
t.Setenv("HOME", "/home/test")

tests := []struct {
name string
config string
repoRoot string
repoErr error
expected string
}{
{
name: "default when no config",
config: "",
repoRoot: "/repo",
expected: "/home/test/.stack/worktrees",
},
{
name: "tilde expands",
config: "~/worktrees",
repoRoot: "/repo",
expected: "/home/test/worktrees",
},
{
name: "env expands",
config: "$HOME/custom",
repoRoot: "/repo",
expected: "/home/test/custom",
},
{
name: "relative uses repo root",
config: ".worktrees",
repoRoot: "/repo",
expected: "/repo/.worktrees",
},
{
name: "relative falls back to home when repo root missing",
config: ".worktrees",
repoErr: errors.New("no repo"),
expected: "/home/test/.worktrees",
},
{
name: "absolute kept as is",
config: "/abs/worktrees",
repoRoot: "/repo",
expected: "/abs/worktrees",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
mockGit := &testutil.MockGitClient{}
mockGit.On("GetRepoRoot").Return(tt.repoRoot, tt.repoErr)
mockGit.On("GetConfig", "stack.worktreesDir").Return(tt.config)

dir, err := getWorktreesBaseDir(mockGit)
assert.NoError(t, err)
assert.Equal(t, tt.expected, dir)
mockGit.AssertExpectations(t)
})
}
}

func TestRunConfigSet(t *testing.T) {
t.Setenv("HOME", "/home/test")

tests := []struct {
name string
input string
expectValue string
expectErr bool
}{
{
name: "default selection",
input: "\n",
expectValue: "~/.stack/worktrees",
},
{
name: "select repo local",
input: "2\n",
expectValue: ".worktrees",
},
{
name: "invalid selection",
input: "3\n",
expectErr: true,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
mockGit := &testutil.MockGitClient{}
mockGit.On("GetRepoRoot").Return("/repo", nil)
if !tt.expectErr {
mockGit.On("SetConfig", "stack.worktreesDir", tt.expectValue).Return(nil)
}

previousStdin := configStdin
configStdin = strings.NewReader(tt.input)
defer func() { configStdin = previousStdin }()

err := runConfigSet(mockGit)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockGit.AssertExpectations(t)
})
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func init() {
rootCmd.AddCommand(renameCmd)
rootCmd.AddCommand(reparentCmd)
rootCmd.AddCommand(worktreeCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
}
Expand Down
Loading