debian_analyzer/
lib.rs

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