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