Skip to content

GitHubProvider.GetRepositoriesAsync cannot see private repositories, so the two hosting providers answer the same contract very differently #100

Description

@matt-edmondson

What happens

GitHubProvider.GetRepositoriesAsync (GitIntegration/GitHubProvider.cs:68) calls client.Repository.GetAllForUser(Owner), which is GET /users/{login}/repos. That endpoint returns public repositories only, and supplying a token does not widen it.

AzureDevOpsProvider.GetRepositoriesAsync has no such restriction — its enumeration returns everything the supplied token can see.

So two implementations of one IGitHostingProvider method differ in coverage in a way a caller cannot detect from the interface. A caller pointed at a GitHub organisation with a valid token silently gets a partial list; the same code against Azure DevOps gets a complete one. The XML doc at GitHubProvider.cs:56-66 states this outright — "A caller that needs private repositories for a specific owner has no equivalent through this provider today" — so it is a known limitation rather than a surprise, but it is a limitation worth closing rather than only documenting.

Why it was left this way

The doc comment records the reasoning, and it is sound as far as it goes: switching to GET /user/repos would stop honouring GitProvider.Owner, because that endpoint always describes the token's own repositories regardless of which owner was configured. That would break callers who name someone else's owner on purpose, which is a worse failure than under-reporting.

But /user/repos is not the only alternative.

Suggested approach

GitHub distinguishes users from organisations, and the organisation case has an endpoint that honours both the owner and the token:

  • GET /orgs/{org}/repos (Octokit: client.Repository.GetAllForOrg(org)) returns private repositories the authenticated token can see, scoped to the named org. For an org owner this closes the gap completely with no change to Owner semantics.
  • For a user owner, /users/{login}/repos stays the only owner-honouring endpoint, so the public-only limit is real there. Where the configured Owner is the token's own login, GET /user/repos with affiliation=owner is exactly equivalent plus private repositories, and can be used for that case specifically.

A reasonable implementation:

  1. Resolve whether Owner names a user or an organisation (GET /users/{login} reports type, or attempt the org call and treat NotFoundException as "not an org").
  2. Organisation → GetAllForOrg.
  3. User matching the authenticated login (from GET /user) → GetAllForCurrentUser with affiliation=owner.
  4. Any other user → the current GetAllForUser, still public-only, which is genuinely all GitHub offers.

Whatever shape is chosen, the resulting coverage should be stated on IGitHostingProvider.GetRepositoriesAsync in terms the interface owns, rather than each provider describing its own behaviour and leaving callers to compare them.

Testing

The hosting layer has no integration tier by design, so this is covered the way the rest of the layer is: FakeHttpMessageHandler through GitProvider's internal Handler seam, with fixtures for the org, own-user and other-user branches, and a fixture for the NotFoundException fallback if the probe approach is used.

Rationale

Enumerating repositories is the entry point to almost everything else this layer does — a caller lists repositories, then opens or clones one, then reads its pull requests. Silently omitting an organisation's private repositories means the most common real-world case (a private org repo, with a token that can plainly see it) is invisible, and the caller has no signal that anything was left out.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions