Skip to content

Commit 601b51e

Browse files
committed
feat: enhance bt status with detailed worktree diagnostics and orphan branch listing
Replace the binary Managed/Unmanaged status with specific diagnostic flags (Outside root, Nested, Name mismatch) so users can immediately understand what is wrong. Also show local branches that have no associated worktree in a separate "Branches without worktrees" section.
1 parent 29a1dc1 commit 601b51e

3 files changed

Lines changed: 111 additions & 18 deletions

File tree

cmd/bt/status.go

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
8484
// Print worktrees
8585
fmt.Println("Worktrees:")
8686
fmt.Printf(" %-20s %-30s %s\n", "BRANCH", "PATH", "STATUS")
87-
var unmanagedWorktrees []string
87+
var warningWorktrees []worktreeWarning
8888

8989
// Determine which worktree we're currently in
9090
cwdAbs, _ := filepath.Abs(cwd)
@@ -97,6 +97,9 @@ func runStatus(cmd *cobra.Command, args []string) error {
9797
}
9898
}
9999

100+
// Track branches that have worktrees
101+
worktreeBranches := make(map[string]bool)
102+
100103
for _, wt := range worktrees {
101104
prefix := " "
102105
// Check if current directory is within this worktree
@@ -108,6 +111,8 @@ func runStatus(cmd *cobra.Command, args []string) error {
108111
branchName := wt.Branch
109112
if branchName == "" {
110113
branchName = "(detached)"
114+
} else {
115+
worktreeBranches[branchName] = true
111116
}
112117

113118
// Check if this worktree is broken (path doesn't exist)
@@ -120,22 +125,54 @@ func runStatus(cmd *cobra.Command, args []string) error {
120125
"[Broken]",
121126
" ⚠️",
122127
)
128+
warningWorktrees = append(warningWorktrees, worktreeWarning{
129+
path: wt.Path,
130+
branch: branchName,
131+
reasons: []string{"broken"},
132+
})
123133
continue
124134
}
125135

126-
// Check if managed and not nested inside another worktree
127-
managed := wtMgr.IsManaged(wt.Path) && !wtMgr.IsNestedInWorktree(wt.Path, allWorktreePaths)
136+
// Determine detailed status flags
137+
var statusParts []string
138+
var reasons []string
139+
140+
isInRepoRoot := wtMgr.IsManaged(wt.Path)
141+
isNested := wtMgr.IsNestedInWorktree(wt.Path, allWorktreePaths)
142+
143+
// Check path-branch name mismatch
144+
relPath, _ := filepath.Rel(repoRoot, wt.Path)
145+
nameMismatch := false
146+
if branchName != "(detached)" && relPath != branchName {
147+
nameMismatch = true
148+
}
149+
150+
if !isInRepoRoot {
151+
statusParts = append(statusParts, "Outside root")
152+
reasons = append(reasons, "outside-root")
153+
}
154+
if isNested {
155+
statusParts = append(statusParts, "Nested")
156+
reasons = append(reasons, "nested")
157+
}
158+
if nameMismatch {
159+
statusParts = append(statusParts, "Name mismatch")
160+
reasons = append(reasons, "name-mismatch")
161+
}
162+
128163
status := "[Managed]"
129164
statusSymbol := ""
130-
131-
if !managed {
132-
status = "[Unmanaged]"
165+
if len(statusParts) > 0 {
166+
status = "[" + strings.Join(statusParts, ", ") + "]"
133167
statusSymbol = " ⚠️"
134-
unmanagedWorktrees = append(unmanagedWorktrees, wt.Path)
168+
warningWorktrees = append(warningWorktrees, worktreeWarning{
169+
path: wt.Path,
170+
branch: branchName,
171+
relPath: relPath,
172+
reasons: reasons,
173+
})
135174
}
136175

137-
relPath, _ := filepath.Rel(repoRoot, wt.Path)
138-
139176
fmt.Printf(" %s %-20s %-30s %s%s\n",
140177
prefix,
141178
branchName,
@@ -147,8 +184,27 @@ func runStatus(cmd *cobra.Command, args []string) error {
147184

148185
fmt.Println()
149186

187+
// Print local branches without worktrees
188+
localBranches, err := wtMgr.ListLocalBranches()
189+
if err == nil {
190+
var orphanBranches []string
191+
for _, branch := range localBranches {
192+
if !worktreeBranches[branch] {
193+
orphanBranches = append(orphanBranches, branch)
194+
}
195+
}
196+
if len(orphanBranches) > 0 {
197+
fmt.Println("Branches without worktrees:")
198+
for _, branch := range orphanBranches {
199+
fmt.Printf(" %-20s (no worktree)\n", branch)
200+
}
201+
fmt.Println()
202+
}
203+
}
204+
150205
// Print warnings
151-
if len(unmanagedWorktrees) > 0 || len(brokenWorktrees) > 0 || defaultBranchMissing {
206+
hasWarnings := len(warningWorktrees) > 0 || len(brokenWorktrees) > 0 || defaultBranchMissing
207+
if hasWarnings {
152208
fmt.Println("Warnings:")
153209
if defaultBranchMissing {
154210
fmt.Printf(" - Default branch worktree '%s' does not exist\n", mgr.Config.Repository.DefaultBranch)
@@ -157,14 +213,24 @@ func runStatus(cmd *cobra.Command, args []string) error {
157213
fmt.Println(" - Use 'main' as default: bt config default-branch --unset")
158214
fmt.Println(" - Or set your default branch: bt config default-branch <branch>")
159215
}
160-
for _, path := range unmanagedWorktrees {
161-
fmt.Printf(" - Worktree at %s is outside managed directory\n", path)
162-
fmt.Printf(" Run 'bt repair %s' to fix it\n", path)
163-
}
164-
for _, bw := range brokenWorktrees {
165-
fmt.Printf(" - Worktree '%s' has broken path (moved or deleted)\n", bw.branch)
166-
fmt.Printf(" Last known: %s\n", bw.oldPath)
167-
fmt.Printf(" Run 'bt repair --fix-paths /new/path' to fix it\n")
216+
for _, ww := range warningWorktrees {
217+
for _, reason := range ww.reasons {
218+
switch reason {
219+
case "outside-root":
220+
fmt.Printf(" - Worktree '%s' at %s is outside repository root\n", ww.branch, ww.path)
221+
fmt.Printf(" Run 'bt repair %s' to move it inside\n", ww.path)
222+
case "nested":
223+
fmt.Printf(" - Worktree '%s' at %s is nested inside another worktree\n", ww.branch, ww.relPath)
224+
fmt.Printf(" Run 'bt repair %s' to fix it\n", ww.path)
225+
case "name-mismatch":
226+
fmt.Printf(" - Worktree '%s' path '%s' does not match branch name\n", ww.branch, ww.relPath)
227+
fmt.Printf(" Expected path: %s\n", ww.branch)
228+
fmt.Printf(" Run 'bt repair %s' to rename it\n", ww.path)
229+
case "broken":
230+
fmt.Printf(" - Worktree '%s' has broken path (moved or deleted)\n", ww.branch)
231+
fmt.Printf(" Run 'bt repair --fix-paths /new/path' to fix it\n")
232+
}
233+
}
168234
}
169235
fmt.Println()
170236
}
@@ -277,6 +343,14 @@ func joinWorktrees(names []string) string {
277343
return result
278344
}
279345

346+
// worktreeWarning holds details about a worktree issue for the warnings section
347+
type worktreeWarning struct {
348+
path string
349+
branch string
350+
relPath string
351+
reasons []string // "outside-root", "nested", "name-mismatch", "broken"
352+
}
353+
280354
// isPathWithin checks if childPath is within parentPath
281355
func isPathWithin(childPath, parentPath string) bool {
282356
// Clean and ensure trailing separator for accurate prefix matching

internal/git/branch.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,20 @@ func (e *Executor) IsCommitHash(ref string) bool {
176176
return err == nil
177177
}
178178

179+
// ListLocalBranches returns a list of all local branch names
180+
func (e *Executor) ListLocalBranches() ([]string, error) {
181+
output, err := e.Execute("for-each-ref", "--format=%(refname:short)", "refs/heads/")
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to list local branches: %w", err)
184+
}
185+
186+
if output == "" {
187+
return []string{}, nil
188+
}
189+
190+
return strings.Split(output, "\n"), nil
191+
}
192+
179193
// PullBranch fast-forwards the specified local branch to its upstream.
180194
// Works in bare repositories by verifying the fast-forward condition and using update-ref.
181195
func (e *Executor) PullBranch(localBranch string) error {

internal/worktree/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ func (m *Manager) ResolveBranch(branchSpec string) (*git.BranchInfo, error) {
200200
return m.Executor.ResolveBranch(branchSpec)
201201
}
202202

203+
// ListLocalBranches returns all local branch names
204+
func (m *Manager) ListLocalBranches() ([]string, error) {
205+
return m.Executor.ListLocalBranches()
206+
}
207+
203208
// IsManaged checks if a worktree is managed by baretree
204209
// A managed worktree must be within the repository root and not inside another worktree
205210
func (m *Manager) IsManaged(worktreePath string) bool {

0 commit comments

Comments
 (0)