Skip to content

feat: add Gitea pull request support (gpr:)#1320

Open
SjB wants to merge 1 commit intomax-sixty:mainfrom
SjB:feat-gitea
Open

feat: add Gitea pull request support (gpr:)#1320
SjB wants to merge 1 commit intomax-sixty:mainfrom
SjB:feat-gitea

Conversation

@SjB
Copy link
Copy Markdown

@SjB SjB commented Mar 6, 2026

Add support for Gitea pull requests via the new gpr:<number> syntax.

  • Introduce GiteaProvider and src/git/remote_ref/gitea.rs to fetch PR metadata via the tea CLI.
  • Add PlatformData::Gitea and wire Gitea handling into remote-ref resolution and branch creation logic.
  • Expose GiteaProvider from remote_ref module and update PR remote discovery to handle GitHub/Gitea.
  • Teach switch command parsing to accept gpr: shortcuts and update CLI docs/help text.
  • Surface CLI syntax in error messages by adding syntax to relevant GitError variants.
  • Add integration tests and snapshots for gpr: behavior, and unit tests for Gitea parsing helpers.
  • Documentation updates to reference gpr: and note tea CLI requirement.

No breaking changes. The gpr: flow requires the tea CLI to be installed and authenticated.

Copilot AI review requested due to automatic review settings March 6, 2026 22:38
Copy link
Copy Markdown
Collaborator

@worktrunk-bot worktrunk-bot left a comment

Choose a reason for hiding this comment

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

Two significant issues need attention before this can be approved.

1. tea api likely does not support {owner}/{repo} template expansion (critical)

The code passes repos/{owner}/{repo}/pulls/{number} to tea api, mirroring what gh api does. However, {owner} and {repo} are template variables that gh expands automatically based on the current repo context — this is a gh-specific feature. The tea api command is a raw HTTP client and (to my knowledge) does not perform this expansion. If so, the literal string {owner}/{repo} would be sent as the URL path, resulting in a 404 from the Gitea API.

This needs verification against the actual tea CLI. If tea doesn't support templates, the owner/repo would need to be resolved from git remotes before constructing the API path.

2. Happy-path integration tests are broken — they capture error output as expected

test_switch_gpr_same_repo and test_switch_gpr_fork are intended to test successful PR checkout, but their snapshots show tea api failed errors with "No gitea login configured" messages. Compare with the equivalent switch_pr_same_repo.snap which shows the full successful flow (fetching, PR info display, branch creation). The mock tea binary isn't intercepting despite identical setup to the working gh mock tests. The snapshots pass CI because they were committed with the error output — the tests aren't actually verifying the happy path.


Minor issues:

  • RefType::syntax() at src/git/error.rs:64 is now dead code — both call sites were replaced with the explicit syntax parameter. Should be removed.
  • configure_mock_tea_env is a line-for-line copy of configure_mock_gh_env. Consider extracting a shared helper.


/// Fetch PR information from Gitea using the `tea` CLI.
fn fetch_pr_info(pr_number: u32, repo_root: &Path) -> anyhow::Result<RemoteRefInfo> {
let api_path = format!("repos/{{owner}}/{{repo}}/pulls/{}", pr_number);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tea api may not support {owner}/{repo} template expansion the way gh api does. gh has special built-in template variable support; tea api is a raw HTTP client. If this is the case, you'd need to resolve the actual owner/repo (e.g., from git remotes) and construct the full path.

Can you confirm this works against an actual Gitea instance? The integration tests all show tea api failed errors, so they don't exercise this code path.

Copy link
Copy Markdown
Collaborator

@worktrunk-bot worktrunk-bot left a comment

Choose a reason for hiding this comment

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

Two significant issues need attention before this can be approved.

1. tea api likely does not support {owner}/{repo} template expansion (critical)

The code at src/git/remote_ref/gitea.rs:82 passes repos/{owner}/{repo}/pulls/{number} to tea api, mirroring what gh api does. However, {owner} and {repo} are template variables that gh expands automatically based on the current repo context — this is a gh-specific feature. The tea api command is a raw HTTP client and (to my knowledge) does not perform this expansion. If so, the literal string {owner}/{repo} would be sent as the URL path, resulting in a 404 from the Gitea API.

This needs verification against the actual tea CLI. If tea doesn't support templates, the owner/repo would need to be resolved from git remotes before constructing the API path.

2. Happy-path integration tests are broken — they capture error output as expected

test_switch_gpr_same_repo and test_switch_gpr_fork are intended to test successful PR checkout, but their snapshots show tea api failed errors with "No gitea login configured" messages. Compare with the equivalent switch_pr_same_repo.snap which shows the full successful flow (fetching, PR info display, branch creation). The mock tea binary isn't intercepting despite identical setup to the working gh mock tests. The snapshots pass CI because they were committed with the error output — the tests aren't actually verifying the happy path.


Minor issues:

  • RefType::syntax() at src/git/error.rs:64 is now dead code — both call sites were replaced with the explicit syntax parameter. Should be removed.
  • configure_mock_tea_env is a line-for-line copy of configure_mock_gh_env. Consider extracting a shared helper.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds Gitea pull request support to the wt switch command via the gpr:<number> syntax, following the existing patterns established for GitHub (pr:) and GitLab (mr:).

Changes:

  • Introduces GiteaProvider in src/git/remote_ref/gitea.rs implementing the RemoteRefProvider trait, using the tea CLI to fetch PR metadata
  • Adds PlatformData::Gitea variant and wires Gitea handling into switch command parsing, remote discovery, and branch creation logic — including adding a syntax field to RefCreateConflict/RefBaseConflict error variants to disambiguate pr: vs gpr:
  • Adds documentation, CLI help text, integration tests, and snapshots for the new gpr: syntax

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/git/remote_ref/gitea.rs New Gitea PR provider with tea CLI integration, response parsing, and unit tests
src/git/remote_ref/mod.rs Exposes GiteaProvider, adds Gitea module doc and test
src/git/remote_ref/info.rs Adds PlatformData::Gitea variant, wires source_ref() and prefixed_local_branch_name(), adds unit tests
src/git/error.rs Adds syntax: &'static str field to RefCreateConflict and RefBaseConflict variants
src/commands/worktree/switch.rs Parses gpr: syntax, refactors find_github_remotefind_pr_remote, adds Gitea handling in resolve_same_repo_ref
src/cli/mod.rs Adds gpr: to help text and shortcut documentation
docs/content/switch.md Documents Gitea PR section in user-facing docs
skills/worktrunk/reference/switch.md Adds gpr: reference to skills documentation
tests/integration_tests/switch.rs Adds integration tests for gpr: create conflict, base conflict, same-repo, fork, not-found, and tea-not-installed scenarios
tests/snapshots/*.snap (6 files) Snapshot files for the new integration tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

use worktrunk::config::UserConfig;
use worktrunk::git::remote_ref::{
self, GitHubProvider, GitLabProvider, RemoteRefInfo, RemoteRefProvider,
self, GitHubProvider, GitLabProvider, GiteaProvider, RemoteRefInfo, RemoteRefProvider,
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The import ordering is not alphabetical: GiteaProvider should come before GitHubProvider and GitLabProvider. Since the project uses edition 2024 and rustfmt (via pre-commit hooks), this should be self, GiteaProvider, GitHubProvider, GitLabProvider, RemoteRefInfo, RemoteRefProvider.

Suggested change
self, GitHubProvider, GitLabProvider, GiteaProvider, RemoteRefInfo, RemoteRefProvider,
self, GiteaProvider, GitHubProvider, GitLabProvider, RemoteRefInfo, RemoteRefProvider,

Copilot uses AI. Check for mistakes.
@@ -30,10 +30,16 @@
//! Uses `glab api projects/:id/merge_requests/<number>`. Fork MRs require additional
//! API calls to fetch source/target project URLs.

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The Gitea doc section is separated from the preceding doc block by a blank line without //! prefix (line 32). This creates a separate doc comment block, unlike the GitHub and GitLab sections which are part of one continuous //! block. The blank line before the ## Gitea section should use //! to maintain a single continuous doc comment block, matching the convention used for the GitHub and GitLab sections above.

Suggested change
//!

Copilot uses AI. Check for mistakes.
Comment on lines +2509 to +2528
fn configure_mock_tea_env(cmd: &mut std::process::Command, mock_bin: &Path) {
cmd.env("MOCK_CONFIG_DIR", mock_bin);

let (path_var_name, current_path) = std::env::vars_os()
.find(|(k, _)| k.eq_ignore_ascii_case("PATH"))
.map(|(k, v)| (k.to_string_lossy().into_owned(), Some(v)))
.unwrap_or(("PATH".to_string(), None));

let mut paths: Vec<std::path::PathBuf> = current_path
.as_deref()
.map(|p| std::env::split_paths(p).collect())
.unwrap_or_default();
paths.insert(0, mock_bin.to_path_buf());
let new_path = std::env::join_paths(&paths)
.unwrap()
.to_string_lossy()
.into_owned();

cmd.env(path_var_name, new_path);
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

configure_mock_tea_env is an exact duplicate of configure_mock_gh_env (defined at line 1513). Consider extracting a shared helper function (e.g., configure_mock_cli_env) to avoid the code duplication, following the pattern of the already shared configure_cli_not_installed_env.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
✗ tea api failed for PR #101
  No gitea login configured. To start using tea, first run
  tea login add
  and then run your command again.
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This snapshot shows "tea api failed for PR #101" instead of the expected create conflict error ("Cannot create branch for gpr:101 — PR already has branch feature-auth"). Compare with the equivalent GitHub test snapshot (switch_pr_create_conflict.snap) which correctly shows the create conflict error after successfully fetching PR info.

The mock tea doesn't appear to be returning the configured response, so this test is exercising the API failure path rather than the --create conflict path it's intended to test. The same issue affects test_switch_gpr_same_repo and test_switch_gpr_fork — all three tests fall back to the API error path instead of testing their intended scenarios.

Suggested change
�[31m✗�[39m �[31mtea api failed for PR #101�[39m
�[107m �[0m No gitea login configured. To start using tea, first run
�[107m �[0m tea login add
�[107m �[0m and then run your command again.
�[31m✗�[39m �[31mCannot create branch for gpr:101PR already has branch feature-auth�[39m

Copilot uses AI. Check for mistakes.
Comment on lines +818 to 822
syntax,
number,
branch,
} => {
let name = ref_type.name();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

With the syntax field now passed explicitly to both RefCreateConflict and RefBaseConflict, the RefType::syntax() method (defined at line 63) is no longer called anywhere in the codebase. It should be removed to avoid dead code and a Clippy warning, or marked with #[allow(dead_code)] if it's intended for future use.

Suggested change
syntax,
number,
branch,
} => {
let name = ref_type.name();
syntax: _,
number,
branch,
} => {
let name = ref_type.name();
let syntax = ref_type.syntax();

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +82
/// Fetch PR information from Gitea using the `tea` CLI.
fn fetch_pr_info(pr_number: u32, repo_root: &Path) -> anyhow::Result<RemoteRefInfo> {
let api_path = format!("repos/{{owner}}/{{repo}}/pulls/{}", pr_number);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The API path uses {owner}/{repo} template variables (literal curly braces in the string), mirroring the gh api pattern. While gh api supports template variable substitution for {owner} and {repo} (resolving them from the current repo context), tea api may not support this same template mechanism. If tea api doesn't resolve {owner} and {repo}, the literal curly braces would be sent as part of the URL path, causing the API request to fail.

Please verify that tea api supports {owner} and {repo} template substitution. If it doesn't, you'd need to resolve the owner and repo from the git remote URL before constructing the API path.

Suggested change
/// Fetch PR information from Gitea using the `tea` CLI.
fn fetch_pr_info(pr_number: u32, repo_root: &Path) -> anyhow::Result<RemoteRefInfo> {
let api_path = format!("repos/{{owner}}/{{repo}}/pulls/{}", pr_number);
/// Resolve Gitea repository owner and name from the git remote URL.
///
/// This inspects `git remote get-url origin` from the given `repo_root` and attempts to
/// extract the last two path components as `<owner>/<repo>`, handling common SSH and
/// HTTPS URL formats used with Gitea.
fn resolve_owner_repo(repo_root: &Path) -> anyhow::Result<(String, String)> {
let output = Cmd::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(repo_root)
.run()
.context("Failed to run git remote get-url origin")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let details = if stderr.trim().is_empty() {
String::from_utf8_lossy(&output.stdout).trim().to_string()
} else {
stderr.trim().to_string()
};
bail!("git remote get-url origin failed: {}", details);
}
let url = String::from_utf8(output.stdout).context("Remote URL is not valid UTF-8")?;
let url = url.trim().trim_end_matches(".git");
// Strip scheme (e.g., https://, ssh://) if present.
let without_scheme = if let Some(idx) = url.find("://") {
&url[idx + 3..]
} else {
url
};
// Strip leading user/host part (e.g., git@host:, host/).
let path_part = if let Some(idx) = without_scheme.find(':') {
&without_scheme[idx + 1..]
} else if let Some(idx) = without_scheme.find('/') {
&without_scheme[idx + 1..]
} else {
without_scheme
};
let segments: Vec<&str> = path_part.split('/').filter(|s| !s.is_empty()).collect();
if segments.len() < 2 {
bail!("Unable to extract owner and repo from git remote URL: {}", url);
}
let owner = segments[segments.len() - 2].to_string();
let repo = segments[segments.len() - 1].to_string();
Ok((owner, repo))
}
/// Fetch PR information from Gitea using the `tea` CLI.
fn fetch_pr_info(pr_number: u32, repo_root: &Path) -> anyhow::Result<RemoteRefInfo> {
let (owner, repo) = resolve_owner_repo(repo_root)?;
let api_path = format!("repos/{owner}/{repo}/pulls/{pr_number}");

Copilot uses AI. Check for mistakes.
worktrunk-bot added a commit that referenced this pull request Mar 7, 2026
The reviewer posted duplicate reviews on PR #1320 because it retried
the review API call without checking if the first attempt had already
succeeded. Add explicit guidance to verify before retrying.

Fixes #1321

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
max-sixty pushed a commit that referenced this pull request Mar 7, 2026
…1322)

## Summary

- Adds explicit guidance in the `review-pr` skill to verify whether a
review was already posted before retrying a failed/stuck API call
- Prevents intra-run duplicate reviews (distinct from cross-run dedup in
#1139)

Fixes #1321

## Context

Run 22784882686 posted the same review body twice on PR #1320 (4 minutes
apart) because the bot thought the first API call was "stuck" and
retried with `gh pr review --comment` without checking if the review had
already been posted.

## Test plan

- [ ] Next external PR review should not produce duplicate reviews even
if the API call is slow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add support for Gitea pull requests via the new `gpr:<number>` syntax.

- Introduce `GiteaProvider` and `src/git/remote_ref/gitea.rs` to fetch PR metadata via the `tea` CLI.
- Add `PlatformData::Gitea` and wire Gitea handling into remote-ref resolution and branch creation logic.
- Expose `GiteaProvider` from `remote_ref` module and update PR remote discovery to handle GitHub/Gitea.
- Teach switch command parsing to accept `gpr:` shortcuts and update CLI docs/help text.
- Surface CLI syntax in error messages by adding `syntax` to relevant `GitError` variants.
- Add integration tests and snapshots for `gpr:` behavior, and unit tests for Gitea parsing helpers.
- Documentation updates to reference `gpr:` and note `tea` CLI requirement.

No breaking changes. The `gpr:` flow requires the `tea` CLI to be installed and authenticated.
@max-sixty
Copy link
Copy Markdown
Owner

tbc, I would be up for merging something like this if we can get it into good shape. I think we can use pr: and then resolve at runtime, unless there's a strong view we need a different prefix

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.

4 participants