feat(git): add git review-url to resolve an issue's Linear review URL - #45
feat(git): add git review-url to resolve an issue's Linear review URL#45oliviasculley wants to merge 3 commits into
git review-url to resolve an issue's Linear review URL#45Conversation
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.
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.
|
|
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! |
|
Thanks for the PR, I will review this ASAP. |
Thermo-nuclear code quality reviewVerdict: request changes 1. [Blocker] Decompose
|
Addresses the review on nesszer#45. Move `review-url` out of `git.rs` into `src/commands/git/review_url.rs`, so `git.rs` holds the command variants and local VCS work again rather than GraphQL queries, feed pagination, and API decoding. `git.rs` returns to roughly its pre-feature size. Replace the raw-`Value` merge pipeline with typed deserialization and a serializable `ReviewEntry`. Sources merge into one map keyed by GitHub pull request URL, with the agent-session fallback inserted first so a notification result replaces it — precedence is now the merge's structure rather than a convention about ordering. Missing fields are `Option`s instead of silent nulls, and a pull request Linear returns without a URL yields no entry rather than one with a null identity. Resolution is modelled as resolved plus unresolved pull requests. Previously an issue with two attached pull requests where only one resolved printed that one URL and exited 0, saying nothing about the other. Unresolved pull requests are now part of the output contract: `-o json` returns `{"resolved": [...], "unresolved": [...]}` and the plain-text form names them on stderr. The command still fails only when it resolved nothing. Drop the private cursor state machine in favour of `paginate_until`, a short-circuiting paginator alongside `paginate_nodes` in `pagination.rs`. It follows the canonical cursor rules — including stopping when a connection claims `hasNextPage` without returning an `endCursor`, which the private loop would have answered by rereading the first page until its five-page cap. It stops as soon as every wanted pull request is found, so the common resolved-on-the-first-page case still costs one request rather than five. Also drop a trailing blank line in `initiatives.rs` that failed `cargo fmt --check` and so kept Clippy from running.
Hopefully this looks good! |
What
linear-cli git review-url <issue>prints the Linear review page URL(s) for the pull requests attached to an issue:How, and why this way
There is no public GitHub-URL → review-URL lookup, and the slug is not on the issue:
issue.attachments(andattachmentsForURL) carry the pull request as asourceType: "github"attachment with rich GitHub metadata and no review URL. So this resolves in two steps:PullRequestNotification— the one public place a pull request is paired with its review page. Itsurlis 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'sgithubpull 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.AgentSession.pullRequestsexposesPullRequest.slugIddirectly, from which the URL is assembled withorganization.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 aPullRequestbut is[Internal]and takes aDiffid nothing public hands out — it rejects a slug, a PR URL, an attachment id, a GitHub node id, and apullRequestIdfrom a notification.Testing
cargo fmtclean on the touched files;cargo clippy --all-targetsadds no new warnings.git::handlegains the&OutputOptionsargument the other command modules already take, so-o json/--formatwork here too.