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