main / internal/app/pull_service.go
3.6 KB · Go Raw
1package app
2
3import (
4 "context"
5 "fmt"
6 "slices"
7 "strings"
8
9 "gitgud/internal/domain"
10)
11
12type PullService struct {
13 prs domain.PullRequestRepository
14 reader domain.GitReader
15 git domain.GitService
16}
17
18func NewPullService(prs domain.PullRequestRepository, reader domain.GitReader, git domain.GitService) *PullService {
19 return &PullService{prs: prs, reader: reader, git: git}
20}
21
22func (s *PullService) Open(ctx context.Context, repo *domain.Repository, author *domain.User, title, body, base, head string) (*domain.PullRequest, error) {
23 if author == nil {
24 return nil, domain.ErrUnauthorized
25 }
26 title = strings.TrimSpace(title)
27 base = strings.TrimSpace(base)
28 head = strings.TrimSpace(head)
29
30 if title == "" {
31 return nil, fmt.Errorf("title is required: %w", domain.ErrValidation)
32 }
33 if base == "" || head == "" || base == head {
34 return nil, fmt.Errorf("base and head must be two different branches: %w", domain.ErrValidation)
35 }
36
37 branches, err := s.reader.Branches(ctx, repo.OwnerName, repo.Name)
38 if err != nil {
39 return nil, err
40 }
41 if !slices.Contains(branches, base) || !slices.Contains(branches, head) {
42 return nil, fmt.Errorf("base and head must be existing branches: %w", domain.ErrValidation)
43 }
44
45 pr := &domain.PullRequest{
46 RepoID: repo.ID,
47 AuthorID: author.ID,
48 Title: title,
49 Body: strings.TrimSpace(body),
50 BaseBranch: base,
51 HeadBranch: head,
52 State: domain.PROpen,
53 }
54 if err := s.prs.Create(ctx, pr); err != nil {
55 return nil, err
56 }
57 return pr, nil
58}
59
60func (s *PullService) Compare(ctx context.Context, repo *domain.Repository, base, head string) (*domain.Comparison, error) {
61 return s.reader.Compare(ctx, repo.OwnerName, repo.Name, base, head)
62}
63
64func (s *PullService) Comment(ctx context.Context, pr *domain.PullRequest, author *domain.User, body string) error {
65 if author == nil {
66 return domain.ErrUnauthorized
67 }
68 body = strings.TrimSpace(body)
69 if body == "" {
70 return fmt.Errorf("comment is required: %w", domain.ErrValidation)
71 }
72 return s.prs.AddComment(ctx, &domain.PRComment{
73 PRID: pr.ID,
74 AuthorID: author.ID,
75 Body: body,
76 })
77}
78
79func (s *PullService) Merge(ctx context.Context, repo *domain.Repository, pr *domain.PullRequest, actor *domain.User) error {
80 if !CanMergePR(repo, actor) {
81 return domain.ErrPermission
82 }
83 if pr.State != domain.PROpen {
84 return fmt.Errorf("pull request is not open: %w", domain.ErrValidation)
85 }
86
87 msg := fmt.Sprintf("Merge pull request #%d from %s", pr.Number, pr.HeadBranch)
88 if err := s.git.Merge(ctx, repo.OwnerName, repo.Name, pr.BaseBranch, pr.HeadBranch, msg, actor.Username, actor.Email); err != nil {
89 return err
90 }
91 return s.prs.SetState(ctx, pr.ID, domain.PRMerged)
92}
93
94func (s *PullService) Close(ctx context.Context, repo *domain.Repository, pr *domain.PullRequest, actor *domain.User) error {
95 if !(actor != nil && (actor.ID == pr.AuthorID || actor.ID == repo.OwnerID)) {
96 return domain.ErrPermission
97 }
98 return s.prs.SetState(ctx, pr.ID, domain.PRClosed)
99}
100
101func (s *PullService) List(ctx context.Context, repoID int64, state domain.PRState) ([]*domain.PullRequest, error) {
102 return s.prs.List(ctx, repoID, state)
103}
104
105func (s *PullService) Get(ctx context.Context, repoID int64, number int) (*domain.PullRequest, error) {
106 return s.prs.ByNumber(ctx, repoID, number)
107}
108
109func (s *PullService) Comments(ctx context.Context, prID int64) ([]*domain.PRComment, error) {
110 return s.prs.Comments(ctx, prID)
111}
112
113func (s *PullService) Counts(ctx context.Context, repoID int64) (open, merged, closed int, err error) {
114 return s.prs.CountByState(ctx, repoID)
115}
116
117func CanMergePR(repo *domain.Repository, actor *domain.User) bool {
118 return actor != nil && actor.ID == repo.OwnerID
119}