main
rb 87 lines 3.82 KB
Raw
1 # frozen_string_literal: true
2
3 # Creates pull requests. All PR creation — web UI or MCP — goes through here so
4 # authorization, branch validation, and per-repo numbering behave identically.
5 class PullRequestService
6 class Error < StandardError; end
7 class NotAuthorized < Error; end
8
9 # Opens a pull request from +head+ into +base+ on +repository+, authored by
10 # +author+. Raises NotAuthorized if the author can't write the repo, Error if a
11 # branch is missing, and ActiveRecord::RecordInvalid on validation failure.
12 def self.create(repository:, author:, title:, head:, base:, body: nil)
13 unless repository.writable_by?(author)
14 raise NotAuthorized, "You don't have permission to open a pull request on #{repository.full_name}."
15 end
16
17 [ head, base ].each do |ref|
18 next if GitRepositoryService.branch_exists?(repository.disk_path, ref)
19 raise Error, "Branch not found: #{ref}"
20 end
21
22 repository.with_lock do
23 repository.pull_requests.create!(
24 user: author,
25 # Issues and pull requests share one per-repo number space (as on
26 # GitHub); Repository#next_issue_number is the single allocator.
27 number: repository.next_issue_number,
28 title: title,
29 head_ref: head,
30 base_ref: base,
31 body: body.to_s,
32 state: "open"
33 )
34 end
35 end
36
37 # Merges +pull_request+'s head into its base as a new merge commit. Only the
38 # repo owner may merge (siGit repos have a single owner, so this is the same
39 # check as opening one). Raises Error if the PR isn't open, a branch has
40 # disappeared, the head is already merged, or the merge has conflicts.
41 def self.merge(pull_request:, merger:)
42 repository = pull_request.repository
43 unless repository.writable_by?(merger)
44 raise NotAuthorized, "You don't have permission to merge pull requests on #{repository.full_name}."
45 end
46 raise Error, "This pull request is already #{pull_request.state}." unless pull_request.open?
47
48 base_sha = GitRepositoryService.commit_sha(repository.disk_path, pull_request.base_ref)
49 head_sha = GitRepositoryService.commit_sha(repository.disk_path, pull_request.head_ref)
50 raise Error, "One of the branches no longer exists." unless base_sha && head_sha
51 raise Error, "This branch is already up to date with #{pull_request.base_ref}." if base_sha == head_sha
52
53 message = "Merge pull request ##{pull_request.number} from #{pull_request.head_ref}\n\n#{pull_request.title}"
54 merge_sha = GitRepositoryService.merge!(
55 repository.disk_path,
56 base: pull_request.base_ref, head: pull_request.head_ref, message: message,
57 author_name: merger.username, author_email: merger.email
58 )
59 raise Error, "This pull request has conflicts and can't be merged automatically." unless merge_sha
60
61 pull_request.update!(state: "merged", merged_at: Time.current)
62 merge_sha
63 end
64
65 # Closes +pull_request+ without merging. Allowed for the repo owner or the
66 # PR's own author.
67 def self.close(pull_request:, actor:)
68 authorize_modify!(pull_request, actor)
69 raise Error, "This pull request is already closed." if pull_request.closed?
70 raise Error, "Merged pull requests can't be closed." if pull_request.merged?
71 pull_request.update!(state: "closed")
72 end
73
74 # Reopens a closed (not merged — that's permanent, like GitHub) pull request.
75 def self.reopen(pull_request:, actor:)
76 authorize_modify!(pull_request, actor)
77 raise Error, "Only closed pull requests can be reopened." unless pull_request.closed?
78 pull_request.update!(state: "open")
79 end
80
81 def self.authorize_modify!(pull_request, actor)
82 repository = pull_request.repository
83 return if repository.writable_by?(actor) || pull_request.user_id == actor&.id
84 raise NotAuthorized, "You don't have permission to modify this pull request."
85 end
86 private_class_method :authorize_modify!
87 end