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
11 changes: 11 additions & 0 deletions internal/commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
"strings"
"time"

"github.com/agarcher/wt/internal/config"
"github.com/agarcher/wt/internal/git"
Expand Down Expand Up @@ -96,6 +97,16 @@ func runCreate(cmd *cobra.Command, args []string) error {
}
}

// Store creation metadata for status tracking
if err := git.SetWorktreeCreatedAt(repoRoot, name, time.Now()); err != nil {
cmd.Printf("Warning: could not store creation time: %v\n", err)
}
if initialCommit, err := git.GetCurrentCommit(worktreePath); err == nil {
if err := git.SetWorktreeInitialCommit(repoRoot, name, initialCommit); err != nil {
cmd.Printf("Warning: could not store initial commit: %v\n", err)
}
}

// Run post-create hooks
if err := hooks.RunPostCreate(cfg, env); err != nil {
cmd.Printf("Warning: post-create hook failed: %v\n", err)
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Add the following to your shell configuration file:
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), script)
return nil
_, err = fmt.Fprint(cmd.OutOrStdout(), script)
return err
},
}
179 changes: 145 additions & 34 deletions internal/commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import (
"os"
"path/filepath"
"strings"
"time"

"github.com/agarcher/wt/internal/config"
"github.com/agarcher/wt/internal/git"
"github.com/spf13/cobra"
)

var verboseFlag bool

func init() {
listCmd.Flags().BoolVarP(&verboseFlag, "verbose", "v", false, "Show detailed status for each worktree")
rootCmd.AddCommand(listCmd)
}

Expand All @@ -24,11 +28,23 @@ var listCmd = &cobra.Command{
Shows each worktree with:
- Name
- Branch
- Commits ahead/behind main branch (↑↓)
- Uncommitted changes indicator
- Unpushed commits indicator`,
- Merged status indicator

Use -v/--verbose for detailed multi-line output including worktree age.`,
RunE: runList,
}

// worktreeInfo holds display information for a worktree
type worktreeInfo struct {
name string
branch string
path string
currentMarker string
status *git.WorktreeStatus
}

func runList(cmd *cobra.Command, args []string) error {
// Find the main repository root
repoRoot, err := config.GetMainRepoRoot()
Expand All @@ -48,17 +64,20 @@ func runList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to list worktrees: %w", err)
}

// Get main branch for comparisons
mainBranch, err := git.GetDefaultBranch(repoRoot)
if err != nil {
mainBranch = "main" // Fallback
}

// Get merged branches cache for efficiency
mergedCache, _ := git.GetMergedBranches(repoRoot, mainBranch)

// Get current directory to highlight current worktree
cwd, _ := os.Getwd()
worktreesDir := filepath.Join(repoRoot, cfg.WorktreeDir)

// Collect managed worktrees (excluding main repo)
type worktreeInfo struct {
name string
branch string
statusStr string
currentMarker string
}
var managedWorktrees []worktreeInfo

for _, wt := range worktrees {
Expand All @@ -75,24 +94,8 @@ func runList(cmd *cobra.Command, args []string) error {
// Get worktree name
name := git.GetWorktreeName(repoRoot, wt.Path, cfg.WorktreeDir)

// Check status
var status []string

hasChanges, err := git.HasUncommittedChanges(wt.Path)
if err == nil && hasChanges {
status = append(status, "uncommitted")
}

hasUnpushed, err := git.HasUnpushedCommits(wt.Path)
if err == nil && hasUnpushed {
status = append(status, "unpushed")
}

// Build status string
statusStr := ""
if len(status) > 0 {
statusStr = fmt.Sprintf("[%s]", strings.Join(status, ", "))
}
// Get full worktree status
status, _ := git.GetWorktreeStatus(repoRoot, wt.Path, name, wt.Branch, mainBranch, mergedCache)

// Check if this is the current worktree
currentMarker := " "
Expand All @@ -103,8 +106,9 @@ func runList(cmd *cobra.Command, args []string) error {
managedWorktrees = append(managedWorktrees, worktreeInfo{
name: name,
branch: wt.Branch,
statusStr: statusStr,
path: wt.Path,
currentMarker: currentMarker,
status: status,
})
}

Expand All @@ -114,16 +118,123 @@ func runList(cmd *cobra.Command, args []string) error {
return nil
}

// Print header and worktrees to stdout
// Print based on verbose flag
if verboseFlag {
printVerboseWorktrees(cmd, managedWorktrees)
} else {
printCompactWorktrees(cmd, managedWorktrees)
}

return nil
}

// formatCompactStatus builds the compact status string with arrows
// Status priority: uncommitted > new > merged (mutually exclusive for the state indicator)
func formatCompactStatus(status *git.WorktreeStatus) string {
var parts []string

if status.CommitsAhead > 0 {
parts = append(parts, fmt.Sprintf("↑%d", status.CommitsAhead))
}
if status.CommitsBehind > 0 {
parts = append(parts, fmt.Sprintf("↓%d", status.CommitsBehind))
}

// State indicator: uncommitted takes priority, then new, then merged
if status.HasUncommittedChanges {
parts = append(parts, "[uncommitted]")
} else if status.IsNew {
parts = append(parts, "[new]")
} else if status.IsMerged && status.CommitsAhead == 0 {
parts = append(parts, "[merged]")
}

return strings.Join(parts, " ")
}

// printCompactWorktrees prints worktrees in compact table format
func printCompactWorktrees(cmd *cobra.Command, worktrees []worktreeInfo) {
out := cmd.OutOrStdout()
_, _ = fmt.Fprintf(out, " %-20s %-30s %s\n", "NAME", "BRANCH", "STATUS")
for _, wt := range worktrees {
statusStr := formatCompactStatus(wt.status)
_, _ = fmt.Fprintf(out, "%s%-20s %-30s %s\n", wt.currentMarker, wt.name, wt.branch, statusStr)
}
}

// printVerboseWorktrees prints worktrees in detailed multi-line format
func printVerboseWorktrees(cmd *cobra.Command, worktrees []worktreeInfo) {
out := cmd.OutOrStdout()
fmt.Fprintf(out, " %-20s %s\n", "NAME", "BRANCH")
for _, wt := range managedWorktrees {
if wt.statusStr != "" {
fmt.Fprintf(out, "%s%-20s %-30s %s\n", wt.currentMarker, wt.name, wt.branch, wt.statusStr)
} else {
fmt.Fprintf(out, "%s%-20s %s\n", wt.currentMarker, wt.name, wt.branch)
separator := strings.Repeat("=", 80)

for _, wt := range worktrees {
_, _ = fmt.Fprintln(out, separator)
_, _ = fmt.Fprintf(out, "%s%s\n", wt.currentMarker, wt.name)
_, _ = fmt.Fprintf(out, " Branch: %s\n", wt.branch)

// Age
if !wt.status.CreatedAt.IsZero() {
age := formatAge(time.Since(wt.status.CreatedAt))
_, _ = fmt.Fprintf(out, " Age: %s\n", age)
}

// Ahead/Behind
if wt.status.CommitsAhead > 0 || wt.status.CommitsBehind > 0 {
aheadStr := "commit"
if wt.status.CommitsAhead != 1 {
aheadStr = "commits"
}
behindStr := "commit"
if wt.status.CommitsBehind != 1 {
behindStr = "commits"
}
_, _ = fmt.Fprintf(out, " Ahead: %d %s Behind: %d %s\n",
wt.status.CommitsAhead, aheadStr,
wt.status.CommitsBehind, behindStr)
}

// Status indicator: uncommitted > new > merged (mutually exclusive)
var statusLabel string
if wt.status.HasUncommittedChanges {
statusLabel = "uncommitted changes"
} else if wt.status.IsNew {
statusLabel = "new"
} else if wt.status.IsMerged && wt.status.CommitsAhead == 0 {
statusLabel = "merged"
}
if statusLabel != "" {
_, _ = fmt.Fprintf(out, " Status: %s\n", statusLabel)
}
}
_, _ = fmt.Fprintln(out, separator)
}

return nil
// formatAge formats a duration as a human-readable age string
func formatAge(d time.Duration) string {
days := int(d.Hours() / 24)

if days == 0 {
hours := int(d.Hours())
if hours == 0 {
return "less than an hour"
}
if hours == 1 {
return "1 hour"
}
return fmt.Sprintf("%d hours", hours)
}

if days == 1 {
return "1 day"
}

weeks := days / 7
if weeks >= 1 {
if weeks == 1 {
return "1 week"
}
return fmt.Sprintf("%d weeks", weeks)
}

return fmt.Sprintf("%d days", days)
}
Loading
Loading