debian_analyzer/
publish.rs

1//! Publishing utilities for updating Vcs-* headers and creating VCS repositories.
2use crate::salsa::guess_repository_url;
3use crate::vcs::determine_browser_url;
4use crate::{get_committer, parseaddr};
5use debian_control::control::Source;
6
7use breezyshim::error::Error as BrzError;
8use breezyshim::forge::create_project;
9use breezyshim::tree::WorkingTree;
10use breezyshim::workspace::check_clean_tree;
11use debian_control::vcs::ParsedVcs;
12use std::path::Path;
13use url::Url;
14
15/// Update the Vcs-* headers in the control file for the given source package.
16pub fn update_control_for_vcs_url(
17    source: &mut Source,
18    vcs_type: breezyshim::foreign::VcsType,
19    vcs_url: &str,
20) {
21    source.as_mut_deb822().insert(
22        match vcs_type {
23            breezyshim::foreign::VcsType::Git => "Vcs-Git",
24            breezyshim::foreign::VcsType::Bazaar => "Vcs-Bzr",
25            breezyshim::foreign::VcsType::Svn => "Vcs-Svn",
26            breezyshim::foreign::VcsType::Hg => "Vcs-Hg",
27            breezyshim::foreign::VcsType::Cvs => "Vcs-Cvs",
28            breezyshim::foreign::VcsType::Darcs => "Vcs-Darcs",
29            breezyshim::foreign::VcsType::Fossil => "Vcs-Fossil",
30            breezyshim::foreign::VcsType::Arch => "Vcs-Arch",
31            breezyshim::foreign::VcsType::Svk => "Vcs-Svk",
32        },
33        vcs_url,
34    );
35    if let Some(url) = determine_browser_url("git", vcs_url, None) {
36        source.as_mut_deb822().insert("Vcs-Browser", url.as_ref());
37    } else {
38        source.as_mut_deb822().remove("Vcs-Browser");
39    }
40}
41
42/// Create a VCS repository for the given source package.
43pub fn create_vcs_url(repo_url: &Url, summary: Option<&str>) -> Result<(), BrzError> {
44    match create_project(repo_url.as_str(), summary) {
45        Ok(()) => {
46            log::info!("Created {}", repo_url);
47            Ok(())
48        }
49        Err(BrzError::ForgeProjectExists(..)) | Err(BrzError::AlreadyControlDir(..)) => {
50            log::debug!("{} already exists", repo_url);
51            Ok(())
52        }
53        Err(e) => Err(e),
54    }
55}
56
57/// Error type for the publish module.
58#[derive(Debug, Clone)]
59pub enum Error {
60    /// No Vcs-* location specified.
61    NoVcsLocation,
62    /// File not found.
63    FileNotFound(std::path::PathBuf),
64    /// Conflicting Vcs-* location already specified.
65    ConflictingVcsAlreadySpecified(String, String, String),
66}
67
68impl std::fmt::Display for Error {
69    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
70        use Error::*;
71        match self {
72            NoVcsLocation => write!(f, "No Vcs-* location specified"),
73            FileNotFound(path) => write!(f, "File not found: {}", path.display()),
74            ConflictingVcsAlreadySpecified(_vcs_type, existing_url, new_url) => write!(
75                f,
76                "Conflicting Vcs-* location already specified: {} vs {}",
77                existing_url, new_url
78            ),
79        }
80    }
81}
82
83/// Update the official VCS location for the given source package.
84pub fn update_official_vcs(
85    wt: &WorkingTree,
86    subpath: &Path,
87    repo_url: Option<&Url>,
88    branch: Option<&str>,
89    committer: Option<&str>,
90    force: Option<bool>,
91) -> Result<ParsedVcs, Error> {
92    let force = force.unwrap_or(false);
93    // TODO(jelmer): Allow creation of the repository as well
94    check_clean_tree(wt, &wt.basis_tree().unwrap(), subpath).unwrap();
95
96    let debian_path = subpath.join("debian");
97    let subpath = match subpath.to_string_lossy().as_ref() {
98        "" | "." => None,
99        _ => Some(subpath.to_path_buf()),
100    };
101    let control_path = debian_path.join("control");
102
103    let editor = match crate::control::TemplatedControlEditor::open(&control_path) {
104        Ok(e) => e,
105        Err(crate::editor::EditorError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
106            return Err(Error::FileNotFound(control_path));
107        }
108        Err(e) => panic!("Failed to open control file: {:?}", e),
109    };
110    let mut source = editor.source().unwrap();
111
112    if let Some(package_vcs) = crate::vcs::source_package_vcs(&source) {
113        let vcs_type = package_vcs.type_str();
114        let existing: ParsedVcs = package_vcs.clone().into();
115        let actual = ParsedVcs {
116            repo_url: repo_url.unwrap().to_string(),
117            branch: branch.map(|s| s.to_string()),
118            subpath: subpath.map(|p| p.to_string_lossy().to_string()),
119        };
120        if existing != actual && !force {
121            return Err(Error::ConflictingVcsAlreadySpecified(
122                vcs_type.to_owned(),
123                existing.to_string(),
124                actual.to_string(),
125            ));
126        }
127        log::debug!("Using existing URL {}", existing);
128        return Ok(existing);
129    }
130    let maintainer_email = parseaddr(source.maintainer().unwrap().as_str())
131        .unwrap()
132        .1
133        .unwrap();
134    let source_name = source.name().unwrap();
135    let mut repo_url = repo_url.map(|u| u.to_owned());
136    if repo_url.is_none() {
137        repo_url = guess_repository_url(source_name.as_str(), maintainer_email.as_str());
138    }
139    let repo_url = match repo_url {
140        Some(url) => url,
141        None => {
142            return Err(Error::NoVcsLocation);
143        }
144    };
145    log::info!("Using repository URL: {}", repo_url);
146    let branch = wt.branch();
147
148    let branch_name = match branch.vcs_type() {
149        breezyshim::foreign::VcsType::Git => Some("debian/main"),
150        _ => None,
151    };
152
153    let vcs_url = ParsedVcs {
154        repo_url: repo_url.to_string(),
155        branch: branch_name.map(|s| s.to_string()),
156        subpath: subpath.map(|p| p.to_string_lossy().to_string()),
157    };
158    update_control_for_vcs_url(&mut source, branch.vcs_type(), &vcs_url.to_string());
159    let parsed_vcs = vcs_url.clone();
160
161    let committer = committer.map_or_else(|| get_committer(wt), |s| s.to_string());
162
163    match wt
164        .build_commit()
165        .message("Set Vcs headers.")
166        .allow_pointless(false)
167        .committer(committer.as_str())
168        .commit()
169    {
170        Ok(_) | Err(BrzError::PointlessCommit) => {}
171        Err(e) => {
172            panic!("Failed to commit: {:?}", e);
173        }
174    }
175
176    Ok(parsed_vcs)
177}