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)
-
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).
-
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.
-
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.
-
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.
-
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.
-
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:
- ADO URL parser +
AzureDevOpsPullRequestRef + recents
serialization (needs-design: open questions 3 + 6).
- Genericize the resolver / metadata-resolver seam over
IReviewRef (needs-design: open question 4(b) vs 4(a)).
- ADO API client + auth provider (
needs-design: open question 1).
- ADO-aware remote-URL matcher + locator integration.
- ADO-aware fetcher (force-push handling, fork repos).
- 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).
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 resolverpipeline (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
IReviewRefimplementer, mirroring the GitHub layout and reusing every
provider-agnostic seam the v0.4.0 work already established.
The resolved diff is the same
(merge-base, head)two-commitcomparison 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
ParsedCommandLinewith two
CommitIshSHAs.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 thereserved provider ID and ADO PRs as the planned implementer.
RecentsJsonSerializer(Services/RecentsJsonSerializer.cs)
— already routes on a
providerdiscriminator; an unknownprovider drops the review-ness of the row instead of failing
the whole load. Adding
"ado"is forward-compatible — olderbinaries 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 forany 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)
—
LibGit2Sharpfetch / lookup / merge-base. The fetch shape isgeneric; the ref-name conventions are provider-specific (GitHub's
refs/pull/N/headis published by the host; ADO's source branchis just a regular
refs/heads/{name}, see open questions).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 : IReviewRefADO identity is organization / project / repository / id, not
GitHub's owner / repo / number. Parallel to
PullRequestRef:ProviderId = "ado"Slugexample:MyOrg/MyProject/MyRepo!123(ADO uses!Nfor PRreferences in commit messages; alternative:
MyOrg/MyProject#123for symmetry with GitHub — pick one in design).
WebUrl: the canonicalhttps://dev.azure.com/...form.IdentityNumber: the PR id.TryParse: accepts both URL forms above plusoptional
DefaultCollectionprefix and percent-encoded project /repo names.
2. New service:
IAzureDevOpsClientParallel to
IGitHubClient. One method —GetPullRequestAsync(AzureDevOpsPullRequestRef, CancellationToken)—returning an
AdoPullRequestInfoprojection of theGET /_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:
IAzureDevOpsAuthProviderParallel to
IGitHubAuthProvider. The v1 GitHub implementationshells out to
gh auth token. Multiple ADO options exist (see openquestions); pick one for the first cut.
4. Generify the resolver seam
IPullRequestMetadataResolver.ResolveAsynccurrently takes aconcrete
PullRequestRef. Choose one of:IAzureDevOpsPullRequestResolverwith its own concrete
Resolve(AzureDevOpsPullRequestRef). Twoparallel pipelines, dispatched on
IReviewRef.ProviderIdby thecoordinator. Simplest, mirrors the GitHub layout most exactly.
IPullRequestResolveroverIReviewRef, withper-provider
IReviewMetadataClientimplementations registered byprovider 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.TryParseThe current entry point pattern-matches on
Uri.Host == "github.com". Switch to a registry ofIReviewUrlParserimplementations, each registered with the hostpatterns it recognizes (
github.com,dev.azure.com,*.visualstudio.com).Unknown URL → today's "not a PR URL" error, unchanged.
6. Extend
RemoteUrlMatcher+RepoUrlKeyRepoUrlKeyis currently(Host, Owner, Repo). ADO is(Host, Organization, Project, Repository)— four parts. Either:RepoUrlKeyto include an optionalProjectthat's""for GitHub and populated for ADO. Stays one type; serialization
picks up an extra
|segment.IRepoIdentityKeywith concreteGitHubRepoUrlKeyand
AdoRepoUrlKeyimplementers;AppSettings.RepoUrlMappingsre-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
RemoteUrlMatcherAdd
dev.azure.comand*.visualstudio.compatterns to theHTTPS / SSH form recognizers, including the
organization@vs-ssh.visualstudio.com:v3/organization/project/repoSSH form ADO uses (different shape from GitHub's
git@github.com:owner/repo).8. Recents serializer wiring
Add
"ado"to the provider switch inRecentsJsonSerializer.TryDeserializeReview. ImplementTryDeserializeAdoPullRequestmirroring 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)
Auth strategy. GitHub uses
gh auth token. ADO options:AppSettings— most portable butstores 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 tokenshape closely.az devops logincached token — depends onaz devopsbeing installed.
Microsoft.Identity.Clientis a hefty add for a single-filepublish.
The
gh auth tokenanalog 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).
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 forthe 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.
Repo identity granularity. ADO repositories are scoped to
(organization, project, repository)and can be moved betweenprojects without changing identity. Should
AdoRepoUrlKeytreatthe 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.
PR ref convention for the fetcher. GitHub publishes a stable
refs/pull/N/headon the upstream. ADO does not auto-publisha per-PR head ref the same way — the PR source is just the
regular
refs/heads/{source-branch}on the same repo. Thefetcher should:
refs/heads/{sourceRefName}andrefs/heads/{targetRefName}from the repo's clone URL.
lastMergeSourceCommit.commitIdas advisory, re-readfrom 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.
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 thatshortcut — the fetcher needs to fetch the source branch from
sourceRepository.remoteUrldirectly. Mostly works the same asthe existing fork path, but the API response needs the source
repo's clone URL projected through.
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 ambiguityif 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
dev.azure.comand*.visualstudio.comforms) launches the diff with merge-base on the left and head
on the right, same as GitHub PRs do today.
(chosen per open question 6).
the latest head SHA.
all surface the same way GitHub PR failures do (per the error
matrix the Phase 4 work documented).
recents.jsonwritten by this version is readable by older v1.xbinaries: ADO rows downgrade to "repo identity, no review-ness",
GitHub rows are unaffected.
section enumerates both providers and notes the auth-tool
prerequisite (az CLI or PAT, per open question 1).
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:
AzureDevOpsPullRequestRef+ recentsserialization (
needs-design: open questions 3 + 6).IReviewRef(needs-design: open question 4(b) vs 4(a)).needs-design: open question 1).NewDiffDialog+ README.v1.x(minor bump per SemVer post-1.0). Adding a new provider is apurely additive change to the user-facing surface, so doesn't
require v2.
The on-disk surfaces this lands on (recents.json's
providerdiscriminator,
AppSettings.RepoUrlMappingskey shape) arestability commitments as of v1.0.0, so any change to existing
fields needs forward/back compat (see open question for
RepoUrlKeyshape).