new: add upstream-aware orig tarball origins and the orig-vendor component
This commit is contained in:
+221
-7
@@ -23,7 +23,8 @@ use std::path::PathBuf;
|
||||
use indicatif::MultiProgress;
|
||||
|
||||
use crate::new::detect::{self, Detection};
|
||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, TemplateId};
|
||||
use crate::new::options::{self, NewCli, NewOptions, SourceDir, SourceFormat, TemplateId};
|
||||
use crate::new::origin::GitOrigin;
|
||||
use crate::new::templates::{self, ProbeResult, ScaffoldOutcome};
|
||||
use crate::ui::prompt;
|
||||
|
||||
@@ -60,15 +61,17 @@ const SOURCE_LABEL: &str = "Where is the source code? ";
|
||||
const LICENSE_LABEL: &str = "License: ";
|
||||
const DIST_LABEL: &str = "Target distribution: ";
|
||||
const SERIES_LABEL: &str = "Target series: ";
|
||||
const ORIG_LABEL: &str = "Where should the orig tarball come from? ";
|
||||
|
||||
/// All select labels, so the separator test can check them in one place.
|
||||
#[cfg(test)]
|
||||
const SELECT_LABELS: [&str; 5] = [
|
||||
const SELECT_LABELS: [&str; 6] = [
|
||||
LANGUAGE_LABEL,
|
||||
SOURCE_LABEL,
|
||||
LICENSE_LABEL,
|
||||
DIST_LABEL,
|
||||
SERIES_LABEL,
|
||||
ORIG_LABEL,
|
||||
];
|
||||
|
||||
/// Run the `pkh new` flow: the wizard on an interactive terminal, plain
|
||||
@@ -88,8 +91,10 @@ fn is_interactive() -> bool {
|
||||
}
|
||||
|
||||
/// The wizard question flow (spec "Proposed UX"), in order:
|
||||
/// package name, language/build system, source location, upstream version,
|
||||
/// Debian revision, one-line description, homepage, license, command name,
|
||||
/// package name, language/build system, source location, upstream version
|
||||
/// (with the checkout-tag offer when the version names an existing tag),
|
||||
/// the orig-tarball origin (quilt + existing project only), Debian
|
||||
/// revision, one-line description, homepage, license, command name,
|
||||
/// maintainer, target distribution, target series, metapackage Depends
|
||||
/// (`empty` template only), git init — then the summary screen and the
|
||||
/// final `Generate?` confirmation. Every question with an explicit flag
|
||||
@@ -205,17 +210,101 @@ async fn run_wizard(mut cli: NewCli) -> Result<NewOptions, Box<dyn Error>> {
|
||||
cli.source = Some(cwd.clone());
|
||||
}
|
||||
|
||||
// 4. Upstream version.
|
||||
// The source format this run will produce: explicit flags win, then the
|
||||
// mode (skeleton → native, existing project → quilt). The wizard
|
||||
// surfaces it in the summary; `options::resolve` re-derives it the same
|
||||
// way for the non-interactive path.
|
||||
let source_format = match (cli.native, cli.quilt) {
|
||||
(true, true) => {
|
||||
return Err("--native and --quilt are mutually exclusive".into());
|
||||
}
|
||||
(true, false) => SourceFormat::Native,
|
||||
(false, true) => SourceFormat::Quilt,
|
||||
(false, false) => {
|
||||
if cli.source.is_none() {
|
||||
SourceFormat::Native
|
||||
} else {
|
||||
SourceFormat::Quilt
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Git origin of the packaged directory: drives the version default, the
|
||||
// checkout-tag offer and the orig-tarball question. Purely local.
|
||||
let packaged_dir = cli.source.clone();
|
||||
let mut origin = packaged_dir.as_deref().and_then(GitOrigin::detect);
|
||||
|
||||
// 4. Upstream version: probed project version, then the tag HEAD sits
|
||||
// on, then `<lasttag>+git<YYYYMMDD>.<hash>`, then 0.1.0.
|
||||
if cli.upstream_version.is_none() {
|
||||
let default = probe
|
||||
.as_ref()
|
||||
.and_then(|p| p.version.clone())
|
||||
.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());
|
||||
let revision = cli.revision.unwrap_or(1);
|
||||
let answer = ask_text("Upstream version", &default, move |version: &str| {
|
||||
options::validate_upstream_version(version, revision)
|
||||
})?;
|
||||
cli.upstream_version = Some(answer);
|
||||
cli.upstream_version = Some(answer.clone());
|
||||
|
||||
// The typed version names an existing tag HEAD is not on: offer to
|
||||
// check the release out (the packaging then matches the version).
|
||||
if let Some(origin_state) = &origin
|
||||
&& let Some(tag) = origin_state.tag_for_version(&answer)
|
||||
&& Some(tag) != origin_state.head_tag.as_deref()
|
||||
{
|
||||
let question = format!(
|
||||
"Version {answer} matches tag {tag}, but HEAD is not that \
|
||||
tag. Check out {tag} now?"
|
||||
);
|
||||
if prompt::confirm(&question, false)? {
|
||||
let dir = packaged_dir.as_deref().expect("origin implies a directory");
|
||||
crate::new::origin::checkout_tag(dir, tag)?;
|
||||
log::info!(
|
||||
"Checked out {tag} (detached HEAD — expected when \
|
||||
packaging a release)"
|
||||
);
|
||||
// HEAD moved: the release/git-archive choices below must
|
||||
// reflect the checkout.
|
||||
origin = GitOrigin::detect(dir);
|
||||
}
|
||||
// Declined (or non-TTY): keep the current tree and the typed
|
||||
// version.
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. Orig-tarball origin (quilt + existing project only): one select
|
||||
// question, options pre-ordered by what the detection found. The
|
||||
// release download is explicit network consent; a skeleton quilt
|
||||
// tree has no upstream history and is only ever snapshotted.
|
||||
if source_format == SourceFormat::Quilt && packaged_dir.is_some() {
|
||||
let choices = orig_origin_choices(origin.as_ref());
|
||||
let labels: Vec<String> = choices.iter().map(|(label, _)| label.clone()).collect();
|
||||
let head_tagged = origin
|
||||
.as_ref()
|
||||
.and_then(|o| o.head_tag.as_deref())
|
||||
.is_some();
|
||||
let default = if head_tagged {
|
||||
labels[0].clone()
|
||||
} else {
|
||||
"Snapshot this working tree".to_string()
|
||||
};
|
||||
let answer = select_from(ORIG_LABEL, &labels, &default, |answer| {
|
||||
labels.contains(&answer.to_string())
|
||||
})?;
|
||||
let chosen = choices
|
||||
.iter()
|
||||
.find(|(label, _)| label == &answer)
|
||||
.map(|(_, value)| *value)
|
||||
.expect("answer comes from the choice list");
|
||||
cli.orig_from = Some(chosen.to_string());
|
||||
if chosen == "path" {
|
||||
let validator = |path: &str| options::validate_orig_path(path);
|
||||
let path = prompt::text("Tarball path or URL", "", Some(&validator))?;
|
||||
cli.orig_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Debian revision.
|
||||
@@ -706,6 +795,37 @@ fn validate_revision_answer(answer: &str) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The choices of the orig-tarball-origin question for a quilt packaging of
|
||||
/// an existing directory: `(menu label, --orig-from value)` pairs,
|
||||
/// pre-ordered by what the git origin detection found — the release
|
||||
/// download and `git archive` only exist when HEAD sits exactly on a tag,
|
||||
/// the release download additionally needs a recognized forge. The working
|
||||
/// tree snapshot is always available (and is the implicit default when HEAD
|
||||
/// is not on a tag).
|
||||
fn orig_origin_choices(origin: Option<&GitOrigin>) -> Vec<(String, &'static str)> {
|
||||
let mut choices: Vec<(String, &'static str)> = Vec::new();
|
||||
if let Some(origin) = origin
|
||||
&& let Some(tag) = &origin.head_tag
|
||||
{
|
||||
if let Some(forge) = &origin.forge {
|
||||
choices.push((
|
||||
format!(
|
||||
"Download the upstream release tarball from {} ({tag})",
|
||||
forge.host()
|
||||
),
|
||||
"release",
|
||||
));
|
||||
}
|
||||
choices.push((
|
||||
format!("Create it from the git tag ({tag}, git archive)"),
|
||||
"git",
|
||||
));
|
||||
}
|
||||
choices.push(("Use a tarball I provide".to_string(), "path"));
|
||||
choices.push(("Snapshot this working tree".to_string(), "snapshot"));
|
||||
choices
|
||||
}
|
||||
|
||||
/// A `debian/watch` template for GitHub/GitLab-hosted projects; `None` when
|
||||
/// the homepage is not one of those hosts (the wizard skips the question).
|
||||
pub fn watch_template(homepage: Option<&str>) -> Option<String> {
|
||||
@@ -784,6 +904,13 @@ pub fn summary_text(opts: &NewOptions, toolchain_pin: Option<&str>) -> String {
|
||||
lines.push(format!(" debian/rules {}", template.rules_dh_line()));
|
||||
}
|
||||
}
|
||||
lines.push(format!(
|
||||
" debian/source/format {}",
|
||||
opts.source_format.deb_string()
|
||||
));
|
||||
if let Some(orig) = &opts.orig {
|
||||
lines.push(format!(" orig tarball {}", orig.label()));
|
||||
}
|
||||
let distribution = if opts.release {
|
||||
opts.series.as_str()
|
||||
} else {
|
||||
@@ -850,7 +977,8 @@ mod tests {
|
||||
series: "resolute".into(),
|
||||
release: false,
|
||||
depends: Vec::new(),
|
||||
native: false,
|
||||
source_format: options::SourceFormat::Native,
|
||||
orig: None,
|
||||
git: true,
|
||||
autopkgtest: false,
|
||||
pkg_config: false,
|
||||
@@ -945,6 +1073,92 @@ mod tests {
|
||||
assert!(watch_template(None).is_none());
|
||||
}
|
||||
|
||||
/// The orig-origin choices are pre-ordered by what the detection found:
|
||||
/// release download only with a forge, git archive only on a tag,
|
||||
/// tarball/snapshot always; snapshot last (the off-tag default).
|
||||
#[test]
|
||||
fn orig_origin_choices_preorder_by_detection() {
|
||||
use crate::new::origin::Forge;
|
||||
let tagged_forge = GitOrigin {
|
||||
forge: Some(Forge::GitHub {
|
||||
owner: "foo".into(),
|
||||
repo: "bar".into(),
|
||||
}),
|
||||
head_tag: Some("v1.4.0".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let choices = orig_origin_choices(Some(&tagged_forge));
|
||||
assert_eq!(choices.len(), 4);
|
||||
assert_eq!(choices[0].1, "release");
|
||||
assert!(choices[0].0.contains("github.com"));
|
||||
assert!(choices[0].0.contains("v1.4.0"));
|
||||
assert_eq!(choices[1].1, "git");
|
||||
assert!(choices[1].0.contains("git archive"));
|
||||
assert_eq!(choices[2].1, "path");
|
||||
assert_eq!(choices[3].1, "snapshot");
|
||||
|
||||
// Tag without a recognized forge: no download option.
|
||||
let tagged = GitOrigin {
|
||||
head_tag: Some("1.0.0".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let choices = orig_origin_choices(Some(&tagged));
|
||||
assert_eq!(choices.len(), 3);
|
||||
assert_eq!(choices[0].1, "git");
|
||||
assert_eq!(choices[1].1, "path");
|
||||
assert_eq!(choices[2].1, "snapshot");
|
||||
|
||||
// Off a tag (or not even a repo): tarball + snapshot only.
|
||||
let off_tag = GitOrigin {
|
||||
last_tag: Some("v1.0.0".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let choices = orig_origin_choices(Some(&off_tag));
|
||||
assert_eq!(choices.len(), 2);
|
||||
assert_eq!(choices[0].1, "path");
|
||||
assert_eq!(choices[1].1, "snapshot");
|
||||
assert_eq!(orig_origin_choices(None).len(), 2);
|
||||
}
|
||||
|
||||
/// The summary surfaces the derived source format and, for quilt, the
|
||||
/// planned orig origin.
|
||||
#[test]
|
||||
fn summary_screen_shows_format_and_orig_origin() {
|
||||
// Native skeleton: the format row, no orig row.
|
||||
let text = summary_text(&opts(Tid::Shell), None);
|
||||
assert!(text.contains("debian/source/format 3.0 (native)"), "{text}");
|
||||
assert!(!text.contains("orig tarball"), "{text}");
|
||||
|
||||
// Quilt over an existing project: both rows.
|
||||
let mut quilt = opts(Tid::Shell);
|
||||
quilt.source_dir = options::SourceDir::Here;
|
||||
quilt.source_format = options::SourceFormat::Quilt;
|
||||
quilt.orig = Some(options::OrigOrigin::GitArchive {
|
||||
tag: "v1.4.0".to_string(),
|
||||
});
|
||||
let text = summary_text(&quilt, None);
|
||||
assert!(text.contains("debian/source/format 3.0 (quilt)"), "{text}");
|
||||
assert!(
|
||||
text.contains("orig tarball git archive (v1.4.0)"),
|
||||
"{text}"
|
||||
);
|
||||
|
||||
// The release-download label of the origin matrix.
|
||||
let mut release = quilt.clone();
|
||||
release.orig = Some(options::OrigOrigin::Release {
|
||||
tag: "v0.14.0".to_string(),
|
||||
forge: crate::new::origin::Forge::GitLab {
|
||||
owner: "foo".into(),
|
||||
repo: "bar".into(),
|
||||
},
|
||||
});
|
||||
let text = summary_text(&release, None);
|
||||
assert!(
|
||||
text.contains("orig tarball release download (v0.14.0)"),
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_screen_skeleton() {
|
||||
let text = summary_text(&opts(Tid::Makefile), None);
|
||||
|
||||
Reference in New Issue
Block a user