Skip to main content

silver_platter/debian/
uploader.rs

1//! Upload packages to the Debian archive.
2use 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")]
21/// Database for storing the last upload attempt time for each package.
22pub struct LastAttemptDatabase {
23    db: tdb::Tdb,
24}
25
26#[cfg(feature = "last-attempt-db")]
27impl LastAttemptDatabase {
28    /// Open the last attempt database.
29    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    /// Get the last upload attempt time for a package.
43    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    /// Set the last upload attempt time for a package.
52    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    /// Set the last upload attempt time for a package to the current time.
59    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/// Errors that can occur when signing a package.
74#[derive(Debug)]
75pub enum SignError {
76    /// debsign failed
77    Failed(String),
78
79    /// I/O error
80    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
100/// debsign a changes file
101pub 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/// Errors that can occur when uploading a package.
120#[derive(Debug)]
121pub enum UploadError {
122    /// Upload failed
123    Failed(String),
124
125    /// I/O error
126    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
146/// dput a changes file
147pub 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")]
165/// Get the key IDs for Debian maintainers.
166pub 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)]
194/// Result of uploading a package.
195pub enum UploadPackageError {
196    /// Package was ignored.
197    Ignored(String, Option<String>),
198
199    /// Package processing failed.
200    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    // TODO(jelmer): check autopkgtest_only ?
225    // from debian.deb822 import Deb822
226    // pkg_source = Deb822(vw.controlfile)
227    // has_testsuite = "Testsuite" in pkg_source
228    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
409/// Errors that can occur when checking a revision.
410pub enum RevisionRejected {
411    /// The committer is not allowed.
412    CommitterNotAllowed(String, Vec<String>),
413
414    /// The commit is too recent.
415    RecentCommits(i64, i64),
416}
417
418/// Check whether a revision can be included in an upload.
419///
420/// # Arguments
421/// * `rev` - revision to check
422/// * `min_commit_age` - minimum age for revisions
423/// * `allowed_committers` - list of allowed committers
424fn check_revision(
425    rev: &dyn Revision,
426    min_commit_age: Option<i64>,
427    allowed_committers: Option<&[String]>,
428) -> Result<(), RevisionRejected> {
429    // TODO(jelmer): deal with timezone
430    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        // TODO(jelmer): Allow tag to prevent automatic uploads
443        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)]
467/// Struct for vcswatch entry
468struct VcswatchEntry {
469    /// Package name
470    package: String,
471
472    /// Control file
473    vcslog: Option<String>,
474
475    /// Number of commits
476    commits: usize,
477
478    /// Control file
479    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
562/// Select packages from the apt repository.
563fn 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
591/// Process a package for upload.
592pub 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        // Shuffle packages vec
648        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
744/// Errors that can occur when preparing a package for upload.
745pub enum PrepareUploadError {
746    /// Failed to run gbp dch
747    GbpDchFailed,
748
749    /// No unuploaded changes since the last upload
750    NoUnuploadedChanges(Version),
751
752    /// The last upload was more recent than the previous upload
753    LastUploadMoreRecent(Version, Version),
754
755    /// The last release revision was not found
756    LastReleaseRevisionNotFound(String, Version),
757
758    /// No unreleased changes
759    NoUnreleasedChanges(Version),
760
761    /// Generated changelog file
762    GeneratedChangelogFile,
763
764    /// No valid GPG signature
765    NoValidGpgSignature(RevisionId, VerificationResult),
766
767    /// Revision rejected
768    Rejected(RevisionRejected),
769
770    /// Build failed
771    BuildFailed,
772
773    /// Missing upstream tarball
774    MissingUpstreamTarball(String, String),
775
776    /// Package version not present
777    PackageVersionNotPresent(String, String),
778
779    /// Missing changelog
780    MissingChangelog,
781
782    /// Changelog parse error
783    ChangelogParseError(String),
784
785    /// Breezy error
786    BrzError(BrzError),
787
788    /// Debian error
789    DebianError(DebianError),
790
791    /// There is a missing nested tree
792    MissingNestedTree(std::path::PathBuf),
793
794    /// Sign error
795    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
807/// Prepare a package for upload.
808pub 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                // TODO(jelmer): gbp dch sometimes fails when there is no existing
840                // open changelog entry; it fails invoking
841                // "dpkg --lt None <old-version>"
842                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        // TODO(jelmer): Check whether this is a team upload
976        // TODO(jelmer): determine whether this is a NMU upload
977    }
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            // TODO: Use NullCommitReporter
995            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
1050/// Process a package for upload.
1051pub 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    // Can't use open_packaging_branch here, since we want to use pkg_source later on.
1076    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}