Expand description
vcs-forge — one PR/MR lifecycle across GitHub, GitLab, and Gitea.
A forge-agnostic facade over the vcs-github,
vcs-gitlab, and vcs-gitea wrappers: it dispatches
the common forge operations (auth, repo view, the PR/MR lifecycle, issues,
releases) to whichever CLI backs the handle and parses each one’s output into
forge-agnostic DTOs (ForgePr, ForgeIssue, ForgeRelease,
ForgeRepo, …) — so a tool can target the forge instead of one specifically.
It is the gh/glab/tea analogue of how vcs-core’s
Repo sits over git and jj.
Unlike a repository, a forge has no filesystem marker (.git/.jj) to
detect — it’s identified by the remote host — so a Forge is
constructed explicitly (Forge::github / Forge::gitlab /
Forge::gitea), optionally guided by ForgeKind::from_remote_url applied
to a remote URL the caller already holds.
§The surface
Forge— the cwd-bound, forge-agnostic handle. Operations run against the bound directory (cwd); the CLI infers the repository from that directory’s git remote.Forge::github/gitlab/giteabuild over the real job-backed runner;atre-binds the cwd, sharing the client;kindreports which forge drives it.ForgeApi— the object-safe trait the common surface lives on. Hold aBox<dyn ForgeApi>/&dyn ForgeApito code against the operations without naming theProcessRunnergeneric. Every method mirrors the like-named inherent method onForge; the trait adds nothing but the&dynboundary.ForgeKind—GitHub/GitLab/Gitea. Its pure, best-effortfrom_remote_urlclassifies the public SaaS hosts (github.com, gitlab.com, gitea.com, codeberg.org, and proper subdomains) with an anchored match — a lookalike likegitlab.com.attacker.netand a self-hosted instance on an arbitrary domain both returnNone(pick the kind yourself).- Unified DTOs —
ForgePr(+ForgePrState),ForgeIssue(+ForgeIssueState),ForgeRelease,ForgeRepo,CiStatus; the inputsPrCreate(open-a-PR spec:new(title, body)then.source(branch)/.target(branch), defaulting to the current branch and repo default) andMergeStrategy(Merge/Squash/Rebase). Each normalises the three CLIs’ shapes — e.g. GitLab’siidbecomesnumber, andOPEN/opened/openall read as one state. Some fields are best-effort:draft, and thebody/urlnot present on lean list output. - Operation groups — auth (
auth_status); the repo (repo_view); the PR/MR lifecycle (pr_list/pr_view/pr_create/pr_merge/pr_mark_ready/pr_close/pr_checks); issues (issue_list/issue_view/issue_create); releases (release_list/release_view). List ops cap at 100 — drop to the wrapped client for more. - Capability gaps —
teahas no current-repo view, draft toggle, checks command, or single-release view, so on a Gitea handlerepo_view,pr_mark_ready,pr_checks, andrelease_viewreturnError::Unsupportedwithout spawning. Classify it withError::is_unsupported.
The wrappers are re-exported (vcs_forge::vcs_github / vcs_gitlab /
vcs_gitea) so anything beyond the portable intersection — a forge-specific op,
or one the facade marks Unsupported — is one constructor away without a new
dependency.
§Recipes
Read the open PRs — depend on the trait so the same code takes a real handle or a test double:
use vcs_forge::{Forge, ForgeApi};
let forge = Forge::github("."); // or ::gitlab(".") / ::gitea(".")
for pr in forge.pr_list().await? {
println!("#{} [{:?}] {}", pr.number, pr.state, pr.title);
}Open a PR/MR with PrCreate — the facade maps source/target to each
CLI’s own flags, and returns the CLI’s success output (a URL on GitHub/GitLab):
use vcs_forge::{Forge, ForgeApi, PrCreate};
let spec = PrCreate::new("Add widget", "Closes #12").source("feature");
let out = forge.pr_create(spec).await?;§Testing
The facade trait has no mock feature — mockall can’t process the
macro-generated ForgeApi signatures. Test the real dispatch instead:
build a Forge over an explicit client wrapping a fake runner — e.g.
Forge::for_github(cwd, GitHub::with_runner(ScriptedRunner::new())) (likewise
for_gitlab / for_gitea) — and
script the canned CLI output, exercising the argv-building and DTO parsing
end to end. The cross-cutting testing patterns live in
vcs-testkit’s guide.
§In-depth guide
Beyond this page, this crate ships a full how-to guide — rendered on docs.rs
from docs/. See the guide module.
Re-exports§
pub use vcs_gitea;pub use vcs_github;pub use vcs_gitlab;
Modules§
- guide
- vcs-forge — the forge facade
Structs§
- Cancellation
Token cancellation - A token which can be used to signal a cancellation request to one or more tasks.
- Forge
- A cwd-bound, forge-agnostic handle. Operations run against the bound directory
(
cwd); the CLI infers the repository from that directory’s git remote. Useatfor a sibling handle bound elsewhere. - Forge
Issue - An issue, unified across the three forges.
- ForgePr
- A pull request (GitHub) / merge request (GitLab) / pull request (Gitea), unified across the three forges.
- Forge
Release - A release, unified across the three forges. (Gitea’s
teaalways lists — it has no single-release view — sorelease_viewisUnsupportedthere.) - Forge
Repo - A repository (GitHub) / project (GitLab), unified. (Gitea’s
teahas no current-repo view, sorepo_viewisUnsupportedthere.) - PrCreate
- Options for
pr_create— the unified open-a-PR/MR spec, mapped to each CLI’s own flags (gh--head/--base, glab--source-branch/--target-branch, tea--head/--base).
Enums§
- CiStatus
- The coarse CI status for a PR/MR, bucketed into the four states a caller acts
on. GitHub aggregates its per-check buckets into this; GitLab maps its
pipeline status; Gitea’s
teahas no checks command, sopr_checksisUnsupportedthere. - Error
- An error from a
Forgeoperation. - Forge
Issue State - The normalised state of a
ForgeIssue, unifying GitHub’sOPEN/CLOSED, GitLab’sopened/closed, and Gitea’sopen/closed. An unknown state reads asOpen— a state we don’t model is treated as live, never silently as resolved. - Forge
Kind - Which forge backs a
Forgehandle. - Forge
PrState - The normalised state of a
ForgePr, unifying GitHub’sOPEN/CLOSED/MERGED, GitLab’sopened/closed/locked/merged, and Gitea’sopen/closed(+ amergedflag). - Merge
Strategy - How
pr_mergemerges — mapped to each CLI’s own merge-strategy flag.
Traits§
- Forge
Api - The forge-agnostic common surface of
Forge, as a trait — so a consumer can hold aBox<dyn ForgeApi>/&dyn ForgeApiand code against the operations without naming theProcessRunnergeneric.