package workspace import ( "context" "errors" "time" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/audit" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/infra/git" ) type worktreeService struct { repo Repository rec audit.Recorder gitr git.Runner pa ProjectAccess dataDir string } // NewWorktreeService 构造 WorktreeService。 func NewWorktreeService(repo Repository, rec audit.Recorder, gitr git.Runner, pa ProjectAccess, dataDir string) WorktreeService { return &worktreeService{repo: repo, rec: rec, gitr: gitr, pa: pa, dataDir: dataDir} } func (s *worktreeService) Create(ctx context.Context, c Caller, wsID uuid.UUID, in CreateWorktreeInput) (*Worktree, error) { ws, err := s.repo.GetWorkspaceByID(ctx, wsID) if err != nil { return nil, err } if _, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil { return nil, err } else if !canWrite { return nil, errs.New(errs.CodeForbidden, "no write access") } if err := ValidateBranch(in.Branch); err != nil { return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid branch") } base := in.BaseBranch if base == "" { base = ws.DefaultBranch } if err := ValidateBranch(base); err != nil { return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid base branch") } wtPath := WorktreePath(s.dataDir, wsID, in.Branch) if err := s.gitr.WorktreeAdd(ctx, ws.MainPath, in.Branch, wtPath, "origin/"+base); err != nil { if git.IsConflict(err) { return nil, errs.Wrap(err, errs.CodeWorktreeBranchConflict, "branch already used") } return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git worktree add") } wt := &Worktree{ ID: uuid.New(), WorkspaceID: wsID, Branch: in.Branch, Path: wtPath, Status: WorktreeStatusIdle, } var saved *Worktree err = s.repo.InTx(ctx, func(tx Tx) error { w, e := tx.InsertWorktree(ctx, wt) if e != nil { return e } saved = w return nil }) if err != nil { // 回滚磁盘 _ = s.gitr.WorktreeRemove(ctx, ws.MainPath, wtPath) return nil, err } s.audit(ctx, &c.UserID, "worktree.create", "worktree", saved.ID.String(), map[string]any{"branch": in.Branch}) return saved, nil } func (s *worktreeService) List(ctx context.Context, c Caller, wsID uuid.UUID) ([]*Worktree, error) { ws, err := s.repo.GetWorkspaceByID(ctx, wsID) if err != nil { return nil, err } if canRead, _, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil { return nil, err } else if !canRead { return nil, errs.New(errs.CodeForbidden, "no read access") } return s.repo.ListWorktreesByWorkspace(ctx, wsID) } func (s *worktreeService) Delete(ctx context.Context, c Caller, wtID uuid.UUID) error { wt, err := s.repo.GetWorktreeByID(ctx, wtID) if err != nil { return err } ws, err := s.repo.GetWorkspaceByID(ctx, wt.WorkspaceID) if err != nil { return err } if _, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil { return err } else if !canWrite { return errs.New(errs.CodeForbidden, "no write access") } if wt.Status == WorktreeStatusActive { return errs.New(errs.CodeWorktreeAlreadyActive, "release before delete") } // 先 set pruning,再调 git rm,然后 DELETE row。中途失败 row 留 pruning。 err = s.repo.InTx(ctx, func(tx Tx) error { _, e := tx.SetWorktreePruning(ctx, wtID) return e }) if err != nil { return err } if rmErr := s.gitr.WorktreeRemove(ctx, ws.MainPath, wt.Path); rmErr != nil { return errs.Wrap(rmErr, errs.CodeGitCmdFailed, "git worktree remove") } if err := s.repo.DeleteWorktree(ctx, wtID); err != nil { return err } s.audit(ctx, &c.UserID, "worktree.delete", "worktree", wtID.String(), nil) return nil } func (s *worktreeService) Acquire(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error) { holder := c.Holder() var out *Worktree err := s.repo.InTx(ctx, func(tx Tx) error { wt, err := tx.GetWorktreeByIDForUpdate(ctx, wtID) if err != nil { return err } switch wt.Status { case WorktreeStatusPruning: return errs.New(errs.CodeNotFound, "worktree being pruned") case WorktreeStatusActive: if wt.ActiveHolder == holder { out = wt // idempotent return nil } return errs.New(errs.CodeWorktreeAlreadyActive, "held by another holder") case WorktreeStatusIdle: updated, err := tx.SetWorktreeActive(ctx, wtID, holder) if err != nil { return err } out = updated return nil } return errs.New(errs.CodeInternal, "unknown worktree status") }) if err != nil { return nil, err } s.audit(ctx, &c.UserID, "worktree.acquire", "worktree", wtID.String(), map[string]any{"holder": holder}) return out, nil } func (s *worktreeService) Release(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error) { holder := c.Holder() var out *Worktree err := s.repo.InTx(ctx, func(tx Tx) error { wt, err := tx.GetWorktreeByIDForUpdate(ctx, wtID) if err != nil { return err } if wt.Status != WorktreeStatusActive { return errs.New(errs.CodeWorktreeNotHeldByCaller, "worktree not active") } if wt.ActiveHolder != holder && !c.IsAdmin { return errs.New(errs.CodeWorktreeNotHeldByCaller, "not held by caller") } updated, err := tx.SetWorktreeIdle(ctx, wtID) if err != nil { return err } out = updated return nil }) if err != nil { return nil, err } s.audit(ctx, &c.UserID, "worktree.release", "worktree", wtID.String(), map[string]any{"holder": holder}) return out, nil } // ReleaseByHolder 释放给定 holder 字符串持有的所有活跃 worktree。 // 实现仅委托 repo;audit 由 caller 在更高层根据自己上下文写入。 func (s *worktreeService) ReleaseByHolder(ctx context.Context, holder string) ([]*Worktree, error) { return s.repo.ReleaseWorktreesByHolder(ctx, holder) } func (s *worktreeService) audit(ctx context.Context, uid *uuid.UUID, action, ttype, tid string, meta map[string]any) { _ = s.rec.Record(ctx, audit.Entry{ UserID: uid, Action: action, TargetType: ttype, TargetID: tid, Metadata: meta, }) } // 防止未使用 import lint 移除 var _ = errors.Is var _ = time.Now