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