Skip to content
Open
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
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ functionality with automated setup, branch tracking, and project-specific hooks.
solution:** `wtp add feature/auth`

wtp automatically generates sensible paths based on branch names. Your
`feature/auth` branch goes to `../worktrees/feature/auth` - no redundant typing,
no path errors.
`feature/auth` branch goes to `.git/wtp/worktrees/feature/auth` - no redundant
typing, no path errors.

### 🧹 Clean Branch Management

Expand Down Expand Up @@ -54,8 +54,8 @@ requirements.

### 📍 Instant Worktree Navigation

**git-worktree pain:** `cd ../../../worktrees/feature/auth` (if you remember the
path) **wtp solution:** `wtp cd feature/auth` with tab completion
**git-worktree pain:** `cd .git/wtp/worktrees/feature/auth` (if you remember
the path) **wtp solution:** `wtp cd feature/auth` with tab completion

Jump between worktrees instantly. Use `wtp cd @` to return to your main
worktree. No more terminal tab confusion.
Expand Down Expand Up @@ -127,20 +127,20 @@ sudo mv wtp /usr/local/bin/ # or add to PATH

```bash
# Create worktree from existing branch (local or remote)
# → Creates worktree at ../worktrees/feature/auth
# → Creates worktree at .git/wtp/worktrees/feature/auth
# Automatically tracks remote branch if not found locally
wtp add feature/auth

# Create worktree with new branch
# → Creates worktree at ../worktrees/feature/new-feature
# → Creates worktree at .git/wtp/worktrees/feature/new-feature
wtp add -b feature/new-feature

# Create new branch from specific commit
# → Creates worktree at ../worktrees/hotfix/urgent
# → Creates worktree at .git/wtp/worktrees/hotfix/urgent
wtp add -b hotfix/urgent abc1234

# Create new branch tracking a different remote branch
# → Creates worktree at ../worktrees/feature/test with branch tracking origin/main
# → Creates worktree at .git/wtp/worktrees/feature/test with branch tracking origin/main
wtp add -b feature/test origin/main

# Remote branch handling examples:
Expand Down Expand Up @@ -188,7 +188,7 @@ wtp uses `.wtp.yml` for project-specific configuration:
version: "1.0"
defaults:
# Base directory for worktrees (relative to project root)
base_dir: "../worktrees"
base_dir: ".git/wtp/worktrees"

hooks:
post_create:
Expand Down Expand Up @@ -326,21 +326,21 @@ evaluates `wtp shell-init <shell>` once for your session—tab completion and

## Worktree Structure

With the default configuration (`base_dir: "../worktrees"`):
With the default configuration (`base_dir: ".git/wtp/worktrees"`):

```
<project-root>/
├── .git/
│ └── wtp/
│ └── worktrees/
│ ├── main/
│ ├── feature/
│ │ ├── auth/ # wtp add feature/auth
│ │ └── payment/ # wtp add feature/payment
│ └── hotfix/
│ └── bug-123/ # wtp add hotfix/bug-123
├── .wtp.yml
└── src/

../worktrees/
├── main/
├── feature/
│ ├── auth/ # wtp add feature/auth
│ └── payment/ # wtp add feature/payment
└── hotfix/
└── bug-123/ # wtp add hotfix/bug-123
```

Branch names with slashes are preserved as directory structure, automatically
Expand Down
6 changes: 6 additions & 0 deletions cmd/wtp/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func addCommand(_ context.Context, cmd *cli.Command) error {
return err
}

errWriter := cmd.Root().ErrWriter
if errWriter == nil {
errWriter = os.Stderr
}
warnLegacyBaseDir(errWriter, mainRepoPath)

// Create command executor
executor := command.NewRealExecutor()

Expand Down
13 changes: 12 additions & 1 deletion cmd/wtp/cd.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,27 @@ func cdToWorktree(_ context.Context, cmd *cli.Command) error {
}

// Initialize repository to check if we're in a git repo
_, err = git.NewRepository(cwd)
repo, err := git.NewRepository(cwd)
if err != nil {
return errors.NotInGitRepository()
}

mainRepoPath, err := repo.GetMainWorktreePath()
if err != nil {
mainRepoPath = repo.Path()
}

// Get the writer from cli.Command
w := cmd.Root().Writer
if w == nil {
w = os.Stdout
}
errWriter := cmd.Root().ErrWriter
if errWriter == nil {
errWriter = os.Stderr
}

warnLegacyBaseDir(errWriter, mainRepoPath)

// Use CommandExecutor-based implementation
executor := command.NewRealExecutor()
Expand Down
6 changes: 3 additions & 3 deletions cmd/wtp/cd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestCdCommand_AlwaysOutputsAbsolutePath(t *testing.T) {
HEAD abc123
branch refs/heads/main

worktree /Users/dev/project/worktrees/feature/auth
worktree /Users/dev/project/main/.git/wtp/worktrees/feature/auth
HEAD def456
branch refs/heads/feature/auth

Expand All @@ -42,13 +42,13 @@ branch refs/heads/feature/auth
{
name: "feature worktree by branch name",
worktreeName: "feature/auth",
expectedPath: "/Users/dev/project/worktrees/feature/auth",
expectedPath: "/Users/dev/project/main/.git/wtp/worktrees/feature/auth",
shouldSucceed: true,
},
{
name: "feature worktree by directory name",
worktreeName: "auth",
expectedPath: "/Users/dev/project/worktrees/feature/auth",
expectedPath: "/Users/dev/project/main/.git/wtp/worktrees/feature/auth",
shouldSucceed: true, // Directory-based resolution works as expected
},
{
Expand Down
2 changes: 1 addition & 1 deletion cmd/wtp/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ version: "1.0"
# Default settings for worktrees
defaults:
# Base directory for worktrees (relative to repository root)
base_dir: ../worktrees
base_dir: .git/wtp/worktrees

# Hooks that run after creating a worktree
hooks:
Expand Down
2 changes: 1 addition & 1 deletion cmd/wtp/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestInitCommand_Success(t *testing.T) {
// Check for required sections
assert.Contains(t, contentStr, "version: \"1.0\"")
assert.Contains(t, contentStr, "defaults:")
assert.Contains(t, contentStr, "base_dir: ../worktrees")
assert.Contains(t, contentStr, "base_dir: .git/wtp/worktrees")
assert.Contains(t, contentStr, "hooks:")
assert.Contains(t, contentStr, "post_create:")

Expand Down
46 changes: 46 additions & 0 deletions cmd/wtp/legacy_warning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

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

"github.com/satococoa/wtp/v2/internal/config"
)

const (
legacyWarningMessage = "Warning: detected legacy worktrees directory at %s\n"
legacyConfigMessage = "Set base_dir: \"../worktrees\" in %s to keep using it, or move worktrees to %s\n"
)

func warnLegacyBaseDir(errWriter io.Writer, repoRoot string) {
if errWriter == nil {
errWriter = os.Stderr
}

if config.FileExists(repoRoot) {
return
}

legacyBaseDir := filepath.Clean(filepath.Join(repoRoot, "..", "worktrees"))
info, err := os.Stat(legacyBaseDir)
if err != nil || !info.IsDir() {
return
}

entries, err := os.ReadDir(legacyBaseDir)
if err != nil || len(entries) == 0 {
return
}

newDefault := filepath.Clean(filepath.Join(repoRoot, config.DefaultBaseDir))
configPath := filepath.Join(repoRoot, config.ConfigFileName)

if _, err := fmt.Fprintf(errWriter, legacyWarningMessage, legacyBaseDir); err != nil {
return
}
if _, err := fmt.Fprintf(errWriter, legacyConfigMessage, configPath, newDefault); err != nil {
return
}
}
83 changes: 83 additions & 0 deletions cmd/wtp/legacy_warning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/satococoa/wtp/v2/internal/config"
)

func TestWarnLegacyBaseDir_WarnsWhenLegacyDirExistsAndNoConfig(t *testing.T) {
baseDir := t.TempDir()
repoRoot := filepath.Join(baseDir, "repo")
if err := os.MkdirAll(repoRoot, 0o755); err != nil {
t.Fatalf("failed to create repo root: %v", err)
}

legacyDir := filepath.Join(repoRoot, "..", "worktrees")
if err := os.MkdirAll(legacyDir, 0o755); err != nil {
t.Fatalf("failed to create legacy dir: %v", err)
}
if err := os.WriteFile(filepath.Join(legacyDir, "placeholder"), []byte("x"), 0o644); err != nil {
t.Fatalf("failed to write placeholder: %v", err)
}

var buf bytes.Buffer
warnLegacyBaseDir(&buf, repoRoot)

output := buf.String()
if output == "" {
t.Fatalf("expected warning output, got empty string")
}
if !bytes.Contains(buf.Bytes(), []byte("legacy worktrees directory")) {
t.Errorf("expected warning message to mention legacy directory, got: %s", output)
}
if !bytes.Contains(buf.Bytes(), []byte(config.ConfigFileName)) {
t.Errorf("expected warning to mention config file, got: %s", output)
}
}

func TestWarnLegacyBaseDir_NoWarningWhenConfigExists(t *testing.T) {
baseDir := t.TempDir()
repoRoot := filepath.Join(baseDir, "repo")
if err := os.MkdirAll(repoRoot, 0o755); err != nil {
t.Fatalf("failed to create repo root: %v", err)
}

configPath := filepath.Join(repoRoot, config.ConfigFileName)
if err := os.WriteFile(configPath, []byte("version: 1.0\n"), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}

legacyDir := filepath.Join(repoRoot, "..", "worktrees")
if err := os.MkdirAll(legacyDir, 0o755); err != nil {
t.Fatalf("failed to create legacy dir: %v", err)
}
if err := os.WriteFile(filepath.Join(legacyDir, "placeholder"), []byte("x"), 0o644); err != nil {
t.Fatalf("failed to write placeholder: %v", err)
}

var buf bytes.Buffer
warnLegacyBaseDir(&buf, repoRoot)

if buf.Len() != 0 {
t.Fatalf("expected no warning output, got: %s", buf.String())
}
}

func TestWarnLegacyBaseDir_NoWarningWhenLegacyMissing(t *testing.T) {
baseDir := t.TempDir()
repoRoot := filepath.Join(baseDir, "repo")
if err := os.MkdirAll(repoRoot, 0o755); err != nil {
t.Fatalf("failed to create repo root: %v", err)
}

var buf bytes.Buffer
warnLegacyBaseDir(&buf, repoRoot)

if buf.Len() != 0 {
t.Fatalf("expected no warning output, got: %s", buf.String())
}
}
6 changes: 6 additions & 0 deletions cmd/wtp/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,16 @@ func listCommand(_ context.Context, cmd *cli.Command) error {
if w == nil {
w = os.Stdout
}
errWriter := cmd.Root().ErrWriter
if errWriter == nil {
errWriter = os.Stderr
}

// Load config to get base_dir
cfg, _ := config.LoadConfig(mainRepoPath)

warnLegacyBaseDir(errWriter, mainRepoPath)

// Resolve display options
opts := resolveListDisplayOptions(cmd, w)

Expand Down
Loading
Loading