Skip to content

Add Azure DevOps pull-request support (parallel to the GitHub PR feature) #14

@geevensingh

Description

@geevensingh

Problem

DiffViewer's PR-review feature (shipped in v0.4.0, polished through
v1.0.0) only handles GitHub pull requests. A meaningful chunk of
real-world Windows users — especially in Microsoft / enterprise /
government orgs — review PRs in Azure DevOps Services (and Azure
DevOps Server / TFS), not GitHub. Today, pasting an ADO PR URL on
the command line falls through to a "not a PR URL" error from
PullRequestRef.TryParse, even though the rest of the resolver
pipeline (local-clone locator, missing-clone prompt, libgit2 fetch
into refs/diffviewer/…, recents persistence) is provider-agnostic
in shape.

Proposal

Add Azure DevOps PR support as a second
IReviewRef
implementer, mirroring the GitHub layout and reusing every
provider-agnostic seam the v0.4.0 work already established.

DiffViewer.exe https://dev.azure.com/{org}/{project}/_git/{repo}/pullrequest/{id}
DiffViewer.exe https://{org}.visualstudio.com/{project}/_git/{repo}/pullrequest/{id}

The resolved diff is the same (merge-base, head) two-commit
comparison as the GitHub path: every existing affordance
(side-by-side / inline, hunk navigation, stage / unstage / revert,
recents, ref picker, find, viewed checkbox) works unchanged on ADO
PR contexts because the resolver returns a normal ParsedCommandLine
with two CommitIsh SHAs.

What already exists (the seams the v0.4.0 work left for us)

The PR-review feature was deliberately built with this extension in
mind. Surfaces already provider-agnostic or ADO-ready:

  • IReviewRef
    (Models/IReviewRef.cs)
    — discriminator interface. Its own xmldoc names "ado" as the
    reserved provider ID and ADO PRs as the planned implementer.
  • RecentsJsonSerializer
    (Services/RecentsJsonSerializer.cs)
    — already routes on a provider discriminator; an unknown
    provider drops the review-ness of the row instead of failing
    the whole load. Adding "ado" is forward-compatible — older
    binaries reading a newer file silently lose the ADO review row
    but keep its repo identity.
  • IPullRequestResolver / PullRequestResolution
    (Services/IPullRequestResolver.cs)
    — the orchestrator's interaction with the coordinator
    (Ready / MissingClone / Failed) is already shaped right for
    any provider. The orchestrator itself is currently typed to
    PullRequestRef — see "What needs to change" below.
  • ILocalRepoLocator + RemoteUrlMatcher
    (Services/LocalRepoLocator.cs,
    Services/RemoteUrlMatcher.cs)
    — match remote URLs on every remote a candidate clone configures,
    for both HTTPS and SSH (scp-style + RFC long form). ADO remote
    URLs follow the same general shape (host, owner, repo / org,
    project, repo), so the locator just needs the matcher updated.
  • PullRequestLocalFetcher
    (Services/PullRequestLocalFetcher.cs)
    LibGit2Sharp fetch / lookup / merge-base. The fetch shape is
    generic; the ref-name conventions are provider-specific (GitHub's
    refs/pull/N/head is published by the host; ADO's source branch
    is just a regular refs/heads/{name}, see open questions).
  • Missing-clone dialog + AppSettings (RepoRoots,
    DefaultCloneDestination, RepoUrlMappings)

    (Models/AppSettings.cs,
    Services/MissingClonePromptHost.cs)
    — provider-agnostic; no changes needed for the basic flow.

What needs to change (concrete plan)

1. New model: AzureDevOpsPullRequestRef : IReviewRef

ADO identity is organization / project / repository / id, not
GitHub's owner / repo / number. Parallel to PullRequestRef:

  • ProviderId = "ado"
  • Slug example: MyOrg/MyProject/MyRepo!123 (ADO uses !N for PR
    references in commit messages; alternative: MyOrg/MyProject#123
    for symmetry with GitHub — pick one in design).
  • WebUrl: the canonical https://dev.azure.com/... form.
  • IdentityNumber: the PR id.
  • TryParse: accepts both URL forms above plus
    optional DefaultCollection prefix and percent-encoded project /
    repo names.

2. New service: IAzureDevOpsClient

Parallel to IGitHubClient. One method —
GetPullRequestAsync(AzureDevOpsPullRequestRef, CancellationToken)
returning an AdoPullRequestInfo projection of the
GET /_apis/git/repositories/{repo}/pullRequests/{prId} response,
narrowed to the fields the resolver needs (sourceRefName,
targetRefName, lastMergeSourceCommit, lastMergeTargetCommit, status,
title, repository.remoteUrl).

3. New service: IAzureDevOpsAuthProvider

Parallel to IGitHubAuthProvider. The v1 GitHub implementation
shells out to gh auth token. Multiple ADO options exist (see open
questions); pick one for the first cut.

4. Generify the resolver seam

IPullRequestMetadataResolver.ResolveAsync currently takes a
concrete PullRequestRef. Choose one of:

  • (a) Add a sibling interface IAzureDevOpsPullRequestResolver
    with its own concrete Resolve(AzureDevOpsPullRequestRef). Two
    parallel pipelines, dispatched on IReviewRef.ProviderId by the
    coordinator. Simplest, mirrors the GitHub layout most exactly.
  • (b) Genericize IPullRequestResolver over IReviewRef, with
    per-provider IReviewMetadataClient implementations registered by
    provider id. Cleaner, but a bigger refactor and touches existing
    GitHub code paths.

Recommend (b) as the architecturally-correct option (per AGENTS.md
§4 "don't default to the minimal change") — it's the shape every
future provider will land into. Recommend (a) only if there's a real
schedule reason.

5. Extend URL routing in CommandLineParser / PullRequestRef.TryParse

The current entry point pattern-matches on
Uri.Host == "github.com". Switch to a registry of
IReviewUrlParser implementations, each registered with the host
patterns it recognizes (github.com, dev.azure.com, *.visualstudio.com).
Unknown URL → today's "not a PR URL" error, unchanged.

6. Extend RemoteUrlMatcher + RepoUrlKey

RepoUrlKey is currently (Host, Owner, Repo). ADO is
(Host, Organization, Project, Repository) — four parts. Either:

  • Bump RepoUrlKey to include an optional Project that's ""
    for GitHub and populated for ADO. Stays one type; serialization
    picks up an extra | segment.
  • Introduce IRepoIdentityKey with concrete GitHubRepoUrlKey
    and AdoRepoUrlKey implementers; AppSettings.RepoUrlMappings
    re-keyed on the interface.

The first is much less churn; the second is cleaner. Both work.
Decide as part of the design phase.

7. New URL parsing in RemoteUrlMatcher

Add dev.azure.com and *.visualstudio.com patterns to the
HTTPS / SSH form recognizers, including the
organization@vs-ssh.visualstudio.com:v3/organization/project/repo
SSH form ADO uses (different shape from GitHub's
git@github.com:owner/repo).

8. Recents serializer wiring

Add "ado" to the provider switch in
RecentsJsonSerializer.TryDeserializeReview. Implement
TryDeserializeAdoPullRequest mirroring the GitHub helper.

9. UI: launch dialog form

Add an Azure DevOps pull request form to the
NewDiffDialog
parallel to the existing GitHub PR form. Same shape: paste a URL,
get a resolved diff. The example URL shown should cover both ADO
URL formats.

10. README + non-goals update

Update the Pull request review
section to enumerate both providers, and call out ADO-specific
caveats (PAT setup if that's the auth choice, Azure DevOps Server /
on-prem TFS support).

Open questions (substantive — block design)

  1. Auth strategy. GitHub uses gh auth token. ADO options:

    • Personal Access Token in AppSettings — most portable but
      stores a long-lived credential in our settings file.
    • az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798
      (Azure CLI; the magic GUID is the ADO resource ID). Mirrors the
      gh auth token shape closely.
    • az devops login cached token — depends on az devops
      being installed.
    • MSAL / interactive browser auth — best UX, biggest dependency.
      Microsoft.Identity.Client is a hefty add for a single-file
      publish.

    The gh auth token analog is the az CLI access-token path.
    Recommended v1 unless there's a reason not to ship the az CLI
    dependency. Long-term, the architecture should allow multiple
    auth providers (PAT fallback for users who don't have az).

  2. Azure DevOps Server (on-prem TFS) support. The on-prem
    product uses the same REST API surface but lives on
    https://{tfs-host}/tfs/{collection}/... URLs. Out of scope for
    the first cut (analogous to "github.com-only in v1"), but the
    URL parser and host registry should be shaped so adding it later
    is configuration, not refactor.

  3. Repo identity granularity. ADO repositories are scoped to
    (organization, project, repository) and can be moved between
    projects without changing identity. Should AdoRepoUrlKey treat
    the project as load-bearing (preserves "I configured this for
    project X" intent) or hide it (matches the GUID identity ADO
    actually persists)? Recommendation: project is part of the key,
    matching how users name and find repos.

  4. PR ref convention for the fetcher. GitHub publishes a stable
    refs/pull/N/head on the upstream. ADO does not auto-publish
    a per-PR head ref the same way — the PR source is just the
    regular refs/heads/{source-branch} on the same repo. The
    fetcher should:

    • Fetch refs/heads/{sourceRefName} and refs/heads/{targetRefName}
      from the repo's clone URL.
    • Compute merge-base locally (same as today's GitHub path).
    • Pin to lastMergeSourceCommit.commitId as advisory, re-read
      from the local fetched ref.

    Verify against the actual REST API before committing — the
    above is the documented shape, but DiffViewer's fetcher
    currently relies on a host-published per-PR ref, which ADO
    simply doesn't have.

  5. Fork PRs. ADO supports cross-fork PRs (sourceRepository ≠
    targetRepository). The GitHub fetcher handles this by fetching
    from the base repo's refs/pull/N/head. ADO doesn't have that
    shortcut — the fetcher needs to fetch the source branch from
    sourceRepository.remoteUrl directly. Mostly works the same as
    the existing fork path, but the API response needs the source
    repo's clone URL projected through.

  6. URL slug rendering. GitHub recents use owner/repo#N (e.g.,
    microsoft/vscode#42). ADO has four parts. Options:

    • MyOrg/MyProject/MyRepo!123 — uses ADO's own ! ref convention.
    • MyOrg/MyRepo#123 — drops project for brevity; risks ambiguity
      if two projects in the same org name a repo identically.
    • MyProject/MyRepo#123 — drops org, similar risk.

    The first is unambiguous and matches ADO's own UI; the others
    look more like GitHub but lose info.

Acceptance

  • Pasting an ADO PR URL (both dev.azure.com and *.visualstudio.com
    forms) launches the diff with merge-base on the left and head
    on the right, same as GitHub PRs do today.
  • The PR appears in the recents dropdown with an ADO-style label
    (chosen per open question 6).
  • Clicking a recents row always re-resolves so a re-pushed PR shows
    the latest head SHA.
  • Auth failures, missing-clone, fork-PR, and force-pushed-PR paths
    all surface the same way GitHub PR failures do (per the error
    matrix the Phase 4 work documented).
  • recents.json written by this version is readable by older v1.x
    binaries: ADO rows downgrade to "repo identity, no review-ness",
    GitHub rows are unaffected.
  • README's Pull request review
    section enumerates both providers and notes the auth-tool
    prerequisite (az CLI or PAT, per open question 1).
  • Tests cover: ADO URL parser, remote-URL matcher patterns, ADO
    API client (with a fake HTTP handler), recents
    serialize / deserialize round-trip, and the orchestrator's
    provider dispatch.

Notes

Scope: large, multi-phase. The v0.4.0 GitHub PR work landed in
nine commits (Phase 1 → Phase 9), and the ADO equivalent will be
similarly sized — somewhat shorter because half the orchestrator,
locator, missing-clone, recents-serialization, and settings work is
already done, but somewhat longer because the auth story is messier
and the URL / ref / repo-identity shapes don't map 1:1 onto
GitHub's.

Splitting into sub-issues will be necessary. Suggested split:

  1. ADO URL parser + AzureDevOpsPullRequestRef + recents
    serialization (needs-design: open questions 3 + 6).
  2. Genericize the resolver / metadata-resolver seam over
    IReviewRef (needs-design: open question 4(b) vs 4(a)).
  3. ADO API client + auth provider (needs-design: open question 1).
  4. ADO-aware remote-URL matcher + locator integration.
  5. ADO-aware fetcher (force-push handling, fork repos).
  6. UI form in NewDiffDialog + README.

v1.x (minor bump per SemVer post-1.0). Adding a new provider is a
purely additive change to the user-facing surface, so doesn't
require v2.

The on-disk surfaces this lands on (recents.json's provider
discriminator, AppSettings.RepoUrlMappings key shape) are
stability commitments as of v1.0.0, so any change to existing
fields needs forward/back compat (see open question for
RepoUrlKey shape).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-designSubstantive open questions to resolve before implementationpriority: mediumWorth doing; not blockingscope: largeBig effort; consider splitting into sub-issues

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions