Files
agentic-coding-workflow/internal/workspace/gitops_service.go
T

201 lines
6.0 KiB
Go

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
}
// ===== 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
}
// ===== 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},
})
}