Skip to content

feat: add Gitea/Forgejo integration for pull request management#3332

Open
gabrielbelli wants to merge 5 commits into
BloopAI:mainfrom
gabrielbelli:feat/gitea-integration
Open

feat: add Gitea/Forgejo integration for pull request management#3332
gabrielbelli wants to merge 5 commits into
BloopAI:mainfrom
gabrielbelli:feat/gitea-integration

Conversation

@gabrielbelli

@gabrielbelli gabrielbelli commented Apr 7, 2026

Copy link
Copy Markdown

Summary

Adds a new Gitea/Forgejo provider to the git-host crate, enabling pull request management for Gitea, Forgejo, and Codeberg instances directly from Vibe Kanban.

  • Implements all GitHostProvider trait methods: create PR, get PR status, list PRs by branch, list open PRs, and get PR comments
  • Uses the Gitea REST API v1 directly via reqwest — no external CLI binary needed (unlike the gh/az CLI approach used by GitHub/Azure providers)
  • Authenticates via GITEA_TOKEN env var, with tea CLI config as fallback
  • Supports self-hosted instances with custom domains via the GITEA_URL env var
  • Auto-detects well-known hostnames (gitea.*, forgejo.*, codeberg.org) and the Gitea PR URL pattern (/pulls/)
  • Includes Forgejo compatibility out of the box (same API)
  • Adds documentation page with step-by-step setup instructions

Architecture

The implementation follows the existing provider pattern:

  • crates/git-host/src/gitea/api.rs — HTTP client wrapping the Gitea REST API v1 with reqwest. Handles token auth, response parsing, and error mapping.
  • crates/git-host/src/gitea/mod.rsGiteaProvider struct implementing the GitHostProvider trait with exponential backoff retry (matching GitHub/Azure providers).
  • crates/git-host/src/detection.rs — URL-based provider detection extended with Gitea patterns (GITEA_URL env var, /pulls/ path, well-known hostnames). Also adds gitea_base_url() helper to extract instance URL from remotes.
  • crates/git-host/src/types.rsGitea variant added to ProviderKind enum.
  • crates/git-host/src/lib.rsGitea(GiteaProvider) variant added to GitHostService enum_dispatch.

Why direct API instead of a CLI?

The GitHub and Azure providers shell out to gh and az CLIs respectively, which manage their own authentication. For Gitea:

  • The tea CLI has much lower adoption than gh/az
  • Gitea instances are self-hosted with arbitrary domains, making browser OAuth impractical without per-instance app registration
  • Direct API via reqwest (already a workspace dependency) is simpler and has zero external dependencies

Files changed

File Change
crates/git-host/src/gitea/api.rs New — Gitea REST API client (reqwest-based)
crates/git-host/src/gitea/mod.rs NewGiteaProvider trait implementation
crates/git-host/src/detection.rs Extended with Gitea URL detection + 11 new unit tests
crates/git-host/src/lib.rs Added gitea module, enum variant, from_url match arm
crates/git-host/src/types.rs Added Gitea to ProviderKind
crates/git-host/Cargo.toml Added reqwest workspace dependency
shared/types.ts Auto-generated — "gitea" added to ProviderKind
docs/integrations/gitea-integration.mdx New — Setup tutorial with troubleshooting guide
docs/docs.json Added nav entry for Gitea integration page
Cargo.lock Updated for new dependency edge

Testing

Unit tests (33 total, all pass)

  • 8 new Gitea-specific tests covering URL parsing, PR URL parsing, repo info extraction, and PR status mapping (open/merged/closed)
  • 3 new detection tests (well-known hostnames, Codeberg, /pulls/ pattern)
  • 3 new detection tests for PR URL patterns and gitea_base_url() helper
  • All 19 existing GitHub/Azure tests continue to pass

End-to-end validation

Manually tested against a live Gitea 1.25.5 instance through the full VK server stack:

Operation Endpoint Result
Create PR POST /workspaces/{id}/pull-requests PR created on Gitea
Get PR status GET /repos/pr-info?url=... Returns full PR detail
List open PRs GET /repos/{id}/prs Lists PRs from Gitea API
Get PR comments GET /workspaces/{id}/pull-requests/comments Returns unified comment list
List PRs by branch Via list_prs_for_branch trait method Returns open + closed PRs

Known limitations

  • Cross-fork PRs are not supported for Gitea (same limitation as the Azure provider)
  • create_workspace_from_pr currently hardcodes GhCli for PR checkout — this is a pre-existing issue that also affects Azure DevOps, and should be addressed in a separate PR
  • Server-side (remote) integration (webhooks, OAuth, repo cloning) is not included — this is planned as future work

Test plan

  • cargo test -p git-host — 33 tests pass
  • cargo clippy -p git-host -p server — zero warnings
  • cargo fmt -p git-host -- --check — formatted
  • pnpm run generate-types:check — TS types up to date
  • cargo check -p git-host -p server — compiles clean
  • End-to-end tested against live Gitea 1.25.5 instance
  • Verify existing GitHub/Azure PR flows are unaffected (no changes to their code paths)

Note

Medium Risk
Adds a new networked reqwest-based provider that uses GITEA_TOKEN and new URL-detection logic, which could affect provider selection and introduces new auth/error-handling paths. Existing GitHub/Azure flows are mostly untouched but share the GitHostService::from_url dispatch that now includes Gitea.

Overview
Adds Gitea/Forgejo (incl. Codeberg) pull request support to the git-host crate by introducing a new GiteaProvider backed by a reqwest REST API client (create PR, fetch PR status, list PRs, and aggregate PR comments).

Extends provider detection to recognize Gitea instances via GITEA_URL and well-known hostnames, plus a new gitea_base_url() helper to derive the correct instance base URL from remote/PR URLs (with added unit tests to avoid false positives). Updates shared types/docs to expose the new ProviderKind::Gitea and adds setup documentation for GITEA_TOKEN/GITEA_URL.

Reviewed by Cursor Bugbot for commit 37acb7f. Bugbot is set up for automated code reviews on this repo. Configure here.

Add a new Gitea provider to the git-host crate, enabling PR creation,
status tracking, branch listing, and comment retrieval for Gitea and
Forgejo instances (including Codeberg).

Unlike the GitHub and Azure providers which shell out to CLI tools, this
uses the Gitea REST API v1 directly via reqwest — no external binary
needed. Authentication is via GITEA_TOKEN env var with tea CLI config
as fallback. Self-hosted instances with custom domains are supported
via the GITEA_URL env var.

Tested end-to-end against a live Gitea 1.25.5 instance.
Comment thread crates/git-host/src/gitea/api.rs
Comment thread crates/git-host/src/gitea/api.rs
- Fetch review comments per-review via /reviews/{id}/comments instead
  of expecting inline comments in the reviews list response (Gitea API
  does not embed comments in the reviews endpoint)
- Separate 401 (AuthFailed) from 403 (InsufficientPermissions) in
  check_response_status so users with wrong token scopes get the
  correct error message
- Add InsufficientPermissions variant to GiteaApiError
- Remove unreachable 403 string-matching in From<GiteaApiError> impl
Comment thread crates/git-host/src/detection.rs Outdated
Comment thread crates/git-host/src/detection.rs
- Add empty-string guard to GITEA_URL detection: str::contains("")
  always returns true in Rust, so an empty/scheme-only GITEA_URL would
  match every URL as Gitea
- Only use GITEA_URL env var in gitea_base_url() when the input URL
  actually matches the configured instance, so interactions with other
  Gitea instances (e.g. Codeberg) derive the correct base URL
- Add 4 edge-case tests covering both bugs
@gabrielbelli

Copy link
Copy Markdown
Author

Addressed Bugbot findings

Round 1 (commit 6940051)

  • Review comments silently empty — Fixed. The Gitea GET /pulls/{index}/reviews endpoint does not embed inline comments; they must be fetched per-review via GET /pulls/{index}/reviews/{id}/comments. Updated get_pr_comments to make per-review calls.
  • HTTP 403 misclassified as auth failure — Fixed. check_response_status now maps 401 → AuthFailed and 403 → InsufficientPermissions separately, with a new InsufficientPermissions variant on GiteaApiError.

Round 2 (commit 08edc93)

  • gitea_base_url ignores URL, always returns GITEA_URL — Fixed. gitea_base_url() now only uses the GITEA_URL env var when the input URL actually matches the configured instance host. Otherwise it derives the base URL from the URL itself (so e.g. Codeberg URLs get the correct base URL even when GITEA_URL points elsewhere).
  • Empty GITEA_URL matches all URLs as Gitea — Fixed. Added an empty-string guard after stripping the scheme — str::contains("") is always true in Rust, so this would have caused every unknown URL to be detected as Gitea.

Testing

All fixes verified end-to-end against a live Gitea 1.25.5 instance through the full VK server stack:

  • General + inline review comments both returned correctly via GET /workspaces/{id}/pull-requests/comments
  • PR listing, PR info by URL, and PR creation all working
  • 4 new edge-case unit tests added for the detection bugs (37 total, all pass)

Comment thread crates/git-host/src/gitea/api.rs
Comment thread crates/git-host/src/gitea/api.rs Outdated
Comment thread crates/git-host/src/gitea/api.rs Outdated
- Read response body on error so Gitea's error messages (e.g. "The
  base branch does not exist") are included in error output instead of
  just the HTTP status code
- Remove unused probe_instance() and GiteaVersionResponse dead code
- Remove unreliable tea CLI config parser that ignored which instance
  a token belonged to — GITEA_TOKEN env var is the only auth method
- Update docs to remove tea CLI fallback reference
Comment thread crates/git-host/src/detection.rs
Comment thread crates/git-host/src/detection.rs Outdated
Security: remove standalone /pulls/ URL pattern detection — it would
send GITEA_TOKEN to any server with /pulls/ in the path. Custom-domain
Gitea instances must set GITEA_URL for detection. Well-known hostnames
(gitea.*, forgejo.*, codeberg.org) still auto-detect.

Also fix gitea_base_url() to force HTTPS scheme for ssh:// and git://
remote URLs, which would otherwise produce unreachable base URLs like
ssh://host for API calls.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 37acb7f. Configure here.

head_branch: Option<&str>,
) -> Result<Vec<PullRequestDetail>, GiteaApiError> {
let mut url = self.api_url(&format!("/repos/{}/{}/pulls", info.owner, info.repo));
url.push_str(&format!("?state={state}&limit=50"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No pagination in list_prs silently drops results

Medium Severity

list_prs fetches only the first page of results (limit=50) without iterating through subsequent pages. The Gitea API supports page and limit pagination parameters, and returns at most 50 items per page. For repos that accumulate more than 50 closed PRs over time, list_prs_for_branch will silently miss matching PRs since it filters by head branch client-side after fetching. Notably, the Gitea API also offers a GET /repos/{owner}/{repo}/pulls/{base}/{head} endpoint that could replace client-side filtering entirely.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 37acb7f. Configure here.

@Juanlucasbg

Copy link
Copy Markdown

Aggressive review summary — PR #3332

1207-line feature: adds Gitea / Forgejo / Codeberg as a third PR-hosting backend alongside GitHub and Azure DevOps. New module crates/git-host/src/gitea/ (api.rs, mod.rs), provider detection by URL/env, types, docs. Adds reqwest to git-host as a workspace dep. Verdict: clean — recommend merge with attention to detection false-positives and the sunsetting policy question.

What it does

Three detection paths:

  1. GITEA_URL env var — operator explicitly registers their instance.
  2. Well-known hostnames: any URL containing gitea. or forgejo. substring.
  3. codeberg.org (the largest Forgejo instance).

The new reqwest dep enables direct REST calls (the existing GitHub provider uses gh CLI; Azure uses az CLI). This is a meaningful architectural difference: the Gitea backend is direct-API-only, not CLI-shimmed. Users don't need a gitea CLI installed.

Findings

  • Adversarial — MED:
    • Substring-match detection false positives: url_lower.contains("gitea.") would match git@example.gitea.invalid:org/repo.git (correct) but also https://forgejo.fake-news.com/foo (Forgejo-themed phishing host that doesn't actually run Forgejo) or any subdomain that happens to contain those strings. Not a security issue (the routing just attempts API calls that fail), but the failure mode is opaque. Tighter detection: host.starts_with("gitea.") || host.starts_with("forgejo."). Same for codeberg.
    • GITEA_URL env priority: If both GITEA_URL and gitea.* substring match a URL, both paths fire. Order in the detect_provider_from_url chain matters — env-var check comes first, then well-known hostnames. Looks correct.
    • HTTP vs HTTPS: gitea_host strips both http:// and https:// — but a Gitea instance on plain HTTP is a security smell. A self-hosted operator setting GITEA_URL=http://gitea.internal is doing it on purpose; document the cleartext credentials risk.
  • Structural — PASS: Provider trait pattern parallels GitHub/Azure. New gitea_base_url helper is the symmetric ancestor of extract_github_repo_from_url etc.
  • Security — LOW: API tokens for Gitea live in env (not visible in this excerpt). Verify they go through SecretString like GitHub/Azure tokens.
  • Sunsetting context — MED: Same concern as Add GitHub Enterprise Server support via configurable base URL #2988 (GHES support) and feat: add Devin CLI executor support #3121 (Devin executor). New third-party integration in a sunsetting product. Maintainer policy decision.

NITs

  • url_lower.contains("gitea.") regex tightening: prefer hostname-anchor instead of substring match.
  • Tests: crates/git-host/src/detection.rs is the kind of pure-string-matching code that begs for a #[test] table. Cover well-known hostnames, env-var override, false-positives (host containing the literal gitea not at the start).
  • Docs: docs/integrations/gitea-integration.mdx is added — make sure the env-var docs match the implementation (HTTPS vs HTTP, trailing-slash normalization, etc.).
  • reqwest dep: Verify git-host is using rustls-only (not native-tls) consistent with the workspace reqwest config.

Verdict

Approve.

— Reviewed by automated single-pass review (new third-party integration; full 4-tool battery skipped — implementation parallels existing providers).

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