Files
kandev/apps/backend/internal/worktree/errors.go
T
João Salavisa b30933240b fix: sanitize multi-repo worktree dirs and polish task-create chip dropdown (#815)
* fix: sanitize multi-repo worktree dirs and polish task-create chip dropdown

Backend: org-prefixed Repository.Name like "owner/repo" was used verbatim
as the worktree subdirectory under the task root, nesting one level too
deep and breaking agentctl's sibling-based multi-repo detection in
scanRepositorySubdirs. Sanitize to a single path-safe segment so all
worktrees land as siblings of the task root.

Web: the multi-repo chip dropdown built its own option list and dropped
the path Badge plus search filtering that the legacy header dropdown
shipped. Restore parity (name + path), widen the popover (288 → 360 px),
stack name and full path on two lines so neither has to truncate
aggressively, hide the Badge for workspace repos without a local clone,
and use a path-aware scorer that floors weak fuzzy matches so unrelated
paths stop surfacing for short queries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address PR review feedback

- Add ErrInvalidRepoName sentinel and use it in createInTaskDir when
  RepoName sanitizes to empty, replacing the misleading reuse of
  ErrTaskDirRequired (Claude/Greptile).
- Reuse repoDirHyphenRun in SanitizeForBranch instead of recompiling the
  regexp per call (CodeRabbit nitpick).
- Document the SanitizeRepoDirName collision edge case in the doc
  comment (Greptile).
- Extract scoreRepo to lib/utils/repo-filter.ts and add unit tests
  pinning the 0.4 fuzzy-match threshold (Claude).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:22:28 +01:00

103 lines
4.4 KiB
Go

// Package worktree provides Git worktree management for concurrent agent execution.
package worktree
import (
"errors"
"fmt"
"strings"
)
var (
// ErrWorktreeExists is returned when attempting to create a worktree that already exists.
ErrWorktreeExists = errors.New("worktree already exists for task")
// ErrWorktreeNotFound is returned when the requested worktree does not exist.
ErrWorktreeNotFound = errors.New("worktree not found")
// ErrRepoNotGit is returned when the repository path is not a Git repository.
ErrRepoNotGit = errors.New("repository is not a git repository")
// ErrBranchExists is returned when the branch name already exists in the repository.
ErrBranchExists = errors.New("branch already exists")
// ErrWorktreeLocked is returned when the worktree is locked by another process.
ErrWorktreeLocked = errors.New("worktree is locked by another process")
// ErrInvalidBaseBranch is returned when the base branch does not exist.
ErrInvalidBaseBranch = errors.New("base branch does not exist")
// ErrWorktreeCorrupted is returned when the worktree directory is corrupted or invalid.
ErrWorktreeCorrupted = errors.New("worktree directory is corrupted")
// ErrGitCommandFailed is returned when a git command fails to execute.
ErrGitCommandFailed = errors.New("git command failed")
// ErrInvalidSession is returned when the session ID is invalid or empty.
ErrInvalidSession = errors.New("invalid or empty session ID")
// ErrBranchCheckedOut is returned when a branch is already checked out in another worktree.
ErrBranchCheckedOut = errors.New("branch is already checked out in another worktree")
// ErrAuthFailed is returned when git authentication fails (e.g. no credentials in non-interactive mode).
ErrAuthFailed = errors.New("git authentication failed")
// ErrNonFastForward is returned when a fetch/pull is rejected due to non-fast-forward updates.
ErrNonFastForward = errors.New("non-fast-forward update rejected")
// ErrGitCryptFailed is returned when git-crypt unlock fails during worktree creation.
ErrGitCryptFailed = errors.New("git-crypt unlock failed")
// ErrTaskDirRequired is returned when a worktree create request is missing
// TaskDirName or RepoName. Worktrees are always placed inside the per-task
// directory (~/.kandev/tasks/{taskDir}/{repo}/), so callers must populate
// both fields.
ErrTaskDirRequired = errors.New("worktree create requires TaskDirName and RepoName")
// ErrInvalidRepoName is returned when RepoName is set but contains no
// characters that survive sanitization (e.g. "!@#$%"), so it cannot be
// used as a directory segment.
ErrInvalidRepoName = errors.New("repo name has no usable characters after sanitization")
)
// containsAuthFailure checks if git output indicates an authentication failure.
func containsAuthFailure(lowerOutput string) bool {
return strings.Contains(lowerOutput, "authentication failed") ||
strings.Contains(lowerOutput, "terminal prompts disabled") ||
strings.Contains(lowerOutput, "could not read username") ||
strings.Contains(lowerOutput, "username for 'https://") ||
strings.Contains(lowerOutput, "askpass")
}
// isBranchCheckedOutError checks if git output indicates a branch is already
// checked out in another worktree. Different git versions use different messages:
// "is already checked out at" or "is already used by worktree at".
func isBranchCheckedOutError(output string) bool {
out := strings.ToLower(output)
return strings.Contains(out, "is already checked out at") ||
strings.Contains(out, "is already used by worktree at")
}
// isFetchRefusedCheckedOut checks if a git fetch error is caused by the branch
// being checked out in another worktree. Git refuses to update a local branch
// ref that is currently checked out.
func isFetchRefusedCheckedOut(output string) bool {
return strings.Contains(strings.ToLower(output), "refusing to fetch into branch")
}
// ClassifyGitError wraps a raw git error with a user-friendly sentinel error
// based on the command output.
func ClassifyGitError(output string, _ error) error {
trimmed := strings.TrimSpace(output)
switch {
case isBranchCheckedOutError(output):
return fmt.Errorf("%w: %s", ErrBranchCheckedOut, trimmed)
case containsAuthFailure(strings.ToLower(output)):
return fmt.Errorf("%w: %s", ErrAuthFailed, trimmed)
case strings.Contains(strings.ToLower(output), "non-fast-forward"):
return fmt.Errorf("%w: %s", ErrNonFastForward, trimmed)
default:
return fmt.Errorf("%w: %s", ErrGitCommandFailed, trimmed)
}
}