Skip to content

Commit 986fd3d

Browse files
committed
feat: add Gitea pull request support (gpr:)
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.
1 parent e83a9a3 commit 986fd3d

15 files changed

Lines changed: 1042 additions & 33 deletions

docs/content/switch.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ wt switch - # Previous worktree (like cd -)
2828
wt switch --create new-feature # Create new branch and worktree
2929
wt switch --create hotfix --base production
3030
wt switch pr:123 # Switch to PR #123's branch
31+
wt switch gpr:123 # Switch to Gitea PR #123's branch
3132
```
3233

3334
## Creating a branch
@@ -64,13 +65,15 @@ wt switch --create temp --no-verify # Skip hooks
6465
| `@` | Current branch/worktree |
6566
| `-` | Previous worktree (like `cd -`) |
6667
| `pr:{N}` | GitHub PR #N's branch |
68+
| `gpr:{N}` | Gitea PR #N's branch |
6769
| `mr:{N}` | GitLab MR !N's branch |
6870

6971
```bash
7072
wt switch - # Back to previous
7173
wt switch ^ # Default branch worktree
7274
wt switch --create fix --base=@ # Branch from current HEAD
7375
wt switch pr:123 # PR #123's branch
76+
wt switch gpr:123 # Gitea PR #123's branch
7477
wt switch mr:101 # MR !101's branch
7578
```
7679

@@ -128,6 +131,18 @@ Requires `gh` CLI to be installed and authenticated. The `--create` flag cannot
128131

129132
**Fork PRs:** The local branch uses the PR's branch name directly (e.g., `feature-fix`), so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
130133

134+
## Gitea pull requests
135+
136+
The `gpr:<number>` syntax resolves the branch for a Gitea pull request. For same-repo PRs, it switches to the branch directly. For fork PRs, it fetches `refs/pull/N/head` and configures `pushRemote` to the fork URL.
137+
138+
```bash
139+
wt switch gpr:101 # Checkout Gitea PR #101
140+
```
141+
142+
Requires `tea` CLI to be installed and authenticated. The `--create` flag cannot be used with `gpr:` syntax since the branch already exists.
143+
144+
**Fork PRs:** The local branch uses the PR's branch name directly, so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
145+
131146
## GitLab merge requests
132147

133148
The `mr:<number>` syntax resolves the branch for a GitLab merge request. For same-project MRs, it switches to the branch directly. For fork MRs, it fetches `refs/merge-requests/N/head` and configures `pushRemote` to the fork URL.
@@ -166,8 +181,8 @@ Usage: <b><span class=c>wt switch</span></b> <span class=c>[OPTIONS]</span> <spa
166181
Branch name or shortcut
167182

168183
Opens interactive picker if omitted. Shortcuts: &#39;^&#39; (default branch),
169-
&#39;-&#39; (previous), &#39;@&#39; (current), &#39;pr:{N}&#39; (GitHub PR), &#39;mr:{N}&#39; (GitLab
170-
MR)
184+
&#39;-&#39; (previous), &#39;@&#39; (current), &#39;pr:{N}&#39; (GitHub PR), &#39;gpr:{N}&#39; (Gitea
185+
PR), &#39;mr:{N}&#39; (GitLab MR)
171186

172187
<span class=c>[EXECUTE_ARGS]...</span>
173188
Additional arguments for --execute command (after --)

skills/worktrunk/reference/switch.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ wt switch - # Previous worktree (like cd -)
1212
wt switch --create new-feature # Create new branch and worktree
1313
wt switch --create hotfix --base production
1414
wt switch pr:123 # Switch to PR #123's branch
15+
wt switch gpr:123 # Switch to Gitea PR #123's branch
1516
```
1617

1718
## Creating a branch
@@ -48,13 +49,15 @@ wt switch --create temp --no-verify # Skip hooks
4849
| `@` | Current branch/worktree |
4950
| `-` | Previous worktree (like `cd -`) |
5051
| `pr:{N}` | GitHub PR #N's branch |
52+
| `gpr:{N}` | Gitea PR #N's branch |
5153
| `mr:{N}` | GitLab MR !N's branch |
5254

5355
```bash
5456
wt switch - # Back to previous
5557
wt switch ^ # Default branch worktree
5658
wt switch --create fix --base=@ # Branch from current HEAD
5759
wt switch pr:123 # PR #123's branch
60+
wt switch gpr:123 # Gitea PR #123's branch
5861
wt switch mr:101 # MR !101's branch
5962
```
6063

@@ -105,6 +108,18 @@ Requires `gh` CLI to be installed and authenticated. The `--create` flag cannot
105108

106109
**Fork PRs:** The local branch uses the PR's branch name directly (e.g., `feature-fix`), so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
107110

111+
## Gitea pull requests
112+
113+
The `gpr:<number>` syntax resolves the branch for a Gitea pull request. For same-repo PRs, it switches to the branch directly. For fork PRs, it fetches `refs/pull/N/head` and configures `pushRemote` to the fork URL.
114+
115+
```bash
116+
wt switch gpr:101 # Checkout Gitea PR #101
117+
```
118+
119+
Requires `tea` CLI to be installed and authenticated. The `--create` flag cannot be used with `gpr:` syntax since the branch already exists.
120+
121+
**Fork PRs:** The local branch uses the PR's branch name directly, so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
122+
108123
## GitLab merge requests
109124

110125
The `mr:<number>` syntax resolves the branch for a GitLab merge request. For same-project MRs, it switches to the branch directly. For fork MRs, it fetches `refs/merge-requests/N/head` and configures `pushRemote` to the fork URL.
@@ -136,8 +151,8 @@ Usage: <b><span class=c>wt switch</span></b> <span class=c>[OPTIONS]</span> <spa
136151
Branch name or shortcut
137152

138153
Opens interactive picker if omitted. Shortcuts: &#39;^&#39; (default branch),
139-
&#39;-&#39; (previous), &#39;@&#39; (current), &#39;pr:{N}&#39; (GitHub PR), &#39;mr:{N}&#39; (GitLab
140-
MR)
154+
&#39;-&#39; (previous), &#39;@&#39; (current), &#39;pr:{N}&#39; (GitHub PR), &#39;gpr:{N}&#39; (Gitea
155+
PR), &#39;mr:{N}&#39; (GitLab MR)
141156

142157
<span class=c>[EXECUTE_ARGS]...</span>
143158
Additional arguments for --execute command (after --)

src/cli/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ wt switch - # Previous worktree (like cd -)
260260
wt switch --create new-feature # Create new branch and worktree
261261
wt switch --create hotfix --base production
262262
wt switch pr:123 # Switch to PR #123's branch
263+
wt switch gpr:123 # Switch to Gitea PR #123's branch
263264
```
264265
265266
## Creating a branch
@@ -296,13 +297,15 @@ wt switch --create temp --no-verify # Skip hooks
296297
| `@` | Current branch/worktree |
297298
| `-` | Previous worktree (like `cd -`) |
298299
| `pr:{N}` | GitHub PR #N's branch |
300+
| `gpr:{N}` | Gitea PR #N's branch |
299301
| `mr:{N}` | GitLab MR !N's branch |
300302
301303
```console
302304
wt switch - # Back to previous
303305
wt switch ^ # Default branch worktree
304306
wt switch --create fix --base=@ # Branch from current HEAD
305307
wt switch pr:123 # PR #123's branch
308+
wt switch gpr:123 # Gitea PR #123's branch
306309
wt switch mr:101 # MR !101's branch
307310
```
308311
@@ -354,6 +357,18 @@ Requires `gh` CLI to be installed and authenticated. The `--create` flag cannot
354357
355358
**Fork PRs:** The local branch uses the PR's branch name directly (e.g., `feature-fix`), so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
356359
360+
## Gitea pull requests
361+
362+
The `gpr:<number>` syntax resolves the branch for a Gitea pull request. For same-repo PRs, it switches to the branch directly. For fork PRs, it fetches `refs/pull/N/head` and configures `pushRemote` to the fork URL.
363+
364+
```console
365+
wt switch gpr:101 # Checkout Gitea PR #101
366+
```
367+
368+
Requires `tea` CLI to be installed and authenticated. The `--create` flag cannot be used with `gpr:` syntax since the branch already exists.
369+
370+
**Fork PRs:** The local branch uses the PR's branch name directly, so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
371+
357372
## GitLab merge requests
358373
359374
The `mr:<number>` syntax resolves the branch for a GitLab merge request. For same-project MRs, it switches to the branch directly. For fork MRs, it fetches `refs/merge-requests/N/head` and configures `pushRemote` to the fork URL.
@@ -385,7 +400,7 @@ To change which branch a worktree is on, use `git switch` inside that worktree.
385400
/// Branch name or shortcut
386401
///
387402
/// Opens interactive picker if omitted.
388-
/// Shortcuts: '^' (default branch), '-' (previous), '@' (current), 'pr:{N}' (GitHub PR), 'mr:{N}' (GitLab MR)
403+
/// Shortcuts: '^' (default branch), '-' (previous), '@' (current), 'pr:{N}' (GitHub PR), 'gpr:{N}' (Gitea PR), 'mr:{N}' (GitLab MR)
389404
#[arg(add = crate::completion::worktree_branch_completer())]
390405
branch: Option<String>,
391406

src/commands/worktree/switch.rs

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use color_print::cformat;
99
use dunce::canonicalize;
1010
use worktrunk::config::UserConfig;
1111
use worktrunk::git::remote_ref::{
12-
self, GitHubProvider, GitLabProvider, RemoteRefInfo, RemoteRefProvider,
12+
self, GitHubProvider, GitLabProvider, GiteaProvider, RemoteRefInfo, RemoteRefProvider,
1313
};
1414
use worktrunk::git::{GitError, RefContext, RefType, Repository};
1515
use worktrunk::styling::{
@@ -57,6 +57,7 @@ fn format_ref_context(ctx: &impl RefContext) -> String {
5757
fn resolve_remote_ref(
5858
repo: &Repository,
5959
provider: &dyn RemoteRefProvider,
60+
syntax: &'static str,
6061
number: u32,
6162
create: bool,
6263
base: Option<&str>,
@@ -66,7 +67,12 @@ fn resolve_remote_ref(
6667

6768
// --base is invalid with pr:/mr: syntax (check early, no network needed)
6869
if base.is_some() {
69-
return Err(GitError::RefBaseConflict { ref_type, number }.into());
70+
return Err(GitError::RefBaseConflict {
71+
ref_type,
72+
syntax,
73+
number,
74+
}
75+
.into());
7076
}
7177

7278
// Fetch ref info (network call via gh/glab CLI)
@@ -85,6 +91,7 @@ fn resolve_remote_ref(
8591
if create {
8692
return Err(GitError::RefCreateConflict {
8793
ref_type,
94+
syntax,
8895
number,
8996
branch: info.source_branch.clone(),
9097
}
@@ -131,7 +138,7 @@ fn resolve_fork_ref(
131138
});
132139
}
133140

134-
// Branch exists but doesn't track this ref - try prefixed name (GitHub only)
141+
// Branch exists but doesn't track this ref - try prefixed name (GitHub/Gitea)
135142
if let Some(prefixed) = info.prefixed_local_branch_name() {
136143
if let Some(prefixed_tracks) =
137144
remote_ref::branch_tracks_ref(repo_root, &prefixed, provider, number)
@@ -162,8 +169,8 @@ fn resolve_fork_ref(
162169
}
163170

164171
// Use prefixed branch name; push won't work (None for fork_push_url)
165-
// This is GitHub-only (GitLab doesn't support prefixed names)
166-
let remote = find_github_remote(repo, info)?;
172+
// GitLab doesn't support prefixed names
173+
let remote = find_pr_remote(repo, info)?;
167174
return Ok(ResolvedTarget {
168175
branch: prefixed,
169176
method: CreationMethod::ForkRef {
@@ -190,8 +197,8 @@ fn resolve_fork_ref(
190197
// Resolve remote and URLs based on platform.
191198
let (fork_push_url, remote) = match ref_type {
192199
RefType::Pr => {
193-
// GitHub: URLs already in info, just find remote
194-
let remote = find_github_remote(repo, info)?;
200+
// GitHub/Gitea: URLs already in info, just find remote
201+
let remote = find_pr_remote(repo, info)?;
195202
(info.fork_push_url.clone(), remote)
196203
}
197204
RefType::Mr => {
@@ -240,24 +247,38 @@ fn resolve_fork_ref(
240247
})
241248
}
242249

243-
/// Find the remote for a GitHub PR (where PR refs live).
244-
fn find_github_remote(repo: &Repository, info: &RemoteRefInfo) -> anyhow::Result<String> {
250+
/// Find the remote for a PR (GitHub/Gitea) where PR refs live.
251+
fn find_pr_remote(repo: &Repository, info: &RemoteRefInfo) -> anyhow::Result<String> {
245252
use worktrunk::git::remote_ref::PlatformData;
246253

247-
let PlatformData::GitHub {
248-
host,
249-
base_owner,
250-
base_repo,
251-
..
252-
} = &info.platform_data
253-
else {
254-
anyhow::bail!("find_github_remote called on non-GitHub ref");
254+
let (host, base_owner, base_repo, suggested_url) = match &info.platform_data {
255+
PlatformData::GitHub {
256+
host,
257+
base_owner,
258+
base_repo,
259+
..
260+
} => (
261+
host,
262+
base_owner,
263+
base_repo,
264+
worktrunk::git::remote_ref::github::fork_remote_url(host, base_owner, base_repo),
265+
),
266+
PlatformData::Gitea {
267+
host,
268+
base_owner,
269+
base_repo,
270+
..
271+
} => (
272+
host,
273+
base_owner,
274+
base_repo,
275+
worktrunk::git::remote_ref::gitea::fork_remote_url(host, base_owner, base_repo),
276+
),
277+
_ => anyhow::bail!("find_pr_remote called on non-PR ref"),
255278
};
256279

257280
repo.find_remote_for_repo(Some(host), base_owner, base_repo)
258281
.ok_or_else(|| {
259-
let suggested_url =
260-
worktrunk::git::remote_ref::github::fork_remote_url(host, base_owner, base_repo);
261282
GitError::NoRemoteForRepo {
262283
owner: base_owner.clone(),
263284
repo: base_repo.clone(),
@@ -293,6 +314,21 @@ fn resolve_same_repo_ref(
293314
suggested_url,
294315
})?
295316
}
317+
PlatformData::Gitea {
318+
host,
319+
base_owner,
320+
base_repo,
321+
..
322+
} => {
323+
let suggested_url =
324+
worktrunk::git::remote_ref::gitea::fork_remote_url(host, base_owner, base_repo);
325+
repo.find_remote_for_repo(Some(host), base_owner, base_repo)
326+
.ok_or_else(|| GitError::NoRemoteForRepo {
327+
owner: base_owner.clone(),
328+
repo: base_repo.clone(),
329+
suggested_url,
330+
})?
331+
}
296332
PlatformData::GitLab {
297333
host,
298334
base_owner,
@@ -342,14 +378,21 @@ fn resolve_switch_target(
342378
if let Some(suffix) = branch.strip_prefix("pr:")
343379
&& let Ok(number) = suffix.parse::<u32>()
344380
{
345-
return resolve_remote_ref(repo, &GitHubProvider, number, create, base);
381+
return resolve_remote_ref(repo, &GitHubProvider, "pr:", number, create, base);
382+
}
383+
384+
// Handle gpr:<number> syntax (Gitea PRs)
385+
if let Some(suffix) = branch.strip_prefix("gpr:")
386+
&& let Ok(number) = suffix.parse::<u32>()
387+
{
388+
return resolve_remote_ref(repo, &GiteaProvider, "gpr:", number, create, base);
346389
}
347390

348391
// Handle mr:<number> syntax (GitLab MRs)
349392
if let Some(suffix) = branch.strip_prefix("mr:")
350393
&& let Ok(number) = suffix.parse::<u32>()
351394
{
352-
return resolve_remote_ref(repo, &GitLabProvider, number, create, base);
395+
return resolve_remote_ref(repo, &GitLabProvider, "mr:", number, create, base);
353396
}
354397

355398
// Regular branch switch

src/git/error.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,16 @@ pub enum GitError {
279279
/// --create flag used with pr:/mr: syntax (conflict - branch already exists)
280280
RefCreateConflict {
281281
ref_type: RefType,
282+
/// CLI syntax used (e.g., "pr:", "gpr:", "mr:")
283+
syntax: &'static str,
282284
number: u32,
283285
branch: String,
284286
},
285287
/// --base flag used with pr:/mr: syntax (conflict - base is predetermined)
286288
RefBaseConflict {
287289
ref_type: RefType,
290+
/// CLI syntax used (e.g., "pr:", "gpr:", "mr:")
291+
syntax: &'static str,
288292
number: u32,
289293
},
290294
/// Branch exists but is tracking a different PR/MR
@@ -811,11 +815,11 @@ impl GitError {
811815

812816
GitError::RefCreateConflict {
813817
ref_type,
818+
syntax,
814819
number,
815820
branch,
816821
} => {
817822
let name = ref_type.name();
818-
let syntax = ref_type.syntax();
819823
write!(
820824
f,
821825
"{}\n{}",
@@ -828,8 +832,11 @@ impl GitError {
828832
)
829833
}
830834

831-
GitError::RefBaseConflict { ref_type, number } => {
832-
let syntax = ref_type.syntax();
835+
GitError::RefBaseConflict {
836+
ref_type,
837+
syntax,
838+
number,
839+
} => {
833840
let name_plural = ref_type.name_plural();
834841
write!(
835842
f,

0 commit comments

Comments
 (0)