package workspace import ( "context" "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" ) // CredentialResolver 是 service 共享的解密回调,避免 GitOpsService 直接持有 *crypto.Encryptor。 type CredentialResolver func(ctx context.Context, wsID uuid.UUID) (*git.Credential, error) type gitOpsService struct { repo Repository rec audit.Recorder gitr git.Runner pa ProjectAccess cred CredentialResolver } // NewGitOpsService 构造 GitOpsService。cred 由 WorkspaceService.loadDecryptedCredential 适配过来。 func NewGitOpsService(repo Repository, rec audit.Recorder, gitr git.Runner, pa ProjectAccess, cred CredentialResolver) GitOpsService { return &gitOpsService{repo: repo, rec: rec, gitr: gitr, pa: pa, cred: cred} } // ===== Main ===== func (s *gitOpsService) StatusOnMain(ctx context.Context, c Caller, wsID uuid.UUID) ([]git.FileStatus, error) { ws, err := s.guardWorkspaceRead(ctx, c, wsID) if err != nil { return nil, err } st, err := s.gitr.Status(ctx, ws.MainPath) if err != nil { return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git status") } return st, nil } func (s *gitOpsService) CommitOnMain(ctx context.Context, c Caller, wsID uuid.UUID, in CommitInput) (string, error) { ws, err := s.guardWorkspaceWrite(ctx, c, wsID) if err != nil { return "", err } sha, err := s.gitr.Commit(ctx, ws.MainPath, in.Message, in.AddAll) if err != nil { return "", errs.Wrap(err, errs.CodeGitCmdFailed, "git commit") } s.auditCommit(ctx, &c.UserID, wsID.String(), "main", in.Message, sha) if in.Push { cred, err := s.cred(ctx, wsID) if err != nil { return sha, err } if err := s.gitr.Push(ctx, ws.MainPath, ws.DefaultBranch, cred); err != nil { return sha, errs.Wrap(err, errs.CodeGitCmdFailed, "git push") } s.auditPush(ctx, &c.UserID, wsID.String(), ws.DefaultBranch) } return sha, nil } func (s *gitOpsService) PushOnMain(ctx context.Context, c Caller, wsID uuid.UUID) error { ws, err := s.guardWorkspaceWrite(ctx, c, wsID) if err != nil { return err } cred, err := s.cred(ctx, wsID) if err != nil { return err } if err := s.gitr.Push(ctx, ws.MainPath, ws.DefaultBranch, cred); err != nil { return errs.Wrap(err, errs.CodeGitCmdFailed, "git push") } s.auditPush(ctx, &c.UserID, wsID.String(), ws.DefaultBranch) return nil } func (s *gitOpsService) LogOnMain(ctx context.Context, c Caller, wsID uuid.UUID, n int) ([]git.Commit, error) { ws, err := s.guardWorkspaceRead(ctx, c, wsID) if err != nil { return nil, err } commits, err := s.gitr.Log(ctx, ws.MainPath, n) if err != nil { return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log") } return commits, nil } // ===== Worktree ===== func (s *gitOpsService) StatusOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) ([]git.FileStatus, error) { wt, ws, err := s.guardWorktreeRead(ctx, c, wtID) if err != nil { return nil, err } _ = ws st, err := s.gitr.Status(ctx, wt.Path) if err != nil { return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git status") } return st, nil } func (s *gitOpsService) CommitOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, in CommitInput) (string, error) { wt, ws, err := s.guardWorktreeWrite(ctx, c, wtID) if err != nil { return "", err } sha, err := s.gitr.Commit(ctx, wt.Path, in.Message, in.AddAll) if err != nil { return "", errs.Wrap(err, errs.CodeGitCmdFailed, "git commit") } s.auditCommit(ctx, &c.UserID, ws.ID.String(), wt.Branch, in.Message, sha) if in.Push { cred, err := s.cred(ctx, ws.ID) if err != nil { return sha, err } if err := s.gitr.Push(ctx, wt.Path, wt.Branch, cred); err != nil { return sha, errs.Wrap(err, errs.CodeGitCmdFailed, "git push") } s.auditPush(ctx, &c.UserID, ws.ID.String(), wt.Branch) } return sha, nil } func (s *gitOpsService) PushOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) error { wt, ws, err := s.guardWorktreeWrite(ctx, c, wtID) if err != nil { return err } cred, err := s.cred(ctx, ws.ID) if err != nil { return err } if err := s.gitr.Push(ctx, wt.Path, wt.Branch, cred); err != nil { return errs.Wrap(err, errs.CodeGitCmdFailed, "git push") } s.auditPush(ctx, &c.UserID, ws.ID.String(), wt.Branch) return nil } func (s *gitOpsService) LogOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, n int) ([]git.Commit, error) { wt, _, err := s.guardWorktreeRead(ctx, c, wtID) if err != nil { return nil, err } commits, err := s.gitr.Log(ctx, wt.Path, n) if err != nil { return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log") } return commits, nil } // ===== guards ===== func (s *gitOpsService) guardWorkspaceRead(ctx context.Context, c Caller, wsID uuid.UUID) (*Workspace, 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 ws, nil } func (s *gitOpsService) guardWorkspaceWrite(ctx context.Context, c Caller, wsID uuid.UUID) (*Workspace, 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") } return ws, nil } func (s *gitOpsService) guardWorktreeRead(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, *Workspace, error) { wt, err := s.repo.GetWorktreeByID(ctx, wtID) if err != nil { return nil, nil, err } ws, err := s.guardWorkspaceRead(ctx, c, wt.WorkspaceID) if err != nil { return nil, nil, err } return wt, ws, nil } func (s *gitOpsService) guardWorktreeWrite(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, *Workspace, error) { wt, err := s.repo.GetWorktreeByID(ctx, wtID) if err != nil { return nil, nil, err } ws, err := s.guardWorkspaceWrite(ctx, c, wt.WorkspaceID) if err != nil { return nil, nil, err } return wt, ws, nil } func (s *gitOpsService) auditCommit(ctx context.Context, uid *uuid.UUID, wsID, target, msg, sha string) { _ = s.rec.Record(ctx, audit.Entry{ UserID: uid, Action: "workspace.commit", TargetType: "workspace", TargetID: wsID, Metadata: map[string]any{"target": target, "sha": sha, "msg": msg}, }) } func (s *gitOpsService) auditPush(ctx context.Context, uid *uuid.UUID, wsID, branch string) { _ = s.rec.Record(ctx, audit.Entry{ UserID: uid, Action: "workspace.push", TargetType: "workspace", TargetID: wsID, Metadata: map[string]any{"branch": branch}, }) }