Skip to main content

debian_workbench/
lib.rs

1//! Library for manipulating Debian packages.
2#![deny(missing_docs)]
3use breezyshim::branch::Branch;
4use breezyshim::dirty_tracker::DirtyTreeTracker;
5use breezyshim::error::Error;
6use breezyshim::tree::{PyTree, Tree, TreeChange, WorkingTree};
7use breezyshim::workingtree::PyWorkingTree;
8use breezyshim::workspace::reset_tree_with_dirty_tracker;
9
10pub mod abstract_control;
11pub mod changelog;
12pub mod config;
13pub mod control;
14pub mod debcargo;
15pub mod debcommit;
16pub mod debhelper;
17pub mod detect_gbp_dch;
18pub mod editor;
19pub mod lintian;
20pub mod maintscripts;
21pub mod patches;
22pub mod publish;
23pub mod relations;
24pub mod release_info;
25pub mod rules;
26pub mod vcs;
27pub mod vendor;
28pub mod versions;
29
30// TODO(jelmer): Import this from ognibuild
31/// Default builder
32pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source";
33
34#[derive(Debug)]
35/// Error applying a change
36pub enum ApplyError<R, E> {
37    /// Error from the callback
38    CallbackError(E),
39    /// Error from the tree
40    BrzError(Error),
41    /// No changes made
42    NoChanges(R),
43}
44
45impl<R, E> From<Error> for ApplyError<R, E> {
46    fn from(e: Error) -> Self {
47        ApplyError::BrzError(e)
48    }
49}
50
51/// Apply a change in a clean tree.
52///
53/// This will either run a callback in a tree, or if the callback fails,
54/// revert the tree to the original state.
55///
56/// The original tree should be clean; you can run check_clean_tree() to
57/// verify this.
58///
59/// # Arguments
60/// * `local_tree` - Local tree
61/// * `subpath` - Subpath to apply changes to
62/// * `basis_tree` - Basis tree to reset to
63/// * `dirty_tracker` - Dirty tracker
64/// * `applier` - Callback to apply changes
65///
66/// # Returns
67/// * `Result<(R, Vec<TreeChange>, Option<Vec<std::path::PathBuf>>), E>` - Result of the callback,
68///   the changes made, and the files that were changed
69pub fn apply_or_revert<R, E, T, U>(
70    local_tree: &T,
71    subpath: &std::path::Path,
72    basis_tree: &U,
73    dirty_tracker: Option<&mut DirtyTreeTracker>,
74    applier: impl FnOnce(&std::path::Path) -> Result<R, E>,
75) -> Result<(R, Vec<TreeChange>, Option<Vec<std::path::PathBuf>>), ApplyError<R, E>>
76where
77    T: PyWorkingTree + breezyshim::tree::PyMutableTree,
78    U: PyTree,
79{
80    let r = match applier(local_tree.abspath(subpath).unwrap().as_path()) {
81        Ok(r) => r,
82        Err(e) => {
83            reset_tree_with_dirty_tracker(
84                local_tree,
85                Some(basis_tree),
86                Some(subpath),
87                dirty_tracker,
88            )
89            .unwrap();
90            return Err(ApplyError::CallbackError(e));
91        }
92    };
93
94    let specific_files = if let Some(relpaths) = dirty_tracker.and_then(|x| x.relpaths()) {
95        let mut relpaths: Vec<_> = relpaths.into_iter().collect();
96        relpaths.sort();
97        // Sort paths so that directories get added before the files they
98        // contain (on VCSes where it matters)
99        local_tree.add(
100            relpaths
101                .iter()
102                .filter_map(|p| {
103                    if local_tree.has_filename(p) && local_tree.is_ignored(p).is_some() {
104                        Some(p.as_path())
105                    } else {
106                        None
107                    }
108                })
109                .collect::<Vec<_>>()
110                .as_slice(),
111        )?;
112        let specific_files = relpaths
113            .into_iter()
114            .filter(|p| local_tree.is_versioned(p))
115            .collect::<Vec<_>>();
116        if specific_files.is_empty() {
117            return Err(ApplyError::NoChanges(r));
118        }
119        Some(specific_files)
120    } else {
121        local_tree.smart_add(&[local_tree.abspath(subpath).unwrap().as_path()])?;
122        if subpath.as_os_str().is_empty() {
123            None
124        } else {
125            Some(vec![subpath.to_path_buf()])
126        }
127    };
128
129    if local_tree.supports_setting_file_ids() {
130        let local_lock = local_tree.lock_read().unwrap();
131        let basis_lock = basis_tree.lock_read().unwrap();
132        breezyshim::rename_map::guess_renames(basis_tree, local_tree).unwrap();
133        std::mem::drop(basis_lock);
134        std::mem::drop(local_lock);
135    }
136
137    let specific_files_ref = specific_files
138        .as_ref()
139        .map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
140
141    let changes = local_tree
142        .iter_changes(
143            basis_tree,
144            specific_files_ref.as_deref(),
145            Some(false),
146            Some(true),
147        )?
148        .collect::<Result<Vec<_>, _>>()?;
149
150    if local_tree.get_parent_ids()?.len() <= 1 && changes.is_empty() {
151        return Err(ApplyError::NoChanges(r));
152    }
153
154    Ok((r, changes, specific_files))
155}
156
157/// A changelog error
158pub enum ChangelogError {
159    /// Not a Debian package
160    NotDebianPackage(std::path::PathBuf),
161}
162
163impl std::fmt::Display for ChangelogError {
164    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
165        match self {
166            ChangelogError::NotDebianPackage(path) => {
167                write!(f, "Not a Debian package: {}", path.display())
168            }
169        }
170    }
171}
172
173/// Add an entry to a changelog.
174///
175/// # Arguments
176/// * `working_tree` - Working tree
177/// * `changelog_path` - Path to the changelog
178/// * `entry` - Changelog entry
179pub fn add_changelog_entry<T: WorkingTree>(
180    working_tree: &T,
181    changelog_path: &std::path::Path,
182    entry: &[&str],
183) -> Result<(), crate::editor::EditorError> {
184    use crate::editor::{Editor, MutableTreeEdit};
185    let mut cl =
186        working_tree.edit_file::<debian_changelog::ChangeLog>(changelog_path, false, true)?;
187
188    cl.try_auto_add_change(
189        entry,
190        debian_changelog::get_maintainer().unwrap(),
191        Some(chrono::Utc::now().fixed_offset()),
192        None,
193    )
194    .unwrap();
195
196    cl.commit()?;
197
198    Ok(())
199}
200
201#[derive(
202    Clone,
203    Copy,
204    PartialEq,
205    Eq,
206    Debug,
207    Default,
208    PartialOrd,
209    Ord,
210    serde::Serialize,
211    serde::Deserialize,
212)]
213/// Certainty of a change.
214pub enum Certainty {
215    #[serde(rename = "possible")]
216    /// Possible change, basically a guess
217    Possible,
218    #[serde(rename = "likely")]
219    /// Likely to be correct, but not certain
220    Likely,
221    #[serde(rename = "confident")]
222    /// Confident change, but not absolutely certain
223    Confident,
224    #[default]
225    #[serde(rename = "certain")]
226    /// Absolutely certain change
227    Certain,
228}
229
230impl std::str::FromStr for Certainty {
231    type Err = String;
232
233    fn from_str(value: &str) -> Result<Self, Self::Err> {
234        match value {
235            "certain" => Ok(Certainty::Certain),
236            "confident" => Ok(Certainty::Confident),
237            "likely" => Ok(Certainty::Likely),
238            "possible" => Ok(Certainty::Possible),
239            _ => Err(format!("Invalid certainty: {}", value)),
240        }
241    }
242}
243
244impl std::fmt::Display for Certainty {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self {
247            Certainty::Certain => write!(f, "certain"),
248            Certainty::Confident => write!(f, "confident"),
249            Certainty::Likely => write!(f, "likely"),
250            Certainty::Possible => write!(f, "possible"),
251        }
252    }
253}
254
255/// Check if the actual certainty is sufficient.
256///
257/// # Arguments
258///
259/// * `actual_certainty` - Actual certainty with which changes were made
260/// * `minimum_certainty` - Minimum certainty to keep changes
261///
262/// # Returns
263///
264/// * `bool` - Whether the actual certainty is sufficient
265pub fn certainty_sufficient(
266    actual_certainty: Certainty,
267    minimum_certainty: Option<Certainty>,
268) -> bool {
269    if let Some(minimum_certainty) = minimum_certainty {
270        actual_certainty >= minimum_certainty
271    } else {
272        true
273    }
274}
275
276/// Return the minimum certainty from a list of certainties.
277pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
278    certainties.iter().min().cloned()
279}
280
281/// Get the committer string for a tree
282pub fn get_committer(working_tree: &dyn PyWorkingTree) -> String {
283    if let Some(committer) = breezyshim::git::get_committer(working_tree) {
284        return committer;
285    }
286
287    let config = working_tree.branch().get_config_stack();
288
289    config
290        .get("email")
291        .unwrap()
292        .map(|x| x.to_string())
293        .unwrap_or_default()
294}
295
296/// Check whether there are any control files present in a tree.
297///
298/// # Arguments
299///
300///   * `tree`: tree to check
301///   * `subpath`: subpath to check
302///
303/// # Returns
304///
305/// whether control file is present
306pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
307    for name in [
308        "debian/control",
309        "debian/control.in",
310        "control",
311        "control.in",
312        "debian/debcargo.toml",
313    ] {
314        let name = subpath.join(name);
315        if tree.has_filename(name.as_path()) {
316            return true;
317        }
318    }
319    false
320}
321
322/// Check whether the package in a tree uses debcargo.
323pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
324    tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
325}
326
327/// Check whether the package in a tree has control files in the root, rather than in debian/.
328pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
329    let debian_path = subpath.join("debian");
330    if tree.has_filename(debian_path.as_path()) {
331        return false;
332    }
333
334    let control_path = subpath.join("control");
335    if tree.has_filename(control_path.as_path()) {
336        return true;
337    }
338
339    tree.has_filename(subpath.join("control.in").as_path())
340}
341
342/// Parse a Debian name and email address from a string.
343pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
344    if let Some((_whole, name, addr)) =
345        lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
346    {
347        let name = match name.trim() {
348            "" => None,
349            x => Some(x.to_string()),
350        };
351        let addr = match addr.trim() {
352            "" => None,
353            x => Some(x.to_string()),
354        };
355
356        return Some((name, addr));
357    } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
358        let addr = Some(addr.trim().to_string());
359
360        return Some((None, addr));
361    } else if input.is_empty() {
362        return None;
363    } else if !input.contains('<') {
364        return Some((None, Some(input.to_string())));
365    }
366    None
367}
368
369/// Run gbp dch
370pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
371    let mut cmd = std::process::Command::new("gbp");
372    cmd.arg("dch").arg("--ignore-branch");
373    cmd.current_dir(path);
374    let status = cmd.status()?;
375    if !status.success() {
376        return Err(std::io::Error::other(format!("gbp dch failed: {}", status)));
377    }
378    Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use serial_test::serial;
385
386    #[test]
387    fn test_parseaddr() {
388        assert_eq!(
389            parseaddr("foo <bar@example.com>").unwrap(),
390            (Some("foo".to_string()), Some("bar@example.com".to_string()))
391        );
392        assert_eq!(parseaddr("foo").unwrap(), (None, Some("foo".to_string())));
393    }
394
395    #[serial]
396    #[test]
397    fn test_git_env() {
398        let td = tempfile::tempdir().unwrap();
399        let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
400
401        let old_name = std::env::var("GIT_COMMITTER_NAME").ok();
402        let old_email = std::env::var("GIT_COMMITTER_EMAIL").ok();
403
404        std::env::set_var("GIT_COMMITTER_NAME", "Some Git Committer");
405        std::env::set_var("GIT_COMMITTER_EMAIL", "committer@example.com");
406
407        let committer = get_committer(&cd);
408
409        if let Some(old_name) = old_name {
410            std::env::set_var("GIT_COMMITTER_NAME", old_name);
411        } else {
412            std::env::remove_var("GIT_COMMITTER_NAME");
413        }
414
415        if let Some(old_email) = old_email {
416            std::env::set_var("GIT_COMMITTER_EMAIL", old_email);
417        } else {
418            std::env::remove_var("GIT_COMMITTER_EMAIL");
419        }
420
421        assert_eq!("Some Git Committer <committer@example.com>", committer);
422    }
423
424    #[serial]
425    #[test]
426    fn test_git_config() {
427        let td = tempfile::tempdir().unwrap();
428        let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
429
430        std::fs::write(
431            td.path().join(".git/config"),
432            b"[user]\nname = Some Git Committer\nemail = other@example.com",
433        )
434        .unwrap();
435
436        assert_eq!(get_committer(&cd), "Some Git Committer <other@example.com>");
437    }
438
439    #[test]
440    fn test_min_certainty() {
441        assert_eq!(None, min_certainty(&[]));
442        assert_eq!(
443            Some(Certainty::Certain),
444            min_certainty(&[Certainty::Certain])
445        );
446        assert_eq!(
447            Some(Certainty::Possible),
448            min_certainty(&[Certainty::Possible])
449        );
450        assert_eq!(
451            Some(Certainty::Possible),
452            min_certainty(&[Certainty::Possible, Certainty::Certain])
453        );
454        assert_eq!(
455            Some(Certainty::Likely),
456            min_certainty(&[Certainty::Likely, Certainty::Certain])
457        );
458        assert_eq!(
459            Some(Certainty::Possible),
460            min_certainty(&[Certainty::Likely, Certainty::Certain, Certainty::Possible])
461        );
462    }
463}