Skip to main content

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