cargo_release/ops/
replace.rs1use 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 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}