package worktrees import ( "bytes" "context" "crypto/sha1" "encoding/hex" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "time" ) type GitRunner func(context.Context, string, ...string) (CommandResult, error) type CommandResult struct { Stdout string Stderr string ExitCode int } type Options struct { Cwd string Name string BaseDir string Env map[string]string Now func() time.Time RunGit GitRunner } type Result struct { Name string `json:"name"` Path string `json:"path"` RepoRoot string `json:"repoRoot"` SourceBranch string `json:"sourceBranch,omitempty"` SourceCommit string `json:"sourceCommit,omitempty"` Reused bool `json:"reused"` } var worktreeNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$`) func Prepare(ctx context.Context, options Options) (Result, error) { cwd, err := resolveCwd(options.Cwd) if err != nil { return Result{}, err } runGit := options.RunGit if runGit == nil { runGit = defaultRunGit } now := options.Now if now == nil { now = time.Now } name := strings.TrimSpace(options.Name) if name == "" { name = defaultWorktreeName(now()) } if err := validateName(name); err != nil { return Result{}, err } repoRoot, err := gitOutput(ctx, runGit, cwd, "rev-parse", "--show-toplevel") if err != nil { return Result{}, fmt.Errorf("not a git repository: %w", err) } repoRoot = filepath.Clean(repoRoot) branch, _ := gitOutput(ctx, runGit, repoRoot, "rev-parse", "--abbrev-ref", "HEAD") commit, _ := gitOutput(ctx, runGit, repoRoot, "rev-parse", "--short", "HEAD") baseDir := strings.TrimSpace(options.BaseDir) if baseDir == "" { baseDir, err = DefaultBaseDir(options.Env) if err != nil { return Result{}, err } } baseDir, err = filepath.Abs(baseDir) if err != nil { return Result{}, fmt.Errorf("resolve worktree dir: %w", err) } repoDir := filepath.Join(baseDir, "zero-worktree-"+repoKey(repoRoot)) target := filepath.Join(repoDir, name) result := Result{ Name: name, Path: target, RepoRoot: repoRoot, SourceBranch: branch, SourceCommit: commit, } reused, err := inspectTarget(target) if err != nil { return Result{}, err } if reused { sameRepo, err := sameGitCommonDir(ctx, runGit, repoRoot, target) if err != nil { return Result{}, fmt.Errorf("inspect existing worktree repository: %w", err) } if !sameRepo { return Result{}, fmt.Errorf("worktree path already exists for a different git repository: %s", target) } result.Reused = true return result, nil } if err := os.MkdirAll(repoDir, 0o700); err != nil { return Result{}, fmt.Errorf("create worktree directory: %w", err) } commandResult, err := runGit(ctx, repoRoot, "worktree", "add", "--detach", target, "HEAD") if err != nil { return Result{}, fmt.Errorf("create git worktree: %w", err) } if commandResult.ExitCode != 0 { message := strings.TrimSpace(firstNonEmpty(commandResult.Stderr, commandResult.Stdout)) if message == "" { message = fmt.Sprintf("git worktree add exited with code %d", commandResult.ExitCode) } return Result{}, fmt.Errorf("create git worktree: %s", message) } return result, nil } func DefaultBaseDir(env map[string]string) (string, error) { if runtime.GOOS == "windows" { if localAppData := strings.TrimSpace(envValue(env, "LOCALAPPDATA")); localAppData != "" { return filepath.Join(localAppData, "zero", "worktrees"), nil } if profile := strings.TrimSpace(envValue(env, "USERPROFILE")); profile != "" { return filepath.Join(profile, "AppData", "Local", "zero", "worktrees"), nil } } if stateHome := strings.TrimSpace(envValue(env, "XDG_STATE_HOME")); stateHome != "" { return filepath.Join(stateHome, "zero", "worktrees"), nil } home := strings.TrimSpace(firstNonEmpty(envValue(env, "HOME"), envValue(env, "USERPROFILE"))) if home == "" { var err error home, err = os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolve user home: %w", err) } } return filepath.Join(home, ".local", "state", "zero", "worktrees"), nil } func resolveCwd(cwd string) (string, error) { if strings.TrimSpace(cwd) == "" { var err error cwd, err = os.Getwd() if err != nil { return "", fmt.Errorf("resolve cwd: %w", err) } } absolute, err := filepath.Abs(cwd) if err != nil { return "", fmt.Errorf("resolve cwd: %w", err) } info, err := os.Stat(absolute) if err != nil || !info.IsDir() { return "", fmt.Errorf("cwd must be an existing directory: %s", absolute) } return filepath.Clean(absolute), nil } func validateName(name string) error { if !worktreeNamePattern.MatchString(name) || strings.ContainsAny(name, `/\`) { return fmt.Errorf("invalid worktree name %q: use letters, numbers, dots, dashes, or underscores", name) } return nil } func inspectTarget(target string) (bool, error) { info, err := os.Stat(target) if err != nil { if os.IsNotExist(err) { return false, nil } return false, fmt.Errorf("inspect worktree path: %w", err) } if !info.IsDir() { return false, fmt.Errorf("worktree path already exists and is not a directory: %s", target) } if _, err := os.Stat(filepath.Join(target, ".git")); err == nil { return true, nil } entries, err := os.ReadDir(target) if err != nil { return false, fmt.Errorf("inspect worktree directory: %w", err) } if len(entries) != 0 { return false, fmt.Errorf("worktree path already exists and is not empty: %s", target) } return false, nil } func gitOutput(ctx context.Context, runGit GitRunner, dir string, args ...string) (string, error) { result, err := runGit(ctx, dir, args...) if err != nil { return "", err } if result.ExitCode != 0 { message := strings.TrimSpace(firstNonEmpty(result.Stderr, result.Stdout)) if message == "" { message = fmt.Sprintf("git exited with code %d", result.ExitCode) } return "", fmt.Errorf("%s", message) } return strings.TrimSpace(result.Stdout), nil } func sameGitCommonDir(ctx context.Context, runGit GitRunner, sourceDir string, targetDir string) (bool, error) { sourceCommonDir, err := gitCommonDir(ctx, runGit, sourceDir) if err != nil { return false, err } targetCommonDir, err := gitCommonDir(ctx, runGit, targetDir) if err != nil { return false, err } return sourceCommonDir == targetCommonDir, nil } func gitCommonDir(ctx context.Context, runGit GitRunner, dir string) (string, error) { value, err := gitOutput(ctx, runGit, dir, "rev-parse", "--git-common-dir") if err != nil { return "", err } if !filepath.IsAbs(value) { value = filepath.Join(dir, value) } absolute, err := filepath.Abs(value) if err != nil { return "", fmt.Errorf("resolve git common dir: %w", err) } resolved, err := filepath.EvalSymlinks(absolute) if err != nil { return "", fmt.Errorf("resolve git common dir: %w", err) } return filepath.Clean(resolved), nil } func defaultRunGit(ctx context.Context, dir string, args ...string) (CommandResult, error) { command := exec.CommandContext(ctx, "git", args...) command.Dir = dir // Capture stdout and stderr separately: callers parse Stdout for values // (rev-parse output) and prefer Stderr for error messages. CombinedOutput // merged the two, letting git's stderr warnings pollute parsed output and // leaving CommandResult.Stderr always empty. var stdout, stderr bytes.Buffer command.Stdout = &stdout command.Stderr = &stderr err := command.Run() exitCode := 0 if err != nil { exitCode = -1 if exitError, ok := err.(*exec.ExitError); ok { exitCode = exitError.ExitCode() err = nil } } return CommandResult{Stdout: stdout.String(), Stderr: stderr.String(), ExitCode: exitCode}, err } func defaultWorktreeName(now time.Time) string { return "task-" + now.UTC().Format("20060102-150405") } func repoKey(repoRoot string) string { sum := sha1.Sum([]byte(filepath.Clean(repoRoot))) hash := hex.EncodeToString(sum[:])[:10] base := filepath.Base(repoRoot) base = strings.ToLower(base) base = strings.Trim(regexp.MustCompile(`[^a-z0-9._-]+`).ReplaceAllString(base, "-"), "-._") if base == "" { base = "repo" } return base + "-" + hash } func envValue(env map[string]string, key string) string { if env != nil { return env[key] } return os.Getenv(key) } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" }