Skip to content

Source control: forge detection and linked pull requests across forges #532

Description

@Tryanks

Summary

Detect the git forge behind a project's remote and show the pull/merge request linked to each thread, for GitHub, GitLab, Gitea, Forgejo, Bitbucket and Azure DevOps, including self-hosted instances, using each forge's CLI where one exists.

Tcode today

Nothing forge-related exists: no remote get-url, no forge detection, no PR model (crates/services/src/git.rs only checks that a remote named origin exists). ureq is already a services dependency for REST fallbacks. The module doc of crates/core/src/git.rs says it "excludes PR/MR operations"; that is a derived comment and should be corrected with this work.

Associations (no model-facing tool)

T3 Code has two concepts; Tcode adopts both, minus T3's agent-facing MCP link tool (explicit non-goal):

  • Branch PR: looked up automatically for the thread's branch (worktree path if any, else project root). One per thread, display-only, re-checked after each turn so a PR the agent opened with its own tools is found without any injection.
  • Linked PRs: explicit links with a stored snapshot, several per thread (key {host, repo, number}). Sources: created (Tcode's own create-PR action, Source control: Open PR action usable during a run, with merge when CI passes #533) and manual (paste a URL, or "Link to thread" on a PR link in the transcript). Links win over the branch PR for display; unlinking falls back to the branch PR.
  • Choosing a PR for a branch: skip unpublished branches; never match via an upstream that is the default branch; prefer open/draft, then most recently updated; require the head owner to match so fork PRs do not collide (GitLab and Forgejo reuse branch names, seen live).

Normalized model (crates/core)

ChangeRequest { forge, host, repo, number, url, title,
  state: Open | Draft | Merged | Closed,
  head_branch, head_owner: Option, base_branch, head_sha: Option,
  merged_at: Option, updated_at,
  checks: Option<Pending | Passing | Failing | NoChecks>,
  review: Option<Approved | ChangesRequested | Required | NoReview>,
  mergeable: Option<bool>, raw_state }

Forge detection order

  1. Explicit user mapping (git host → forge kind + optional web/API host; the git host and web host can differ).
  2. Known public hosts: github.com, *.ghe.com, gitlab.com, codeberg.org (Forgejo), gitea.com, bitbucket.org, dev.azure.com, *.visualstudio.com.
  3. Hosts configured in the CLIs as the source of truth for self-hosted instances: gh auth status --json hosts, the hosts section of glab's config.yml, tea logins list -o json, fj's keys.json. (T3 Code has no refinement for GitHub Enterprise on domains without a github label; Tcode should.)
  4. Unauthenticated probes, cached per host with a short timeout: /api/forgejo/v1/version 200 → Forgejo; /api/v1/version → Gitea; /api/v4/version 401 with x-gitlab-meta → GitLab; /api/v3/meta → GitHub Enterprise Server.

Normalize SSH hosts first (strip port, apply SSH config aliases and fj aliases).

Per-forge backends (crates/services/src/forge.rs, via the process helpers)

Forge PR for branch Pitfalls
GitHub / GHES gh pr list -H X -s all --json …; checks via gh pr checks --json bucket REST head= without owner: is silently ignored; reviewDecision is "" when not required
GitLab (incl. self-managed) glab mr list --source-branch X -A -F json mr view <branch> prompts on multiple matches; head_pipeline only on single-MR GET; glab auth status has no JSON
Gitea tea api '/repos/{owner}/{repo}/pulls/BASE/OWNER:X' tea pulls list cannot filter by branch; list JSON values are strings, no draft
Forgejo / Codeberg REST /pulls?head=X&state=all (v16+), token from fj's keys.json or tea fj has no JSON output; AGit PRs keep the branch in head.label
Bitbucket Cloud / DC REST only (no official CLI); API tokens (app passwords ended 2026-06) Repeat state=; defaults to OPEN
Azure DevOps az repos pr list --source-branch X --status all -o json Checks come from policy evaluations

Gerrit and SourceHut are out of scope (Gerrit cannot see local branch names; SourceHut has no PRs). Universal token fallback: git credential fill with GIT_TERMINAL_PROMPT=0.

Refresh

Host-side, with no client required: branch PR after each turn end and on branch/worktree changes; linked PRs sweep every minute (open PRs every minute, closed every 15 min, merged never unless requested); group by PR key so a PR shared by threads is read once; per-(forge, host, credential) rate-limit pauses honouring Retry-After, exponential backoff 30 s → 15 min. Missing or logged-out CLIs appear in Settings with the install/login hint; background lookups back off instead of looping.

UI

Sidebar badge (state glyph + number; +N for several links), thread header badge with checks and review glyphs, open in browser, link/unlink actions. Strings in both locales.

Reference

T3 Code origin/main: apps/server/src/sourceControl/*, ThreadPullRequestReactor.ts, PullRequestSyncReactor.ts, packages/shared/src/threadPullRequests.ts, migrations 042/048/050; #8160, #10101, #10839, #11436.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions