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>
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.
|
tbc, I would be up for merging something like this if we can get it into good shape. I think we can use |
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.