new: ignore vendored rust artifacts in the generated gitignore

This commit is contained in:
2026-09-17 10:37:58 +02:00
parent bac82f0afe
commit bc3d07abaa
4 changed files with 183 additions and 20 deletions
+62 -12
View File
@@ -323,15 +323,25 @@ pub const ROOT_GITIGNORE_ENTRIES: [&str; 6] = [
"target/",
];
/// Merge the root `.gitignore` entries into `existing` (the current file
/// contents, when there is one): missing entries are appended, an existing
/// file is never overwritten just to duplicate entries. Returns the new
/// contents, or `None` when nothing has to be written.
pub fn merge_root_gitignore(existing: Option<&str>) -> Option<String> {
/// Comment heading a root `.gitignore` freshly created by pkh (the skeleton
/// build-artifact section).
pub const ROOT_GITIGNORE_HEADER: &str = "# pkh build artifacts";
/// Merge `entries` into the root `.gitignore` contents `existing` (the
/// current file contents, when there is one): missing entries are appended,
/// an existing file is never overwritten just to duplicate entries. A fresh
/// file is headed by the `header` comment when one is given; appending to a
/// user file adds bare entries. Returns the new contents, or `None` when
/// nothing has to be written.
pub fn merge_gitignore_entries(
existing: Option<&str>,
entries: &[&str],
header: Option<&str>,
) -> Option<String> {
let have: HashSet<&str> = existing
.map(|content| content.lines().map(str::trim).collect())
.unwrap_or_default();
let missing: Vec<&str> = ROOT_GITIGNORE_ENTRIES
let missing: Vec<&str> = entries
.iter()
.copied()
.filter(|entry| !have.contains(entry))
@@ -346,8 +356,11 @@ pub fn merge_root_gitignore(existing: Option<&str>) -> Option<String> {
}
// Section comment only for a fresh file; appending to a user file adds
// bare entries.
if existing.is_none() {
out.push_str("# pkh build artifacts\n");
if existing.is_none()
&& let Some(header) = header
{
out.push_str(header);
out.push('\n');
}
for entry in missing {
out.push_str(entry);
@@ -746,17 +759,30 @@ mod tests {
}
#[test]
fn root_gitignore_merge() {
fn gitignore_merge() {
// Fresh file: header + all entries.
let fresh = merge_root_gitignore(None).unwrap();
let fresh =
merge_gitignore_entries(None, &ROOT_GITIGNORE_ENTRIES, Some(ROOT_GITIGNORE_HEADER))
.unwrap();
assert!(fresh.starts_with("# pkh build artifacts\n"));
for entry in ROOT_GITIGNORE_ENTRIES {
assert!(fresh.contains(entry), "{entry} missing");
}
// A fresh file without a header carries the bare entries.
assert_eq!(
merge_gitignore_entries(None, &["a/", "b"], None).unwrap(),
"a/\nb\n"
);
// Existing file: only the missing entries are appended, nothing lost.
let existing = "*.deb\nnode_modules/\n";
let merged = merge_root_gitignore(Some(existing)).unwrap();
let merged = merge_gitignore_entries(
Some(existing),
&ROOT_GITIGNORE_ENTRIES,
Some(ROOT_GITIGNORE_HEADER),
)
.unwrap();
assert!(merged.starts_with(existing));
assert!(merged.contains("*.dsc\n"));
assert!(!merged.contains("*.deb\n*.deb"));
@@ -766,7 +792,31 @@ mod tests {
.iter()
.map(|e| format!("{e}\n"))
.collect();
assert!(merge_root_gitignore(Some(&full)).is_none());
assert!(
merge_gitignore_entries(
Some(&full),
&ROOT_GITIGNORE_ENTRIES,
Some(ROOT_GITIGNORE_HEADER),
)
.is_none()
);
}
/// The vendoring entries of the rust template merge into an existing
/// user `.gitignore` like any other entry set: appended after the
/// user's lines, no header comment, idempotent.
#[test]
fn gitignore_merge_appends_template_entries() {
let entries = ["vendor/", ".cargo/config.toml"];
let merged =
merge_gitignore_entries(Some("# my project\n*.log\n"), &entries, None).unwrap();
assert_eq!(merged, "# my project\n*.log\nvendor/\n.cargo/config.toml\n");
// Already ignored: nothing to write.
assert!(
merge_gitignore_entries(Some("vendor/\n.cargo/config.toml\n"), &entries, None)
.is_none()
);
}
#[test]
+59 -8
View File
@@ -3,7 +3,9 @@
//!
//! This module orchestrates a scaffold run: target directory checks, project
//! detection, in-memory rendering of every file (all-or-nothing write), the
//! template post-write hook (e.g. `cargo vendor`), orig tarball creation
//! root `.gitignore` merge (skeleton build artifacts plus the template's own
//! entries, e.g. the rust vendored layout), the template post-write hook
//! (e.g. `cargo vendor`), orig tarball creation
//! (from the origin the run decided on — see [`origin`] and [`orig`]), git
//! initialization, structural verification and the next-steps message.
//! The interactive wizard ([`questions`]) fills a [`options::NewCli`] from
@@ -35,8 +37,10 @@ use templates::{OutputFile, ScaffoldOutcome};
/// 1. resolve the template from the registry,
/// 2. check the target directory (refuse an existing `debian/control`),
/// 3. render every file in memory and check for collisions,
/// 4. write the files all-or-nothing (plus the root `.gitignore` in skeleton
/// mode, appending to an existing one),
/// 4. write the files all-or-nothing (plus the root `.gitignore` merge:
/// the skeleton build-artifact entries in skeleton mode and the
/// template's own entries — rust: the vendored layout — in every mode,
/// appending to an existing file),
/// 5. run the template post-write hook (e.g. `cargo vendor`, so the vendored
/// sources land inside the orig tarball created next),
/// 6. create the orig tarball (quilt only, refusing overwrites),
@@ -171,13 +175,25 @@ fn scaffold_steps(opts: &NewOptions, pb: &ProgressBar) -> Result<ScaffoldOutcome
pb.set_message("Writing files");
debian::write_files(&target, &files)?;
// Root .gitignore: skeleton mode only, never overwriting an existing
// file (append the missing entries instead).
if skeleton
&& let Some(contents) = debian::merge_root_gitignore(
// Root .gitignore: the skeleton build-artifact entries in skeleton
// mode, plus the template's own entries (rust: the vendored layout) in
// every mode — missing ones appended, an existing file never
// overwritten.
let template_gitignore = template.gitignore_entries(opts);
let mut entries: Vec<&str> = Vec::new();
let mut header = None;
if skeleton {
entries.extend(debian::ROOT_GITIGNORE_ENTRIES);
header = Some(debian::ROOT_GITIGNORE_HEADER);
}
entries.extend(template_gitignore.iter().map(String::as_str));
if !entries.is_empty()
&& let Some(contents) = debian::merge_gitignore_entries(
std::fs::read_to_string(target.join(".gitignore"))
.ok()
.as_deref(),
&entries,
header,
)
{
std::fs::write(target.join(".gitignore"), contents)?;
@@ -502,7 +518,8 @@ mod tests {
scaffold_in(&tree, opts(TemplateId::Shell, "runtool", SourceDir::Here)).unwrap();
// debian/ lands directly in the directory; no skeleton file, no
// root .gitignore (skeleton mode only), no debian/install (the
// root .gitignore (the shell template contributes none and the
// artifact entries are skeleton-only), no debian/install (the
// generated one would reference the non-existent skeleton script),
// and the existing script is left alone.
assert!(tree.join("debian/control").exists());
@@ -662,6 +679,40 @@ mod tests {
assert!(config.contains("[source.crates-io]"), "{config}");
assert!(config.contains("[net]\noffline = true"), "{config}");
}
// Root .gitignore: the skeleton build-artifact entries plus the
// template's vendoring entries (contributed in every mode).
let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap();
assert!(gitignore.contains("# pkh build artifacts"), "{gitignore}");
assert!(gitignore.contains("*.deb"), "{gitignore}");
assert!(gitignore.contains("target/"), "{gitignore}");
assert!(gitignore.contains("vendor/\n"), "{gitignore}");
assert!(gitignore.contains(".cargo/config.toml\n"), "{gitignore}");
}
/// The rust template's vendoring entries land in the root `.gitignore`
/// in every mode: a Here-mode tree gets them appended to its existing
/// file (custom lines kept, no header comment), while the skeleton-only
/// build-artifact entries do not appear.
#[test]
#[serial]
fn scaffold_rust_here_merges_vendoring_gitignore_entries() {
let dir = tempdir().unwrap();
let tree = dir.path().join("packdir");
std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join(".gitignore"), "# my project\n*.log\n").unwrap();
scaffold_in(&tree, opts(TemplateId::Rust, "mytool", SourceDir::Here)).unwrap();
let gitignore = std::fs::read_to_string(tree.join(".gitignore")).unwrap();
assert!(
gitignore.starts_with("# my project\n*.log\n"),
"{gitignore}"
);
assert!(gitignore.contains("vendor/\n"), "{gitignore}");
assert!(gitignore.contains(".cargo/config.toml\n"), "{gitignore}");
// The build-artifact entries stay skeleton-only.
assert!(!gitignore.contains("*.deb"), "{gitignore}");
assert!(!gitignore.contains("# pkh build artifacts"), "{gitignore}");
}
/// End-to-end vendored rust quilt package: the vendoring hook creates
+51
View File
@@ -137,6 +137,15 @@ pub trait Template: Sync {
Vec::new()
}
/// Root `.gitignore` entries the template contributes in every mode
/// (the skeleton-mode build-artifact entries of
/// [`crate::new::debian::ROOT_GITIGNORE_ENTRIES`] are separate), merged
/// into the root `.gitignore` after the files are written — missing
/// ones appended, an existing file never overwritten. Default: none.
fn gitignore_entries(&self, _opts: &NewOptions) -> Vec<String> {
Vec::new()
}
/// Extra defaults derived from project metadata in `dir` (detect.rs);
/// `None` when the project carries nothing this template can read.
fn probe(&self, _dir: &Path) -> Option<ProbeResult> {
@@ -248,6 +257,48 @@ mod tests {
);
}
/// Only the rust template contributes root `.gitignore` entries of its
/// own — exactly the vendoring pair, and unconditionally (its vendoring
/// hook runs in every mode); every other template contributes nothing.
#[test]
fn gitignore_entries_are_rust_only() {
let o = NewOptions {
name: "mytool".into(),
template: TemplateId::Rust,
source_dir: SourceDir::Here,
upstream_version: "0.1.0".into(),
revision: 1,
summary: "A tool".into(),
long_description: "A tool".into(),
homepage: None,
license: crate::new::options::License::Mit,
command: "mytool".into(),
maintainer: ("Jane".into(), "jane@example.com".into()),
dist: "ubuntu".into(),
series: "resolute".into(),
release: false,
depends: Vec::new(),
source_format: crate::new::options::SourceFormat::Quilt,
orig: None,
git: false,
autopkgtest: false,
pkg_config: false,
watch: None,
};
for id in TemplateId::all() {
let entries = get(id).unwrap().gitignore_entries(&o);
if id == TemplateId::Rust {
assert_eq!(
entries,
vec!["vendor/".to_string(), ".cargo/config.toml".to_string()]
);
} else {
assert!(entries.is_empty(), "{id} contributes {entries:?}");
}
}
}
/// The final `debian/rules` of every template must be valid-looking
/// make: `#!/usr/bin/make -f` shebang, exactly one `%:` target whose
/// recipe is the template's dh line, tab-indented recipes only, and no
+11
View File
@@ -75,6 +75,17 @@ impl Template for Rust {
vec!["cargo:native".to_string(), "rustc:native".to_string()]
}
/// Root `.gitignore` entries contributed in every mode: the vendoring
/// hook runs unconditionally for this template, so `vendor/` and
/// `.cargo/config.toml` exist (or will exist) in every scaffolded rust
/// tree and must stay out of the repository. `.cargo/config.toml` is
/// the subtle one: it points cargo at `vendor/`, so committing it would
/// break plain `cargo build` for anyone cloning the repository without
/// the vendored sources.
fn gitignore_entries(&self, _opts: &NewOptions) -> Vec<String> {
vec!["vendor/".to_string(), ".cargo/config.toml".to_string()]
}
fn architecture(&self, _opts: &NewOptions) -> &'static str {
"any"
}