All packages
Git for Laravel
Merging & reviewing PRs
Close, approve and merge a pull request:
use RoundlyConsulting\Git\Enums\MergeMethod;
$pr = Git::github()->repo('acme/acme')->pullRequest($number);
$pr->close(); // PullRequest, state closed — never merges
$pr->approve('LGTM'); // the review's own state, e.g. "APPROVED"
$sha = $pr->merge(
MergeMethod::Squash, // Merge | Squash | Rebase — a repository may disallow any of them
sha: $headSha, // the head commit you decided about
title: 'Add CI (#7)',
message: 'Squashed by the release bot.',
);- A merge the forge refuses throws. 405 (branch protection, a required check, a disallowed merge method) and 409 (conflict, or sha no longer matching) arrive as RequestException — a successful return always means it merged, so there is no merged: false to inspect.
- Pass sha — the head commit you decided about. Without it, a push landing between the review and the merge is merged unseen; with it, that answers 409.
- close() only ever closes — it never merges and never reopens.
- GitHub refuses an account approving its own pull request. For an App, every pull request the App opened is its own, so a service that authors pull requests cannot also approve them.
Publishing a review
A whole review in one call — a verdict, a summary and inline comments anchored to the diff:
use RoundlyConsulting\Git\Dto\Input\{NewReview, NewReviewComment};
use RoundlyConsulting\Git\Enums\{DiffSide, ReviewEvent};
$pr = Git::github()->repo('acme/acme')->pullRequest($number);
$review = $pr->review(new NewReview(
event: ReviewEvent::Comment, // Comment | Approve | RequestChanges
body: 'Two findings, one blocking.',
comments: [
new NewReviewComment('app/Foo.php', 42, 'This nulls out on the retry.'),
new NewReviewComment('app/Bar.php', 7, 'Deleted line, so anchor it left.', DiffSide::Left),
// A finding about a block: lines 30–36, inclusive.
new NewReviewComment('app/Baz.php', 36, 'This whole branch is unreachable.', startLine: 30),
],
));- One request, not one per comment. GitHub publishes a review atomically, so a failure never leaves half a review on the pull request.
- Approve and RequestChanges are refused on your own pull request (422), exactly as approve() is. An account that authors pull requests can only ever Comment on its own work — so put the verdict a reader acts on in the body.
- A comment anchored off the diff is also a 422 — the line was never changed. Anchor deleted lines with DiffSide::Left; the default is Right.
- startLine must come before line; an inverted or collapsed span is rejected locally with InvalidArgumentException.
Reading reviews back
$reviews = $pr->reviews();
$reviews->reviews; // list<PullRequestReview> — state, body, author, submittedAt
$reviews->comments; // list<PullRequestReviewComment> — body, path, line, side, reviewId
$reviews->isEmpty(); // nobody has reviewed it at all
$latest = $reviews->latest(); // the newest SUBMITTED review, or null
$latest?->state; // 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | …
$reviews->commentsFor($latest); // just the findings that review published
foreach ($reviews->comments as $comment) {
if ($comment->isOutdated()) { // line is null — the anchor was dropped
continue;
}
}- Use latest(), not end($reviews->reviews). GitHub serves reviews oldest-first, but a pending draft has no submittedAt and still sorts last — so the naive read hands back a verdict nobody published.
- Both endpoints are walked to the end (maxPages, 5 by default; perPage 100), because one page of a long thread is its oldest reviews.
- submittedAt is null for a pending draft, line is null on an outdated comment whose anchor GitHub dropped, and createdAt is null rather than now when the payload carries no timestamp.
GitLab and Bitbucket throw FeatureNotSupportedException for closing, approving, reviewing and merging — check supports(Feature::MergePullRequest) if you drive more than one forge.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.