feat: add Gitea pull request support (gpr:)#1320
Conversation
worktrunk-bot
left a comment
There was a problem hiding this comment.
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()atsrc/git/error.rs:64is now dead code — both call sites were replaced with the explicitsyntaxparameter. Should be removed.configure_mock_tea_envis a line-for-line copy ofconfigure_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); |
There was a problem hiding this comment.
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.
worktrunk-bot
left a comment
There was a problem hiding this comment.
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()atsrc/git/error.rs:64is now dead code — both call sites were replaced with the explicitsyntaxparameter. Should be removed.configure_mock_tea_envis a line-for-line copy ofconfigure_mock_gh_env. Consider extracting a shared helper.
There was a problem hiding this comment.
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
GiteaProviderinsrc/git/remote_ref/gitea.rsimplementing theRemoteRefProvidertrait, using theteaCLI to fetch PR metadata - Adds
PlatformData::Giteavariant and wires Gitea handling into switch command parsing, remote discovery, and branch creation logic — including adding asyntaxfield toRefCreateConflict/RefBaseConflicterror variants to disambiguatepr:vsgpr: - 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_remote → find_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, |
There was a problem hiding this comment.
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.
| self, GitHubProvider, GitLabProvider, GiteaProvider, RemoteRefInfo, RemoteRefProvider, | |
| self, GiteaProvider, GitHubProvider, GitLabProvider, RemoteRefInfo, RemoteRefProvider, |
| @@ -30,10 +30,16 @@ | |||
| //! Uses `glab api projects/:id/merge_requests/<number>`. Fork MRs require additional | |||
| //! API calls to fetch source/target project URLs. | |||
|
|
|||
There was a problem hiding this comment.
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.
| //! |
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| [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. |
There was a problem hiding this comment.
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.
| �[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:101 — PR already has branch feature-auth�[39m |
| syntax, | ||
| number, | ||
| branch, | ||
| } => { | ||
| let name = ref_type.name(); |
There was a problem hiding this comment.
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.
| syntax, | |
| number, | |
| branch, | |
| } => { | |
| let name = ref_type.name(); | |
| syntax: _, | |
| number, | |
| branch, | |
| } => { | |
| let name = ref_type.name(); | |
| let syntax = ref_type.syntax(); |
| /// 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); |
There was a problem hiding this comment.
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.
| /// 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}"); |
…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>
|
tbc, I would be up for merging something like this if we can get it into good shape. I think we can use |
Add Gitea support for `pr:<N>` using the `tea` CLI and wire it into the unified remote-ref resolution flow. - Introduce `GiteaProvider` implementing `RemoteRefProvider` (`src/git/remote_ref/gitea.rs`) that calls `tea api` to fetch PR info, extract source branch, and compute fork push URLs. - Add `PlatformData::Gitea` and update `RemoteRefInfo` helpers (source_ref, prefixed local branch) so forked Gitea PRs behave like GitHub forks. - Add `GitRemoteUrl::is_gitea()` and export `GiteaProvider` from `remote_ref` module. - Implement provider selection logic: - honor `[forge] platform` config override, - detect from primary remote URL, - fall back to ambiguous mode that tries GitHub (`gh`) then Gitea (`tea`) before erroring. - surface helpful error messages when detection indicates GitLab or when both providers fail. - Update switch command logic to use the new selection and resolution functions. - Update CLI docs and help text to mention Gitea and explain provider detection/fallback. - Add integration tests and snapshot fixtures for Gitea PR flows and error cases. No breaking changes to public behavior; `pr:` now supports both GitHub and Gitea repositories.
worktrunk-bot
left a comment
There was a problem hiding this comment.
Thanks for iterating — pr: auto-detection with explicit config override is a cleaner direction than a separate gpr: prefix. Three blockers, though:
1. All six new test_switch_gpr_* tests fail locally (cargo test --test integration test_switch_gpr). The test bodies were updated to pass "pr:101" / "pr:42", but the committed snapshots still record "gpr:101" / "gpr:42" in the info.args block and still capture the "No gitea login configured" error text. cargo insta test --accept wasn't re-run after the gpr: → pr: rename, and the happy-path snapshots (switch_gpr_same_repo, switch_gpr_fork) still capture the same failure the previous review flagged — mock tea is never invoked because the ambiguous fallback tries gh first and git.example.com doesn't match is_gitea()'s contains("gitea") check.
2. ambiguous_pr_error produces a multiline error that trips debug_assert! at src/main.rs:1189 (Multiline error without context: ...). The {gh_err:#} / {tea_err:#} alternate-format includes each inner error's chain separated by newlines, which get embedded verbatim into the outer message. In debug builds this panics; in release it renders as a generic "Command failed" with a garbled gutter. This is visible in the --create test failure output. Inline suggestion below.
3. tea api repos/{owner}/{repo}/pulls/N was flagged in the previous review and is unchanged (src/git/remote_ref/gitea.rs:82). {owner}/{repo} is a gh-specific template; tea api is a raw HTTP client and sends the literal braces, which Gitea returns 404 for. The mock tests don't exercise this path because the mock binary accepts any argv. github.rs resolves owner and repo_name from the primary remote URL before formatting the path — the same pattern is needed here.
Aside: configure_mock_tea_env is still line-for-line configure_mock_gh_env (flagged last round). Not a blocker but worth extracting before this lands.
| anyhow::anyhow!( | ||
| "Failed to resolve pr:{number} for {context} with both providers; GitHub (gh): {gh_err:#}; Gitea (tea): {tea_err:#}. Hint: set [forge] platform = \"github\" or \"gitea\" in .config/wt.toml to disambiguate.", | ||
| ) |
There was a problem hiding this comment.
{gh_err:#} / {tea_err:#} expand to multi-line text (the inner error's chain is joined with \n). Embedding that into a single anyhow!(...) string produces a message with embedded newlines, which fails debug_assert! at src/main.rs:1189 (Multiline error without context). Simplest fix is to flatten before formatting:
| anyhow::anyhow!( | |
| "Failed to resolve pr:{number} for {context} with both providers; GitHub (gh): {gh_err:#}; Gitea (tea): {tea_err:#}. Hint: set [forge] platform = \"github\" or \"gitea\" in .config/wt.toml to disambiguate.", | |
| ) | |
| let gh = format!("{gh_err:#}").replace('\n', " "); | |
| let tea = format!("{tea_err:#}").replace('\n', " "); | |
| anyhow::anyhow!( | |
| "Failed to resolve pr:{number} for {context} with both providers; GitHub (gh): {gh}; Gitea (tea): {tea}. Hint: set [forge] platform = \"github\" or \"gitea\" in .config/wt.toml to disambiguate.", | |
| ) |
Or chain with .context() so the provider errors land in the printed chain instead of the top-level message.
| /// Fetch PR information from Gitea using the `tea` CLI. | ||
| fn fetch_pr_info(pr_number: u32, repo: &Repository) -> anyhow::Result<RemoteRefInfo> { | ||
| let repo_root = repo.repo_path()?; | ||
| let api_path = format!("repos/{{owner}}/{{repo}}/pulls/{}", pr_number); |
There was a problem hiding this comment.
This reads repos/{owner}/{repo}/pulls/N literally — tea api doesn't expand {owner}/{repo} the way gh api does. The integration tests pass through this line because the mock tea ignores its argv, but against real Gitea this will always 404.
github.rs:99-124 has the pattern — resolve owner/repo_name from the primary remote URL (or gh repo set-default's equivalent, if Gitea has one) before formatting the path, then format!("repos/{}/{}/pulls/{}", owner, repo_name, pr_number). The existing git::GitRemoteUrl::parse(&url) + .owner() / .repo() helpers work for Gitea URLs too.
Add support for Gitea pull requests via the new
gpr:<number>syntax.GiteaProviderandsrc/git/remote_ref/gitea.rsto fetch PR metadata via theteaCLI.PlatformData::Giteaand wire Gitea handling into remote-ref resolution and branch creation logic.GiteaProviderfromremote_refmodule and update PR remote discovery to handle GitHub/Gitea.gpr:shortcuts and update CLI docs/help text.syntaxto relevantGitErrorvariants.gpr:behavior, and unit tests for Gitea parsing helpers.gpr:and noteteaCLI requirement.No breaking changes. The
gpr:flow requires theteaCLI to be installed and authenticated.