Skip to main content

update_version/parsers/
mod.rs

1use anyhow::Result;
2use log::{debug, info};
3use semver::Version;
4use std::path::{Path, PathBuf};
5use thiserror::Error;
6
7pub mod package_json_parser;
8pub mod tauri_config_parser;
9pub mod toml_parser;
10
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum ParsingError {
14    #[error("No versions found in directory: {0}")]
15    NoVersionFoundError(String),
16}
17
18/// Options controlling how directory walking behaves with respect to ignore files.
19#[derive(Debug, Clone, Default)]
20pub struct WalkOptions {
21    /// When `true`, disables all ignore file processing (.gitignore, .uvignore, etc.).
22    /// When `false` (default), ignore files are respected.
23    pub no_ignore: bool,
24}
25
26pub trait Parser {
27    fn update_version(
28        path: impl AsRef<Path>,
29        version: &Version,
30        options: &WalkOptions,
31    ) -> Result<Vec<PathBuf>> {
32        info!("Updating version to {}", version);
33        let files = Self::get_matching_files(path, options)?;
34        let version_regex = Self::version_match_regex()?;
35        for file in &files {
36            debug!("Checking file: '{}'", file.display());
37            let contents = std::fs::read_to_string(file)?;
38            let new_contents = version_regex
39                .replace(contents.as_str(), Self::version_line_format(version)?)
40                .to_string();
41            std::fs::write(file, new_contents)?;
42        }
43        Ok(files)
44    }
45    fn increment_version(path: impl AsRef<Path>, options: &WalkOptions) -> Result<Vec<PathBuf>> {
46        let path = path.as_ref();
47        let current_version = Self::get_current_version(path, options)?;
48        let mut new_version = current_version.clone();
49        if current_version.pre.is_empty() {
50            new_version.patch += 1;
51        } else {
52            new_version.pre = semver::Prerelease::EMPTY;
53        }
54        new_version.build = semver::BuildMetadata::EMPTY;
55        debug!(
56            "Incrementing version from {} -> {}",
57            current_version, new_version
58        );
59        Self::update_version(path, &new_version, options)
60    }
61    fn get_current_version(path: impl AsRef<Path>, options: &WalkOptions) -> Result<Version> {
62        let path = path.as_ref();
63        let files = Self::get_matching_files(path, options)?;
64        let version_regex = Self::version_match_regex()?;
65
66        for file in files {
67            let contents = std::fs::read_to_string(file)?;
68            if let Some(captures) = version_regex.captures(contents.as_str())
69                && let Some(version) = captures.get(2)
70            {
71                let version = version.as_str();
72                debug!("Found current version: {}", version);
73                return Ok(Version::parse(version)?);
74            }
75        }
76
77        Err(ParsingError::NoVersionFoundError(path.to_string_lossy().to_string()).into())
78    }
79
80    fn get_matching_files(path: impl AsRef<Path>, options: &WalkOptions) -> Result<Vec<PathBuf>> {
81        debug!("Checking matching files");
82        let mut files: Vec<PathBuf> = vec![];
83        let path = path.as_ref();
84        let filename_regex = Self::filename_match_regex()?;
85
86        let mut builder = ignore::WalkBuilder::new(path);
87
88        if options.no_ignore {
89            // Disable ignore file processing but keep hidden file filtering
90            // so .git/ and other hidden directories are still skipped
91            builder.git_ignore(false);
92            builder.git_global(false);
93            builder.git_exclude(false);
94        } else {
95            builder.add_custom_ignore_filename(".uvignore");
96        }
97
98        for item in builder.build() {
99            let item = item?;
100            let path = item.path();
101            if filename_regex.is_match(path.to_string_lossy().as_ref()) {
102                files.push(path.to_path_buf());
103            }
104        }
105
106        // Sort by path depth (shallowest first) then lexicographically for deterministic ordering
107        files.sort_by(|a, b| {
108            a.components().count().cmp(&b.components().count())
109                .then_with(|| a.cmp(b))
110        });
111
112        debug!("Found files: {:?}", files);
113        Ok(files)
114    }
115
116    fn version_match_regex() -> Result<regex::Regex>;
117    fn filename_match_regex() -> Result<regex::Regex>;
118    fn version_line_format(version: &Version) -> Result<String>;
119}