Skip to content

feat(git): browse a connection's repositories when linking - #691

Merged
pikann merged 3 commits into
Paca-AI:masterfrom
gorlix:feat/git-repository-picker
Nov 3, 2026
Merged

pikann merged 3 commits into
Paca-AI:masterfrom
gorlix:feat/git-repository-picker

Conversation

@gorlix

@gorlix gorlix commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Refs #656 (item 3, "display the repository list picker")

What

Linking a repository meant typing group/subgroup/project by hand. The provider SPI already has ListRepositories, but core never called it. This wires it up.

  • API: GET /projects/{id}/git/connections/{identityId}/repositories?q=&cursor=, gated by git:write (it uses the connection's credential, like linking). The connection must be one the project can use (the workspace's or its own); anything else is GIT_NOT_FOUND. Returns {items, next_cursor, supported}.
  • Unsupported providers: a provider answering unsupported gives 200 with supported: false and no items, so the form falls back to the text field without an error.
  • Provider errors go through failWithProviderMessage (fix(git): surface the provider's message when linking a repository or registering a webhook fails #660), so the platform's reason shows under the list.
  • Web: a Browse repositories button in the link form (shown when a connection is chosen or is the only one). Debounced search, Load more, private repositories marked, already linked ones disabled. Picking one fills the repository field; typing a path or URL still works and linking is unchanged.
  • Strings in all 9 locales; docs updated (git-provider-spi.md, git-agent-workflow.md).

No SPI change, no migration, MCP unchanged.

Tests

  • Go: service (query/cursor/identity passed to the provider, workspace and own connection visible, another project's and an agent's not found, unsupported, error propagation) and handler (args, no leak of the provider's opaque ref, unsupported, 404/400/422 with provider_message, 400 on q over 200 or cursor over 1024 chars).
  • Router: route coverage pins git:write on the picker (git:read alone gets 403).
  • Web (vitest): list and pick then link, debounced search, load more, linked marker, unsupported fallback, error with platform reason, several connections need a choice.
  • go vet, go test ./..., golangci-lint (0 issues), tsc -b, biome, i18n parity.
  • End to end in an isolated stack, with the real com.paca.gitlab 0.6.0 wasm against GitLab CE 19.5.1 (this branch merged with the private hosts one, Chrome headless): list, search, link, Load more over 35 projects, and a revoked token showing the provider's message with the text field still usable. A 403 on the listing itself was not exercised.

🤖 Generated with Claude Code

gorlix and others added 3 commits November 2, 2026 10:43
Add GET /projects/{id}/git/connections/{identityId}/repositories, which
pages ListRepositories of the provider for a connection the project can
use, and a searchable picker in the link form. Typing a path or URL stays
possible; a provider that cannot list falls back to it.

Refs Paca-AI#656

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… gate

q is limited to 200 characters and cursor to 1024; longer values get a 400.
A route coverage test pins git:write so git:read alone gets a 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web input caps the search at 200 characters, so a multibyte search
under that cap was refused with 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestions only — two small robustness points inline.

Reviewed changes

A repository picker for the Git link form, backed by a new endpoint that calls the provider SPI's ListRepositories.

  • API — GET /projects/{id}/git/connections/{identityId}/repositories?q=&cursor=, pinned to git:write. The service confines the connection with visibleIdentity (workspace-shared or the project's own), maps a provider unsupported answer to a quiet 200 {supported:false}, and routes other provider errors through failWithProviderMessage. The DTO returns only public fields, so no provider_ref/clone URL leaks.
  • Web — GitRepositoryPicker (debounced search, Load more, linked repos disabled, private marked, unsupported/error fallback) wired into the link form; picking only fills the field, so typed path/URL linking is unchanged.
  • Tests & docs — Go service/handler/route-coverage tests, the vitest picker suite, strings in all nine locales, and SPI/workflow docs.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

// Upper bounds on the picker's inputs, which are forwarded to the platform.
const (
maxRepositorySearchLen = 200
maxRepositoryCursorLen = 1024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cursor is opaque and provider-issued, and the web forwards next_cursor verbatim on Load more — so any provider whose cursor exceeds 1024 bytes gets a hard 400 on page 2, without the graceful fallback the rest of this endpoint has. Since the bound is enforced on data core itself receives from the plugin, consider lifting it, or documenting it in docs/plugins/git-provider-spi.md (which this PR already touches) so provider authors know to keep cursors short.

Technical details
# Cursor length cap vs. provider-issued cursors

## Affected sites
- services/api/internal/transport/http/handler/git_handler.go:482 — `maxRepositoryCursorLen = 1024`, applied at :501
- apps/web/src/components/projects/settings/GitRepositoryPicker.tsx:55 — `getNextPageParam` forwards the provider's `next_cursor`

## Required outcome
- A provider that returns a `NextCursor` longer than the cap must not make **Load more** fail; the cap should be documented as a provider contract, raised, or removed.

<Skeleton className="h-8 w-full" />
</div>
);
} else if (list.isError) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In React Query v5 a failed fetchNextPage sets status to error while keeping the existing data, so list.isError is true here and this branch replaces the already-loaded repositories with "Couldn't load the list" — even though the list did load and only one more page failed. Consider gating the full-replace on !list.data and surfacing a next-page failure near Load more instead, so a transient error (or an over-long cursor, above) does not wipe the visible list.

@pikann pikann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for the contribution! 🚀

@pikann
pikann merged commit b0c4dd3 into Paca-AI:master Nov 3, 2026
7 checks passed
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