com.worxbend.codeberg4s.pulls

Members list

Type members

Classlikes

final case class ChangedFile

One file a pull request touches, as GET /repos/{owner}/{repo}/pulls/{index}/files reports it.

One file a pull request touches, as GET /repos/{owner}/{repo}/pulls/{index}/files reports it.

Forgejo's ChangedFile, which is '''not''' the CommitAffectedFiles that com.worxbend.codeberg4s.repositories.CommitFile models: that one has two keys and no counts, this one carries the line arithmetic and three URLs. The two are kept apart rather than merged, because merging them would produce a model whose fields are populated depending on which endpoint answered.

status '''is''' shared, though — it is the same vocabulary Forgejo derives from Git's status letters, so this model reuses the repository wave's com.worxbend.codeberg4s.repositories.CommitFileStatus per docs/LEDGER.md rather than declaring a second enum with the same seven cases. golden/pull/files-list.json shows "changed".

No diff text reaches this model. The patch is a separate document — GET /repos/{owner}/{repo}/pulls/{index}.diff — and it is not JSON, so it is not this endpoint's to return.

Value parameters

additions

lines added; 0 when the instance did not report a count

changes

Forgejo's own total. Usually additions + deletions, and read from the payload rather than recomputed, because a client that recomputes a server-side number quietly disagrees with the web UI

contentsUrl

the API URL of the file's contents at the pull request's head commit

deletions

lines removed

filename

the file's path relative to the repository root, as Git records it

previousFilename

where the file was before, populated only for a rename

rawUrl

the browser URL of the raw file at that same commit

status

what the pull request does to the file, absent when the instance sent a value this library does not recognise; see com.worxbend.codeberg4s.repositories.CommitFileStatus.parse

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class CreatePullRequest(title: String, head: PullRequestHead, base: BranchName, body: Option[String], assignees: Vector[String], labels: Vector[LabelId], milestone: Option[MilestoneId], dueDate: Option[Instant])

Everything POST /repos/{owner}/{repo}/pulls may be told, as one value.

Everything POST /repos/{owner}/{repo}/pulls may be told, as one value.

A command type rather than an eight-parameter method: the parameters are almost all optional and almost all Option[String] or Option[Long], and a call site passing five Nones in a fixed order is a defect waiting to happen. Built by naming what should be set:

for command <- CreatePullRequest.of("fix the hook quoting", head, base)
yield command.withBody("backport of #13679").labelled(Vector(bug))

'''Only what is set is sent.''' An unset field contributes no JSON key, so the instance applies its own default rather than this library's idea of one.

Value parameters

assignees

the logins to assign, empty for none. Forgejo silently ignores a login the caller may not assign

base

the branch to merge into. A com.worxbend.codeberg4s.repositories.BranchName and not a PullRequestHead, because a base is always a branch of the repository being posted to — Forgejo has no cross-repository base

dueDate

a deadline for the pull request

head

the branch to merge from, in the one spelling Forgejo accepts; see PullRequestHead

labels

the labels to attach, by com.worxbend.codeberg4s.issues.LabelIdCreatePullRequestOption.labels is a list of ids, not of names

title

the pull request title; validated by CreatePullRequest.of

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object CreateReview

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class CreateReview(body: Option[String], event: Option[ReviewState], commit: Option[CommitSha], comments: Vector[NewReviewComment])

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews may be told, as one value.

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews may be told, as one value.

Forgejo's CreatePullReviewOptions. One call posts a summary, an intent, and any number of inline remarks at once, which is what the web UI's "submit review" button does; posting the remarks one at a time through com.worxbend.codeberg4s.pulls.PullRequestApi.createReviewComment produces a different thing — comments attached to a pending review — and is the reason both endpoints exist.

==The event decides whether a review is submitted or left pending==

event is Forgejo's ReviewStateType, and this library reuses ReviewState for it rather than declaring a near-duplicate enum. Only four of the five cases are meaningful as an '''event''':

  • ReviewState.Approved, ReviewState.RequestChanges and ReviewState.Comment submit the review immediately;
  • ReviewState.Pending — and an absent event, which Forgejo treats the same way — leaves it as the reviewer's own unsent draft, to be finished later with com.worxbend.codeberg4s.pulls.PullRequestApi.submitReview;
  • ReviewState.RequestReview is not an event at all. Forgejo rejects it with a 422, because asking someone else to review is com.worxbend.codeberg4s.pulls.PullRequestApi.requestReviews and a different endpoint.

'''Only what is set is sent''', so an unset field lets the instance apply its own default rather than this library's idea of one.

Value parameters

body

the review's summary text. Forgejo requires one for ReviewState.Comment and for ReviewState.RequestChanges, and answers 422 without it

comments

the inline remarks to post with the review, in order; empty for a summary-only review

commit

the head commit the review is made against, absent to let Forgejo pin the review to whatever the head is now. Naming it explicitly is what keeps a review from silently attaching to commits the reviewer never read

event

what the review says; see the note above

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
enum DiffFormat

Which non-JSON representation of a pull request to fetch — the {diffType} of /repos/{owner}/{repo}/pulls/{index}.{diffType}.

Which non-JSON representation of a pull request to fetch — the {diffType} of /repos/{owner}/{repo}/pulls/{index}.{diffType}.

The two are not interchangeable, and the difference is not cosmetic:

  • Diff is a plain unified diff of the whole pull request, with no commit metadata. It is what git diff produces and what git apply consumes;
  • Patch is a mailbox of one git format-patch message per commit, each with its own author, date and message. It is what git am consumes, and it preserves authorship that Diff discards.

A caller that means to replay someone's commits wants Patch; a caller that means to look at the net change wants Diff. Choosing wrongly produces a document that applies with the wrong tool and no error until it does.

This is a path segment, not a query parameter, and it is the reason the endpoint's path has a dot in it.

Attributes

Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object DiffRequest

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class DiffRequest(format: DiffFormat, includeBinary: Boolean)

What to ask GET /repos/{owner}/{repo}/pulls/{index}.{diffType} for.

What to ask GET /repos/{owner}/{repo}/pulls/{index}.{diffType} for.

Two decisions travel together — which representation, and whether binary changes are included — so they are one value rather than two parameters whose order a call site has to remember. The flag is a named builder for the reason MergePullRequest gives: download(pull, format, true) makes a reader reconstruct what the true was.

client.pulls.download(owner, name, number, DiffRequest.of(DiffFormat.Diff).includingBinary)

Value parameters

format

which document to fetch; see DiffFormat

includeBinary

whether Forgejo should embed binary file changes. false unless includingBinary was called, because the embedded form multiplies the response size — a pull request that touches one image can turn a two-kilobyte diff into a megabyte one, and the library will not choose that on a caller's behalf. Set it when the result has to be applicable with git apply, which is the only thing it is for

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object DismissReview

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class DismissReview(message: Option[String], priors: Boolean)

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals may be told, as one value.

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals may be told, as one value.

Forgejo's DismissPullReviewOptions. Dismissing does '''not''' delete a review: it stays on the listing with Review.isDismissed set and stops counting towards the base branch's required-approval rule. Deleting one is com.worxbend.codeberg4s.pulls.PullRequestApi.deleteReview and is irreversible; com.worxbend.codeberg4s.pulls.PullRequestApi.undismissReview undoes this.

Value parameters

message

the reason recorded against the dismissal, absent to record none. Worth setting — it is the only explanation the reviewer ever sees

priors

whether to dismiss the reviewer's '''earlier''' reviews of the same pull request as well. false unless includingPriors was called: dismissing more than the review that was named is not a default this library picks

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class EditPullRequest(title: Option[String], body: Option[String], assignees: Option[Vector[String]], labels: Option[Vector[LabelId]], milestone: Option[MilestoneId], state: Option[IssueStateChange], base: Option[BranchName], dueDate: Option[Instant], unsetDueDate: Boolean, allowMaintainerEdit: Option[Boolean])

Everything PATCH /repos/{owner}/{repo}/pulls/{index} may be told, as one value.

Everything PATCH /repos/{owner}/{repo}/pulls/{index} may be told, as one value.

Built by starting from EditPullRequest.Empty and naming only what should change:

EditPullRequest.Empty.withTitle("fix the hook quoting").allowingMaintainerEdit

'''Only what is set is sent, and that is the whole contract of a PATCH.''' An unset field contributes no JSON key, so the instance leaves that property alone. Three consequences are worth spelling out because they are easy to get wrong:

  • assignees and labels '''replace''' rather than add, so an empty vector clears them. That is why both are Option[Vector[…]]: absent leaves the list alone, present-and-empty empties it.
  • clearing a deadline is withoutDueDate, which sends unset_due_date: true, and not dueBy with some sentinel instant. Forgejo needs the separate flag because due_date: null is indistinguishable from an absent key on its side.
  • withBase '''retargets the pull request''', which makes Forgejo recompute the diff against a different branch. It is the one field here that can change what the pull request means rather than how it is labelled.

An EditPullRequest.Empty sent as-is is a well-formed request that changes nothing.

Value parameters

allowMaintainerEdit

whether the base repository's maintainers may push to the head branch; absent leaves the setting alone

state

the lifecycle transition to apply. com.worxbend.codeberg4s.issues.IssueStateChange and not a type of this group's own: Forgejo's EditPullRequestOption.state takes the same two open/closed spellings as EditIssueOption.state, and docs/LEDGER.md forbids forking a model that already exists. There is deliberately no transition that merges — merging is MergePullRequest and a different endpoint

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class MergePullRequest(style: MergeStyle, title: Option[String], message: Option[String], headCommit: Option[CommitSha], mergedCommit: Option[CommitSha], deleteBranchAfterMerge: Boolean, forceMerge: Boolean, mergeWhenChecksSucceed: Boolean)

Everything POST /repos/{owner}/{repo}/pulls/{index}/merge may be told, as one value.

Everything POST /repos/{owner}/{repo}/pulls/{index}/merge may be told, as one value.

This is the request behind the most destructive operation in this library, and the type is shaped accordingly.

'''The style is mandatory and is not a String.''' MergeStyle is the only argument MergePullRequest.using takes, because MergePullRequestOption.Do is the only property Forgejo declares as required, and the difference between two of its six values is the difference between rewriting a branch's history and not.

'''Every flag is a named method, not a Boolean parameter.''' merge(pull, force = true, delete = true) makes a reader reconstruct which true was which, and one of them deletes a branch. SCALA_CODE_STYLE.md bans that shape outright; here it would be a footgun as well as a style violation.

==Making a retry safe==

expecting is the one thing that makes repeating this call safe, and it is worth using deliberately. It sends head_commit_id, which Forgejo compares against the branch's actual head and refuses the merge if it has moved — so a merge sent twice after a timeout cannot merge a commit the caller never saw. Without it, a repeat merges whatever the head has since become. com.worxbend.codeberg4s.pulls.PullRequestApi.merge never retries by itself for exactly this reason.

Value parameters

deleteBranchAfterMerge

whether to delete the head branch once the merge lands. false unless deletingSourceBranch was called — deleting someone's branch is not a default this library picks

forceMerge

whether to merge past failing status checks and unsatisfied review requirements. Requires the credentials to be allowed to do so, and is refused with a 405 otherwise

headCommit

the head the caller believes it is merging; see the note above

mergeWhenChecksSucceed

whether to schedule the merge instead of performing it. Forgejo answers such a request with a success and merges later, so a 200 from this variant does '''not''' mean the pull request is merged

mergedCommit

the commit that already holds the merge, for MergeStyle.ManuallyMerged only. Forgejo's MergeCommitID

message

the merge commit's body, same caveat

style

how to integrate the commits; see MergeStyle

title

the merge commit's subject, absent to let Forgejo compose one. Ignored by MergeStyle.FastForwardOnly, which creates no commit

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
enum MergeStyle

How Forgejo should integrate a pull request's commits when it is merged.

How Forgejo should integrate a pull request's commits when it is merged.

The wire value is MergePullRequestOption.Do, and it is the '''only''' required property of that request body. Modelling it as an enum rather than as a String is not decoration: the six spellings below are the complete set Forgejo accepts, a seventh comes back as a 405, and the difference between two of them is the difference between rewriting a branch's history and not. A typo in a string literal would be discovered by a production merge.

A repository can forbid any of these — allow_merge_commits, allow_rebase, allow_rebase_explicit, allow_squash_merge and allow_fast_forward_only_merge are all fields of com.worxbend.codeberg4s.repositories.wire.RepositoryDto, and golden/pull/single-open.json shows one repository with allow_fast_forward_only_merge: false and its fork with true. Asking for a style the repository forbids is a 405, not a validation error this library can catch in advance.

Attributes

Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class NewReviewComment(body: String, path: String, newPosition: Option[Long], oldPosition: Option[Long], extraLinesCount: Option[Long])

One inline remark to write, as CreatePullReviewComment expects it.

One inline remark to write, as CreatePullReviewComment expects it.

Used twice: on its own as the body of POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments, and as an element of CreateReview.comments when a whole review is posted in one call. The pinned spec expresses that reuse literally — CreatePullReviewCommentOptions is a $ref to CreatePullReviewComment and nothing else — so this library models it once.

==Which line, and which side of the diff==

Forgejo carries two line numbers and uses 0 for "not this side": new_position addresses the line as it appears '''after''' the change, old_position addresses it as it appeared '''before'''. Getting them the wrong way round anchors the remark to an unrelated line rather than failing, which is why there is no constructor taking both. Pick the one that says what is meant:

for remark <- NewReviewComment.onNewLine("modules/git/hook.go", 42L, "this quoting is still wrong")
yield remark.spanning(3L)

NewReviewComment.onFile is the third possibility — a remark about the file as a whole, with neither position set — which Forgejo accepts and renders at the top of the file's diff.

Value parameters

body

the remark, as Markdown source; trimmed and required by every constructor

extraLinesCount

how many further lines the remark covers, absent for a single-line remark. Forgejo reads an absent value and a 0 identically, so this library sends neither unless spanning was called

newPosition

the line on the new side of the diff, absent for a remark on the old side or on the file as a whole

oldPosition

the line on the old side of the diff, absent for a remark on the new side or on the file as a whole

path

the file the remark is about, relative to the repository root, exactly as com.worxbend.codeberg4s.pulls.ChangedFile.filename reports it

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class PullRequest

A pull request on a Codeberg or Forgejo instance.

A pull request on a Codeberg or Forgejo instance.

Curated, not generated (ADR-0001), and owned by this group per docs/LEDGER.md. Label, Milestone and com.worxbend.codeberg4s.users.User are the issue and user waves' models, embedded unchanged: Forgejo returns the identical objects inside a PullRequest as inside an Issue, so forking them would be a review-blocking defect.

==The lifecycle is a state, not five fields==

state, closed_at, merged, merged_at, merge_commit_sha and merged_by collapse into state. That is the single most important thing about this model, and PullRequestState explains why — including the trap that a merged pull request reports state: "closed" on the wire.

==Keys deliberately not modelled==

Four of Forgejo's thirty-eight PullRequest keys are kept on com.worxbend.codeberg4s.pulls.wire.PullRequestDto and nowhere else:

  • assignee duplicates the first element of assignees, exactly as it does on an issue;
  • requested_reviewers_teams is an array of Forgejo's Team model, which docs/LEDGER.md assigns to the organisation wave. It is [] on every pull request in the fixtures, and inventing a shape from an empty array would be a guess;
  • pin_order describes a repository's pinned-pull-request ordering rather than the pull request;
  • flow is Forgejo's AGit indicator, 0 on all five captured pull requests, and means nothing to a caller who did not push with AGit.

Value parameters

allowsMaintainerEdit

whether the base repository's maintainers may push to the head branch

assignees

everyone assigned, empty when unassigned — Forgejo sends null, not [], in that case

author

the account that opened it, absent for content imported from another forge

base

the branch the pull request merges '''into'''

changedFileCount

how many files the pull request touches; the com.worxbend.codeberg4s.pulls.ChangedFile listing is the detail

commentCount

ordinary issue-style comments, as the instance counts them; 0 when it did not say

diffUrl

the browser URL of the unified diff — a .diff document, not JSON, and not fetched by this library

dueDate

the deadline set on the pull request, absent when it has none

head

the branch the pull request merges '''from'''; its repository differs from the base's for a fork pull request

id

the instance-wide row identifier. Almost never what a caller wants: no endpoint accepts it, and every URL uses number

isDraft

whether the pull request is marked as not ready for review. Independent of state: a draft is an open pull request that Forgejo refuses to merge, not a fourth lifecycle state

isMergeable

the instance's own verdict, absent when it did not report one. Advisory: it is computed in the background, so it can be stale, and a merge can still be refused by a branch protection rule that this flag knows nothing about

mergeBase

the common ancestor the diff is computed against

number

the per-repository index every endpoint and every human uses; see PullRequestNumber

patchUrl

the browser URL of the mailbox-format patch series

requestedReviewers

the accounts whose review was asked for. Not the accounts that reviewed: those are on the reviews endpoint, and a reviewer who has already submitted may still appear here

reviewCommentCount

comments made on the diff during a review, which Forgejo counts separately from commentCount

state

open, closed, or merged together with the evidence of the merge; see PullRequestState

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class PullRequestBranch

One end of a pull request: the branch it merges into, or the branch it merges from.

One end of a pull request: the branch it merges into, or the branch it merges from.

Forgejo's PRBranchInfo, carried twice on every pull request as base and head. repository is a complete com.worxbend.codeberg4s.repositories.Repository object, not a reduced one — on golden/pull/single-open.json the head's repository is trim21/forgejo, a fork whose own parent is the full forgejo/forgejo — which is why this model reuses the repository wave's type per docs/LEDGER.md rather than inventing a smaller one.

Value parameters

label

Forgejo's display label for the branch: the bare branch name for a same-repository pull request, and the fork's branch name for a cross-repository one

ref

the Git reference this end points at. '''Not always a branch that exists''': golden/pull/list-closed.json shows pull request 13726 whose head ref is refs/pull/13726/head, because the fork branch it was opened from was deleted when it merged. Reading it back with GET /repos/{owner}/{repo}/branches/{branch} would answer 404, so treat it as provenance rather than as something to fetch. A value com.worxbend.codeberg4s.repositories.BranchName refuses — a traversal segment, say — costs the caller this field and not the whole pull request

repository

the repository this end lives in, when the endpoint supplied it

repositoryId

the numeric id of the repository this end lives in. Worth reading even when repository is present: comparing base.repositoryId with head.repositoryId is how a caller tells a fork pull request from an internal one

sha

the commit at this end when the pull request was last synchronised

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class PullRequestQuery(state: Option[StateFilter], sort: Option[PullRequestSort], milestone: Option[MilestoneId], labels: Vector[LabelId], poster: Option[String], base: Option[BranchName], head: Option[PullRequestHead])

The filters GET /repos/{owner}/{repo}/pulls accepts, as one value rather than as seven parameters.

The filters GET /repos/{owner}/{repo}/pulls accepts, as one value rather than as seven parameters.

Built by starting from PullRequestQuery.Empty and naming what should change:

PullRequestQuery.Empty
 .withState(StateFilter.All)
 .sortedBy(PullRequestSort.RecentUpdate)
 .withBase(mainBranch)

'''Only what is set is sent.''' Every field here is absent by default and an absent field contributes no query parameter at all, which matters because Forgejo's defaults are not this library's to guess: omitting state means open pull requests only, and sending state= empty is not the same request. See com.worxbend.codeberg4s.issues.StateFilter, which this group reuses rather than forking — the endpoint takes the same open/closed/all vocabulary as the issue listing.

The builder methods exist because .scalafix.conf bans default arguments, and a seven-argument copy at every call site would be worse than either. Each returns a new query; the type is immutable and safe to share.

Value parameters

base

restrict to pull requests merging into this branch

head

restrict to pull requests merging from this branch; see PullRequestHead for the owner:branch form

labels

restrict to pull requests carrying '''all''' of these labels, by id. Sent as a repeated labels parameter rather than as one comma-joined value — the spec declares collectionFormat: multi here, where the issue listing declares a comma-joined string

milestone

restrict to one milestone, by id. An id and not a title, unlike the issue listing's milestones parameter, which is Forgejo's inconsistency and not this library's

poster

restrict to pull requests opened by this login

sort

the ordering to ask for; absent means the instance's default

state

which lifecycle states to include; absent means Forgejo's own default of open only. Note that closed here includes '''merged''' pull requests, because Forgejo's state does — see PullRequestState

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

The orderings GET /repos/{owner}/{repo}/pulls accepts in its sort parameter.

The orderings GET /repos/{owner}/{repo}/pulls accepts in its sort parameter.

Forgejo's own seven spellings, none of which is a plain field name and two of which are run together without a separator (recentupdate, leastupdate). An enum rather than a String for the reason MergeStyle gives: a misspelling is not rejected, it is ignored, and the caller silently gets the instance's default ordering instead of the one they asked for.

There is no case for the default. Leaving PullRequestQuery.sort unset is what asks for it, and that is a different request from asking for any of these — see PullRequestQuery.

Attributes

Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Where a PullRequest is in its lifecycle, with the evidence of a merge attached to the one state that has any.

Where a PullRequest is in its lifecycle, with the evidence of a merge attached to the one state that has any.

A pull request is open, closed without being merged, or merged. Forgejo spreads that across '''five''' independent fields — state, closed_at, merged, merged_at, merge_commit_sha and merged_by — which is exactly the shape SCALA_CODE_STYLE.md forbids a domain model to keep: as six optional fields a caller can build a pull request that is open and merged, or merged with no merge commit and closed at a time it was never closed, and every reader has to remember which combinations the server actually produces.

==state alone cannot tell you==

'''A merged pull request reports state: "closed".''' golden/pull/single-merged.json is pull request 13726 with "state": "closed", "merged": true, a merged_at, a merge_commit_sha and a merged_by, and golden/pull/list-closed.json contains that same pull request beside 13730 and 13711, which are "closed" and '''not''' merged. Anything that branches on state and stops there reports every merge as a rejection. That is why from looks at the merge evidence before it looks at state.

==Why the merged fields are still optional==

They are measured, not assumed. In golden/pull/list-closed.json pull request 13726 arrives with merged: true, merged_at and merge_commit_sha all populated and "merged_by": null — while golden/pull/single-merged.json, the '''same''' pull request read through GET /pulls/{index}, carries a full merged_by user. The listing endpoint simply does not resolve the merging account. Making mergedBy mandatory would therefore fail every merged pull request on every listing, so Merged keeps each piece of evidence optional and only guarantees that an Open or a Closed pull request has none of it.

Attributes

Companion
object
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
enum
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class Review

One review of a pull request, as GET /repos/{owner}/{repo}/pulls/{index}/reviews reports it.

One review of a pull request, as GET /repos/{owner}/{repo}/pulls/{index}/reviews reports it.

Owned by this group per docs/LEDGER.md. A "review" here is any row on that endpoint, which includes the review '''requests''' Forgejo records when someone is asked to look at a pull request: on golden/pull/reviews-list.json two of the three rows are ReviewState.RequestReview with an empty body and no commit, and only the third is an approval. state is what separates them.

Forgejo's PullReview also carries a team — a review requested from a whole team rather than an account. That field is null on all three fixture rows and its payload is the organisation wave's Team model, so it is kept on com.worxbend.codeberg4s.pulls.wire.ReviewDto and not modelled here.

Value parameters

author

the account that reviewed, or whose review was requested

body

the review's summary text, absent when the reviewer left none. Empty on every row of the fixture, which com.worxbend.codeberg4s.codec.JsonFields.text reads as absence

commentCount

how many inline diff comments the review carries; 0 for a review request and for a summary-only review

commit

the head commit the review was made against, absent for a review request — nothing has been reviewed yet, so there is nothing to pin it to

htmlUrl

the browser URL of the review, absent for a review request — "" on the fixture's two request rows and a real anchor on the approval

id

the instance-wide identifier; see ReviewId

isDismissed

whether a maintainer dismissed the review, which removes it from the approval count without deleting it

isOfficial

whether the review counts towards the base branch's required-approval rule. A review from someone without write access is recorded but not official

isStale

whether the pull request has moved on since commit, which makes the verdict advisory rather than current

state

what the review said, absent when the instance sent a value this library does not recognise — including the "" it sends for a stateless row. See ReviewState.parse

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class ReviewComment

One inline remark attached to a diff line, as GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments reports it.

One inline remark attached to a diff line, as GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments reports it.

Forgejo's PullReviewComment. This is '''not''' com.worxbend.codeberg4s.issues.Comment: a pull request's ordinary conversation lives on the issue endpoints and carries no path, no diff hunk and no line numbers, whereas every value here is anchored to a position in a file. The two are deliberately separate models, because merging them would produce one whose half-populated fields depend on which endpoint answered.

==Derived from the pinned spec, not from a capture==

golden/MANIFEST.md records no review-comment fixture — the anonymous harvest could reach /pulls/{n}/reviews but every review of the harvested pull requests carried comments_count: 0, so there was nothing to capture. The fifteen fields below are the fifteen properties of the pinned spec's PullReviewComment definition, and docs/HAZARDS.md §1 is why every one of them is treated as absent-able. Should a capture ever contradict this model, the capture wins.

==The two positions, and what 0 means==

Forgejo types position and original_position as uint64 and uses 0 as "not this side of the diff": a comment on an added line has a position and 0 for originalPosition, and a comment on a removed line has it the other way round. That sentinel is preserved rather than folded into an Option, because 0 is also what an absent key decodes to and the two are genuinely indistinguishable on the wire. Read them together with path: a comment with neither position set is a remark about the file rather than about a line.

Value parameters

author

the account that wrote the remark

body

the remark itself, as Markdown source

commit

the commit the comment currently applies to

diffHunk

the excerpt of the diff the remark is anchored to, as Forgejo rendered it at the time

extraLinesCount

how many further lines after position the remark covers; 0 is a single-line comment, which is Forgejo's own reading of the field

id

the instance-wide identifier, and the only way to address the comment; see ReviewCommentId

originalCommit

the commit the comment was written against, which differs from commit once the branch has moved

originalPosition

the line in the '''old''' side of the diff, 0 when the comment is not on the old side

path

the file the remark is about, relative to the repository root

position

the line in the '''new''' side of the diff, 0 when the comment is not on the new side

resolver

the account that marked the conversation resolved, absent while it is still open

reviewId

the review the comment was written as part of, absent when the instance sent nothing or sent Forgejo's 0 placeholder for a comment not yet attached to a submitted review

Attributes

Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type
object ReviewId

Attributes

Supertypes
class Object
trait Matchable
class Any
Self type
ReviewId.type
object ReviewRequest

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class ReviewRequest(reviewers: Vector[Username], teams: Vector[String])

Who to ask for a review, or whose request to withdraw — Forgejo's PullReviewRequestOptions.

Who to ask for a review, or whose request to withdraw — Forgejo's PullReviewRequestOptions.

One type for two operations, because Forgejo sends the same body to both: com.worxbend.codeberg4s.pulls.PullRequestApi.requestReviews adds the named reviewers and com.worxbend.codeberg4s.pulls.PullRequestApi.removeReviewRequests removes them. Nothing about the value says which; the endpoint does.

==Accounts and teams are different keys, not one list==

reviewers holds account handles and team_reviewers holds team '''names''', and Forgejo will not look a value up in the other list. So the two are separate here as well, and each is built from an already-validated type:

  • an account is a com.worxbend.codeberg4s.users.Username, which has already rejected a blank handle and a handle containing a slash;
  • a team is a com.worxbend.codeberg4s.organizations.Team — the wave-5 model, consumed rather than re-described. Forgejo identifies a team by name and a name is easy to mistype, so this library takes the team value a caller read from client.organizations instead of a string it cannot check.

'''An empty list contributes no key.''' Asking for nobody is not the same request as asking for the default, and a PullReviewRequestOptions with both lists empty is a 422 rather than a no-op.

Value parameters

reviewers

the accounts to ask, in order

teams

the team names to ask, in order

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

What a Review said.

What a Review said.

Forgejo's ReviewStateType, and the one enum in this group whose wire values are '''upper''' case with underscores — "APPROVED", "REQUEST_REVIEW" — rather than the lowercase spellings the rest of the API uses. golden/pull/reviews-list.json shows two of them on one pull request; the others are read from Forgejo's own ReviewStateType constants rather than measured, which is exactly why parse answers None on an unrecognised value instead of failing.

Note that a review request is itself a review here: asking someone to review produces a RequestReview row on the reviews endpoint, so a caller counting approvals must filter rather than count.

Attributes

Companion
object
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object ReviewState

Attributes

Companion
enum
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
object SubmitReview

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class SubmitReview(event: ReviewState, body: Option[String])

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} may be told, as one value.

Everything POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} may be told, as one value.

Forgejo's SubmitPullReviewOptions: it finishes a review that already exists as the reviewer's pending draft — created by CreateReview with no event, and filled in with com.worxbend.codeberg4s.pulls.PullRequestApi.createReviewComment — and turns it into a submitted verdict that everyone can see.

'''The event is required here''', unlike on CreateReview, because submitting without saying what the review says has no meaning: Forgejo answers 422. ReviewState.Pending is equally meaningless — it is what the review already is — and ReviewState.RequestReview belongs to a different endpoint entirely; both come back as a 422 rather than as anything this library can catch in advance.

Value parameters

body

the summary text, absent to keep whatever the pending review already carried. Forgejo requires one for ReviewState.Comment and ReviewState.RequestChanges when the draft has none

event

what the review says: ReviewState.Approved, ReviewState.RequestChanges or ReviewState.Comment

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

How POST /repos/{owner}/{repo}/pulls/{index}/update should bring a pull request's branch up to date with its base.

How POST /repos/{owner}/{repo}/pulls/{index}/update should bring a pull request's branch up to date with its base.

This operation writes to the '''head''' branch, and the two cases differ in whether that write is destructive:

  • Merge records a merge commit of the base into the head. Every existing commit keeps its sha, so anyone who has the branch checked out can fast-forward;
  • Rebase replays the head's commits onto the base and '''force-pushes''' the result. Every commit gets a new sha, and every existing checkout of the branch — and every review pinned to one of the old commits — is left pointing at history that no longer exists.

Forgejo has no third option and no "leave it to the repository's preference": its handler reads the style query parameter and treats anything other than the literal rebase as a merge. Naming one of these is therefore exactly as expressive as omitting the parameter, and considerably harder to get wrong by accident, which is why com.worxbend.codeberg4s.pulls.PullRequestApi.updateBranch requires it.

Attributes

Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Types

opaque type PullRequestHead

The branch a pull request is opened '''from''', in the spelling CreatePullRequestOption.head expects.

The branch a pull request is opened '''from''', in the spelling CreatePullRequestOption.head expects.

Forgejo overloads one string with two meanings: a bare branch names a branch of the same repository, and owner:branch names a branch of a fork. golden/pull/single-open.json is the second kind — head fix-pep691 in trim21/forgejo against base forgejo in forgejo/forgejo.

There is no from(value: String) here, and that is the point. The two spellings are produced by the two constructors below out of values that are '''already''' validated — an com.worxbend.codeberg4s.repositories.Owner cannot contain a / or a control character, and a com.worxbend.codeberg4s.repositories.BranchName cannot contain a traversal segment — so this type never has to parse a colon back out of a string and never has to decide what a:b:c meant. Both constructors are total, which is why neither returns an Either.

Attributes

opaque type PullRequestNumber

The number a repository gives a pull request — the {index} of /repos/{owner}/{repo}/pulls/{index}.

The number a repository gives a pull request — the {index} of /repos/{owner}/{repo}/pulls/{index}.

This is '''not''' PullRequest.id. Forgejo returns both: id is the instance-wide row identifier, which no endpoint in this group accepts, and number is the per-repository index every URL and every human uses. They are both int64 and both plausible values for each other, so keeping them in different types is the only thing that stops a 404 — or, worse, a successful read of an unrelated pull request.

==Why this is not IssueNumber==

Forgejo numbers issues and pull requests from one shared per-repository sequence, so pull request 13731 and issue 13731 are the same underlying row and GET /issues/13731 really does answer with the pull request. The two types are still kept apart, because the '''endpoints''' are not interchangeable: GET /pulls/{index} on an issue number that belongs to a plain issue answers 404, and GET /issues/{index} on a pull request answers with the issue-shaped projection rather than with a PullRequest. A single type would make both mistakes silent, and neither is worth the saving of one opaque type. There is deliberately no conversion between them for the same reason.

Attributes

opaque type ReviewCommentId

The instance-wide identifier of a ReviewComment — the {comment} of /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment}.

The instance-wide identifier of a ReviewComment — the {comment} of /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment}.

Kept apart from ReviewId even though both are int64 and both appear in the same path, one segment from each other. /reviews/{id}/comments/{comment} with the two swapped is a request Forgejo answers 404 to, and a 404 on a review comment reads like "the maintainer deleted it" rather than like a caller bug. The types are what make that mistake fail to compile instead.

==No fixture proves the shape of the values==

golden/MANIFEST.md holds no capture of a review-comment payload, so the only thing asserted here is Forgejo's declared type — a positive int64, as every other row identifier in the API is. See ReviewComment for what that means for the model as a whole.

Attributes

opaque type ReviewId

The instance-wide identifier of a Review — the {id} of /repos/{owner}/{repo}/pulls/{index}/reviews/{id}.

The instance-wide identifier of a Review — the {id} of /repos/{owner}/{repo}/pulls/{index}/reviews/{id}.

Reviews are addressed by this id and never by a per-pull-request index: the ids on golden/pull/reviews-list.json are 1654064, 1654067 and 1654076 for three reviews of the '''same''' pull request, which is what an instance-wide sequence looks like.

Attributes