package git import ( "bufio" "bytes" "context" "strings" ) // WorktreeAdd 在 dir(main worktree)创建一个支线 worktree。 // // path: 绝对路径,由 workspace 包计算 // branch: 用 -B 强制创建/重置 —— 已存在的分支会被重置(本地未推送 // commit 会丢失,可从 reflog 找回)。 // startPoint: 为空时分支基于当前 HEAD;非空时分支基于该 start-point, // 且 --track 会把 upstream 指向它。 // // 失败时如果 stderr 含 "already exists" / "is already used by worktree", // errors.IsConflict 返回 true,调用方应映射为 WORKTREE_BRANCH_CONFLICT。 func (r *DefaultRunner) WorktreeAdd(ctx context.Context, dir, branch, path, startPoint string) error { ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout) defer cancel() args := []string{"worktree", "add", "--track", "-B", branch, path} if startPoint != "" { args = append(args, startPoint) } _, _, err := r.exec(ctx, dir, nil, args...) return err } // WorktreeRemove 删除 path 这个 worktree(包含磁盘目录)。 // 即使 worktree 当前不存在 / 已被外部 rm 掉,调用 git worktree remove --force 也会成功 // 或返回特定 stderr,调用方据此决定是否吞错。 func (r *DefaultRunner) WorktreeRemove(ctx context.Context, dir, path string) error { ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout) defer cancel() _, _, err := r.exec(ctx, dir, nil, "worktree", "remove", "--force", path) return err } // WorktreeList 返回 dir 下所有 worktree 的元数据(含 main)。 func (r *DefaultRunner) WorktreeList(ctx context.Context, dir string) ([]Worktree, error) { ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout) defer cancel() out, _, err := r.exec(ctx, dir, nil, "worktree", "list", "--porcelain") if err != nil { return nil, err } return parseWorktreeList(out), nil } // parseWorktreeList 解析 git worktree list --porcelain 输出。 // 每个 worktree 由若干 KV 行组成,记录间空行分隔: // // worktree /abs/path // HEAD // branch refs/heads/ (or "detached") // bare (only for bare main) func parseWorktreeList(buf []byte) []Worktree { var out []Worktree var cur Worktree flush := func() { if cur.Path != "" { out = append(out, cur) } cur = Worktree{} } sc := bufio.NewScanner(bytes.NewReader(buf)) for sc.Scan() { line := sc.Text() if line == "" { flush() continue } switch { case strings.HasPrefix(line, "worktree "): cur.Path = strings.TrimPrefix(line, "worktree ") case strings.HasPrefix(line, "HEAD "): cur.HEAD = strings.TrimPrefix(line, "HEAD ") case strings.HasPrefix(line, "branch "): cur.Branch = strings.TrimPrefix(strings.TrimPrefix(line, "branch "), "refs/heads/") case line == "bare": cur.Bare = true } } flush() return out }