build: stop redistributing the previous source on binNMU uploads

dpkg-genchanges/genbuildinfo handle a binary-only upload by referencing
the previous source version textually (Source: pkg (prev),
Binary-Only: yes, Binary-Only-Changes) while distributing no source
files at all: pkh instead pulled the previous .dsc and its tarballs
into both documents whenever they sat next to the artifacts, re-uploading
the whole source on every binNMU.

Drop that redistribution (and include_dsc_artifacts with it), and emit
the missing Binary-Only: yes field, which the new differential test
against real dpkg-buildpackage -b caught. The binNMU case shares its
runner with the regular binary metadata differential; a unit test pins
the exclusion even with the previous artifacts present.
This commit is contained in:
2026-09-18 00:24:42 +02:00
parent 2b017dcf43
commit af870cb7cb
3 changed files with 191 additions and 212 deletions
+109 -196
View File
@@ -16,8 +16,6 @@ use std::sync::Arc;
use crate::context::Context;
use crate::debian::{ChecksumEntry, ControlInfo, FileChecksums, FilesList};
use super::parse_checksum_field;
/// Digests of one artifact.
#[derive(Debug, Clone, Default)]
struct ArtifactHashes {
@@ -57,7 +55,7 @@ pub struct BinaryMetadataOptions {
/// `dpkg-genchanges -b`: sorted `Binary` list, encounter-order `Architecture`
/// accumulation, sorted `Description` lines formatted like dpkg, `.buildinfo`
/// registration in `debian/files`, and binary-NMU handling (`Source:
/// pkg (prev)` + previous `.dsc` redistribution when present).
/// pkg (prev)` + `Binary-Only-Changes`, with no source files distributed).
pub fn generate_binary_metadata(
ctx: &Arc<Context>,
package_dir: &Path,
@@ -137,28 +135,24 @@ pub fn generate_binary_metadata(
}
// ------------------------------------------------------------------
// Binary-NMU: redistribute the previous source when present
// Binary-NMU: reference the previous source version, textually only
// ------------------------------------------------------------------
let sversion = entry.version.no_epoch();
let mut source_display = entry.source.clone();
let mut binary_only_changes = None;
if entry.binary_only {
// A binary-only upload must reference the previous source version;
// a changelog that cannot yield it is a hard error, like in the
// source-build path. Reuse the changelog read above instead of
// reading the file a second time.
// Like dpkg-genchanges/genbuildinfo, a binary-only upload references
// the previous source version in the `Source` field and records the
// entry in `Binary-Only-Changes`, but distributes NO source files:
// the previous `.dsc` and its tarballs already sit in the archive,
// and are not re-uploaded even when present next to the tree.
if let Some(prev) = &previous_entry {
source_display = format!("{} ({})", entry.source, prev.version.full());
binary_only_changes = Some(format!(
"{}\n\n -- {} <{}> {}",
entry.changes_field, entry.maintainer_name, entry.maintainer_email, entry.date_raw
));
let dsc_name = format!("{}_{}.dsc", entry.source, prev.version.no_epoch());
let dsc_path = upload_dir.join(&dsc_name);
if ctx.exists(&dsc_path)? {
include_dsc_artifacts(ctx, upload_dir, &dsc_name, &mut checksums)?;
}
}
}
@@ -297,6 +291,7 @@ pub fn generate_binary_metadata(
date: entry.date_raw.clone(),
source: source_display,
binaries,
binary_only: entry.binary_only,
built_for_profiles: opts.profiles.clone(),
architecture: arch_values.join(" "),
version: entry.version.full(),
@@ -400,90 +395,6 @@ fn hashes_in_context(
Ok(out)
}
/// Pull the `.dsc` checksums (and its referenced tarballs) into the
/// checksum registry, mirroring how binary-NMU uploads redistribute the
/// previous source.
fn include_dsc_artifacts(
ctx: &Arc<Context>,
upload_dir: &Path,
dsc_name: &str,
checksums: &mut FileChecksums,
) -> Result<(), Box<dyn Error>> {
let dsc_content = ctx.read_file(&upload_dir.join(dsc_name))?;
let para = crate::debian::control::parse_paragraphs(
crate::debian::control::strip_clearsigned_armour(&dsc_content),
)
.into_iter()
.next()
.ok_or_else(|| format!("'{dsc_name}' is empty"))?;
// Names and partial checksums are filled from the very same validated
// lines, so a listed name can never miss its checksum entry.
// Distribution order follows the Checksums fields (Checksums-Sha1 then
// Checksums-Sha256), like dpkg-genchanges; the `Files` field only
// supplements the md5 digests.
let mut names: Vec<String> = Vec::new();
let mut partials: BTreeMap<String, super::PartialChecksum> = BTreeMap::new();
for field in ["Checksums-Sha1", "Checksums-Sha256", "Files"] {
let Some(value) = para.get(field) else {
continue;
};
for cl in parse_checksum_field(field, value)
.map_err(|e| format!("cannot parse '{dsc_name}': {e}"))?
{
let slot = partials.entry(cl.name.clone()).or_default();
match field {
"Checksums-Sha1" => slot.sha1 = Some(cl.digest),
"Checksums-Sha256" => slot.sha256 = Some(cl.digest),
_ => slot.md5 = Some(cl.digest),
}
slot.size = Some(cl.size);
if field != "Files" && !names.contains(&cl.name) {
names.push(cl.name);
}
}
}
// The .dsc itself is hashed fresh (it may be signed/rewritten); the
// tarballs reuse the .dsc-recorded digests, like dpkg-genchanges does.
let dsc_hashes =
hashes_in_context(ctx, upload_dir, std::slice::from_ref(&dsc_name.to_string()))?;
if let Some(h) = dsc_hashes.get(dsc_name) {
checksums.insert_entry(
dsc_name,
ChecksumEntry {
size: h.size,
md5: h.md5.clone(),
sha1: h.sha1.clone(),
sha256: h.sha256.clone(),
// No SHA-512 digest available (see above).
sha512: String::new(),
},
);
}
for name in &names {
if name == dsc_name {
continue;
}
let p = partials
.get(name)
.ok_or_else(|| format!("file '{name}' listed in '{dsc_name}' has no checksum entry"))?;
checksums.insert_entry(
name,
ChecksumEntry {
size: p.size.unwrap_or(0),
md5: p.md5.clone().unwrap_or_default(),
sha1: p.sha1.clone().unwrap_or_default(),
sha256: p.sha256.clone().unwrap_or_default(),
// The `.dsc` records no SHA-512 (dpkg only writes
// sha1/sha256 there).
sha512: String::new(),
},
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -521,109 +432,11 @@ mod tests {
assert!(!environment.contains("DEBIAN_FRONTEND"), "{environment}");
}
/// A minimal previous-version `.dsc` with a 3-column Checksums-Sha1
/// field, a 4-column Checksums-Sha256 line and a 3-column `Files`.
/// Regression: the old code filled `names` from any line with a third
/// column but `partials` only from exactly-3-column lines, so the
/// "bogus" name landed in `names` alone and `&partials["bogus"]`
/// panicked. It must produce a build error instead.
#[test]
fn dsc_four_column_checksum_line_errors_instead_of_panicking() {
let dsc_name = "hello_1.0-1.dsc";
let dsc = "\
Format: 3.0 (native)
Source: hello
Binary: hello
Architecture: any
Version: 1.0-1
Maintainer: A B <a@b.c>
Checksums-Sha1:
aaa111 12 hello_1.0.orig.tar.xz
Checksums-Sha256:
bbb222 12 bogus hello_1.0-1.debian.tar.xz
Files:
ddd333 12 hello_1.0.orig.tar.xz
";
let base = tempfile::tempdir().expect("tempdir");
std::fs::write(base.path().join(dsc_name), dsc).expect("write dsc");
let ctx = Arc::new(
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
);
let mut checksums = FileChecksums::new();
let err = include_dsc_artifacts(&ctx, base.path(), dsc_name, &mut checksums)
.expect_err("malformed Checksums-Sha256 line must fail the build");
let err = err.to_string();
assert!(err.contains("Checksums-Sha256"), "{err}");
assert!(err.contains("hello_1.0-1.debian.tar.xz"), "{err}");
}
/// Happy path: tarball entries are assembled from the Checksums fields
/// (sha1/sha256) and merged with the legacy 5-column `Files` md5, in
/// Checksums-Sha1 order, with the `.dsc` itself hashed fresh first.
#[test]
fn include_dsc_artifacts_merges_legacy_files_layout() {
let dsc_name = "hello_1.0-1.dsc";
let tarball = "hello_1.0.orig.tar.xz";
let dsc = "\
Format: 3.0 (quilt)
Source: hello
Binary: hello
Architecture: any
Version: 1.0-1
Maintainer: A B <a@b.c>
Checksums-Sha1:
aaa111 12 hello_1.0.orig.tar.xz
Checksums-Sha256:
bbb222 12 hello_1.0.orig.tar.xz
Files:
ddd333 12 devel optional hello_1.0.orig.tar.xz
";
let base = tempfile::tempdir().expect("tempdir");
std::fs::write(base.path().join(dsc_name), dsc).expect("write dsc");
std::fs::write(base.path().join(tarball), "tarball bytes").expect("write tarball");
let ctx = Arc::new(
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
);
let mut checksums = FileChecksums::new();
include_dsc_artifacts(&ctx, base.path(), dsc_name, &mut checksums)
.expect("valid dsc must parse");
let collected: Vec<(String, crate::debian::ChecksumEntry)> = checksums
.iter()
.map(|(k, e)| (k.clone(), e.clone()))
.collect();
assert_eq!(
collected
.iter()
.map(|(k, _)| k.as_str())
.collect::<Vec<_>>(),
vec![dsc_name, tarball],
".dsc first, then Checksums-Sha1 order"
);
// The .dsc is hashed fresh from disk.
let dsc_entry = &collected[0].1;
assert_eq!(dsc_entry.size, dsc.len() as u64);
assert_eq!(dsc_entry.md5.len(), 32);
assert_eq!(dsc_entry.sha1.len(), 40);
assert_eq!(dsc_entry.sha256.len(), 64);
// The tarball reuses the .dsc-recorded digests, including the
// legacy 5-column `Files` md5 (section/priority skipped).
let tar_entry = &collected[1].1;
assert_eq!(tar_entry.size, 12);
assert_eq!(tar_entry.md5, "ddd333");
assert_eq!(tar_entry.sha1, "aaa111");
assert_eq!(tar_entry.sha256, "bbb222");
}
/// A binary-only (binNMU) build whose changelog cannot yield the
/// previous entry (malformed second header, unbalanced parenthesis) must
/// fail the metadata generation with a diagnostic naming the problem,
/// instead of silently emitting a plain `Source:` `.changes` with no
/// `Binary-Only-Changes` and no redistributed previous `.dsc`.
/// `Binary-Only-Changes` and no previous-version reference.
#[test]
fn binary_only_prev_version_parse_failure_errors_instead_of_wrong_metadata() {
let changelog = "\
@@ -739,4 +552,104 @@ Description: test package
#[cfg(unix)]
assert!(err.contains("Permission denied"), "{err}");
}
/// A binary-only (binNMU) build references the previous source version
/// (`Source: pkg (prev)`, `Binary-Only-Changes`) but must NOT
/// redistribute any source file: like dpkg-genchanges/genbuildinfo, the
/// previous `.dsc` and its tarballs stay out of both documents even when
/// they exist next to the artifacts.
#[test]
fn binary_only_metadata_references_previous_source_without_redistributing_it() {
let changelog = "\
hello (1.0-1+b1) unstable; urgency=medium, binary-only=yes
* Binary-only rebuild.
-- A B <a@b.c> Mon, 01 Jan 2024 00:00:00 +0000
hello (1.0-1) unstable; urgency=medium
* Initial release.
-- A B <a@b.c> Sun, 31 Dec 2023 00:00:00 +0000
";
let control = "\
Source: hello
Section: devel
Priority: optional
Maintainer: A B <a@b.c>
Package: hello
Architecture: all
Description: test package
";
let base = tempfile::tempdir().expect("tempdir");
let tree = base.path().join("hello-1.0");
std::fs::create_dir_all(tree.join("debian")).expect("mkdir tree");
std::fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
std::fs::write(tree.join("debian/control"), control).expect("write control");
std::fs::write(
tree.join("debian/files"),
"hello_1.0-1+b1_all.deb devel optional\n",
)
.expect("write files");
std::fs::write(base.path().join("hello_1.0-1+b1_all.deb"), "deb payload")
.expect("write deb");
// The trap: the previous source artifacts sit right next to the
// binaries, as they would after a source build. dpkg does not
// redistribute them for a binary-only upload, and neither must we.
std::fs::write(
base.path().join("hello_1.0-1.dsc"),
"Format: 3.0 (quilt)\nSource: hello\nBinary: hello\nArchitecture: any\nVersion: \
1.0-1\nMaintainer: A B <a@b.c>\nChecksums-Sha1:\n aaa111 12 \
hello_1.0.orig.tar.xz\n",
)
.expect("write previous dsc");
std::fs::write(base.path().join("hello_1.0.orig.tar.xz"), "tarball bytes")
.expect("write previous tarball");
let ctx = Arc::new(
crate::context::Context::new(crate::context::ContextConfig::Local).expect("context"),
);
let opts = BinaryMetadataOptions {
profiles: Vec::new(),
vendor: "debian".to_string(),
exported_env: BTreeMap::new(),
build_arch: "amd64".to_string(),
host_arch: "amd64".to_string(),
};
let (buildinfo_path, changes_path) =
generate_binary_metadata(&ctx, &tree, base.path(), &opts)
.expect("binNMU metadata generation must succeed");
let changes = std::fs::read_to_string(&changes_path).expect("read changes");
let buildinfo = std::fs::read_to_string(&buildinfo_path).expect("read buildinfo");
// The previous version is referenced textually.
assert!(
changes.contains("Source: hello (1.0-1)"),
"changes must reference the previous version: {changes}"
);
assert!(
buildinfo.contains("Binary-Only-Changes"),
"buildinfo must record the binary-only entry: {buildinfo}"
);
// ... but no source file is distributed, on either side.
for (doc, text) in [("changes", &changes), ("buildinfo", &buildinfo)] {
assert!(
!text.contains("hello_1.0-1.dsc"),
"{doc} must not redistribute the previous .dsc: {text}"
);
assert!(
!text.contains("hello_1.0.orig.tar.xz"),
"{doc} must not redistribute the previous tarball: {text}"
);
}
// The distributed set is exactly the binary artifacts + buildinfo.
assert!(
changes.contains("hello_1.0-1+b1_all.deb") && changes.contains(".buildinfo"),
"changes must distribute the deb and the buildinfo: {changes}"
);
}
}
+7
View File
@@ -65,6 +65,9 @@ pub struct ChangesInput {
pub source: String,
/// Sorted binary package names with artifacts (empty for source-only).
pub binaries: Vec<String>,
/// Whether the changelog entry is a binary-only (binNMU) upload
/// (`Binary-Only: yes` field).
pub binary_only: bool,
/// Active build profiles (`Built-For-Profiles`); omitted when empty.
pub built_for_profiles: Vec<String>,
/// `Architecture` field value in encounter order (e.g. `source`,
@@ -142,6 +145,9 @@ pub fn render_changes(input: &ChangesInput) -> Paragraph {
let joined = input.binaries.join(" ");
p.set("Binary", &wrap_long(&joined));
}
if input.binary_only {
p.set("Binary-Only", "yes");
}
if !input.built_for_profiles.is_empty() {
p.set("Built-For-Profiles", &input.built_for_profiles.join(" "));
}
@@ -329,6 +335,7 @@ mod tests {
date: "Sat, 22 Aug 2026 10:00:00 +0000".to_string(),
source: "pkg".to_string(),
binaries: vec![],
binary_only: false,
built_for_profiles: vec![],
architecture: "source".to_string(),
version: "1.0".to_string(),
+75 -16
View File
@@ -638,6 +638,7 @@ pub fn run_source_build(
date: entry.date_raw.clone(),
source: source_display.clone(),
binaries: Vec::new(), // source-only upload
binary_only: entry.binary_only,
built_for_profiles: profiles.clone(),
architecture: "source".to_string(),
version: entry.version.full(),
@@ -1863,23 +1864,50 @@ Provides: virtual-thing (= 2.0), plain-virtual
#[test]
fn diff_binary_build_metadata() {
const NAME: &str = "pkh-diff-m";
let control = format!(
"Source: {NAME}\nSection: utils\nPriority: optional\nMaintainer: {MAINTAINER}\nBuild-Depends: libc6\n\n\
Package: {NAME}\nArchitecture: any\nDescription: test package main\n long description\n\n\
Package: {NAME}-u\nPackage-Type: udeb\nArchitecture: all\nDescription: test udeb\n short\n"
);
let changelog = format!(
"{NAME} (1.0-1) unstable; urgency=medium\n\n * Binary build test.\n\n -- {MAINTAINER} {DATE}\n"
);
let rules = format!(
"#!/usr/bin/make -f\nV = $(shell dpkg-parsechangelog -S Version)\nA = $(shell dpkg-architecture -qDEB_HOST_ARCH)\n\nbuild:\n\tmkdir -p debian/tmp/usr/bin\n\tprintf '#!/bin/sh\\necho hi\\n' > debian/tmp/usr/bin/hello\n\tchmod 755 debian/tmp/usr/bin/hello\n\ttouch $@\n\nbinary: build\n\trm -rf debian/{NAME} debian/{NAME}-u\n\tmkdir -p debian/{NAME}/usr/bin debian/{NAME}/DEBIAN\n\tcp -r debian/tmp/. debian/{NAME}/\n\tdpkg-gencontrol -p{NAME} -Pdebian/{NAME}\n\tdpkg-deb --build debian/{NAME} ..\n\tmkdir -p debian/{NAME}-u/usr/share debian/{NAME}-u/DEBIAN\n\techo data > debian/{NAME}-u/usr/share/data.txt\n\tdpkg-gencontrol -p{NAME}-u -Pdebian/{NAME}-u\n\tdpkg-deb --build debian/{NAME}-u ..\n\tmv ../{NAME}-u_$(V)_all.deb ../{NAME}-u_$(V)_all.udeb\n\nclean:\n\trm -rf debian/tmp debian/{NAME} debian/{NAME}-u build-stamp debian/files debian/*.substvars\n\n.PHONY: build binary clean\n"
);
diff_binary_metadata_case(NAME, &changelog, "1.0-1", false);
}
/// debian/control shared by the binary-metadata differential cases: one
/// arch:any deb and one arch:all udeb.
fn binary_test_control(name: &str) -> String {
format!(
"Source: {name}\nSection: utils\nPriority: optional\nMaintainer: {MAINTAINER}\nBuild-Depends: libc6\n\n\
Package: {name}\nArchitecture: any\nDescription: test package main\n long description\n\n\
Package: {name}-u\nPackage-Type: udeb\nArchitecture: all\nDescription: test udeb\n short\n"
)
}
/// debian/rules driving dpkg-gencontrol/dpkg-deb directly (no debhelper).
fn binary_test_rules(name: &str) -> String {
format!(
"#!/usr/bin/make -f\nV = $(shell dpkg-parsechangelog -S Version)\nA = $(shell dpkg-architecture -qDEB_HOST_ARCH)\n\nbuild:\n\tmkdir -p debian/tmp/usr/bin\n\tprintf '#!/bin/sh\\necho hi\\n' > debian/tmp/usr/bin/hello\n\tchmod 755 debian/tmp/usr/bin/hello\n\ttouch $@\n\nbinary: build\n\trm -rf debian/{name} debian/{name}-u\n\tmkdir -p debian/{name}/usr/bin debian/{name}/DEBIAN\n\tcp -r debian/tmp/. debian/{name}/\n\tdpkg-gencontrol -p{name} -Pdebian/{name}\n\tdpkg-deb --build debian/{name} ..\n\tmkdir -p debian/{name}-u/usr/share debian/{name}-u/DEBIAN\n\techo data > debian/{name}-u/usr/share/data.txt\n\tdpkg-gencontrol -p{name}-u -Pdebian/{name}-u\n\tdpkg-deb --build debian/{name}-u ..\n\tmv ../{name}-u_$(V)_all.deb ../{name}-u_$(V)_all.udeb\n\nclean:\n\trm -rf debian/tmp debian/{name} debian/{name}-u build-stamp debian/files debian/*.substvars\n\n.PHONY: build binary clean\n"
)
}
/// Both-sides binary metadata comparison for one changelog: golden
/// `dpkg-buildpackage -b` against the pkh deb flow (rules targets with a
/// dpkg-buildpackage-like environment) plus native metadata generation.
/// `artifact_version` is the full version the artifacts are named after;
/// `with_prev_source` additionally places the previous version's `.dsc`
/// and tarball next to the tree on both sides (the binNMU trap: they must
/// not be redistributed).
fn diff_binary_metadata_case(
name: &str,
changelog: &str,
artifact_version: &str,
with_prev_source: bool,
) {
let control = binary_test_control(name);
let rules = binary_test_rules(name);
let write_tree = |root: &Path| {
fs::create_dir_all(root.join(format!("{NAME}/debian/source"))).expect("mkdir tree");
let tree = root.join(NAME);
fs::create_dir_all(root.join(format!("{name}/debian/source"))).expect("mkdir tree");
let tree = root.join(name);
fs::write(tree.join("debian/control"), &control).expect("write control");
fs::write(tree.join("debian/changelog"), &changelog).expect("write changelog");
fs::write(tree.join("debian/changelog"), changelog).expect("write changelog");
fs::write(tree.join("debian/source/format"), "3.0 (native)\n").expect("write format");
fs::write(tree.join("debian/rules"), &rules).expect("write rules");
#[cfg(unix)]
@@ -1900,6 +1928,20 @@ Provides: virtual-thing (= 2.0), plain-virtual
let golden_tree = write_tree(&golden_root);
let ours_tree = write_tree(&ours_root);
if with_prev_source {
for root in [&golden_root, &ours_root] {
let dsc = format!(
"Format: 3.0 (native)\nSource: {name}\nBinary: {name}\nArchitecture: all\n\
Version: 1.0-1\nMaintainer: {MAINTAINER}\nChecksums-Sha1:\n aaa111 12 \
{name}_1.0.tar.xz\nChecksums-Sha256:\n bbb222 12 {name}_1.0.tar.xz\nFiles:\n \
ddd333 12 utils optional {name}_1.0.tar.xz\n"
);
fs::write(root.join(format!("{name}_1.0-1.dsc")), dsc).expect("write previous dsc");
fs::write(root.join(format!("{name}_1.0.tar.xz")), "tarball byte")
.expect("write previous tarball");
}
}
// Golden side: real dpkg-buildpackage binary build.
let status = crate::test_support::run_logged(
Command::new("dpkg-buildpackage")
@@ -1915,7 +1957,7 @@ Provides: virtual-thing (= 2.0), plain-virtual
// rules targets directly by default (missing Rules-Requires-Root is
// treated as 'no'), so no fakeroot wrapper here either.
let entry =
crate::debian::parse_changelog_entry_from_str(&changelog).expect("parse changelog");
crate::debian::parse_changelog_entry_from_str(changelog).expect("parse changelog");
let vendor = env::current_vendor();
let profiles = env::resolve_build_profiles(&[], &vendor);
let parallel = env::num_parallel();
@@ -1959,15 +2001,32 @@ Provides: virtual-thing (= 2.0), plain-virtual
// Compare artifacts.
assert_changes_equivalent(
&golden_root.join(format!("{NAME}_1.0-1_amd64.changes")),
&ours_root.join(format!("{NAME}_1.0-1_amd64.changes")),
&golden_root.join(format!("{name}_{artifact_version}_amd64.changes")),
&ours_root.join(format!("{name}_{artifact_version}_amd64.changes")),
);
assert_buildinfo_equivalent(
&golden_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
&ours_root.join(format!("{NAME}_1.0-1_amd64.buildinfo")),
&golden_root.join(format!("{name}_{artifact_version}_amd64.buildinfo")),
&ours_root.join(format!("{name}_{artifact_version}_amd64.buildinfo")),
);
}
/// Differential check of the binary-only (binNMU) metadata against real
/// `dpkg-buildpackage -b`: with the previous source artifacts sitting
/// next to the tree (as after a source build), the `.changes` must
/// distribute only the binaries and the `.buildinfo`, both documents
/// referencing the previous version textually only. Regression guard for
/// the previous-source redistribution pkh used to emit.
#[test]
fn diff_binmu_binary_metadata() {
const NAME: &str = "pkh-diff-r";
let changelog = format!(
"{NAME} (1.0-1+b1) unstable; urgency=medium, binary-only=yes\n\n * Binary-only \
rebuild.\n\n -- {MAINTAINER} {DATE}\n\n{NAME} (1.0-1) unstable; urgency=medium\n\n \
* Initial release.\n\n -- {MAINTAINER} {DATE}\n"
);
diff_binary_metadata_case(NAME, &changelog, "1.0-1+b1", true);
}
#[test]
fn diff_native_minimal() {
differential_case(&FixtureSpec::new("pkh-diff-a", "1.0-1", "unstable"));