intelli_shell/process/
changelog.rs1use color_eyre::Result;
2use crossterm::style::{Attribute, Stylize};
3use tokio_util::sync::CancellationToken;
4use tracing::instrument;
5
6use crate::{
7 cli::ChangelogProcess,
8 config::Config,
9 errors::AppError,
10 format_error,
11 process::{Process, ProcessOutput},
12 service::{CURRENT_VERSION, IntelliShellService},
13 utils::{VersionExt, render_markdown_to_ansi},
14};
15
16impl Process for ChangelogProcess {
17 #[instrument(skip_all)]
18 async fn execute(
19 self,
20 config: Config,
21 service: IntelliShellService,
22 cancellation_token: CancellationToken,
23 ) -> Result<ProcessOutput> {
24 let from_tag = self.from.to_tag();
25
26 if let Some(ref to) = self.to
28 && &self.from > to
29 {
30 return Ok(ProcessOutput::fail().stderr(format_error!(
31 config.theme,
32 "Invalid criteria: from ({}) > to ({})",
33 from_tag.cyan(),
34 to.to_tag().cyan()
35 )));
36 }
37
38 let all_releases = match service.get_or_fetch_releases(false, cancellation_token).await {
40 Ok(r) => r,
41 Err(AppError::UserFacing(err)) => {
42 return Ok(ProcessOutput::fail().stderr(format_error!(config.theme, "{err}")));
43 }
44 Err(AppError::Unexpected(report)) => return Err(report),
45 };
46
47 if let Some(ref to) = self.to
49 && !all_releases.iter().any(|r| &r.version >= to)
50 {
51 return Ok(ProcessOutput::fail().stderr(format_error!(
52 config.theme,
53 "It looks like {} hasn't been released yet! \nYou can omit the '--to' flag to see all available \
54 releases up to the latest.",
55 to.to_tag().red()
56 )));
57 }
58
59 let filtered_releases = all_releases
61 .iter()
62 .filter(|r| {
63 if r.version < self.from {
64 return false;
65 }
66 if let Some(ref t) = self.to
67 && &r.version > t
68 {
69 return false;
70 }
71 true
72 })
73 .collect::<Vec<_>>();
74
75 if filtered_releases.is_empty() {
77 return Ok(
78 ProcessOutput::fail().stderr(format_error!(config.theme, "No releases found matching the criteria"))
79 );
80 }
81
82 let filtered_releases = filtered_releases
84 .into_iter()
85 .filter(|r| {
86 if r.version < self.from {
87 return false;
88 }
89 if let Some(ref t) = self.to
90 && &r.version > t
91 {
92 return false;
93 }
94 if self.major && (r.version.minor != 0 || r.version.patch != 0) {
95 return false;
96 }
97 if self.minor && r.version.patch != 0 {
98 return false;
99 }
100 true
101 })
102 .collect::<Vec<_>>();
103
104 if filtered_releases.is_empty() {
106 let filter_type = match (self.major, self.minor) {
107 (true, _) => "major",
108 (_, true) => "minor",
109 _ => "relevant",
110 };
111
112 let msg = match self.to {
113 Some(to_ver) => format!(
114 "⚠️ No {} releases found between {} and {}",
115 filter_type,
116 from_tag.cyan(),
117 to_ver.to_tag().cyan()
118 ),
119 None => format!("⚠️ No {} releases found after {}", filter_type, from_tag.cyan()),
120 };
121
122 return Ok(ProcessOutput::success().stderr(msg));
123 }
124
125 if let Some(oldest_release) = all_releases.last()
127 && self.from < oldest_release.version
128 {
129 eprintln!(
130 "⚠️ {} {}",
131 config.theme.error.apply(from_tag.red()),
132 config
133 .theme
134 .primary
135 .apply("is too old, please check GitHub Releases to view full changelog.")
136 );
137 } else if !all_releases.iter().any(|r| r.version == self.from) {
138 eprintln!(
139 "⚠️ {} {}",
140 config.theme.error.apply(from_tag.red()),
141 config
142 .theme
143 .primary
144 .apply("doesn't exist, but here's the changelog for newer releases.")
145 );
146 }
147
148 let changelog = filtered_releases.iter().rev().fold(String::new(), |mut acc, r| {
150 let mut title = r.title.as_str();
151 let mut body = r.body.as_deref().unwrap_or("").trim_end();
152
153 if r.title == r.tag && body.starts_with("## ") {
155 if let Some((first_line, rest)) = body.split_once('\n') {
156 title = first_line.trim_start_matches("## ").trim();
157 body = rest.trim();
158 } else {
159 title = body.trim_start_matches("## ").trim();
160 body = "";
161 }
162 }
163
164 let is_current = r.version == *CURRENT_VERSION;
165 let is_current_mark = if is_current { " (current)" } else { "" };
166 let header = if title != r.tag {
167 format!("{}{} - {}", r.tag, is_current_mark, title)
168 } else {
169 format!("{}{}", r.tag, is_current_mark)
170 };
171
172 let line_width = 60usize;
173 let line_len = line_width.saturating_sub(header.len() + 4);
174 let line = "─".repeat(line_len);
175
176 let mut style = if is_current {
177 config.theme.highlight_accent_full()
178 } else {
179 config.theme.highlight_primary_full()
180 };
181 style.attributes.set(Attribute::Bold);
182
183 acc.push_str(&style.apply(&format!("── {header} {line}")).to_string());
184 acc.push_str("\n\n");
185
186 if !body.is_empty() {
187 acc.push_str(&render_markdown_to_ansi(body, &config.theme));
188 acc.push_str("\n\n");
189 }
190 acc
191 });
192
193 Ok(ProcessOutput::success().stdout(format!("\n{}\n", changelog.trim_end())))
194 }
195}