new: add upstream-aware orig tarball origins and the orig-vendor component
This commit is contained in:
+571
-5
@@ -15,6 +15,8 @@ use crate::debian::DebianVersion;
|
||||
use crate::debian::deps::{Deps, ParseOpts};
|
||||
use crate::distro_info;
|
||||
use crate::new::detect::{self, Detection};
|
||||
use crate::new::origin::{Forge, GitOrigin};
|
||||
use crate::new::templates;
|
||||
|
||||
/// Build systems / project kinds `pkh new` knows about.
|
||||
///
|
||||
@@ -133,6 +135,82 @@ pub enum SourceDir {
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl SourceDir {
|
||||
/// The directory being packaged, when it already exists on disk (`None`
|
||||
/// for the skeleton mode, whose sources are rendered in memory).
|
||||
pub fn existing_dir(&self, cwd: &Path) -> Option<PathBuf> {
|
||||
match self {
|
||||
SourceDir::Skeleton => None,
|
||||
SourceDir::Here => Some(cwd.to_path_buf()),
|
||||
SourceDir::Path(path) => Some(path.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Debian source format of the scaffolded package: an explicit, derived
|
||||
/// choice rather than a boolean afterthought.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SourceFormat {
|
||||
/// `3.0 (native)`: no orig tarball at all. The default for a fresh,
|
||||
/// self-authored skeleton.
|
||||
Native,
|
||||
/// `3.0 (quilt)`: upstream sources in the orig tarball, packaging in
|
||||
/// `debian/`. The default when packaging an existing project.
|
||||
Quilt,
|
||||
}
|
||||
|
||||
impl SourceFormat {
|
||||
/// The value written to `debian/source/format`.
|
||||
pub fn deb_string(&self) -> &'static str {
|
||||
match self {
|
||||
SourceFormat::Native => super::debian::SOURCE_FORMAT_NATIVE,
|
||||
SourceFormat::Quilt => super::debian::SOURCE_FORMAT_QUILT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How the orig tarball of a quilt package is produced. The right answer
|
||||
/// depends on where the upstream sources come from, so the wizard offers
|
||||
/// them pre-ordered by what git origin detection found (release download
|
||||
/// and `git archive` only exist when HEAD sits exactly on a tag).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum OrigOrigin {
|
||||
/// Download the upstream release tarball of `tag` from the forge of the
|
||||
/// `origin` remote and repack it (explicit network consent).
|
||||
Release {
|
||||
/// Release tag the tarball is downloaded for.
|
||||
tag: String,
|
||||
/// Forge the tarball is downloaded from.
|
||||
forge: Forge,
|
||||
},
|
||||
/// `git archive` the tag HEAD sits on: offline and deterministic.
|
||||
GitArchive {
|
||||
/// Release tag archived into the tarball.
|
||||
tag: String,
|
||||
},
|
||||
/// Repack a tarball the user provides (a local path or an http(s) URL).
|
||||
Provided {
|
||||
/// Path or URL of the user-provided tarball.
|
||||
source: String,
|
||||
},
|
||||
/// Snapshot the working tree into the orig tarball (the only option for
|
||||
/// a skeleton, and the fallback when the upstream history is unknown).
|
||||
Snapshot,
|
||||
}
|
||||
|
||||
impl OrigOrigin {
|
||||
/// Human-readable description used in the wizard summary and the
|
||||
/// post-scaffold report.
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
OrigOrigin::Release { tag, .. } => format!("release download ({tag})"),
|
||||
OrigOrigin::GitArchive { tag } => format!("git archive ({tag})"),
|
||||
OrigOrigin::Provided { source } => format!("user tarball ({source})"),
|
||||
OrigOrigin::Snapshot => "working tree snapshot".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upstream license of the package: a curated SPDX list plus a free-text
|
||||
/// fallback for anything else (including "unknown" until the user picks one).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -242,8 +320,13 @@ pub struct NewOptions {
|
||||
/// Runtime Depends clauses of the metapackage flavor (canonically
|
||||
/// rendered; empty for every other flavor).
|
||||
pub depends: Vec<String>,
|
||||
/// Use the `3.0 (native)` source format (no orig tarball).
|
||||
pub native: bool,
|
||||
/// Debian source format: `3.0 (native)` for a fresh skeleton by
|
||||
/// default, `3.0 (quilt)` when packaging an existing project; either
|
||||
/// can be forced with `--native` / `--quilt`.
|
||||
pub source_format: SourceFormat,
|
||||
/// How the orig tarball is produced (quilt only; `None` with the native
|
||||
/// format, which has no orig tarball).
|
||||
pub orig: Option<OrigOrigin>,
|
||||
/// Initialize a git repository (gitignores are written regardless).
|
||||
pub git: bool,
|
||||
/// Write the autopkgtest smoke test (`debian/tests/control` +
|
||||
@@ -320,6 +403,12 @@ pub struct NewCli {
|
||||
pub release: bool,
|
||||
/// `--native`.
|
||||
pub native: bool,
|
||||
/// `--quilt` (mutually exclusive with `--native`).
|
||||
pub quilt: bool,
|
||||
/// `--orig-from <release|git|path|snapshot>`.
|
||||
pub orig_from: Option<String>,
|
||||
/// `--orig-path <file|url>` (required by `--orig-from path`).
|
||||
pub orig_path: Option<String>,
|
||||
/// True unless `--no-git`.
|
||||
pub git: bool,
|
||||
/// `--defaults`.
|
||||
@@ -604,11 +693,65 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
||||
// Everything below has a default and is validated as it is resolved.
|
||||
validate_source_name(&name)?;
|
||||
|
||||
let upstream_version = cli.upstream_version.unwrap_or_else(|| "0.1.0".to_string());
|
||||
// Source format: explicit flags win (--native / --quilt, mutually
|
||||
// exclusive), otherwise the mode decides — a fresh, self-authored
|
||||
// skeleton is native by default, an existing project is quilt.
|
||||
let source_format = match (cli.native, cli.quilt) {
|
||||
(true, true) => {
|
||||
return Err("--native and --quilt are mutually exclusive".to_string());
|
||||
}
|
||||
(true, false) => SourceFormat::Native,
|
||||
(false, true) => SourceFormat::Quilt,
|
||||
(false, false) => match source_dir {
|
||||
SourceDir::Skeleton => SourceFormat::Native,
|
||||
SourceDir::Here | SourceDir::Path(_) => SourceFormat::Quilt,
|
||||
},
|
||||
};
|
||||
|
||||
// Git origin state of the packaged directory: needed for the version
|
||||
// default (tag / +git scheme) and for the orig-tarball plan. Probing is
|
||||
// purely local (git config and refs, no network).
|
||||
let existing_dir = source_dir.existing_dir(&std::env::current_dir().unwrap_or_default());
|
||||
let needs_origin = cli.upstream_version.is_none()
|
||||
|| (source_format == SourceFormat::Quilt
|
||||
&& (cli.orig_from.is_some() || existing_dir.is_some()));
|
||||
let origin = if needs_origin {
|
||||
existing_dir.as_deref().and_then(GitOrigin::detect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Upstream version default: probed project version, else the tag HEAD
|
||||
// sits on, else `<lasttag>+git<YYYYMMDD>.<hash>`, else 0.1.0.
|
||||
let upstream_version = match cli.upstream_version {
|
||||
Some(ref version) => version.clone(),
|
||||
None => {
|
||||
let probed = template
|
||||
.and_then(templates::get)
|
||||
.and_then(|t| t.probe(existing_dir.as_deref()?))
|
||||
.and_then(|probe| probe.version)
|
||||
.filter(|version| {
|
||||
validate_upstream_version(version, cli.revision.unwrap_or(1)).is_ok()
|
||||
});
|
||||
let derived = probed
|
||||
.or_else(|| origin.as_ref().and_then(GitOrigin::head_tag_version))
|
||||
.or_else(|| origin.as_ref().and_then(GitOrigin::git_version))
|
||||
.unwrap_or_else(|| "0.1.0".to_string());
|
||||
if derived != "0.1.0" {
|
||||
log::info!("No --upstream-version given, defaulting to '{derived}'");
|
||||
}
|
||||
derived
|
||||
}
|
||||
};
|
||||
let revision = cli.revision.unwrap_or(1);
|
||||
validate_upstream_version(&upstream_version, revision)
|
||||
.map_err(|e| format!("Invalid upstream version: {e}"))?;
|
||||
|
||||
// Orig-tarball plan (quilt only). Without an explicit --orig-from the
|
||||
// non-interactive default never touches the network: `git archive` of
|
||||
// the tag when HEAD sits on one, a working-tree snapshot otherwise.
|
||||
let orig = resolve_orig_origin(&cli, &source_dir, source_format, origin.as_ref())?;
|
||||
|
||||
let homepage = match &cli.homepage {
|
||||
Some(h) => {
|
||||
validate_homepage(h)?;
|
||||
@@ -673,7 +816,8 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
||||
series,
|
||||
release: cli.release,
|
||||
depends,
|
||||
native: cli.native,
|
||||
source_format,
|
||||
orig,
|
||||
git: cli.git,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
@@ -681,6 +825,111 @@ pub async fn resolve(cli: NewCli) -> Result<NewOptions, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the orig-tarball plan of a quilt scaffold from the CLI answers
|
||||
/// and the detected git origin. Fails early with a pointed message when an
|
||||
/// explicit `--orig-from` mode cannot apply to this tree; the implicit,
|
||||
/// non-interactive default never touches the network (`git archive` of the
|
||||
/// tag when HEAD sits exactly on one, a working-tree snapshot otherwise).
|
||||
fn resolve_orig_origin(
|
||||
cli: &NewCli,
|
||||
source_dir: &SourceDir,
|
||||
source_format: SourceFormat,
|
||||
origin: Option<&GitOrigin>,
|
||||
) -> Result<Option<OrigOrigin>, String> {
|
||||
if source_format == SourceFormat::Native {
|
||||
if cli.orig_from.is_some() || cli.orig_path.is_some() {
|
||||
return Err(
|
||||
"--orig-from/--orig-path need a quilt package (drop --native \
|
||||
or pass --quilt to build with an orig tarball)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let skeleton = matches!(source_dir, SourceDir::Skeleton);
|
||||
let head_tag = || origin.and_then(|o| o.head_tag.clone());
|
||||
|
||||
let plan = match cli.orig_from.as_deref() {
|
||||
Some("snapshot") => OrigOrigin::Snapshot,
|
||||
Some("path") => {
|
||||
let source = cli.orig_path.clone().ok_or_else(|| {
|
||||
"--orig-from path requires --orig-path <tarball file or URL>".to_string()
|
||||
})?;
|
||||
validate_orig_path(&source)?;
|
||||
OrigOrigin::Provided { source }
|
||||
}
|
||||
Some("git") => {
|
||||
if skeleton {
|
||||
return Err("a fresh skeleton has no upstream git history; use \
|
||||
--orig-from snapshot (or drop --quilt)"
|
||||
.to_string());
|
||||
}
|
||||
match head_tag() {
|
||||
Some(tag) => OrigOrigin::GitArchive { tag },
|
||||
None => {
|
||||
return Err("--orig-from git requires HEAD to be exactly on a \
|
||||
release tag"
|
||||
.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("release") => {
|
||||
if skeleton {
|
||||
return Err("a fresh skeleton has no upstream release to download; \
|
||||
use --orig-from snapshot (or drop --quilt)"
|
||||
.to_string());
|
||||
}
|
||||
let Some(tag) = head_tag() else {
|
||||
return Err("--orig-from release requires HEAD to be exactly on a \
|
||||
release tag"
|
||||
.to_string());
|
||||
};
|
||||
let forge = origin.and_then(|o| o.forge.clone()).ok_or_else(|| {
|
||||
"--orig-from release needs a github.com or gitlab.com origin \
|
||||
remote (self-hosted forges are not recognized)"
|
||||
.to_string()
|
||||
})?;
|
||||
OrigOrigin::Release { tag, forge }
|
||||
}
|
||||
Some(other) => {
|
||||
return Err(format!(
|
||||
"Unknown --orig-from value '{other}'. Supported: release, \
|
||||
git, path, snapshot."
|
||||
));
|
||||
}
|
||||
None => {
|
||||
// Skeletons (and anything without an upstream history) can only
|
||||
// be snapshotted; on a tag, git archive is the offline default.
|
||||
match (skeleton, head_tag()) {
|
||||
(false, Some(tag)) => OrigOrigin::GitArchive { tag },
|
||||
_ => OrigOrigin::Snapshot,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if cli.orig_path.is_some() && !matches!(plan, OrigOrigin::Provided { .. }) {
|
||||
return Err("--orig-path is only valid together with --orig-from path".to_string());
|
||||
}
|
||||
Ok(Some(plan))
|
||||
}
|
||||
|
||||
/// Validate a `--orig-path` value (and the wizard's tarball answer): either
|
||||
/// an http(s) URL (downloaded at scaffold time) or an existing local
|
||||
/// tarball.
|
||||
pub fn validate_orig_path(source: &str) -> Result<(), String> {
|
||||
if source.starts_with("http://") || source.starts_with("https://") {
|
||||
return Ok(());
|
||||
}
|
||||
if Path::new(source).is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"'{source}' is not an http(s) URL and not an existing file: \
|
||||
--orig-path must point at a tarball to repack"
|
||||
))
|
||||
}
|
||||
|
||||
/// Set of names no template may produce twice (collision check).
|
||||
pub(crate) fn check_file_collisions(paths: &[String]) -> Result<(), String> {
|
||||
let mut seen: HashSet<&String> = HashSet::new();
|
||||
@@ -788,7 +1037,8 @@ mod tests {
|
||||
series: "sid".into(),
|
||||
release: false,
|
||||
depends: Vec::new(),
|
||||
native: false,
|
||||
source_format: SourceFormat::Quilt,
|
||||
orig: None,
|
||||
git: false,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
@@ -937,6 +1187,10 @@ mod tests {
|
||||
assert_eq!(opts.depends, vec!["hello (>= 1.0)", "hello-data"]);
|
||||
assert!(opts.git);
|
||||
assert!(!opts.release);
|
||||
// An existing project (Path mode) is quilt by default; an empty,
|
||||
// non-git directory has no better orig plan than a tree snapshot.
|
||||
assert_eq!(opts.source_format, SourceFormat::Quilt);
|
||||
assert_eq!(opts.orig, Some(OrigOrigin::Snapshot));
|
||||
// Series: the development series of the current vendor (lowercased,
|
||||
// matching the distro-info keys).
|
||||
let dist = crate::build::env::current_vendor().to_lowercase();
|
||||
@@ -979,4 +1233,316 @@ mod tests {
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("--release requires --series"), "{err}");
|
||||
}
|
||||
|
||||
/// Minimal CLI answers for a scripted source directory.
|
||||
fn cli_for(dir: &std::path::Path) -> NewCli {
|
||||
NewCli {
|
||||
name: Some("mytool".into()),
|
||||
source: Some(dir.to_path_buf()),
|
||||
lang: Some("empty".into()),
|
||||
description: Some("A tool".into()),
|
||||
maintainer: Some("Jane <jane@example.com>".into()),
|
||||
git: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A git repo fixture: init, commit, optional tag, optional extra
|
||||
/// commit (needs the git CLI; returns (repo dir, HEAD short hash)).
|
||||
fn git_repo(tag: Option<&str>, second_commit: bool) -> (tempfile::TempDir, String) {
|
||||
let run = |dir: &std::path::Path, args: &[&str]| {
|
||||
let status = std::process::Command::new("git")
|
||||
.args([
|
||||
"-c",
|
||||
"user.name=T",
|
||||
"-c",
|
||||
"user.email=t@example.invalid",
|
||||
"-c",
|
||||
"commit.gpgsign=false",
|
||||
])
|
||||
.args(args)
|
||||
.current_dir(dir)
|
||||
.status()
|
||||
.expect("git should be runnable");
|
||||
assert!(status.success(), "git {args:?} failed");
|
||||
};
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
run(dir.path(), &["init", "-q"]);
|
||||
std::fs::write(dir.path().join("f"), "one\n").unwrap();
|
||||
run(dir.path(), &["add", "f"]);
|
||||
run(dir.path(), &["commit", "-q", "-m", "first"]);
|
||||
if let Some(tag) = tag {
|
||||
run(dir.path(), &["tag", tag]);
|
||||
}
|
||||
if second_commit {
|
||||
std::fs::write(dir.path().join("f"), "two\n").unwrap();
|
||||
run(dir.path(), &["add", "f"]);
|
||||
run(dir.path(), &["commit", "-q", "-m", "second"]);
|
||||
}
|
||||
let hash = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.current_dir(dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
let hash = String::from_utf8_lossy(&hash.stdout).trim().to_string();
|
||||
(dir, hash)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_skeleton_is_native_without_orig_tarball() {
|
||||
// A self-authored skeleton defaults to 3.0 (native): no orig
|
||||
// tarball, no orig plan.
|
||||
let cli = NewCli {
|
||||
name: Some("mytool".into()),
|
||||
lang: Some("shell".into()),
|
||||
description: Some("A tool".into()),
|
||||
maintainer: Some("Jane <jane@example.com>".into()),
|
||||
git: true,
|
||||
..Default::default()
|
||||
};
|
||||
let opts = resolve(cli).await.unwrap();
|
||||
assert_eq!(opts.source_format, SourceFormat::Native);
|
||||
assert_eq!(opts.orig, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_skeleton_forced_quilt_snapshots_the_tree() {
|
||||
let cli = NewCli {
|
||||
name: Some("mytool".into()),
|
||||
lang: Some("shell".into()),
|
||||
description: Some("A tool".into()),
|
||||
maintainer: Some("Jane <jane@example.com>".into()),
|
||||
quilt: true,
|
||||
git: true,
|
||||
..Default::default()
|
||||
};
|
||||
let opts = resolve(cli).await.unwrap();
|
||||
assert_eq!(opts.source_format, SourceFormat::Quilt);
|
||||
assert_eq!(opts.orig, Some(OrigOrigin::Snapshot));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_skeleton_rejects_history_dependent_orig_modes() {
|
||||
// A skeleton has no upstream git history: only the snapshot mode
|
||||
// makes sense.
|
||||
for mode in ["git", "release"] {
|
||||
let cli = NewCli {
|
||||
name: Some("mytool".into()),
|
||||
lang: Some("shell".into()),
|
||||
description: Some("A tool".into()),
|
||||
maintainer: Some("Jane <jane@example.com>".into()),
|
||||
quilt: true,
|
||||
orig_from: Some(mode.to_string()),
|
||||
git: true,
|
||||
..Default::default()
|
||||
};
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("no upstream"), "{mode}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_existing_project_forced_native() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.native = true;
|
||||
let opts = resolve(cli).await.unwrap();
|
||||
assert_eq!(opts.source_format, SourceFormat::Native);
|
||||
assert_eq!(opts.orig, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_native_and_quilt_conflict() {
|
||||
let cli = NewCli {
|
||||
name: Some("mytool".into()),
|
||||
lang: Some("shell".into()),
|
||||
description: Some("A tool".into()),
|
||||
maintainer: Some("Jane <jane@example.com>".into()),
|
||||
native: true,
|
||||
quilt: true,
|
||||
..Default::default()
|
||||
};
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("mutually exclusive"), "{err}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_head_on_tag_defaults_to_git_archive_and_tag_version() {
|
||||
if std::process::Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (dir, _hash) = git_repo(Some("v1.2.3"), false);
|
||||
let opts = resolve(cli_for(dir.path())).await.unwrap();
|
||||
|
||||
assert_eq!(opts.source_format, SourceFormat::Quilt);
|
||||
// Version default: the tag version, not 0.1.0.
|
||||
assert_eq!(opts.upstream_version, "1.2.3");
|
||||
// Non-interactive orig default on a tag: git archive (no network).
|
||||
assert_eq!(
|
||||
opts.orig,
|
||||
Some(OrigOrigin::GitArchive {
|
||||
tag: "v1.2.3".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_between_releases_snapshots_with_git_version() {
|
||||
if std::process::Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (dir, _hash) = git_repo(Some("v1.2.3"), true);
|
||||
let opts = resolve(cli_for(dir.path())).await.unwrap();
|
||||
|
||||
let expected = crate::new::origin::GitOrigin::detect(dir.path())
|
||||
.unwrap()
|
||||
.git_version()
|
||||
.unwrap();
|
||||
assert_eq!(opts.upstream_version, expected);
|
||||
assert!(opts.upstream_version.starts_with("1.2.3+git"));
|
||||
assert_eq!(opts.orig, Some(OrigOrigin::Snapshot));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_probed_project_version_beats_the_tag() {
|
||||
if std::process::Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (dir, _) = git_repo(Some("v9.9.9"), false);
|
||||
// A Cargo.toml without src/ makes `cargo metadata` fail, so the
|
||||
// template probe falls back to its deterministic line parse.
|
||||
std::fs::write(
|
||||
dir.path().join("Cargo.toml"),
|
||||
"[package]\nname = \"x\"\nversion = \"3.3.3\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.lang = Some("rust".into());
|
||||
let opts = resolve(cli).await.unwrap();
|
||||
assert_eq!(opts.upstream_version, "3.3.3");
|
||||
// The orig plan still comes from the tag.
|
||||
assert_eq!(
|
||||
opts.orig,
|
||||
Some(OrigOrigin::GitArchive {
|
||||
tag: "v9.9.9".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_orig_from_modes() {
|
||||
if std::process::Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (dir, _) = git_repo(Some("v1.2.3"), false);
|
||||
|
||||
// snapshot is always allowed.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("snapshot".into());
|
||||
assert_eq!(resolve(cli).await.unwrap().orig, Some(OrigOrigin::Snapshot));
|
||||
|
||||
// release requires a recognized forge remote.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("release".into());
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("github.com or gitlab.com"), "{err}");
|
||||
|
||||
// After adding a recognized remote the release plan resolves.
|
||||
std::process::Command::new("git")
|
||||
.args(["remote", "add", "origin", "https://github.com/foo/bar.git"])
|
||||
.current_dir(dir.path())
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
.then_some(())
|
||||
.unwrap();
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("release".into());
|
||||
assert_eq!(
|
||||
resolve(cli).await.unwrap().orig,
|
||||
Some(OrigOrigin::Release {
|
||||
tag: "v1.2.3".to_string(),
|
||||
forge: crate::new::origin::Forge::GitHub {
|
||||
owner: "foo".into(),
|
||||
repo: "bar".into()
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// path requires --orig-path, and an existing file or a URL.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("path".into());
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("--orig-path"), "{err}");
|
||||
|
||||
let tarball = dir.path().join("upstream.tar.gz");
|
||||
std::fs::write(&tarball, b"not really a tarball").unwrap();
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("path".into());
|
||||
cli.orig_path = Some(tarball.to_string_lossy().into_owned());
|
||||
assert_eq!(
|
||||
resolve(cli).await.unwrap().orig,
|
||||
Some(OrigOrigin::Provided {
|
||||
source: tarball.to_string_lossy().into_owned()
|
||||
})
|
||||
);
|
||||
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("path".into());
|
||||
cli.orig_path = Some("https://example.com/upstream.tar.gz".into());
|
||||
assert_eq!(
|
||||
resolve(cli).await.unwrap().orig,
|
||||
Some(OrigOrigin::Provided {
|
||||
source: "https://example.com/upstream.tar.gz".to_string()
|
||||
})
|
||||
);
|
||||
|
||||
// A missing local path is rejected early.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_from = Some("path".into());
|
||||
cli.orig_path = Some(
|
||||
dir.path()
|
||||
.join("missing.tar.gz")
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
);
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("not an http(s) URL"), "{err}");
|
||||
|
||||
// --orig-path without --orig-from path is a misuse.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.orig_path = Some(tarball.to_string_lossy().into_owned());
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("only valid together"), "{err}");
|
||||
|
||||
// git requires HEAD exactly on a tag.
|
||||
let (off, _) = git_repo(Some("v1.0.0"), true);
|
||||
let mut cli = cli_for(off.path());
|
||||
cli.orig_from = Some("git".into());
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("exactly on a release tag"), "{err}");
|
||||
|
||||
// ... and the orig flags need quilt.
|
||||
let mut cli = cli_for(dir.path());
|
||||
cli.native = true;
|
||||
cli.orig_from = Some("git".into());
|
||||
let err = resolve(cli).await.unwrap_err();
|
||||
assert!(err.contains("need a quilt package"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user