cargo_release/ops/
replace.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::config::Replace;
5use crate::error::CargoResult;
6
7pub static NOW: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
8    time::OffsetDateTime::now_utc()
9        .format(time::macros::format_description!("[year]-[month]-[day]"))
10        .unwrap()
11});
12
13#[derive(Clone, Default, Debug)]
14pub struct Template<'a> {
15    pub prev_version: Option<&'a str>,
16    pub prev_metadata: Option<&'a str>,
17    pub version: Option<&'a str>,
18    pub metadata: Option<&'a str>,
19    pub crate_name: Option<&'a str>,
20    pub date: Option<&'a str>,
21
22    pub prefix: Option<&'a str>,
23    pub tag_name: Option<&'a str>,
24}
25
26impl Template<'_> {
27    pub fn render(&self, input: &str) -> String {
28        const PREV_VERSION: &str = "{{prev_version}}";
29        const PREV_METADATA: &str = "{{prev_metadata}}";
30        const VERSION: &str = "{{version}}";
31        const METADATA: &str = "{{metadata}}";
32        const CRATE_NAME: &str = "{{crate_name}}";
33        const DATE: &str = "{{date}}";
34
35        const PREFIX: &str = "{{prefix}}";
36        const TAG_NAME: &str = "{{tag_name}}";
37
38        let mut s = input.to_owned();
39        s = render_var(s, PREV_VERSION, self.prev_version);
40        s = render_var(s, PREV_METADATA, self.prev_metadata);
41        s = render_var(s, VERSION, self.version);
42        s = render_var(s, METADATA, self.metadata);
43        s = render_var(s, CRATE_NAME, self.crate_name);
44        s = render_var(s, DATE, self.date);
45
46        s = render_var(s, PREFIX, self.prefix);
47        s = render_var(s, TAG_NAME, self.tag_name);
48        s
49    }
50}
51
52fn render_var(mut template: String, var_name: &str, var_value: Option<&str>) -> String {
53    if let Some(var_value) = var_value {
54        template = template.replace(var_name, var_value);
55    } else if template.contains(var_name) {
56        log::warn!("Unrendered {var_name} present in template {template:?}");
57    }
58    template
59}
60
61pub fn do_file_replacements(
62    replace_config: &[Replace],
63    template: &Template<'_>,
64    cwd: &Path,
65    prerelease: bool,
66    noisy: bool,
67    dry_run: bool,
68) -> CargoResult<bool> {
69    // Since we don't have a convenient insert-order map, let's do sorted, rather than random.
70    let mut by_file = BTreeMap::new();
71    for replace in replace_config {
72        let file = replace.file.clone();
73        by_file.entry(file).or_insert_with(Vec::new).push(replace);
74    }
75
76    for (path, replaces) in by_file {
77        let file = cwd.join(&path);
78        log::debug!("processing replacements for file {}", file.display());
79        if !file.exists() {
80            anyhow::bail!("unable to find file {} to perform replace", file.display());
81        }
82        let data = std::fs::read_to_string(&file)?;
83        let mut replaced = data.clone();
84
85        for replace in replaces {
86            if prerelease && !replace.prerelease {
87                log::debug!("pre-release, not replacing {}", replace.search);
88                continue;
89            }
90
91            let pattern = replace.search.as_str();
92            let r = regex::RegexBuilder::new(pattern).multi_line(true).build()?;
93
94            let min = replace.min.or(replace.exactly).unwrap_or(1);
95            let max = replace.max.or(replace.exactly).unwrap_or(usize::MAX);
96            let actual = r.find_iter(&replaced).count();
97            if actual < min {
98                anyhow::bail!(
99                    "for `{}` in '{}', at least {} replacements expected, found {}",
100                    pattern,
101                    path.display(),
102                    min,
103                    actual
104                );
105            } else if max < actual {
106                anyhow::bail!(
107                    "for `{}` in '{}', at most {} replacements expected, found {}",
108                    pattern,
109                    path.display(),
110                    max,
111                    actual
112                );
113            }
114
115            let to_replace = replace.replace.as_str();
116            let replacer = template.render(to_replace);
117
118            replaced = r.replace_all(&replaced, replacer.as_str()).into_owned();
119        }
120
121        if data != replaced {
122            if dry_run {
123                if noisy {
124                    let _ = crate::ops::shell::status(
125                        "Replacing",
126                        format!(
127                            "in {}\n{}",
128                            path.display(),
129                            crate::ops::diff::unified_diff(&data, &replaced, &path, "replaced")
130                        ),
131                    );
132                } else {
133                    let _ =
134                        crate::ops::shell::status("Replacing", format!("in {}", path.display()));
135                }
136            } else {
137                std::fs::write(&file, replaced)?;
138            }
139        } else {
140            log::trace!("{} is unchanged", file.display());
141        }
142    }
143    Ok(true)
144}