Skip to content
Draft
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
113 changes: 25 additions & 88 deletions cmd/worktree.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"bufio"
"fmt"
"os"
"path/filepath"
Expand All @@ -17,8 +16,8 @@ var worktreePrune bool

var worktreeCmd = &cobra.Command{
Use: "worktree <branch-name> [base-branch]",
Short: "Create a worktree in .worktrees/ directory",
Long: `Create a git worktree in the .worktrees/ directory for the specified branch.
Short: "Create a worktree in ~/.stack/worktrees/<reponame> directory",
Long: `Create a git worktree in the ~/.stack/worktrees/<reponame> directory for the specified 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
Expand Down Expand Up @@ -76,19 +75,20 @@ func init() {
}

func runWorktree(gitClient git.GitClient, githubClient github.GitHubClient, branchName, baseBranch string) error {
// Get repo root
repoRoot, err := gitClient.GetRepoRoot()
// Get home directory
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get repo root: %w", err)
return fmt.Errorf("failed to get home directory: %w", err)
}

// Ensure .worktrees is in .gitignore
if err := ensureWorktreesIgnored(repoRoot); err != nil {
return fmt.Errorf("failed to update .gitignore: %w", err)
// Get repository name
repoName, err := gitClient.GetRepoName()
if err != nil {
return fmt.Errorf("failed to get repo name: %w", err)
}

// Worktree path
worktreePath := filepath.Join(repoRoot, ".worktrees", branchName)
// Worktree path: ~/.stack/worktrees/<reponame>/<branchname>
worktreePath := filepath.Join(homeDir, ".stack", "worktrees", repoName, branchName)

// Check if worktree already exists
if _, err := os.Stat(worktreePath); err == nil {
Expand Down Expand Up @@ -196,17 +196,23 @@ func createWorktreeForExisting(gitClient git.GitClient, branchName, worktreePath
}

func runWorktreePrune(gitClient git.GitClient, githubClient github.GitHubClient) error {
// Get repo root
repoRoot, err := gitClient.GetRepoRoot()
// Get home directory
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get repo root: %w", err)
return fmt.Errorf("failed to get home directory: %w", err)
}

worktreesDir := filepath.Join(repoRoot, ".worktrees")
// Get repository name
repoName, err := gitClient.GetRepoName()
if err != nil {
return fmt.Errorf("failed to get repo name: %w", err)
}

// Check if .worktrees directory exists
worktreesDir := filepath.Join(homeDir, ".stack", "worktrees", repoName)

// Check if ~/.stack/worktrees/<reponame> directory exists
if _, err := os.Stat(worktreesDir); os.IsNotExist(err) {
fmt.Println("No .worktrees directory found.")
fmt.Printf("No ~/.stack/worktrees/%s directory found.\n", repoName)
return nil
}

Expand All @@ -216,7 +222,7 @@ func runWorktreePrune(gitClient git.GitClient, githubClient github.GitHubClient)
return fmt.Errorf("failed to list worktrees: %w", err)
}

// Filter to only worktrees in .worktrees/ directory
// Filter to only worktrees in ~/.stack/worktrees/<reponame> directory
var worktreesToCheck []struct {
path string
branch string
Expand All @@ -231,7 +237,7 @@ func runWorktreePrune(gitClient git.GitClient, githubClient github.GitHubClient)
}

if len(worktreesToCheck) == 0 {
fmt.Println("No worktrees found in .worktrees/ directory.")
fmt.Printf("No worktrees found in ~/.stack/worktrees/%s directory.\n", repoName)
return nil
}

Expand Down Expand Up @@ -291,72 +297,3 @@ func runWorktreePrune(gitClient git.GitClient, githubClient github.GitHubClient)

return nil
}

func ensureWorktreesIgnored(repoRoot string) error {
gitignorePath := filepath.Join(repoRoot, ".gitignore")

// Check if .worktrees is already in .gitignore
if _, err := os.Stat(gitignorePath); err == nil {
file, err := os.Open(gitignorePath)
if err != nil {
return err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == ".worktrees" || line == ".worktrees/" {
return nil // Already ignored
}
}
if err := scanner.Err(); err != nil {
return err
}
}

if dryRun {
fmt.Println(" [DRY RUN] Adding .worktrees to .gitignore")
return nil
}

// Append .worktrees to .gitignore
file, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()

// Check if file ends with newline, if not add one
info, err := file.Stat()
if err != nil {
return err
}

var prefix string
if info.Size() > 0 {
// Read last byte to check for newline
tempFile, err := os.Open(gitignorePath)
if err != nil {
return err
}
defer tempFile.Close()

buf := make([]byte, 1)
_, err = tempFile.ReadAt(buf, info.Size()-1)
if err != nil {
return err
}
if buf[0] != '\n' {
prefix = "\n"
}
}

_, err = file.WriteString(prefix + ".worktrees/\n")
if err != nil {
return err
}

fmt.Println("Added .worktrees/ to .gitignore")
return nil
}
14 changes: 14 additions & 0 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ func (c *gitClient) GetRepoRoot() (string, error) {
return c.runCmd("rev-parse", "--show-toplevel")
}

// GetRepoName returns the name of the git repository (directory name)
func (c *gitClient) GetRepoName() (string, error) {
repoRoot, err := c.GetRepoRoot()
if err != nil {
return "", err
}
// Extract the last component of the path (the directory name)
parts := strings.Split(repoRoot, "/")
if len(parts) == 0 {
return "", fmt.Errorf("invalid repo root path: %s", repoRoot)
}
return parts[len(parts)-1], nil
}

// GetCurrentBranch returns the name of the currently checked out branch
func (c *gitClient) GetCurrentBranch() (string, error) {
return c.runCmd("branch", "--show-current")
Expand Down
1 change: 1 addition & 0 deletions internal/git/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git
// GitClient defines the interface for all git operations
type GitClient interface {
GetRepoRoot() (string, error)
GetRepoName() (string, error)
GetCurrentBranch() (string, error)
ListBranches() ([]string, error)
GetConfig(key string) string
Expand Down
5 changes: 5 additions & 0 deletions internal/testutil/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ func (m *MockGitClient) GetRepoRoot() (string, error) {
return args.String(0), args.Error(1)
}

func (m *MockGitClient) GetRepoName() (string, error) {
args := m.Called()
return args.String(0), args.Error(1)
}

func (m *MockGitClient) GetCurrentBranch() (string, error) {
args := m.Called()
return args.String(0), args.Error(1)
Expand Down