Skip to main content

debian_workbench/
publish.rs

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