release_kit/release_engine/
cliff.rs1use camino::Utf8Path;
12
13use crate::diagnostic::{Diagnostic, Reason};
14use crate::error::RkError;
15
16fn run(root: &Utf8Path, args: &[&str]) -> Result<String, RkError> {
18 let mut command = std::process::Command::new(crate::probes::git_cliff_bin());
19 for var in crate::maintenance::GIT_HOOK_VARS {
20 command.env_remove(var);
21 }
22 let output = command
23 .args(args)
24 .current_dir(root.as_std_path())
25 .output()
26 .map_err(|source| {
27 RkError::refusal(
28 Diagnostic::new(
29 Reason::PrerequisiteUnmet,
30 format!("git-cliff did not run: {source}"),
31 )
32 .expected("git-cliff on PATH at the version versions.toml pins")
33 .action("install git-cliff at its pin, or put it in the recorded development environment")
34 .target_state("unchanged"),
35 )
36 })?;
37 if !output.status.success() {
38 return Err(RkError::subprocess(
39 Diagnostic::new(
40 Reason::SubprocessFailed,
41 format!(
42 "git-cliff {} failed: {}",
43 args.join(" "),
44 super::last_line(&output.stderr)
45 ),
46 )
47 .expected("git-cliff to read the landed cliff.toml and the history"),
48 ));
49 }
50 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
51}
52
53#[must_use]
55pub fn pin() -> Option<String> {
56 crate::registry::version_of("git-cliff")
57}
58
59pub fn require_pin(root: &Utf8Path) -> Result<String, RkError> {
66 let pinned = pin().unwrap_or_default();
67 let answer = run(root, &["--version"])?;
68 let found = crate::probes::parse_cli_version(&answer);
69 let wanted = crate::probes::parse_cli_version(&pinned);
70 if found.is_none() || found != wanted {
71 return Err(RkError::refusal(
72 Diagnostic::new(
73 Reason::ToolOffPin,
74 format!(
75 "git-cliff reports {:?}, and versions.toml pins {pinned}",
76 answer.trim()
77 ),
78 )
79 .expected(format!("git-cliff {pinned}"))
80 .action(format!(
81 "install git-cliff {pinned}, or move the recorded development environment to it"
82 ))
83 .target_state("unchanged"),
84 ));
85 }
86 Ok(pinned)
87}
88
89fn include(paths: &[String]) -> Vec<String> {
92 paths
93 .iter()
94 .flat_map(|path| {
95 [
96 "--include-path".to_owned(),
97 super::packed::cliff_pattern(path),
98 ]
99 })
100 .collect()
101}
102
103fn run_on(root: &Utf8Path, args: &[&str], paths: &[String]) -> Result<String, RkError> {
105 let filter = include(paths);
106 let mut all: Vec<&str> = args.to_vec();
107 all.extend(filter.iter().map(String::as_str));
108 run(root, &all)
109}
110
111pub fn bumped(root: &Utf8Path, paths: &[String]) -> Result<String, RkError> {
119 let answer = run_on(root, &["--bumped-version"], paths)?;
120 Ok(answer
121 .lines()
122 .rev()
123 .find(|line| !line.trim().is_empty())
124 .unwrap_or_default()
125 .trim()
126 .to_owned())
127}
128
129pub fn changelog(root: &Utf8Path, tag: &str, paths: &[String]) -> Result<(), RkError> {
140 let write = if root.join("CHANGELOG.md").exists() {
141 "--prepend"
142 } else {
143 "-o"
144 };
145 run_on(
146 root,
147 &["--unreleased", "--tag", tag, write, "CHANGELOG.md"],
148 paths,
149 )
150 .map(drop)
151}
152
153pub fn notes(root: &Utf8Path, tag: &str, out: &Utf8Path, paths: &[String]) -> Result<(), RkError> {
160 run_on(
161 root,
162 &["--unreleased", "--tag", tag, "-o", out.as_str()],
163 paths,
164 )
165 .map(drop)
166}