diff --git a/src/new/questions.rs b/src/new/questions.rs index 1c41771..ebc1d36 100644 --- a/src/new/questions.rs +++ b/src/new/questions.rs @@ -102,12 +102,8 @@ fn is_interactive() -> bool { /// answer is skipped (flag > detected/probe > default merge order). async fn run_wizard(mut cli: NewCli) -> Result> { let cwd = std::env::current_dir()?; - let detect_dir = cli.source.clone().unwrap_or_else(|| cwd.clone()); - let detection = detect::detect(&detect_dir); - let probe = match &detection { - Detection::Single(id) => templates::get(*id).and_then(|t| t.probe(&detect_dir)), - _ => None, - }; + let mut detect_dir = cli.source.clone().unwrap_or_else(|| cwd.clone()); + let (detection, mut probe) = detect_and_probe(&detect_dir); // Whether the flags imply a fresh skeleton (name given, no --source). let implied_skeleton = cli.source.is_none() && cli.name.is_some(); // Whether the language question is skipped by a confident detection @@ -249,6 +245,26 @@ async fn run_wizard(mut cli: NewCli) -> Result> { let packaged_dir = cli.source.clone(); let mut origin = packaged_dir.as_deref().and_then(GitOrigin::detect); + // The probe data read above describes `detect_dir` — the cwd unless + // `--source` pointed somewhere. When the source-location answer just + // redirected the wizard to another directory, re-run the detection + + // probe there (the same code path as the initial pass) so every + // following probe-derived default — upstream version, description, + // homepage, license, command, the pkg-config hint — describes the + // project actually being packaged. The detected directory itself is + // never re-probed (the data is already fresh and the scan is not free), + // and explicit flags keep winning throughout: the probe only ever feeds + // the defaults of questions without a flag answer. + if let Some(chosen) = packaged_dir.as_deref() + && !same_directory(chosen, &detect_dir) + { + let (_, refreshed) = detect_and_probe(chosen); + probe = refreshed; + // The license sniff and the pkg-config hint read the same directory + // the probe data now comes from. + detect_dir = chosen.to_path_buf(); + } + // 4. Upstream version: probed project version, then the tag HEAD sits // on, then `+git.`, then 0.1.0. A raw probe // that fails `validate_upstream_version` (e.g. `1.0-2` or `v1.0`) is @@ -637,6 +653,33 @@ pub async fn offer_verification( } } +/// Detection + template probe of one candidate directory: the probe data +/// exists only for a confidently detected single-template project. The one +/// code path for both wizard passes — the initial sweep of the detected +/// directory and the re-probe when the source-location answer redirects the +/// wizard elsewhere. +fn detect_and_probe(dir: &std::path::Path) -> (Detection, Option) { + let detection = detect::detect(dir); + let probe = match &detection { + Detection::Single(id) => templates::get(*id).and_then(|t| t.probe(dir)), + _ => None, + }; + (detection, probe) +} + +/// Whether two paths name the same directory: lexical equality first, then +/// (for spellings like `.` next to the absolute cwd) the canonicalized +/// forms. A path that cannot be canonicalized only ever equals itself. +fn same_directory(a: &std::path::Path, b: &std::path::Path) -> bool { + if a == b { + return true; + } + match (a.canonicalize(), b.canonicalize()) { + (Ok(a), Ok(b)) => a == b, + _ => false, + } +} + /// The preselected default of the pkg-config opt-in question: whether the /// project's build file hints at pkg-config usage (`dependency(` in /// meson.build, `pkg_check_modules` / `find_package(PkgConfig` in @@ -1520,4 +1563,92 @@ mod tests { ); } } + + /// The probe helper is the same code path for the initial pass and the + /// re-probe: pointed at directory A it reports A's project, pointed at + /// directory B it reports B's. So when the source-location answer + /// redirects the wizard from the detected directory to another one, the + /// refreshed defaults (upstream version, description, homepage, license) + /// describe the project actually being packaged — regression: they used + /// to keep coming from the original directory. + #[test] + fn detect_and_probe_reports_the_directory_it_is_given() { + use tempfile::tempdir; + + let manifest = |name: &str, version: &str, license: &str| { + format!( + "[package]\nname = \"{name}\"\nversion = \"{version}\"\n\ + edition = \"2021\"\ndescription = \"{name} does {name} \ + things\"\nhomepage = \"https://{name}.example.com\"\n\ + license = \"{license}\"\n" + ) + }; + let dir_a = tempdir().unwrap(); + std::fs::write( + dir_a.path().join("Cargo.toml"), + manifest("alpha", "0.1.0", "MIT"), + ) + .unwrap(); + let dir_b = tempdir().unwrap(); + std::fs::write( + dir_b.path().join("Cargo.toml"), + manifest("beta", "2.9.9", "GPL-3.0+"), + ) + .unwrap(); + + // The initial pass over dir A. + let (detection_a, probe_a) = detect_and_probe(dir_a.path()); + assert_eq!(detection_a, Detection::Single(Tid::Rust)); + let probe_a = probe_a.expect("dir A is a rust project"); + assert_eq!(probe_a.name.as_deref(), Some("alpha")); + assert_eq!(probe_a.version.as_deref(), Some("0.1.0")); + assert_eq!(probe_a.license.as_deref(), Some("MIT")); + + // The user chose dir B instead: the refreshed probe comes from B, + // never from A. + let (detection_b, probe_b) = detect_and_probe(dir_b.path()); + assert_eq!(detection_b, Detection::Single(Tid::Rust)); + let probe_b = probe_b.expect("dir B is a rust project"); + assert_eq!(probe_b.name.as_deref(), Some("beta")); + assert_eq!(probe_b.version.as_deref(), Some("2.9.9")); + assert_eq!( + probe_b.description.as_deref(), + Some("beta does beta things") + ); + assert_eq!( + probe_b.homepage.as_deref(), + Some("https://beta.example.com") + ); + assert_eq!(probe_b.license.as_deref(), Some("GPL-3.0+")); + + // A directory without project markers probes to nothing (the + // questions fall back to their plain defaults). + let dir_c = tempdir().unwrap(); + let (detection_c, probe_c) = detect_and_probe(dir_c.path()); + assert_eq!(detection_c, Detection::Empty); + assert!(probe_c.is_none()); + } + + /// The re-probe skip predicate: the detected/cwd directory never + /// re-probes — lexically identical paths and spellings of the same + /// directory (`.`, a trailing slash) all compare equal, different + /// directories and non-existent paths do not. + #[test] + fn same_directory_compares_spellings_of_one_directory() { + use tempfile::tempdir; + + let dir = tempdir().unwrap(); + let other = tempdir().unwrap(); + assert!(same_directory(dir.path(), dir.path())); + + // `.` names the current directory. + assert!(same_directory( + std::path::Path::new("."), + &std::env::current_dir().unwrap() + )); + + assert!(!same_directory(dir.path(), other.path())); + // A path that cannot be canonicalized only ever equals itself. + assert!(!same_directory(dir.path(), &dir.path().join("missing"))); + } }