defmodule GitGud.PullRequests.PullRequest do
use Ecto.Schema
import Ecto.Changeset
alias GitGud.Accounts.User
alias GitGud.Federation.Actor
alias GitGud.PullRequests.PrComment
alias GitGud.PullRequests.PrReview
alias GitGud.Repositories.Repository
@valid_states ~w(open closed merged)
schema "pull_requests" do
field :number, :integer
field :title, :string
field :body, :string
field :state, :string, default: "open"
field :source_ref, :string
field :target_ref, :string
field :head_sha, :binary
field :base_sha, :binary
field :mergeable, :boolean
field :mergeable_checked_at, :utc_datetime
field :closed_at, :utc_datetime
field :merged_at, :utc_datetime
field :merge_commit_sha, :binary
# Federated-origin metadata. Set when the PR was opened by a remote
# AP actor via an inbox `Offer`. Local-origin PRs have these as nil.
field :activity_url, :string
field :source_clone_url, :string
field :moderated_at, :utc_datetime
field :moderation_note, :string
field :original_body, :string
belongs_to :repository, Repository
belongs_to :source_repository, Repository
belongs_to :source_actor, Actor
belongs_to :author, User
belongs_to :merged_by, User
belongs_to :moderated_by, User
has_many :comments, PrComment
has_many :reviews, PrReview
timestamps(type: :utc_datetime)
end
@doc "True if the PR's source repo is different from its target repo."
def cross_repo?(%__MODULE__{source_repository_id: sid, repository_id: rid})
when is_integer(sid),
do: sid != rid
def cross_repo?(_), do: false
@doc "True if the PR was opened via an inbound ActivityPub `Offer`."
def federated?(%__MODULE__{source_actor_id: sid}) when is_integer(sid), do: true
def federated?(_), do: false
@doc """
Creation changeset. `repository_id`, `author_id`, `number`, `head_sha`,
and `base_sha` are programmatic — never user input.
"""
def create_changeset(pr, attrs) do
pr
|> cast(attrs, [:title, :body, :source_ref, :target_ref])
|> validate_required([:title, :source_ref, :target_ref])
|> validate_length(:title, min: 1, max: 255)
|> validate_length(:body, max: 65_535)
|> validate_length(:source_ref, max: 250)
|> validate_length(:target_ref, max: 250)
|> validate_distinct_refs()
end
defp validate_distinct_refs(cs) do
src = get_field(cs, :source_ref)
tgt = get_field(cs, :target_ref)
if is_binary(src) and src == tgt,
do: add_error(cs, :source_ref, "must differ from target_ref"),
else: cs
end
def update_changeset(pr, attrs) do
pr
|> cast(attrs, [:title, :body])
|> validate_required([:title])
|> validate_length(:title, min: 1, max: 255)
|> validate_length(:body, max: 65_535)
end
def state_changeset(pr, "closed"),
do:
change(pr,
state: "closed",
closed_at: DateTime.utc_now(:second)
)
def state_changeset(pr, "open"),
do: change(pr, state: "open", closed_at: nil)
def merged_changeset(pr, merge_commit_sha, %User{id: uid}) do
now = DateTime.utc_now(:second)
change(pr,
state: "merged",
merged_at: now,
closed_at: now,
merge_commit_sha: merge_commit_sha,
merged_by_id: uid
)
end
def mergeability_changeset(pr, result) do
change(pr,
mergeable: result,
mergeable_checked_at: DateTime.utc_now(:second)
)
end
def valid_states, do: @valid_states
def moderate_changeset(pr, moderator, note) do
original = pr.original_body || pr.body
change(pr, %{
original_body: original,
body: "",
moderation_note: note,
moderated_by_id: moderator.id,
moderated_at: DateTime.utc_now(:second)
})
end
def restore_changeset(pr) do
change(pr, %{
body: pr.original_body || pr.body,
original_body: nil,
moderation_note: nil,
moderated_by_id: nil,
moderated_at: nil
})
end
def moderated?(%__MODULE__{moderated_at: nil}), do: false
def moderated?(%__MODULE__{}), do: true
end
4.0 KiB · text
5af55d9