[+] Forgejo (#3)

* [+] Forgejo, tangled

* [-] Tangled
This commit is contained in:
2026-05-07 00:45:01 -04:00
committed by GitHub
parent 39ba96051c
commit 7b65d919d6
5 changed files with 93 additions and 8 deletions
+10
View File
@@ -33,6 +33,7 @@ pub enum ProviderKind {
Github,
Gitlab,
Gitea,
Forgejo,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@@ -190,6 +191,7 @@ impl SiteConfig {
}
ProviderKind::Gitlab => format!("{}/api/v4", trim_end(&self.base_url)),
ProviderKind::Gitea => format!("{}/api/v1", trim_end(&self.base_url)),
ProviderKind::Forgejo => format!("{}/api/v1", trim_end(&self.base_url)),
}
}
}
@@ -400,6 +402,14 @@ mod tests {
.api_base(),
"https://gitea.example.test/api/v1"
);
assert_eq!(
SiteConfig {
base_url: "https://forgejo.example.test".to_string(),
..site("forgejo", ProviderKind::Forgejo)
}
.api_base(),
"https://forgejo.example.test/api/v1"
);
}
fn site(name: &str, provider: ProviderKind) -> SiteConfig {
+31 -4
View File
@@ -369,7 +369,7 @@ fn matching_sites<'a>(config: &'a Config, target: &ProfileTarget) -> Vec<&'a Sit
}
fn prompt_provider_styled(theme: &ColorfulTheme, base_url: &str) -> Result<ProviderKind> {
let options = ["GitHub", "GitLab", "Gitea"];
let options = ["GitHub", "GitLab", "Gitea", "Forgejo"];
let index = Select::with_theme(theme)
.with_prompt(format!("Provider for {base_url}"))
.items(options)
@@ -378,7 +378,8 @@ fn prompt_provider_styled(theme: &ColorfulTheme, base_url: &str) -> Result<Provi
Ok(match index {
0 => ProviderKind::Github,
1 => ProviderKind::Gitlab,
_ => ProviderKind::Gitea,
2 => ProviderKind::Gitea,
_ => ProviderKind::Forgejo,
})
}
@@ -477,6 +478,11 @@ fn pat_instruction_lines(provider: &ProviderKind, base_url: &str) -> Vec<String>
format!("Open: {url}"),
"Generate a new token, allow repository access, then paste it here.".to_string(),
],
ProviderKind::Forgejo => vec![
"Create a personal access token with repository permissions.".to_string(),
format!("Open: {url}"),
"Generate a new token, allow repository access, then paste it here.".to_string(),
],
}
}
@@ -686,7 +692,11 @@ where
"github" => return Ok(ProviderKind::Github),
"gitlab" => return Ok(ProviderKind::Gitlab),
"gitea" => return Ok(ProviderKind::Gitea),
_ => writeln!(writer, "Provider must be github, gitlab, or gitea.")?,
"forgejo" => return Ok(ProviderKind::Forgejo),
_ => writeln!(
writer,
"Provider must be github, gitlab, gitea, or forgejo."
)?,
}
}
}
@@ -842,6 +852,8 @@ fn known_provider_from_host(host: &str) -> Option<ProviderKind> {
Some(ProviderKind::Github)
} else if host == "gitlab.com" || host.ends_with(".gitlab.com") || host.contains("gitlab") {
Some(ProviderKind::Gitlab)
} else if host == "codeberg.org" || host.contains("forgejo") {
Some(ProviderKind::Forgejo)
} else if host.contains("gitea") {
Some(ProviderKind::Gitea)
} else {
@@ -855,6 +867,14 @@ fn detect_provider_from_instance(base_url: &str) -> Option<ProviderKind> {
.build()
.ok()?;
let base = trim_url_end(base_url);
if client
.get(format!("{base}/api/forgejo/v1/version"))
.send()
.ok()
.is_some_and(|response| response.status().is_success())
{
return Some(ProviderKind::Forgejo);
}
if client
.get(format!("{base}/api/v1/version"))
.send()
@@ -937,7 +957,7 @@ fn detect_namespace_kind_public(
.is_some_and(|items| !items.is_empty())
.then_some(NamespaceKind::User)
}
ProviderKind::Gitea => {
ProviderKind::Gitea | ProviderKind::Forgejo => {
if client
.get(format!("{api_base}/orgs/{namespace}"))
.send()
@@ -1003,6 +1023,7 @@ fn default_base_url(provider: &ProviderKind) -> &'static str {
ProviderKind::Github => "https://github.com",
ProviderKind::Gitlab => "https://gitlab.com",
ProviderKind::Gitea => "https://gitea.example.com",
ProviderKind::Forgejo => "https://forgejo.example.com",
}
}
@@ -1065,6 +1086,7 @@ fn provider_slug(provider: &ProviderKind) -> &'static str {
ProviderKind::Github => "github",
ProviderKind::Gitlab => "gitlab",
ProviderKind::Gitea => "gitea",
ProviderKind::Forgejo => "forgejo",
}
}
@@ -1078,6 +1100,7 @@ fn token_creation_url(provider: &ProviderKind, base_url: &str) -> String {
format!("{base}/-/user_settings/personal_access_tokens?name=git-sync&scopes=api")
}
ProviderKind::Gitea => format!("{base}/user/settings/applications"),
ProviderKind::Forgejo => format!("{base}/user/settings/applications"),
}
}
@@ -1449,5 +1472,9 @@ mod tests {
token_creation_url(&ProviderKind::Gitea, "gitea.example.test"),
"https://gitea.example.test/user/settings/applications"
);
assert_eq!(
token_creation_url(&ProviderKind::Forgejo, "forgejo.example.test"),
"https://forgejo.example.test/user/settings/applications"
);
}
}
+28
View File
@@ -121,6 +121,7 @@ enum ProviderArg {
Github,
Gitlab,
Gitea,
Forgejo,
}
#[derive(Clone, Debug, ValueEnum)]
@@ -303,6 +304,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Github => Self::Github,
ProviderArg::Gitlab => Self::Gitlab,
ProviderArg::Gitea => Self::Gitea,
ProviderArg::Forgejo => Self::Forgejo,
}
}
}
@@ -397,6 +399,32 @@ mod tests {
assert_eq!(args.jobs, 8);
}
#[test]
fn cli_accepts_new_provider_kinds() {
for (name, expected) in [("forgejo", ProviderKind::Forgejo)] {
let cli = Cli::try_parse_from([
"git-sync",
"config",
"site",
"add",
"--name",
name,
"--provider",
name,
"--base-url",
"https://example.test",
"--token",
"token",
])
.unwrap();
let Command::Config(ConfigCommand::Site(SiteCommand::Add(args))) = cli.command else {
panic!("parsed unexpected command");
};
assert_eq!(ProviderKind::from(args.provider), expected);
}
}
#[test]
fn endpoint_parser_supports_aliases_and_rejects_bad_kinds() {
let endpoint = parse_endpoint("github:organization:MewoLab").unwrap();
+20 -4
View File
@@ -44,6 +44,7 @@ impl<'a> ProviderClient<'a> {
ProviderKind::Github => self.github_list_repos(endpoint),
ProviderKind::Gitlab => self.gitlab_list_repos(endpoint),
ProviderKind::Gitea => self.gitea_list_repos(endpoint),
ProviderKind::Forgejo => self.gitea_list_repos(endpoint),
}
}
@@ -62,6 +63,9 @@ impl<'a> ProviderClient<'a> {
self.gitlab_create_repo(endpoint, name, visibility, description)
}
ProviderKind::Gitea => self.gitea_create_repo(endpoint, name, visibility, description),
ProviderKind::Forgejo => {
self.gitea_create_repo(endpoint, name, visibility, description)
}
}
}
@@ -75,6 +79,7 @@ impl<'a> ProviderClient<'a> {
ProviderKind::Github => self.github_detect_namespace_kind(namespace),
ProviderKind::Gitlab => self.gitlab_detect_namespace_kind(namespace),
ProviderKind::Gitea => self.gitea_detect_namespace_kind(namespace),
ProviderKind::Forgejo => self.gitea_detect_namespace_kind(namespace),
}
}
@@ -92,7 +97,9 @@ impl<'a> ProviderClient<'a> {
.clone()
.unwrap_or_else(|| match self.site.provider {
ProviderKind::Github => "x-access-token".to_string(),
ProviderKind::Gitlab | ProviderKind::Gitea => "oauth2".to_string(),
ProviderKind::Gitlab | ProviderKind::Gitea | ProviderKind::Forgejo => {
"oauth2".to_string()
}
});
url.set_username(&username)
.map_err(|_| anyhow!("failed to set username on clone URL"))?;
@@ -262,7 +269,7 @@ impl<'a> ProviderClient<'a> {
let repos: Vec<GiteaRepo> = self.paged_get(&url)?;
Ok(repos.into_iter().map(Into::into).collect())
}
NamespaceKind::Group => bail!("Gitea endpoints use kind 'user' or 'org'"),
NamespaceKind::Group => bail!("Gitea/Forgejo endpoints use kind 'user' or 'org'"),
}
}
@@ -278,7 +285,7 @@ impl<'a> ProviderClient<'a> {
NamespaceKind::Org => {
format!("{}/orgs/{}/repos", self.site.api_base(), endpoint.namespace)
}
NamespaceKind::Group => bail!("Gitea endpoints use kind 'user' or 'org'"),
NamespaceKind::Group => bail!("Gitea/Forgejo endpoints use kind 'user' or 'org'"),
};
let body = json!({
"name": name,
@@ -377,7 +384,7 @@ impl<'a> ProviderClient<'a> {
.context("PAT contains invalid header characters")?,
);
}
ProviderKind::Gitea => {
ProviderKind::Gitea | ProviderKind::Forgejo => {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("token {}", self.token))
@@ -538,6 +545,15 @@ mod tests {
.unwrap(),
"https://oauth2:secret@gitlab.example.test/alice/repo.git"
);
let forgejo_site = site(ProviderKind::Forgejo, None);
let forgejo = ProviderClient::new(&forgejo_site).unwrap();
assert_eq!(
forgejo
.authenticated_clone_url("https://forgejo.example.test/alice/repo.git")
.unwrap(),
"https://oauth2:secret@forgejo.example.test/alice/repo.git"
);
}
#[test]