1use crate::vcs::{open_branch, BranchOpenError};
3use breezyshim::branch::{Branch, GenericBranch};
4use breezyshim::debian::apt::{Apt, LocalApt, RemoteApt};
5use breezyshim::debian::error::Error as DebianError;
6use breezyshim::error::Error as BrzError;
7use breezyshim::gpg::VerificationResult;
8use breezyshim::repository::Repository;
9use breezyshim::revisionid::RevisionId;
10use breezyshim::tree::{MutableTree, Tree, WorkingTree};
11use breezyshim::workingtree::GenericWorkingTree;
12use debversion::Version;
13use std::collections::HashMap;
14use std::path::Path;
15use std::str::FromStr;
16
17#[cfg(feature = "last-attempt-db")]
18use trivialdb as tdb;
19
20#[cfg(feature = "last-attempt-db")]
21pub struct LastAttemptDatabase {
23 db: tdb::Tdb,
24}
25
26#[cfg(feature = "last-attempt-db")]
27impl LastAttemptDatabase {
28 pub fn open(path: &Path) -> Self {
30 Self {
31 db: tdb::Tdb::open(
32 path,
33 None,
34 tdb::Flags::empty(),
35 libc::O_RDWR | libc::O_CREAT,
36 0o755,
37 )
38 .unwrap(),
39 }
40 }
41
42 pub fn get(&self, package: &str) -> Option<chrono::DateTime<chrono::FixedOffset>> {
44 let key = package.to_string().into_bytes();
45 self.db.fetch(&key).unwrap().map(|value| {
46 let value = String::from_utf8(value).unwrap();
47 chrono::DateTime::parse_from_rfc3339(&value).unwrap()
48 })
49 }
50
51 pub fn set(&mut self, package: &str, value: chrono::DateTime<chrono::FixedOffset>) {
53 let key = package.to_string().into_bytes();
54 let value = value.to_rfc3339();
55 self.db.store(&key, value.as_bytes(), None).unwrap();
56 }
57
58 pub fn refresh(&mut self, package: &str) {
60 self.set(package, chrono::Utc::now().into());
61 }
62}
63
64#[cfg(feature = "last-attempt-db")]
65impl Default for LastAttemptDatabase {
66 fn default() -> Self {
67 let xdg_dirs = xdg::BaseDirectories::with_prefix("silver-platter");
68 let last_attempt_path = xdg_dirs.place_data_file("last-upload-attempt.tdb").unwrap();
69 Self::open(last_attempt_path.as_path())
70 }
71}
72
73#[derive(Debug)]
75pub enum SignError {
76 Failed(String),
78
79 IOError(std::io::Error),
81}
82
83impl From<std::io::Error> for SignError {
84 fn from(e: std::io::Error) -> Self {
85 SignError::IOError(e)
86 }
87}
88
89impl std::fmt::Display for SignError {
90 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
91 match self {
92 SignError::Failed(s) => write!(f, "Failed to sign: {}", s),
93 SignError::IOError(e) => write!(f, "I/O error: {}", e),
94 }
95 }
96}
97
98impl std::error::Error for SignError {}
99
100pub fn debsign(path: &Path, keyid: Option<&str>) -> Result<(), SignError> {
102 let mut args = vec!["debsign".to_string()];
103 if let Some(keyid) = keyid {
104 args.push(format!("-k{}", keyid));
105 }
106 args.push(path.file_name().unwrap().to_string_lossy().to_string());
107 let status = std::process::Command::new("debsign")
108 .args(&args)
109 .current_dir(path.parent().unwrap())
110 .status()?;
111
112 if !status.success() {
113 Err(SignError::Failed("debsign failed".to_string()))
114 } else {
115 Ok(())
116 }
117}
118
119#[derive(Debug)]
121pub enum UploadError {
122 Failed(String),
124
125 IOError(std::io::Error),
127}
128
129impl From<std::io::Error> for UploadError {
130 fn from(e: std::io::Error) -> Self {
131 UploadError::IOError(e)
132 }
133}
134
135impl std::fmt::Display for UploadError {
136 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
137 match self {
138 UploadError::Failed(s) => write!(f, "Failed to upload: {}", s),
139 UploadError::IOError(e) => write!(f, "I/O error: {}", e),
140 }
141 }
142}
143
144impl std::error::Error for UploadError {}
145
146pub fn dput_changes(path: &Path, host: Option<&str>) -> Result<(), UploadError> {
148 let mut binding = std::process::Command::new("dput");
149 let mut cmd = binding.current_dir(path.parent().unwrap());
150 if let Some(host) = host {
151 cmd = cmd.arg(host)
152 };
153 let status = cmd
154 .arg(path.file_name().unwrap().to_string_lossy().to_string())
155 .status()?;
156
157 if !status.success() {
158 Err(UploadError::Failed("dput failed".to_string()))
159 } else {
160 Ok(())
161 }
162}
163
164#[cfg(feature = "gpg")]
165pub fn get_maintainer_keys(context: &mut gpgme::Context) -> Result<Vec<String>, gpgme::Error> {
167 context.import("/usr/share/keyrings/debian-keyring.gpg")?;
168
169 let mut ids = vec![];
170
171 for key in context.keys()? {
172 if let Err(err) = key {
173 eprintln!("Error getting key: {}", err);
174 continue;
175 }
176
177 let key = key.unwrap();
178
179 if let Ok(key_id) = key.id() {
180 ids.push(key_id.to_string());
181 }
182
183 for subkey in key.subkeys() {
184 if let Ok(subkey_id) = subkey.id() {
185 ids.push(subkey_id.to_string());
186 }
187 }
188 }
189
190 Ok(ids)
191}
192
193#[derive(Clone, Debug)]
194pub enum UploadPackageError {
196 Ignored(String, Option<String>),
198
199 ProcessingFailure(String, Option<String>),
201}
202
203fn vcswatch_prescan_package(
204 _package: &str,
205 vw: &VcswatchEntry,
206 exclude: Option<&[String]>,
207 min_commit_age: Option<i64>,
208 allowed_committers: Option<&[String]>,
209) -> Result<Option<chrono::DateTime<chrono::Utc>>, UploadPackageError> {
210 if let Some(exclude) = exclude {
211 if exclude.contains(&vw.package) {
212 return Err(UploadPackageError::Ignored(
213 "excluded".to_string(),
214 Some("Excluded".to_string()),
215 ));
216 }
217 }
218 if vw.url.is_none() || vw.vcs.is_none() {
219 return Err(UploadPackageError::ProcessingFailure(
220 "not-in-vcs".to_string(),
221 Some("Not in VCS".to_string()),
222 ));
223 }
224 if vw.commits == 0 {
229 return Err(UploadPackageError::Ignored(
230 "no-unuploaded-changes".to_string(),
231 Some("No unuploaded changes".to_string()),
232 ));
233 }
234 if vw.status.as_deref() == Some("ERROR") {
235 log::warn!("vcswatch: unable to access {}: {:?}", vw.package, vw.error);
236 return Err(UploadPackageError::ProcessingFailure(
237 "vcs-inaccessible".to_string(),
238 Some(format!("Unable to access vcs: {:?}", vw.error)),
239 ));
240 }
241 if let Some(last_scan) = vw.last_scan.as_ref() {
242 log::debug!("vcswatch last scanned at: {}", last_scan);
243 }
244 if vw.vcs.as_deref() == Some("Git") {
245 if let Some(vcslog) = vw.vcslog.as_ref() {
246 match check_git_commits(vcslog, min_commit_age, allowed_committers) {
247 Err(RevisionRejected::CommitterNotAllowed(committer, allowed_committers)) => {
248 log::warn!(
249 "{}: committer {} not in allowed list: {:?}",
250 vw.package,
251 committer,
252 allowed_committers,
253 );
254 return Err(UploadPackageError::Ignored(
255 "committer-not-allowed".to_string(),
256 Some(format!(
257 "committer {} not in allowed list: {:?}",
258 committer, allowed_committers
259 )),
260 ));
261 }
262 Err(RevisionRejected::RecentCommits(commit_age, min_commit_age)) => {
263 log::info!(
264 "{}: Recent commits ({} days < {} days), skipping.",
265 vw.package,
266 commit_age,
267 min_commit_age,
268 );
269 return Err(UploadPackageError::Ignored(
270 "recent-commits".to_string(),
271 Some(format!(
272 "Recent commits ({} days < {} days)",
273 commit_age, min_commit_age
274 )),
275 ));
276 }
277 Ok(ts) => {
278 return Ok(Some(ts));
279 }
280 }
281 }
282 }
283 Ok(None)
284}
285
286fn check_git_commits(
287 vcslog: &str,
288 min_commit_age: Option<i64>,
289 allowed_committers: Option<&[String]>,
290) -> Result<chrono::DateTime<chrono::Utc>, RevisionRejected> {
291 #[allow(dead_code)]
292 pub struct GitRevision {
293 commit_id: String,
294 headers: HashMap<String, String>,
295 message: String,
296 }
297
298 impl Revision for GitRevision {
299 fn committer(&self) -> Option<&str> {
300 GitRevision::committer(self)
301 }
302
303 fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
304 GitRevision::timestamp(self)
305 }
306 }
307
308 impl GitRevision {
309 pub fn committer(&self) -> Option<&str> {
310 if let Some(committer) = self.headers.get("Committer") {
311 Some(committer)
312 } else {
313 self.headers.get("Author").map(|s| s.as_str())
314 }
315 }
316
317 pub fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
318 let datestr = self.headers.get("Date").unwrap();
319
320 chrono::DateTime::parse_from_rfc2822(datestr)
321 .unwrap()
322 .to_utc()
323 }
324
325 pub fn from_lines(lines: &[&str]) -> Self {
326 let mut commit_id: Option<String> = None;
327 let mut message = vec![];
328 let mut headers = std::collections::HashMap::new();
329 for (i, line) in lines.iter().enumerate() {
330 if let Some(cid) = line.strip_prefix("commit ") {
331 commit_id = Some(cid.to_string());
332 } else if line == &"" {
333 message = lines[i + 1..].to_vec();
334 break;
335 } else {
336 let mut parts = line.split(": ");
337 let name = parts.next().unwrap();
338 let value = parts.next().unwrap();
339 headers.insert(name.to_string(), value.to_string());
340 }
341 }
342 Self {
343 commit_id: commit_id.unwrap(),
344 headers,
345 message: message.join("\n"),
346 }
347 }
348 }
349
350 let mut last_commit_ts: Option<chrono::DateTime<chrono::Utc>> = None;
351 let mut lines: Vec<String> = vec![];
352 for line in vcslog.lines() {
353 if line.is_empty()
354 && lines
355 .last()
356 .unwrap()
357 .chars()
358 .next()
359 .unwrap()
360 .is_whitespace()
361 {
362 let gitrev = GitRevision::from_lines(
363 lines
364 .iter()
365 .map(|s| s.as_ref())
366 .collect::<Vec<_>>()
367 .as_slice(),
368 );
369 if last_commit_ts.is_none() {
370 last_commit_ts = Some(gitrev.timestamp());
371 }
372 check_revision(&gitrev, min_commit_age, allowed_committers)?;
373 lines = vec![];
374 } else {
375 lines.push(line.to_string());
376 }
377 }
378 if !lines.is_empty() {
379 let gitrev = GitRevision::from_lines(
380 lines
381 .iter()
382 .map(|s| s.as_ref())
383 .collect::<Vec<_>>()
384 .as_slice(),
385 );
386 if last_commit_ts.is_none() {
387 last_commit_ts = Some(gitrev.timestamp());
388 }
389 check_revision(&gitrev, min_commit_age, allowed_committers)?;
390 }
391 Ok(last_commit_ts.unwrap())
392}
393
394trait Revision {
395 fn committer(&self) -> Option<&str>;
396 fn timestamp(&self) -> chrono::DateTime<chrono::Utc>;
397}
398
399impl Revision for breezyshim::repository::Revision {
400 fn committer(&self) -> Option<&str> {
401 Some(self.committer.as_str())
402 }
403
404 fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
405 chrono::DateTime::from_timestamp(self.timestamp as i64, 0).unwrap()
406 }
407}
408
409pub enum RevisionRejected {
411 CommitterNotAllowed(String, Vec<String>),
413
414 RecentCommits(i64, i64),
416}
417
418fn check_revision(
425 rev: &dyn Revision,
426 min_commit_age: Option<i64>,
427 allowed_committers: Option<&[String]>,
428) -> Result<(), RevisionRejected> {
429 if let Some(min_commit_age) = min_commit_age {
431 let commit_time = rev.timestamp();
432 let time_delta = chrono::Utc::now().signed_duration_since(commit_time);
433 if time_delta.num_days() < min_commit_age {
434 return Err(RevisionRejected::RecentCommits(
435 time_delta.num_days(),
436 min_commit_age,
437 ));
438 }
439 }
440
441 if let Some(allowed_committers) = allowed_committers.as_ref() {
442 let committer = rev.committer().unwrap();
444 let committer_email = match breezyshim::config::extract_email_address(committer) {
445 Some(email) => email,
446 None => {
447 log::warn!("Unable to extract email from {}", committer);
448 return Err(RevisionRejected::CommitterNotAllowed(
449 committer.to_string(),
450 allowed_committers.iter().map(|s| s.to_string()).collect(),
451 ));
452 }
453 };
454
455 if !allowed_committers.contains(&committer_email) {
456 return Err(RevisionRejected::CommitterNotAllowed(
457 committer_email,
458 allowed_committers.iter().map(|s| s.to_string()).collect(),
459 ));
460 }
461 }
462
463 Ok(())
464}
465
466#[derive(serde::Deserialize)]
467struct VcswatchEntry {
469 package: String,
471
472 vcslog: Option<String>,
474
475 commits: usize,
477
478 url: Option<String>,
480 last_scan: Option<String>,
481 status: Option<String>,
482 error: Option<String>,
483 vcs: Option<String>,
484 archive_version: Option<debversion::Version>,
485}
486
487fn vcswatch_prescan_packages(
488 packages: &[String],
489 inc_stats: &mut dyn FnMut(&str),
490 exclude: Option<&[String]>,
491 min_commit_age: Option<i64>,
492 allowed_committers: Option<&[String]>,
493) -> Result<(Vec<String>, usize, HashMap<String, VcswatchEntry>), Box<dyn std::error::Error>> {
494 log::info!("Using vcswatch to prescan {} packages", packages.len());
495
496 let url = url::Url::parse("https://qa.debian.org/data/vcswatch/vcswatch.json.gz")?;
497 let client = reqwest::blocking::Client::new();
498 let request = client
499 .request(reqwest::Method::GET, url)
500 .header(
501 "User-Agent",
502 format!("silver-platter/{}", env!("CARGO_PKG_VERSION")),
503 )
504 .build()?;
505
506 let response = client.execute(request)?;
507
508 assert!(
509 response.status().is_success(),
510 "Failed to fetch vcswatch data"
511 );
512
513 let d = flate2::read::GzDecoder::new(response);
514 let entries: Vec<VcswatchEntry> = serde_json::from_reader(d)?;
515
516 let vcswatch = entries
517 .into_iter()
518 .map(|e| (e.package.clone(), e))
519 .collect::<HashMap<_, _>>();
520
521 let mut by_ts = HashMap::new();
522 let mut failures = 0;
523 for package in packages.iter() {
524 let vw = if let Some(p) = vcswatch.get(package) {
525 p
526 } else {
527 continue;
528 };
529 match vcswatch_prescan_package(package, vw, exclude, min_commit_age, allowed_committers) {
530 Err(UploadPackageError::ProcessingFailure(reason, _description)) => {
531 inc_stats(reason.as_str());
532 failures += 1;
533 }
534 Err(UploadPackageError::Ignored(reason, _description)) => {
535 inc_stats(reason.as_str());
536 }
537 Ok(ts) => {
538 by_ts.insert(package, ts);
539 }
540 }
541 }
542
543 let mut ts_items = by_ts.into_iter().collect::<Vec<_>>();
544 ts_items.sort_by(|a, b| b.1.cmp(&a.1));
545 let packages = ts_items
546 .into_iter()
547 .map(|(k, _)| k.to_string())
548 .collect::<Vec<_>>();
549
550 Ok((packages, failures, vcswatch))
551}
552
553fn find_last_release_revid(
554 branch: &GenericBranch,
555 version: &Version,
556) -> Result<RevisionId, BrzError> {
557 let db = breezyshim::debian::import_dsc::DistributionBranch::new(branch, branch, None, None);
558 db.revid_of_version(version)
559 .map_err(|e| BrzError::UnknownFormat(format!("Failed to find revid for version: {}", e)))
560}
561
562fn select_apt_packages(
564 apt_repo: &dyn Apt,
565 package_names: Option<&[String]>,
566 maintainer: Option<&[String]>,
567) -> Vec<String> {
568 let mut packages = vec![];
569
570 for source in apt_repo.iter_sources() {
571 if let Some(maintainer) = maintainer {
572 let m = source.maintainer().unwrap();
573 let (_fullname, email) = debian_changelog::parseaddr(&m);
574 if !maintainer.contains(&email.to_string()) {
575 continue;
576 }
577 }
578
579 if let Some(package_names) = package_names {
580 if !package_names.contains(&source.package().unwrap()) {
581 continue;
582 }
583 }
584
585 packages.push(source.package().unwrap());
586 }
587
588 packages
589}
590
591pub fn main(
593 mut packages: Vec<String>,
594 acceptable_keys: Option<Vec<String>>,
595 gpg_verification: bool,
596 min_commit_age: Option<i64>,
597 diff: bool,
598 builder: String,
599 mut maintainer: Option<Vec<String>>,
600 vcswatch: bool,
601 exclude: Option<Vec<String>>,
602 autopkgtest_only: bool,
603 allowed_committers: Option<Vec<String>>,
604 debug: bool,
605 shuffle: bool,
606 verify_command: Option<String>,
607 apt_repository: Option<String>,
608 apt_repository_key: Option<std::path::PathBuf>,
609) -> Result<(), i32> {
610 let mut ret = Ok(());
611
612 if packages.is_empty() && maintainer.is_none() {
613 if let Some((_name, email)) = debian_changelog::get_maintainer() {
614 log::info!("Processing packages maintained by {}", email);
615 maintainer = Some(vec![email]);
616 }
617 }
618
619 if !vcswatch {
620 log::info!(
621 "Use --vcswatch to only process packages for which vcswatch found pending commits."
622 )
623 }
624
625 let apt_repo: Box<dyn Apt> = if let Some(apt_repository) = apt_repository.as_ref() {
626 Box::new(RemoteApt::from_string(apt_repository, apt_repository_key.as_deref()).unwrap())
627 as _
628 } else {
629 Box::new(LocalApt::new(None).unwrap()) as _
630 };
631
632 if let Some(maintainer) = maintainer.as_ref() {
633 packages = select_apt_packages(
634 apt_repo.as_ref(),
635 Some(packages.as_slice()),
636 Some(maintainer),
637 );
638 }
639
640 if packages.is_empty() {
641 log::info!("No packages found.");
642 return Err(1);
643 }
644
645 if shuffle {
646 use rand::seq::SliceRandom;
647 let mut rng = rand::rng();
649 packages.shuffle(&mut rng);
650 }
651
652 let mut stats = HashMap::new();
653
654 let mut inc_stats = |result: &str| {
655 *stats.entry(result.to_string()).or_insert(0) += 1;
656 };
657
658 let mut extra_data: Option<HashMap<String, VcswatchEntry>> = None;
659
660 if vcswatch {
661 let (new_packages, failures, new_extra_data) = vcswatch_prescan_packages(
662 packages.as_slice(),
663 &mut &mut inc_stats,
664 exclude.as_deref(),
665 min_commit_age,
666 allowed_committers.as_deref(),
667 )
668 .unwrap();
669 packages = new_packages;
670 extra_data = Some(new_extra_data);
671 if failures > 0 {
672 ret = Err(1);
673 }
674 };
675
676 if packages.len() > 1 {
677 log::info!(
678 "Uploading {} packages: {}",
679 packages.len(),
680 packages.join(", ")
681 );
682 }
683
684 #[cfg(feature = "last-attempt-db")]
685 let mut last_attempt = LastAttemptDatabase::default();
686
687 #[cfg(feature = "last-attempt-db")]
688 {
689 let orig_packages = packages.clone();
690
691 let last_attempt_key = |p: &String| -> (chrono::DateTime<chrono::FixedOffset>, usize) {
692 let t = last_attempt.get(p).unwrap_or(chrono::Utc::now().into());
693 (t, orig_packages.iter().position(|i| i == p).unwrap())
694 };
695
696 packages.sort_by_key(last_attempt_key);
697 }
698
699 for package in packages.iter() {
700 let extra_package = extra_data.as_ref().and_then(|d| d.get(package));
701
702 match process_package(
703 apt_repo.as_ref(),
704 package,
705 &builder,
706 exclude.as_deref(),
707 autopkgtest_only,
708 gpg_verification,
709 acceptable_keys.as_deref(),
710 debug,
711 diff,
712 min_commit_age,
713 allowed_committers.as_deref(),
714 extra_package.and_then(|p| p.vcs.as_deref()),
715 extra_package.and_then(|p| p.url.as_deref()),
716 extra_package.map(|p| p.package.as_str()),
717 extra_package.and_then(|p| p.archive_version.as_ref()),
718 verify_command.as_deref(),
719 ) {
720 Err(UploadPackageError::ProcessingFailure(reason, _description)) => {
721 inc_stats(reason.as_str());
722 ret = Err(1);
723 }
724 Err(UploadPackageError::Ignored(reason, _description)) => inc_stats(reason.as_str()),
725 Ok(_) => {
726 inc_stats("success");
727 }
728 }
729
730 #[cfg(feature = "last-attempt-db")]
731 last_attempt.refresh(package);
732 }
733
734 if packages.len() > 1 {
735 log::info!("Results:");
736 for (error, c) in stats.iter() {
737 log::info!(" {}: {}", error, c);
738 }
739 }
740
741 ret
742}
743
744pub enum PrepareUploadError {
746 GbpDchFailed,
748
749 NoUnuploadedChanges(Version),
751
752 LastUploadMoreRecent(Version, Version),
754
755 LastReleaseRevisionNotFound(String, Version),
757
758 NoUnreleasedChanges(Version),
760
761 GeneratedChangelogFile,
763
764 NoValidGpgSignature(RevisionId, VerificationResult),
766
767 Rejected(RevisionRejected),
769
770 BuildFailed,
772
773 MissingUpstreamTarball(String, String),
775
776 PackageVersionNotPresent(String, String),
778
779 MissingChangelog,
781
782 ChangelogParseError(String),
784
785 BrzError(BrzError),
787
788 DebianError(DebianError),
790
791 MissingNestedTree(std::path::PathBuf),
793
794 SignError(SignError),
796}
797
798impl From<BrzError> for PrepareUploadError {
799 fn from(e: BrzError) -> Self {
800 match e {
801 BrzError::MissingNestedTree(p) => PrepareUploadError::MissingNestedTree(p),
802 e => PrepareUploadError::BrzError(e),
803 }
804 }
805}
806
807pub fn prepare_upload_package(
809 local_tree: &GenericWorkingTree,
810 subpath: &std::path::Path,
811 pkg: &str,
812 last_uploaded_version: Option<&debversion::Version>,
813 builder: &str,
814 gpg_strategy: Option<breezyshim::gpg::GPGStrategy>,
815 min_commit_age: Option<i64>,
816 allowed_committers: Option<&[String]>,
817 _apt: Option<&dyn Apt>,
818) -> Result<(std::path::PathBuf, Option<String>), PrepareUploadError> {
819 let _builder = builder.to_string();
820 let debian_path = subpath.join("debian");
821 #[cfg(feature = "detect-update-changelog")]
822 let run_gbp_dch = {
823 let cl_behaviour = debian_analyzer::detect_gbp_dch::guess_update_changelog(
824 local_tree,
825 debian_path.as_path(),
826 None,
827 );
828 match cl_behaviour {
829 Some(cl_behaviour) => cl_behaviour.update_changelog,
830 None => true,
831 }
832 };
833 #[cfg(not(feature = "detect-update-changelog"))]
834 let run_gbp_dch = false;
835 if run_gbp_dch {
836 match crate::debian::gbp_dch(local_tree.abspath(subpath).unwrap().as_path()) {
837 Ok(_) => {}
838 Err(_) => {
839 return Err(PrepareUploadError::GbpDchFailed);
843 }
844 }
845 local_tree
846 .build_commit()
847 .message("update changelog\n\nGbp-Dch: Ignore")
848 .specific_files(&[&debian_path.join("changelog")])
849 .commit()
850 .unwrap();
851 }
852 let (cl, _top_level) = debian_analyzer::changelog::find_changelog(
853 local_tree,
854 std::path::Path::new(""),
855 Some(false),
856 )
857 .map_err(|e| match e {
858 debian_analyzer::changelog::FindChangelogError::MissingChangelog(..) => {
859 PrepareUploadError::MissingChangelog
860 }
861 debian_analyzer::changelog::FindChangelogError::AddChangelog(..) => {
862 panic!("changelog not versioned - should never happen");
863 }
864 debian_analyzer::changelog::FindChangelogError::ChangelogParseError(reason) => {
865 PrepareUploadError::ChangelogParseError(reason)
866 }
867 debian_analyzer::changelog::FindChangelogError::BrzError(o) => {
868 PrepareUploadError::BrzError(o)
869 }
870 })?;
871
872 let first_block = match cl.iter().next() {
873 Some(e) => e,
874 None => {
875 return Err(PrepareUploadError::NoUnuploadedChanges(
876 last_uploaded_version.unwrap().clone(),
877 ));
878 }
879 };
880 if let Some(last_uploaded_version) = last_uploaded_version {
881 if let Some(first_version) = first_block.version() {
882 if first_version == *last_uploaded_version {
883 return Err(PrepareUploadError::NoUnuploadedChanges(first_version));
884 }
885 }
886
887 if let Some(previous_version_in_branch) =
888 debian_analyzer::changelog::find_previous_upload(&cl)
889 {
890 if *last_uploaded_version > previous_version_in_branch {
891 return Err(PrepareUploadError::LastUploadMoreRecent(
892 last_uploaded_version.clone(),
893 previous_version_in_branch,
894 ));
895 }
896 }
897 }
898
899 if let Some(last_uploaded_version) = last_uploaded_version {
900 log::info!("Checking revisions since {}", last_uploaded_version);
901 }
902 let lock = local_tree.lock_read();
903 let last_release_revid: RevisionId = if let Some(last_uploaded_version) = last_uploaded_version
904 {
905 match find_last_release_revid(&local_tree.branch(), last_uploaded_version) {
906 Ok(revid) => revid,
907 Err(BrzError::NoSuchTag(..)) => {
908 return Err(PrepareUploadError::LastReleaseRevisionNotFound(
909 pkg.to_string(),
910 last_uploaded_version.clone(),
911 ));
912 }
913 Err(e) => {
914 panic!("Unexpected error: {:?}", e);
915 }
916 }
917 } else {
918 breezyshim::revisionid::RevisionId::null()
919 };
920 let graph = local_tree.branch().repository().get_graph();
921 let revids = graph
922 .iter_lefthand_ancestry(
923 &local_tree.branch().last_revision(),
924 Some(&[last_release_revid]),
925 )?
926 .collect::<Result<Vec<RevisionId>, _>>()?;
927 if revids.is_empty() {
928 log::info!("No pending changes");
929 return Err(PrepareUploadError::NoUnuploadedChanges(
930 first_block.version().unwrap(),
931 ));
932 }
933 if let Some(gpg_strategy) = gpg_strategy {
934 log::info!("Verifying GPG signatures...");
935 let result = breezyshim::gpg::bulk_verify_signatures(
936 &local_tree.branch().repository(),
937 revids.iter().collect::<Vec<_>>().as_slice(),
938 &gpg_strategy,
939 )
940 .unwrap();
941 for (revid, result) in result {
942 if !result.is_valid() {
943 return Err(PrepareUploadError::NoValidGpgSignature(revid, result));
944 }
945 }
946 }
947 for (_revid, rev) in local_tree.branch().repository().iter_revisions(revids) {
948 if let Some(rev) = rev {
949 check_revision(&rev, min_commit_age, allowed_committers)
950 .map_err(PrepareUploadError::Rejected)?;
951 }
952 }
953
954 if first_block.is_unreleased().unwrap_or(false) {
955 return Err(PrepareUploadError::NoUnreleasedChanges(
956 first_block.version().unwrap(),
957 ));
958 }
959 std::mem::drop(lock);
960 let mut qa_upload = false;
961 #[allow(unused_mut)]
962 let mut team_upload = false;
963 let control_path = local_tree
964 .abspath(debian_path.join("control").as_path())
965 .unwrap();
966 let mut f = local_tree.get_file_text(control_path.as_path()).unwrap();
967 let control =
968 debian_control::Control::from_str(std::str::from_utf8_mut(f.as_mut_slice()).unwrap())
969 .unwrap();
970 let source = control.source().unwrap();
971 let maintainer = source.maintainer().unwrap();
972 let (_, e) = debian_changelog::parseaddr(&maintainer);
973 if e == "packages@qa.debian.org" {
974 qa_upload = true;
975 }
978 if qa_upload || team_upload {
979 let changelog_path = local_tree.abspath(&debian_path.join("changelog")).unwrap();
980 let f = local_tree.get_file(changelog_path.as_path()).unwrap();
981 let cl = debian_changelog::ChangeLog::read_relaxed(f).unwrap();
982 let message = if qa_upload {
983 Some("QA Upload.")
984 } else if team_upload {
985 Some("Team Upload.")
986 } else {
987 None
988 };
989 if let Some(message) = message {
990 cl.iter().next().unwrap().ensure_first_line("Team upload.");
991 local_tree
992 .put_file_bytes_non_atomic(changelog_path.as_path(), cl.to_string().as_bytes())
993 .unwrap();
994 local_tree
996 .build_commit()
997 .message(&format!("Mention {}", message))
998 .allow_pointless(true)
999 .specific_files(&[debian_path.join("changelog").as_path()])
1000 .commit()
1001 .unwrap();
1002 }
1003 }
1004 let _tag_name = match breezyshim::debian::release::release(local_tree, subpath) {
1005 Ok(tag_name) => tag_name,
1006 Err(breezyshim::debian::release::ReleaseError::GeneratedFile) => {
1007 return Err(PrepareUploadError::GeneratedChangelogFile);
1008 }
1009 Err(e) => {
1010 panic!("Unexpected error: {:?}", e);
1011 }
1012 };
1013 let target_dir = tempfile::tempdir().unwrap();
1014 let builder = if let Some(last_uploaded_version) = last_uploaded_version {
1015 builder.replace(
1016 "${LAST_VERSION}",
1017 last_uploaded_version.to_string().as_str(),
1018 )
1019 } else {
1020 builder.to_string()
1021 };
1022 let target_changes = breezyshim::debian::build_helper(
1023 local_tree,
1024 subpath,
1025 &local_tree.branch(),
1026 target_dir.path(),
1027 builder.as_str(),
1028 false,
1029 _apt,
1030 )
1031 .map_err(|e| match e {
1032 DebianError::BrzError(o) => PrepareUploadError::BrzError(o),
1033 DebianError::MissingUpstreamTarball { package, version } => {
1034 PrepareUploadError::MissingUpstreamTarball(package, version)
1035 }
1036 DebianError::PackageVersionNotPresent { package, version } => {
1037 PrepareUploadError::PackageVersionNotPresent(package, version)
1038 }
1039 DebianError::BuildFailed => PrepareUploadError::BuildFailed,
1040 e => PrepareUploadError::DebianError(e),
1041 })?;
1042 let source = target_changes.get("source").unwrap();
1043 debsign(std::path::Path::new(&source), None).map_err(|e| {
1044 log::warn!("Failed to sign changes file: {:?}", e);
1045 PrepareUploadError::SignError(e)
1046 })?;
1047 Ok((source.into(), Some(_tag_name)))
1048}
1049
1050pub fn process_package(
1052 apt_repo: &dyn Apt,
1053 package: &str,
1054 builder: &str,
1055 exclude: Option<&[String]>,
1056 autopkgtest_only: bool,
1057 gpg_verification: bool,
1058 acceptable_keys: Option<&[String]>,
1059 _debug: bool,
1060 diff: bool,
1061 min_commit_age: Option<i64>,
1062 allowed_committers: Option<&[String]>,
1063 vcs_type: Option<&str>,
1064 vcs_url: Option<&str>,
1065 source_name: Option<&str>,
1066 archive_version: Option<&debversion::Version>,
1067 verify_command: Option<&str>,
1068) -> Result<(), UploadPackageError> {
1069 let mut archive_version = archive_version.cloned();
1070 let mut source_name = source_name.map(|s| s.to_string());
1071 let mut vcs_type = vcs_type.map(|s| s.to_string());
1072 let mut vcs_url = vcs_url.map(|s| s.to_string());
1073 let exclude = exclude.unwrap_or(&[]);
1074 log::info!("Processing {}", package);
1075 let mut has_testsuite;
1077 if !package.contains('/') {
1078 let pkg_source = match crate::debian::apt_get_source_package(apt_repo, package) {
1079 Some(pkg_source) => pkg_source,
1080 None => {
1081 log::info!("{}: package not found in apt", package);
1082 return Err(UploadPackageError::ProcessingFailure(
1083 "not-in-apt".to_string(),
1084 Some("Package not found in apt".to_string()),
1085 ));
1086 }
1087 };
1088 if vcs_type.is_none() || vcs_url.is_none() {
1089 (vcs_type, vcs_url) = match debian_analyzer::vcs::vcs_field(&pkg_source) {
1090 Some((t, u)) => (Some(t), Some(u)),
1091 None => {
1092 log::info!(
1093 "{}: no declared vcs location, skipping",
1094 pkg_source.package().unwrap()
1095 );
1096 return Err(UploadPackageError::ProcessingFailure(
1097 "not-in-vcs".to_string(),
1098 Some("No declared vcs location".to_string()),
1099 ));
1100 }
1101 };
1102 }
1103 source_name = Some(source_name.unwrap_or_else(|| pkg_source.package().unwrap()));
1104 if exclude.contains(source_name.as_ref().unwrap()) {
1105 return Err(UploadPackageError::Ignored("excluded".to_string(), None));
1106 }
1107 archive_version = Some(archive_version.unwrap_or_else(|| pkg_source.version().unwrap()));
1108 has_testsuite = Some(pkg_source.testsuite().is_some());
1109 } else {
1110 vcs_url = Some(vcs_url.unwrap_or(package.to_owned()));
1111 has_testsuite = None;
1112 }
1113 let parsed_vcs: debian_control::vcs::ParsedVcs = vcs_url.as_ref().unwrap().parse().unwrap();
1114 let location: url::Url = parsed_vcs.repo_url.parse().unwrap();
1115 let branch_name = parsed_vcs.branch;
1116 let subpath = std::path::PathBuf::from(parsed_vcs.subpath.unwrap_or("".to_string()));
1117 let probers = crate::probers::select_probers(vcs_type.as_deref());
1118 let main_branch = match open_branch(
1119 &location,
1120 None,
1121 Some(
1122 probers
1123 .iter()
1124 .map(|p| p.as_ref())
1125 .collect::<Vec<_>>()
1126 .as_slice(),
1127 ),
1128 branch_name.as_deref(),
1129 ) {
1130 Ok(b) => b,
1131 Err(
1132 BranchOpenError::Unavailable { description, .. }
1133 | BranchOpenError::TemporarilyUnavailable { description, .. },
1134 ) => {
1135 log::info!(
1136 "{}: branch unavailable: {}",
1137 vcs_url.as_ref().unwrap(),
1138 description
1139 );
1140 return Err(UploadPackageError::ProcessingFailure(
1141 "vcs-inaccessible".to_string(),
1142 Some(format!("Unable to access vcs: {:?}", description)),
1143 ));
1144 }
1145 Err(BranchOpenError::RateLimited {
1146 url: _,
1147 description: _,
1148 retry_after,
1149 }) => {
1150 log::info!(
1151 "{}: rate limited by server (retrying after {})",
1152 vcs_url.unwrap(),
1153 retry_after.map_or("unknown".to_string(), |i| i.to_string())
1154 );
1155 return Err(UploadPackageError::ProcessingFailure(
1156 "rate-limited".to_string(),
1157 Some(format!(
1158 "Rate limited by server (retrying after {})",
1159 retry_after.map_or("unknown".to_string(), |i| i.to_string())
1160 )),
1161 ));
1162 }
1163 Err(BranchOpenError::Missing { description, .. }) => {
1164 log::info!("{}: branch not found: {}", vcs_url.unwrap(), description);
1165 return Err(UploadPackageError::ProcessingFailure(
1166 "vcs-inaccessible".to_string(),
1167 Some(format!("Unable to access vcs: {:?}", description)),
1168 ));
1169 }
1170 Err(BranchOpenError::Other(description)) => {
1171 log::info!(
1172 "{}: error opening branch: {}",
1173 vcs_url.unwrap(),
1174 description
1175 );
1176 return Err(UploadPackageError::ProcessingFailure(
1177 "vcs-error".to_string(),
1178 Some(format!("Unable to access vcs: {:?}", description)),
1179 ));
1180 }
1181 Err(BranchOpenError::Unsupported { description, .. }) => {
1182 log::info!("{}: branch not found: {}", vcs_url.unwrap(), description);
1183 return Err(UploadPackageError::ProcessingFailure(
1184 "vcs-unsupported".to_string(),
1185 Some(format!("Unable to access vcs: {:?}", description)),
1186 ));
1187 }
1188 };
1189 let mut ws_builder = crate::workspace::Workspace::builder();
1190 ws_builder = ws_builder.additional_colocated_branches(
1191 crate::debian::pick_additional_colocated_branches(&main_branch),
1192 );
1193 let ws = ws_builder.main_branch(main_branch).build().unwrap();
1194 if source_name.is_none() {
1195 let control_path = subpath.join("debian/control");
1196 let control_text = ws
1197 .local_tree()
1198 .get_file_text(control_path.as_path())
1199 .unwrap();
1200 let control = debian_control::Control::from_str(
1201 std::str::from_utf8(control_text.as_slice()).unwrap(),
1202 )
1203 .unwrap();
1204 let source_name = control.source().unwrap().name().unwrap();
1205 let pkg_source = match crate::debian::apt_get_source_package(apt_repo, &source_name) {
1206 Some(p) => p,
1207 None => {
1208 log::info!("{}: package not found in apt", package);
1209 return Err(UploadPackageError::ProcessingFailure(
1210 "not-in-apt".to_owned(),
1211 Some("Package not found in apt".to_owned()),
1212 ));
1213 }
1214 };
1215 archive_version = pkg_source.version();
1216 has_testsuite = Some(control.source().unwrap().testsuite().is_some());
1217 }
1218 let has_testsuite = has_testsuite.unwrap();
1219 let source_name = source_name.unwrap();
1220 if exclude.contains(&source_name) {
1221 return Err(UploadPackageError::Ignored("excluded".to_string(), None));
1222 }
1223 if autopkgtest_only
1224 && !has_testsuite
1225 && !ws
1226 .local_tree()
1227 .has_filename(&subpath.join("debian/tests/control"))
1228 {
1229 log::info!("{}: Skipping, package has no autopkgtest.", source_name);
1230 return Err(UploadPackageError::Ignored(
1231 "no-autopkgtest".to_owned(),
1232 None,
1233 ));
1234 }
1235 let branch_config = ws.local_tree().branch().get_config();
1236 let gpg_strategy = if gpg_verification {
1237 let gpg_strategy = breezyshim::gpg::GPGStrategy::new(&branch_config);
1238 let acceptable_keys = if let Some(acceptable_keys) = acceptable_keys {
1239 acceptable_keys.iter().map(|s| s.to_string()).collect()
1240 } else {
1241 #[cfg(feature = "gpg")]
1242 {
1243 let mut context = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp).unwrap();
1244 get_maintainer_keys(&mut context).unwrap()
1245 }
1246 #[cfg(not(feature = "gpg"))]
1247 {
1248 vec![]
1249 }
1250 };
1251 gpg_strategy.set_acceptable_keys(acceptable_keys.as_slice());
1252 Some(gpg_strategy)
1253 } else {
1254 None
1255 };
1256
1257 let (target_changes, tag_name) = match prepare_upload_package(
1258 ws.local_tree(),
1259 std::path::Path::new(&subpath),
1260 &source_name,
1261 archive_version.as_ref(),
1262 builder,
1263 gpg_strategy,
1264 min_commit_age,
1265 allowed_committers,
1266 Some(apt_repo),
1267 ) {
1268 Ok(r) => r,
1269 Err(PrepareUploadError::GbpDchFailed) => {
1270 log::warn!("{}: 'gbp dch' failed to run", source_name);
1271 return Err(UploadPackageError::ProcessingFailure(
1272 "gbp-dch-failed".to_string(),
1273 None,
1274 ));
1275 }
1276 Err(PrepareUploadError::MissingUpstreamTarball(package, version)) => {
1277 log::warn!(
1278 "{}: missing upstream tarball: {} {}",
1279 source_name,
1280 package,
1281 version
1282 );
1283 return Err(UploadPackageError::ProcessingFailure(
1284 "missing-upstream-tarball".to_string(),
1285 Some(format!("Missing upstream tarball: {} {}", package, version)),
1286 ));
1287 }
1288 Err(PrepareUploadError::Rejected(RevisionRejected::CommitterNotAllowed(
1289 committer,
1290 allowed_committers,
1291 ))) => {
1292 log::warn!(
1293 "{}: committer {} not in allowed list: {:?}",
1294 source_name,
1295 committer,
1296 allowed_committers,
1297 );
1298 return Err(UploadPackageError::Ignored(
1299 "committer-not-allowed".to_string(),
1300 Some(format!(
1301 "committer {} not in allowed list: {:?}",
1302 committer, allowed_committers
1303 )),
1304 ));
1305 }
1306 Err(PrepareUploadError::BuildFailed) => {
1307 log::warn!("{}: package failed to build", source_name);
1308 return Err(UploadPackageError::ProcessingFailure(
1309 "build-failed".to_string(),
1310 None,
1311 ));
1312 }
1313 Err(PrepareUploadError::LastReleaseRevisionNotFound(source_name, version)) => {
1314 log::warn!(
1315 "{}: Unable to find revision matching last release {}, skipping.",
1316 source_name,
1317 version,
1318 );
1319 return Err(UploadPackageError::ProcessingFailure(
1320 "last-release-missing".to_string(),
1321 Some(format!(
1322 "Unable to find revision matching last release {}",
1323 version
1324 )),
1325 ));
1326 }
1327 Err(PrepareUploadError::LastUploadMoreRecent(archive_version, vcs_version)) => {
1328 log::warn!(
1329 "{}: Last upload ({}) was more recent than VCS ({})",
1330 source_name,
1331 archive_version,
1332 vcs_version,
1333 );
1334 return Err(UploadPackageError::ProcessingFailure(
1335 "last-upload-not-in-vcs".to_string(),
1336 Some(format!(
1337 "Last upload ({}) was more recent than VCS ({})",
1338 archive_version, vcs_version
1339 )),
1340 ));
1341 }
1342 Err(PrepareUploadError::ChangelogParseError(reason)) => {
1343 log::info!("{}: Error parsing changelog: {}", source_name, reason);
1344 return Err(UploadPackageError::ProcessingFailure(
1345 "changelog-parse-error".to_string(),
1346 Some(reason),
1347 ));
1348 }
1349 Err(PrepareUploadError::MissingChangelog) => {
1350 log::info!("{}: No changelog found, skipping.", source_name);
1351 return Err(UploadPackageError::ProcessingFailure(
1352 "missing-changelog".to_string(),
1353 None,
1354 ));
1355 }
1356 Err(PrepareUploadError::GeneratedChangelogFile) => {
1357 log::info!(
1358 "{}: Changelog is generated and unable to update, skipping.",
1359 source_name,
1360 );
1361 return Err(UploadPackageError::ProcessingFailure(
1362 "generated-changelog".to_string(),
1363 None,
1364 ));
1365 }
1366 Err(PrepareUploadError::Rejected(RevisionRejected::RecentCommits(
1367 commit_age,
1368 _max_commit_age,
1369 ))) => {
1370 log::info!(
1371 "{}: Recent commits ({} days), skipping.",
1372 source_name,
1373 commit_age,
1374 );
1375 return Err(UploadPackageError::Ignored(
1376 "recent-commits".to_string(),
1377 Some(format!("Recent commits ({} days)", commit_age)),
1378 ));
1379 }
1380 Err(PrepareUploadError::NoUnuploadedChanges(_version)) => {
1381 log::info!("{}: No unuploaded changes, skipping.", source_name,);
1382 return Err(UploadPackageError::Ignored(
1383 "no-unuploaded-changes".to_string(),
1384 Some("No unuploaded changes".to_string()),
1385 ));
1386 }
1387 Err(PrepareUploadError::NoUnreleasedChanges(_version)) => {
1388 log::info!("{}: No unreleased changes, skipping.", source_name,);
1389 return Err(UploadPackageError::Ignored(
1390 "no-unreleased-changes".to_string(),
1391 Some("No unreleased changes".to_string()),
1392 ));
1393 }
1394 Err(PrepareUploadError::MissingNestedTree(_)) => {
1395 log::error!("{}: missing nested tree", source_name);
1396 return Err(UploadPackageError::ProcessingFailure(
1397 "missing-nested-tree".to_string(),
1398 None,
1399 ));
1400 }
1401 Err(PrepareUploadError::BrzError(e)) => {
1402 log::error!("{}: error: {:?}", source_name, e);
1403 return Err(UploadPackageError::ProcessingFailure(
1404 "vcs-error".to_string(),
1405 Some(format!("{:?}", e)),
1406 ));
1407 }
1408 Err(PrepareUploadError::DebianError(e)) => {
1409 log::error!("{}: error: {:?}", source_name, e);
1410 return Err(UploadPackageError::ProcessingFailure(
1411 "debian-error".to_string(),
1412 Some(format!("{:?}", e)),
1413 ));
1414 }
1415 Err(PrepareUploadError::NoValidGpgSignature(revid, _code)) => {
1416 log::info!(
1417 "{}: No valid GPG signature for revision {}",
1418 source_name,
1419 revid
1420 );
1421 return Err(UploadPackageError::ProcessingFailure(
1422 "no-valid-gpg-signature".to_string(),
1423 Some(format!("No valid GPG signature for revision {}", revid)),
1424 ));
1425 }
1426 Err(PrepareUploadError::PackageVersionNotPresent(package, version)) => {
1427 log::warn!(
1428 "{}: package version {} not present in repository",
1429 package,
1430 version
1431 );
1432 return Err(UploadPackageError::ProcessingFailure(
1433 "package-version-not-present".to_string(),
1434 Some(format!(
1435 "Package version {} not present in repository",
1436 version
1437 )),
1438 ));
1439 }
1440 Err(PrepareUploadError::SignError(e)) => {
1441 log::warn!("{}: Failed to sign changes file: {:?}", source_name, e);
1442 return Err(UploadPackageError::ProcessingFailure(
1443 "sign-error".to_string(),
1444 Some(format!("{:?}", e)),
1445 ));
1446 }
1447 };
1448
1449 if let Some(verify_command) = verify_command {
1450 match std::process::Command::new(verify_command)
1451 .arg(&target_changes)
1452 .status()
1453 {
1454 Ok(o) => {
1455 if o.code() == Some(1) {
1456 return Err(UploadPackageError::Ignored(
1457 "verify-command-declined".to_string(),
1458 Some(format!(
1459 "{}: Verify command {} declined upload",
1460 source_name, verify_command
1461 )),
1462 ));
1463 } else if o.code() != Some(0) {
1464 return Err(UploadPackageError::ProcessingFailure(
1465 "verify-command-error".to_string(),
1466 Some(format!(
1467 "{}: Error running verify command {}: returncode {}",
1468 source_name,
1469 verify_command,
1470 o.code().unwrap()
1471 )),
1472 ));
1473 }
1474 }
1475 Err(e) => {
1476 return Err(UploadPackageError::ProcessingFailure(
1477 "verify-command-error".to_string(),
1478 Some(format!(
1479 "{}: Error running verify command {}: {}",
1480 source_name, verify_command, e
1481 )),
1482 ));
1483 }
1484 }
1485 }
1486
1487 let mut tags = HashMap::new();
1488 if let Some(tag_name) = tag_name.as_ref() {
1489 log::info!("Pushing tag {}", tag_name);
1490 tags.insert(
1491 tag_name.to_string(),
1492 ws.local_tree()
1493 .branch()
1494 .tags()
1495 .unwrap()
1496 .lookup_tag(tag_name)
1497 .unwrap(),
1498 );
1499 }
1500 match ws.push(Some(tags)) {
1501 Ok(_) => {}
1502 Err(crate::workspace::Error::PermissionDenied(..)) => {
1503 log::info!(
1504 "{}: Permission denied pushing to branch, skipping.",
1505 source_name,
1506 );
1507 return Err(UploadPackageError::ProcessingFailure(
1508 "vcs-permission-denied".to_string(),
1509 None,
1510 ));
1511 }
1512 Err(e) => {
1513 log::error!("{}: Error pushing: {}", source_name, e);
1514 return Err(UploadPackageError::ProcessingFailure(
1515 "push-error".to_string(),
1516 Some(format!("{:?}", e)),
1517 ));
1518 }
1519 }
1520 dput_changes(&target_changes, None).map_err(|e| {
1521 log::error!("{}: Error uploading: {}", source_name, e);
1522 UploadPackageError::ProcessingFailure("upload-error".to_string(), Some(format!("{:?}", e)))
1523 })?;
1524 if diff {
1525 ws.show_diff(Box::new(std::io::stdout()), None, None)
1526 .unwrap();
1527 }
1528 std::mem::drop(ws);
1529 Ok(())
1530}