Skip to main content

gen_changelog/
package.rs

1use std::{
2    collections::BTreeMap,
3    path::{Path, PathBuf},
4};
5
6use cargo_toml::Manifest;
7use lazy_regex::{Lazy, Regex, lazy_regex};
8
9use crate::Error;
10
11/// Regular expression pattern for update rust crate commits.
12///
13/// Captures named groups:
14/// - `crate`: The name of the crate updated
15static CRATE: Lazy<Regex> = lazy_regex!(r"^.+update rust crate (?P<crate>[\w-]+).+$");
16
17/// A RustPackage structure that contains key data from the rust package
18/// manifest.
19///
20/// A RustPackage consists of:
21/// - the root of the package source relative to the workspace
22/// - a list of the dependencies used by the package in the workspace
23#[derive(Debug, Clone, Default)]
24pub struct RustPackage {
25    pub root: String,
26    pub dependencies: Vec<String>,
27}
28
29impl RustPackage {
30    fn new(root: String, dependencies: Vec<String>) -> RustPackage {
31        RustPackage { root, dependencies }
32    }
33
34    /// Test to determine if a commit is related to the rust package
35    ///
36    /// # Returns
37    ///
38    /// - true or false
39    ///
40    /// # Inputs
41    ///
42    /// - subject - the subject line of the commit
43    /// - files_in_commit - a list of the files changed in the commit
44    ///
45    /// # Tests
46    ///
47    /// ## Update to dependency
48    ///
49    /// True if the `subject` indicates an update to a crate on which this
50    /// package depends.
51    ///
52    /// ## Files in the package
53    ///
54    /// True if any of the files changed in the commit are located in the
55    /// package's directory tree.
56    pub fn is_related_to_package(&self, subject: &str, files_in_commit: Vec<PathBuf>) -> bool {
57        if self.is_update_to_package_dependency(subject) {
58            if !log::log_enabled!(log::Level::Debug) {
59                log::debug!("Commit is an update to a dependency");
60            }
61            if !log::log_enabled!(log::Level::Trace) {
62                log::trace!("Commit is an update to a dependency ({subject})");
63            }
64            return true;
65        }
66
67        if self.is_commit_to_package_file(files_in_commit) {
68            if !log::log_enabled!(log::Level::Debug) {
69                log::debug!("File in package directory tree");
70            }
71            if !log::log_enabled!(log::Level::Trace) {
72                log::trace!("File in package directory tree ({})", self.root);
73            }
74            return true;
75        }
76
77        log::trace!(
78            "Commit is not related to the {} package",
79            self.root
80                .split('/')
81                .next_back()
82                .unwrap_or("***not found***")
83        );
84        false
85    }
86
87    fn is_update_to_package_dependency(&self, subject: &str) -> bool {
88        if let Some(caps) = CRATE.captures(subject) {
89            self.dependencies.iter().any(|d| *d == caps[1])
90        } else {
91            false
92        }
93    }
94
95    fn is_commit_to_package_file(&self, files_in_commit: Vec<PathBuf>) -> bool {
96        let filter = &self.root;
97
98        for file in files_in_commit {
99            log::debug!("Test `{}`", file.display());
100
101            if file.display().to_string().starts_with(filter) {
102                return true;
103            }
104        }
105
106        false
107    }
108}
109
110/// A RustPackage structure that contains key data from the rust package
111/// manifest.
112///
113/// A RustPackage consists of:
114/// - the root of the package source relative to the workspace
115/// - a list of the dependencies used by the package in the workspace
116///
117/// # Example
118#[derive(Debug, Clone)]
119pub struct RustPackages {
120    /// BTreeMap of the packages in the workspace by name
121    pub packages_by_name: BTreeMap<String, RustPackage>,
122}
123
124impl RustPackages {
125    /// Create a RustPackages struct gathering the required data from the
126    /// packages that are members of the workspace to create a `[RustPackage]`
127    /// and collect into a `[BTreeMap]` identified by name.
128    pub fn new(root: &Path) -> Result<RustPackages, Error> {
129        log::debug!("getting the rust packages in the repository");
130        let mut packages = BTreeMap::new();
131        log::debug!("Starting from the root `{}`", root.display());
132
133        // Expecting the Cargo.toml in the root to be a workspace
134        let ws_toml = root.join("Cargo.toml");
135        let ws = Manifest::from_path(ws_toml)?;
136        if let Some(workspace) = ws.workspace {
137            for member in workspace.members {
138                let pkg_root = root.join(&member);
139                let pkg_toml = pkg_root.join("Cargo.toml");
140                let pkg_manifest = Manifest::from_path(pkg_toml)?;
141
142                // Expecting to have a package to process and add to the list of packages
143                if let Some(pkg) = pkg_manifest.package {
144                    let mut dependencies: Vec<String> =
145                        pkg_manifest.dependencies.keys().cloned().collect();
146                    dependencies
147                        .append(&mut pkg_manifest.dev_dependencies.keys().cloned().collect());
148                    dependencies
149                        .append(&mut pkg_manifest.build_dependencies.keys().cloned().collect());
150
151                    let rust_package = RustPackage::new(member, dependencies);
152
153                    packages.insert(pkg.name, rust_package);
154                }
155            }
156        }
157
158        Ok(RustPackages {
159            packages_by_name: packages,
160        })
161    }
162}