Skip to content

feat(git): add git review-url to resolve an issue's Linear review URL - #45

Open
oliviasculley wants to merge 2 commits into
nesszer:masterfrom
oliviasculley:feat/git-review-url
Open

feat(git): add git review-url to resolve an issue's Linear review URL#45
oliviasculley wants to merge 2 commits into
nesszer:masterfrom
oliviasculley:feat/git-review-url

Conversation

@oliviasculley

@oliviasculley oliviasculley commented Aug 5, 2026

Copy link
Copy Markdown

What

linear-cli git review-url <issue> prints the Linear review page URL(s) for the pull requests attached to an issue:

$ linear-cli git review-url ABC-262
https://linear.app/acme/review/readings-read-side-aggregation-stage-1b-abc-262-3f92e33afbc6

$ linear-cli git review-url ABC-262 -o json --compact
[{"number":303,"reviewUrl":"https://linear.app/acme/review/readings-…-3f92e33afbc6","status":"merged","title":"Readings read-side aggregation","url":"https://github.com/acme/app/pull/303"}]

How, and why this way

There is no public GitHub-URL → review-URL lookup, and the slug is not on the issue: issue.attachments (and attachmentsForURL) carry the pull request as a sourceType: "github" attachment with rich GitHub metadata and no review URL. So this resolves in two steps:

  1. PullRequestNotification — the one public place a pull request is paired with its review page. Its url is the review URL (review/<title-slug>-<id>), so it's returned verbatim; a #comment-<id> anchor from a comment notification is trimmed. The command matches the issue's github pull request attachments against the feed. The feed has no server-side filter for pull requests, so it's walked newest-first for at most 5 pages — the one part of this I'd happily replace if there's a filter I missed.
  2. Agent sessions — fallback. AgentSession.pullRequests exposes PullRequest.slugId directly, from which the URL is assembled with organization.urlKey.

Results are merged per pull request, so a PR reachable both ways is listed once.

What stays unresolvable is a pull request with no notification and no agent session — in practice one opened minutes ago with no CI result, comment, or review yet. The command reports that (naming the GitHub URL as the alternative) rather than emitting a URL that would 404. Happy to switch that to exit 0 with empty output if you'd prefer it for scripting.

For completeness on what was ruled out: Query.diff(id:) reaches a PullRequest but is [Internal] and takes a Diff id nothing public hands out — it rejects a slug, a PR URL, an attachment id, a GitHub node id, and a pullRequestId from a notification.

Testing

  • 7 unit tests on pure helpers: attachment filtering (GitHub pull requests only, deduped, non-PR URLs and other sources ignored), notification matching (only the requested PR, URL used verbatim, comment anchor trimmed), merge precedence, and the agent-session path (URL from slug, dedupe across sessions, and never a bogus URL when a slug is missing or blank).
  • Full suite green: 557 tests. cargo fmt clean on the touched files; cargo clippy --all-targets adds no new warnings.
  • Verified against a live workspace on all three paths: an ordinary branch-detected PR resolves through notifications, an agent-session PR resolves through the fallback, and a PR opened an hour earlier with no activity reports the limitation.

git::handle gains the &OutputOptions argument the other command modules already take, so -o json / --format work here too.

Linear's review page for a pull request (linear.app/<workspace>/review/<slug>) has
no public lookup from a GitHub PR URL, and the slug appears nowhere on the issue or
its attachments — `issue.attachments` and `attachmentsForURL` return the GitHub URL
and GitHub metadata only. The slug lives on `PullRequest.slugId`, and the one path
that reaches a `PullRequest` with a personal API key is the agent sessions attached
to an issue (`Query.diff` is [Internal] and takes a `Diff` id nothing hands out).

So `git review-url <issue>` walks `issue.agentSessions.pullRequests`, pairs each
`slugId` with `organization.urlKey`, and prints the review URL — one per line, or
`-o json` for the PR number, state, title and GitHub URL alongside it. A pull
request linked by more than one session is listed once.

The limitation is inherent to the API rather than to this command, so it is stated
in `--help`, in the README, and in the error raised when an issue resolves to no
slug, which points at the GitHub PR URL instead of failing silently.
@oliviasculley
oliviasculley marked this pull request as draft August 5, 2026 18:51
The first pass claimed a review URL exists only for pull requests linked to an
agent session. That is wrong: Linear creates a review page for any pull request it
detects from a branch, and `PullRequestNotification` exposes it — `url` on the
notification is the review page itself (`review/<title-slug>-<id>`), alongside the
`pullRequest` it belongs to.

So `review-url` now matches the issue's `github` pull request attachments against
the notification feed and returns that URL verbatim, which also preserves the
human-readable title slug instead of dropping it by assembling `review/<id>` by
hand. A comment notification's `#comment-<id>` anchor is trimmed so the result is
the page, not a position in it. The agent-session path stays as the fallback for a
pull request with no notifications, and results are merged per pull request so a PR
reachable both ways is listed once.

The feed has no server-side pull request filter, so it is walked newest-first for
at most 5 pages; a pull request whose activity is older than that falls through to
the fallback. What remains genuinely unresolvable is a pull request with no
notification at all — typically one opened minutes ago with no CI result, comment,
or review yet — and the error says so rather than emitting a URL that would 404.
@oliviasculley

oliviasculley commented Aug 5, 2026

Copy link
Copy Markdown
Author

Pushed a correction. My first pass claimed the review URL only exists for agent-session PRs — that was wrong. Linear creates a review page for any PR it detects from a branch, and PullRequestNotification.url is that page, so the command now resolves through the notification feed (returning the canonical URL, title slug and all) and keeps the agent-session path only as a fallback. Description updated to match.

@oliviasculley
oliviasculley marked this pull request as ready for review August 5, 2026 20:36
@oliviasculley

Copy link
Copy Markdown
Author

I'm not too familiar with rust but hopefully this is good, and the command design makes sense, I'm happy to change anything about this!

@Finesssee

Copy link
Copy Markdown
Collaborator

Thanks for the PR, I will review this ASAP.

@Finesssee

Copy link
Copy Markdown
Collaborator

Thermo-nuclear code quality review

Verdict: request changes

1. [Blocker] Decompose git.rs before merging

src/commands/git.rs:184-410, 847-992

This PR pushes git.rs from 653 to 1,042 physical lines. The new code also has little to do with local Git/VCS operations: it owns GraphQL queries, notification pagination, API decoding, source precedence, deduplication, rendering, and tests.

Move this feature into a focused module such as src/commands/git/review_url.rs; leave only the command variant and dispatch in git.rs. Extraction should be combined with the simplifications below rather than merely moving all 389 lines unchanged.

2. [High] Partial resolution incorrectly exits successfully

src/commands/git.rs:381-399

The command only errors when entries.is_empty(). Given two attached PRs where one resolves through notifications and the other has neither a recent notification nor an agent-session slug, it prints the first URL and exits 0 without mentioning the unresolved PR. That contradicts the documented promise that an unresolvable PR is reported and makes scripting unreliable.

Model the result explicitly as resolved and unresolved PRs. Verify every attached PR URL was resolved before printing, or intentionally expose unresolved entries in the output contract. Add a mixed-result test covering one resolved and one unresolved attachment.

3. [High] Replace the raw-JSON merge pipeline with a typed resolution model

src/commands/git.rs:214-288

The 227-line production feature contains 35 string-indexed JSON accesses. Both sources construct anonymous Value objects, and merge_review_entries then reads those objects back through string keys. Missing fields silently become null or empty strings, while source precedence and identity are implicit conventions.

Define typed deserialization models plus a serializable ReviewEntry, then merge into one map keyed by GitHub PR URL. Insert the agent-session fallback first and let notification results replace it, or use an explicit precedence method. This removes much of merge_review_entries, the repeated json! construction, and fragile indexing while making required fields and unresolved states explicit.

4. [Medium] Do not add another private cursor-pagination implementation

src/commands/git.rs:299-337

This duplicates the canonical machinery in src/pagination.rs. It also handles malformed pagination less safely: if hasNextPage is true but endCursor is absent, it sends after: null and can reread the first page until the five-page cap.

Use paginate_nodes with a 500-item limit and 100-item page size. If stopping as soon as every PR is found materially matters, add a reusable short-circuiting paginator rather than maintaining another cursor state machine inside a feature command.

Validation

  • PR merge code compiled successfully in CI.
  • 359 unit tests and 203 integration tests passed, with zero failures.
  • git diff --check passes.
  • The PR check is red because cargo fmt --check found an unrelated trailing blank line in unchanged src/commands/initiatives.rs; Clippy therefore did not run.
  • No test currently covers partial resolution across multiple attached PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants