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 run_command(
45 cmd: &str,
46 vars: &VersionVariables,
47 work_dir: &Path,
48 capture: bool,
49 dry_run: bool,
50) -> Result<Option<String>> {
51 let rendered = render(cmd, &vars.to_map());
52 if dry_run {
53 log::info!("{}", t!("exec.dry_run", cmd = rendered));
54 eprintln!("[dry-run] {rendered}");
55 return Ok(None);
56 }
57 log::info!("{}", t!("exec.running", cmd = rendered));
58
59 let (program, flag) = if cfg!(windows) {
60 ("cmd", "/C")
61 } else {
62 ("sh", "-c")
63 };
64 let mut command = Command::new(program);
65 command
66 .arg(flag)
67 .arg(&rendered)
68 .current_dir(work_dir)
69 .envs(env_vars(vars));
70 if capture {
71 command.stdout(Stdio::piped()).stderr(Stdio::inherit());
72 }
73
74 if capture {
75 let output = command
76 .output()
77 .with_context(|| t!("exec.run_failed", cmd = rendered))?;
78 if !output.status.success() {
79 bail!(
80 "{}",
81 t!(
82 "exec.cmd_failed",
83 code = format!("{:?}", output.status.code()),
84 cmd = rendered
85 )
86 );
87 }
88 Ok(Some(String::from_utf8_lossy(&output.stdout).into_owned()))
89 } else {
90 let status = command
91 .status()
92 .with_context(|| t!("exec.run_failed", cmd = rendered))?;
93 if !status.success() {
94 bail!(
95 "{}",
96 t!(
97 "exec.cmd_failed",
98 code = format!("{:?}", status.code()),
99 cmd = rendered
100 )
101 );
102 }
103 Ok(None)
104 }
105}
106
107pub fn run_version_hook(
110 cmd: &str,
111 vars: &VersionVariables,
112 work_dir: &Path,
113 dry_run: bool,
114) -> Result<Option<String>> {
115 let out = run_command(cmd, vars, work_dir, true, dry_run)?;
116 Ok(out.and_then(|s| {
117 s.lines()
118 .map(str::trim)
119 .find(|l| !l.is_empty())
120 .map(String::from)
121 }))
122}
123
124pub fn run_hooks(
128 hooks: &BTreeMap<String, String>,
129 extra_prepare: Option<&str>,
130 vars: &VersionVariables,
131 work_dir: &Path,
132 dry_run: bool,
133) -> Result<()> {
134 let mut result = Ok(());
135 'outer: for &name in &HOOK_ORDER {
136 if let Some(cmd) = hooks.get(name) {
137 if let Err(e) = run_command(cmd, vars, work_dir, false, dry_run) {
138 result = Err(e.context(t!("exec.hook_failed", name = name).to_string()));
139 break 'outer;
140 }
141 }
142 if name == "prepare" {
143 if let Some(cmd) = extra_prepare {
144 if let Err(e) = run_command(cmd, vars, work_dir, false, dry_run) {
145 result = Err(e.context(t!("exec.exec_prepare_failed").to_string()));
146 break 'outer;
147 }
148 }
149 }
150 }
151
152 if result.is_err() {
153 if let Some(fail_cmd) = hooks.get("fail") {
154 log::warn!("{}", t!("exec.running_fail_hook"));
155 let _ = run_command(fail_cmd, vars, work_dir, false, dry_run);
156 }
157 }
158 result
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn render_substitutes_and_preserves() {
167 let mut m = BTreeMap::new();
168 m.insert("SemVer".to_string(), "1.2.3".to_string());
169 assert_eq!(render("echo {SemVer}", &m), "echo 1.2.3");
170 assert_eq!(render("echo {Unknown}", &m), "echo {Unknown}");
172 assert_eq!(render("echo $HOME {SemVer}", &m), "echo $HOME 1.2.3");
174 }
175}