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