1use crate::output::VersionVariables;
9use anyhow::{bail, Context, Result};
10use regex::Regex;
11use rust_i18n::t;
12use std::collections::BTreeMap;
13use std::path::Path;
14use std::process::{Command, Stdio};
15
16pub const HOOK_ORDER: [&str; 4] = ["verify", "prepare", "publish", "success"];
18
19fn render(cmd: &str, map: &BTreeMap<String, String>) -> String {
21 let re = Regex::new(r"\{(?<t>[A-Za-z0-9_:]+)\}").unwrap();
22 re.replace_all(cmd, |c: ®ex::Captures| {
23 let t = &c["t"];
24 if let Some(env_var) = t.strip_prefix("env:") {
25 std::env::var(env_var).unwrap_or_default()
26 } else if let Some(v) = map.get(t) {
27 v.clone()
28 } else {
29 format!("{{{t}}}") }
31 })
32 .into_owned()
33}
34
35fn env_vars(vars: &VersionVariables) -> Vec<(String, String)> {
37 vars.to_map()
38 .into_iter()
39 .map(|(k, v)| (format!("GitVersion_{k}"), v))
40 .collect()
41}
42
43fn cargo_env_vars(vars: &VersionVariables) -> Vec<(String, String)> {
49 vec![
50 ("CARGO_PKG_VERSION".into(), vars.sem_ver.clone()),
51 ("CARGO_PKG_VERSION_MAJOR".into(), vars.major.to_string()),
52 ("CARGO_PKG_VERSION_MINOR".into(), vars.minor.to_string()),
53 ("CARGO_PKG_VERSION_PATCH".into(), vars.patch.to_string()),
54 ("CARGO_PKG_VERSION_PRE".into(), vars.pre_release_tag.clone()),
55 ]
56}
57
58fn run_command(
60 cmd: &str,
61 vars: &VersionVariables,
62 work_dir: &Path,
63 capture: bool,
64 dry_run: bool,
65) -> Result<Option<String>> {
66 let rendered = render(cmd, &vars.to_map());
67 if dry_run {
68 log::info!("{}", t!("exec.dry_run", cmd = rendered));
69 eprintln!("[dry-run] {rendered}");
70 return Ok(None);
71 }
72 log::info!("{}", t!("exec.running", cmd = rendered));
73
74 let (program, flag) = if cfg!(windows) {
75 ("cmd", "/C")
76 } else {
77 ("sh", "-c")
78 };
79 let mut command = Command::new(program);
80 command
81 .arg(flag)
82 .arg(&rendered)
83 .current_dir(work_dir)
84 .envs(env_vars(vars))
85 .envs(cargo_env_vars(vars));
86 if capture {
87 command.stdout(Stdio::piped()).stderr(Stdio::inherit());
88 }
89
90 if capture {
91 let output = command
92 .output()
93 .with_context(|| t!("exec.run_failed", cmd = rendered))?;
94 if !output.status.success() {
95 bail!(
96 "{}",
97 t!(
98 "exec.cmd_failed",
99 code = format!("{:?}", output.status.code()),
100 cmd = rendered
101 )
102 );
103 }
104 Ok(Some(String::from_utf8_lossy(&output.stdout).into_owned()))
105 } else {
106 let status = command
107 .status()
108 .with_context(|| t!("exec.run_failed", cmd = rendered))?;
109 if !status.success() {
110 bail!(
111 "{}",
112 t!(
113 "exec.cmd_failed",
114 code = format!("{:?}", status.code()),
115 cmd = rendered
116 )
117 );
118 }
119 Ok(None)
120 }
121}
122
123pub fn run_version_hook(
126 cmd: &str,
127 vars: &VersionVariables,
128 work_dir: &Path,
129 dry_run: bool,
130) -> Result<Option<String>> {
131 let out = run_command(cmd, vars, work_dir, true, dry_run)?;
132 Ok(out.and_then(|s| {
133 s.lines()
134 .map(str::trim)
135 .find(|l| !l.is_empty())
136 .map(String::from)
137 }))
138}
139
140pub fn run_hooks(
144 hooks: &BTreeMap<String, String>,
145 extra_prepare: Option<&str>,
146 vars: &VersionVariables,
147 work_dir: &Path,
148 dry_run: bool,
149) -> Result<()> {
150 let mut result = Ok(());
151 'outer: for &name in &HOOK_ORDER {
152 if let Some(cmd) = hooks.get(name) {
153 if let Err(e) = run_command(cmd, vars, work_dir, false, dry_run) {
154 result = Err(e.context(t!("exec.hook_failed", name = name).to_string()));
155 break 'outer;
156 }
157 }
158 if name == "prepare" {
159 if let Some(cmd) = extra_prepare {
160 if let Err(e) = run_command(cmd, vars, work_dir, false, dry_run) {
161 result = Err(e.context(t!("exec.exec_prepare_failed").to_string()));
162 break 'outer;
163 }
164 }
165 }
166 }
167
168 if result.is_err() {
169 if let Some(fail_cmd) = hooks.get("fail") {
170 log::warn!("{}", t!("exec.running_fail_hook"));
171 let _ = run_command(fail_cmd, vars, work_dir, false, dry_run);
172 }
173 }
174 result
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn render_substitutes_and_preserves() {
183 let mut m = BTreeMap::new();
184 m.insert("SemVer".to_string(), "1.2.3".to_string());
185 assert_eq!(render("echo {SemVer}", &m), "echo 1.2.3");
186 assert_eq!(render("echo {Unknown}", &m), "echo {Unknown}");
188 assert_eq!(render("echo $HOME {SemVer}", &m), "echo $HOME 1.2.3");
190 }
191
192 #[test]
193 fn cargo_env_vars_mirror_cargo_names() {
194 let vars = VersionVariables {
195 major: 1,
196 minor: 2,
197 patch: 3,
198 sem_ver: "1.2.3-alpha.4".into(),
199 pre_release_tag: "alpha.4".into(),
200 ..Default::default()
201 };
202 let map: BTreeMap<_, _> = cargo_env_vars(&vars).into_iter().collect();
203 assert_eq!(map["CARGO_PKG_VERSION"], "1.2.3-alpha.4");
204 assert_eq!(map["CARGO_PKG_VERSION_MAJOR"], "1");
205 assert_eq!(map["CARGO_PKG_VERSION_MINOR"], "2");
206 assert_eq!(map["CARGO_PKG_VERSION_PATCH"], "3");
207 assert_eq!(map["CARGO_PKG_VERSION_PRE"], "alpha.4");
208 }
209}