new: drive the forge hosts and tarball templates from data/forges.yml
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
## Forge hosts recognized by `pkh new` origin detection, with the
|
||||||
|
## release-tarball URL templates of each: `Forge::parse`
|
||||||
|
## (src/new/origin.rs) matches a git remote's host against the map keys,
|
||||||
|
## and the tarball download substitutes {owner}, {repo} and {tag} into the
|
||||||
|
## templates. Like host_keys.yml, this file exists so that static
|
||||||
|
## endpoints are data: adding a forge is a YAML entry, not a code change
|
||||||
|
## (self-hosted instances are deliberately absent — the download URL
|
||||||
|
## shapes differ per instance).
|
||||||
|
##
|
||||||
|
## tarball_templates are tried sequentially in file order, best candidate
|
||||||
|
## first (GitHub prefers the codeload direct link: no redirect).
|
||||||
|
## `kind` documents the forge family the URL shapes belong to; the
|
||||||
|
## templates fully describe the URLs, so nothing branches on it (yet) —
|
||||||
|
## but it must be one of the known kinds, enforced at load time.
|
||||||
|
##
|
||||||
|
## Where the values come from: each forge's release-archive download URL
|
||||||
|
## shapes, verified against the live forges —
|
||||||
|
## github: codeload.github.com/<owner>/<repo>/tar.gz/refs/tags/<tag>
|
||||||
|
## and github.com/<owner>/<repo>/archive/refs/tags/<tag>.tar.gz
|
||||||
|
## gitlab: gitlab.com/<owner>/<repo>/-/archive/<tag>/<repo>-<tag>.tar.gz
|
||||||
|
|
||||||
|
forges:
|
||||||
|
github.com:
|
||||||
|
kind: github
|
||||||
|
tarball_templates:
|
||||||
|
- https://codeload.github.com/{owner}/{repo}/tar.gz/refs/tags/{tag}
|
||||||
|
- https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz
|
||||||
|
gitlab.com:
|
||||||
|
kind: gitlab
|
||||||
|
tarball_templates:
|
||||||
|
- https://gitlab.com/{owner}/{repo}/-/archive/{tag}/{repo}-{tag}.tar.gz
|
||||||
+3
-3
@@ -8,9 +8,9 @@
|
|||||||
//! This module is deliberately not a central registry: each file is
|
//! This module is deliberately not a central registry: each file is
|
||||||
//! embedded by the module that owns it (distro_info.rs owns
|
//! embedded by the module that owns it (distro_info.rs owns
|
||||||
//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml,
|
//! data/distro_info.yml, launchpad.rs owns data/launchpad.yml,
|
||||||
//! apt/keyring.rs owns data/keyserver.yml, put/ssh.rs owns
|
//! apt/keyring.rs owns data/keyserver.yml, new/origin.rs owns
|
||||||
//! data/host_keys.yml, quirks.rs owns data/quirks.yml) through the
|
//! data/forges.yml, put/ssh.rs owns data/host_keys.yml, quirks.rs owns
|
||||||
//! [`embed_data!`] macro below, so data
|
//! data/quirks.yml) through the [`embed_data!`] macro below, so data
|
||||||
//! and its accessors stay together and a diff touching one domain cannot
|
//! and its accessors stay together and a diff touching one domain cannot
|
||||||
//! half-touch another. The macro embeds the file at compile time and
|
//! half-touch another. The macro embeds the file at compile time and
|
||||||
//! parses it once into a `lazy_static` on first use; since the data ships
|
//! parses it once into a `lazy_static` on first use; since the data ships
|
||||||
|
|||||||
+1
-4
@@ -1537,10 +1537,7 @@ mod tests {
|
|||||||
resolve(cli).await.unwrap().orig,
|
resolve(cli).await.unwrap().orig,
|
||||||
Some(OrigOrigin::Release {
|
Some(OrigOrigin::Release {
|
||||||
tag: "v1.2.3".to_string(),
|
tag: "v1.2.3".to_string(),
|
||||||
forge: crate::new::origin::Forge::GitHub {
|
forge: crate::new::origin::Forge::parse("https://github.com/foo/bar.git").unwrap(),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -1535,10 +1535,8 @@ mod tests {
|
|||||||
"0.1.0",
|
"0.1.0",
|
||||||
&OrigOrigin::Release {
|
&OrigOrigin::Release {
|
||||||
tag: "v0.1.0".to_string(),
|
tag: "v0.1.0".to_string(),
|
||||||
forge: Forge::GitHub {
|
forge: Forge::parse("https://github.com/pkh-nonexistent-org/pkh-nonexistent-repo")
|
||||||
owner: "pkh-nonexistent-org".into(),
|
.unwrap(),
|
||||||
repo: "pkh-nonexistent-repo".into(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
+143
-59
@@ -8,48 +8,85 @@
|
|||||||
//! - is HEAD exactly on a tag, and which upstream version does it name,
|
//! - is HEAD exactly on a tag, and which upstream version does it name,
|
||||||
//! - which is the last tag reachable from HEAD (for the
|
//! - which is the last tag reachable from HEAD (for the
|
||||||
//! `<tag>+git<YYYYMMDD>.<hash>` version scheme),
|
//! `<tag>+git<YYYYMMDD>.<hash>` version scheme),
|
||||||
//! - where does the `origin` remote point: only `github.com` and
|
//! - where does the `origin` remote point: only the hosts of the bundled
|
||||||
//! `gitlab.com` are recognized as forges — self-hosted GitLab instances
|
//! forge table (`data/forges.yml`) are recognized as forges — self-hosted
|
||||||
//! are deliberately not (the release-download URL shapes differ).
|
//! GitLab instances are deliberately not part of it (the
|
||||||
|
//! release-download URL shapes differ), and the table also carries each
|
||||||
|
//! forge's tarball URL templates.
|
||||||
//!
|
//!
|
||||||
//! Detection never touches the network: adding a remote stores its URL in
|
//! Detection never touches the network: adding a remote stores its URL in
|
||||||
//! the local config only, which is all this module reads.
|
//! the local config only, which is all this module reads.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
/// A forge hosting the project, parsed from the `origin` remote URL.
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::data::embed_data;
|
||||||
|
|
||||||
|
/// Family a forge belongs to. Documentation of which URL-shape family a
|
||||||
|
/// table entry belongs to — the tarball templates fully describe the
|
||||||
|
/// URLs, so nothing branches on the kind (yet). Parsed strictly: an
|
||||||
|
/// unknown kind fails the load rather than being silently ignored.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum ForgeKind {
|
||||||
|
/// `github.com` and its codeload archive service
|
||||||
|
GitHub,
|
||||||
|
/// `gitlab.com`
|
||||||
|
GitLab,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry of the bundled forge table: the release-tarball URL
|
||||||
|
/// templates of a forge host
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ForgeEntry {
|
||||||
|
/// Family of the forge (see [`ForgeKind`]). Kept even though nothing
|
||||||
|
/// branches on it (the templates fully describe the URLs): it
|
||||||
|
/// documents the entry's URL-shape family and the strict enum
|
||||||
|
/// validates it at load time.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
kind: ForgeKind,
|
||||||
|
/// Release-tarball URL templates, best candidate first (tried
|
||||||
|
/// sequentially by the download)
|
||||||
|
tarball_templates: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bundled forge table (`data/forges.yml`): host name → entry
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ForgesData {
|
||||||
|
/// Recognized forge hosts
|
||||||
|
forges: HashMap<String, ForgeEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_data! {
|
||||||
|
static ref FORGES_DATA: ForgesData = "../../data/forges.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A forge hosting the project, parsed from the `origin` remote URL: one
|
||||||
|
/// of the hosts of the bundled forge table, plus the repository it points
|
||||||
|
/// at.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Forge {
|
pub struct Forge {
|
||||||
/// `github.com/<owner>/<repo>`
|
/// Host name of the forge (the key of its `forges.yml` entry).
|
||||||
GitHub {
|
host: &'static str,
|
||||||
/// Repository owner (user or organization).
|
/// Repository owner (user or organization).
|
||||||
owner: String,
|
owner: String,
|
||||||
/// Repository name, without the `.git` suffix.
|
/// Repository name, without the `.git` suffix.
|
||||||
repo: String,
|
repo: String,
|
||||||
},
|
|
||||||
/// `gitlab.com/<owner>/<repo>`
|
|
||||||
GitLab {
|
|
||||||
/// Repository owner (user or group).
|
|
||||||
owner: String,
|
|
||||||
/// Repository name, without the `.git` suffix.
|
|
||||||
repo: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Forge {
|
impl Forge {
|
||||||
/// Host name of the forge.
|
/// Host name of the forge.
|
||||||
pub fn host(&self) -> &'static str {
|
pub fn host(&self) -> &'static str {
|
||||||
match self {
|
self.host
|
||||||
Forge::GitHub { .. } => "github.com",
|
|
||||||
Forge::GitLab { .. } => "gitlab.com",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a remote URL into a [`Forge`], accepting the `https://`,
|
/// Parse a remote URL into a [`Forge`], accepting the `https://`,
|
||||||
/// `http://`, `git://` and `git@host:` spellings. Only `github.com` and
|
/// `http://`, `git://` and `git@host:` spellings. Only the hosts
|
||||||
/// `gitlab.com` are recognized; anything else (self-hosted GitLab,
|
/// listed in the bundled forge table are recognized; anything else
|
||||||
/// Bitbucket, plain URLs…) yields `None`.
|
/// (self-hosted GitLab, Bitbucket, plain URLs…) yields `None`.
|
||||||
pub fn parse(url: &str) -> Option<Forge> {
|
pub fn parse(url: &str) -> Option<Forge> {
|
||||||
let url = url.trim();
|
let url = url.trim();
|
||||||
// Normalize `git@host:path` to `host/path` and strip any scheme.
|
// Normalize `git@host:path` to `host/path` and strip any scheme.
|
||||||
@@ -74,34 +111,34 @@ impl Forge {
|
|||||||
if owner.is_empty() || repo.is_empty() {
|
if owner.is_empty() || repo.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match host {
|
// The table key doubles as the Forge's host, so the two can never
|
||||||
"github.com" => Some(Forge::GitHub {
|
// disagree about how the forge is spelled.
|
||||||
owner: owner.to_string(),
|
let (host, _entry) = FORGES_DATA.forges.get_key_value(host)?;
|
||||||
repo: repo.to_string(),
|
Some(Forge {
|
||||||
}),
|
host,
|
||||||
"gitlab.com" => Some(Forge::GitLab {
|
owner: owner.to_string(),
|
||||||
owner: owner.to_string(),
|
repo: repo.to_string(),
|
||||||
repo: repo.to_string(),
|
})
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Release-tarball URLs of `tag`, best candidate first. GitHub prefers
|
/// Release-tarball URLs of `tag`, best candidate first: the templates
|
||||||
/// the codeload direct link (no redirect) and falls back to the
|
/// of the forge's table entry, with `{owner}`, `{repo}` and `{tag}`
|
||||||
/// `github.com` archive URL; GitLab has a single archive URL.
|
/// substituted, in file order (the download tries them sequentially).
|
||||||
pub fn release_tarball_urls(&self, tag: &str) -> Vec<String> {
|
pub fn release_tarball_urls(&self, tag: &str) -> Vec<String> {
|
||||||
match self {
|
let entry = FORGES_DATA
|
||||||
Forge::GitHub { owner, repo } => vec![
|
.forges
|
||||||
format!("https://codeload.github.com/{owner}/{repo}/tar.gz/refs/tags/{tag}"),
|
.get(self.host)
|
||||||
format!("https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz"),
|
.expect("the host of a parsed Forge is a key of the forge table");
|
||||||
],
|
entry
|
||||||
Forge::GitLab { owner, repo } => {
|
.tarball_templates
|
||||||
vec![format!(
|
.iter()
|
||||||
"https://gitlab.com/{owner}/{repo}/-/archive/{tag}/{repo}-{tag}.tar.gz"
|
.map(|template| {
|
||||||
)]
|
template
|
||||||
}
|
.replace("{owner}", &self.owner)
|
||||||
}
|
.replace("{repo}", &self.repo)
|
||||||
|
.replace("{tag}", tag)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,35 +336,40 @@ mod tests {
|
|||||||
fn forge_parses_remote_url_shapes() {
|
fn forge_parses_remote_url_shapes() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("https://github.com/foo/bar.git"),
|
Forge::parse("https://github.com/foo/bar.git"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("git@github.com:foo/bar.git"),
|
Forge::parse("git@github.com:foo/bar.git"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("git://github.com/foo/bar"),
|
Forge::parse("git://github.com/foo/bar"),
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("https://gitlab.com/foo/bar/-/tree/main"),
|
Forge::parse("https://gitlab.com/foo/bar/-/tree/main"),
|
||||||
Some(Forge::GitLab {
|
Some(Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Forge::parse("ssh://git@gitlab.com/foo/bar.git"),
|
Forge::parse("ssh://git@gitlab.com/foo/bar.git"),
|
||||||
Some(Forge::GitLab {
|
Some(Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
@@ -342,7 +384,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn forge_release_tarball_urls() {
|
fn forge_release_tarball_urls() {
|
||||||
let gh = Forge::GitHub {
|
let gh = Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into(),
|
repo: "bar".into(),
|
||||||
};
|
};
|
||||||
@@ -353,7 +396,8 @@ mod tests {
|
|||||||
"https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz".to_string(),
|
"https://github.com/foo/bar/archive/refs/tags/v1.2.3.tar.gz".to_string(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
let gl = Forge::GitLab {
|
let gl = Forge {
|
||||||
|
host: "gitlab.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into(),
|
repo: "bar".into(),
|
||||||
};
|
};
|
||||||
@@ -363,6 +407,45 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every table entry must be substitutable: a template missing one of
|
||||||
|
/// the placeholders would download from a literal `{name}` URL.
|
||||||
|
#[test]
|
||||||
|
fn forge_templates_carry_every_placeholder() {
|
||||||
|
for (host, entry) in &FORGES_DATA.forges {
|
||||||
|
assert!(
|
||||||
|
!entry.tarball_templates.is_empty(),
|
||||||
|
"forge '{host}' has no tarball template"
|
||||||
|
);
|
||||||
|
for template in &entry.tarball_templates {
|
||||||
|
for placeholder in ["{owner}", "{repo}", "{tag}"] {
|
||||||
|
assert!(
|
||||||
|
template.contains(placeholder),
|
||||||
|
"template '{template}' of forge '{host}' lacks {placeholder}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `kind` field is validated at load time: an unknown forge family
|
||||||
|
/// fails the parse instead of being silently accepted.
|
||||||
|
#[test]
|
||||||
|
fn forge_kind_is_validated() {
|
||||||
|
assert!(serde_yaml::from_str::<ForgeEntry>("kind: github").is_err());
|
||||||
|
assert!(
|
||||||
|
serde_yaml::from_str::<ForgeEntry>(
|
||||||
|
"kind: github\ntarball_templates: ['https://h/{owner}/{repo}/{tag}']"
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
serde_yaml::from_str::<ForgeEntry>(
|
||||||
|
"kind: codeberg\ntarball_templates: ['https://h/{owner}/{repo}/{tag}']"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sanitized_tag_versions() {
|
fn sanitized_tag_versions() {
|
||||||
assert_eq!(sanitized_tag_version("v1.2.3"), Some("1.2.3".to_string()));
|
assert_eq!(sanitized_tag_version("v1.2.3"), Some("1.2.3".to_string()));
|
||||||
@@ -466,7 +549,8 @@ mod tests {
|
|||||||
let origin = GitOrigin::detect(dir.path()).expect("detected");
|
let origin = GitOrigin::detect(dir.path()).expect("detected");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
origin.forge,
|
origin.forge,
|
||||||
Some(Forge::GitHub {
|
Some(Forge {
|
||||||
|
host: "github.com",
|
||||||
owner: "foo".into(),
|
owner: "foo".into(),
|
||||||
repo: "bar".into()
|
repo: "bar".into()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1295,10 +1295,7 @@ mod tests {
|
|||||||
fn orig_origin_choices_preorder_by_detection() {
|
fn orig_origin_choices_preorder_by_detection() {
|
||||||
use crate::new::origin::Forge;
|
use crate::new::origin::Forge;
|
||||||
let tagged_forge = GitOrigin {
|
let tagged_forge = GitOrigin {
|
||||||
forge: Some(Forge::GitHub {
|
forge: Some(Forge::parse("https://github.com/foo/bar").unwrap()),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into(),
|
|
||||||
}),
|
|
||||||
head_tag: Some("v1.4.0".into()),
|
head_tag: Some("v1.4.0".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1362,10 +1359,7 @@ mod tests {
|
|||||||
let mut release = quilt.clone();
|
let mut release = quilt.clone();
|
||||||
release.orig = Some(options::OrigOrigin::Release {
|
release.orig = Some(options::OrigOrigin::Release {
|
||||||
tag: "v0.14.0".to_string(),
|
tag: "v0.14.0".to_string(),
|
||||||
forge: crate::new::origin::Forge::GitLab {
|
forge: crate::new::origin::Forge::parse("https://gitlab.com/foo/bar").unwrap(),
|
||||||
owner: "foo".into(),
|
|
||||||
repo: "bar".into(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
let text = summary_text(&release, None);
|
let text = summary_text(&release, None);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
Reference in New Issue
Block a user