1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Display,
4 process::{Child, ExitStatus, Output},
5};
6
7use semver::{BuildMetadata, Prerelease, Version};
8use tracing::{info, instrument};
9
10use crate::{Action, OutputExt, Package, ReadToml, Result, current_span};
11
12#[derive(Debug, Default)]
13pub struct Tasks {
14 tasks: HashMap<Task, Option<Child>>,
15 completed: HashSet<Task>,
16}
17
18impl Tasks {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 #[instrument(skip(self))]
24 pub fn append(&mut self, tasks: Vec<(Task, Child)>) {
25 for (task, child) in tasks {
26 tracing::debug!("Adding {task:?} to tasks");
27 self.insert(task, Some(child));
28 }
29 }
30
31 pub fn vec_keys(&self) -> Vec<Task> {
33 self.keys().cloned().collect()
34 }
35
36 pub fn incomplete_tasks(&self) -> Vec<Task> {
38 self.tasks
39 .keys()
40 .filter(|&k| !self.completed.contains(k))
41 .cloned()
42 .collect()
43 }
44
45 pub fn completed_tasks(&self) -> Vec<Task> {
49 self.completed.iter().cloned().collect()
50 }
51
52 pub fn complete_task(&mut self, task: &Task) -> bool {
53 self.completed.insert(task.clone())
54 }
55
56 pub fn all_tasks_but_delete_tag(&self) -> Vec<Task> {
57 self.tasks
58 .keys()
59 .filter(|&k| !k.is_delete_git_tag())
60 .cloned()
61 .collect()
62 }
63 pub fn remaining_tasks(&self) -> bool {
65 !self.incomplete_tasks().is_empty()
66 }
67
68 pub fn remaining_tasks_left(&self) -> usize {
69 self.incomplete_tasks().len()
70 }
71
72 pub fn version_change_tasks(&self) -> Vec<Task> {
73 self.tasks
74 .keys()
75 .filter(|&k| !self.completed.contains(k) && k.is_version_change())
76 .cloned()
77 .collect()
78 }
79
80 pub fn delete_tag(&self) -> Option<&Task> {
81 self.tasks.keys().find(|k| k.is_delete_git_tag())
82 }
83}
84
85#[derive(Hash, PartialEq, Debug, Eq, Clone)]
86pub enum Task {
87 Push(String),
88 Publish,
89 Print,
90 Tree,
91 Set {
92 version: Version,
93 package: Package<ReadToml>,
94 },
95 Bump {
96 package: Package<ReadToml>,
97 bump: Action,
98 pre: Option<Prerelease>,
99 build: Option<BuildMetadata>,
100 force: bool,
101 },
102 BumpWorkspace {
103 bump: Action,
104 pre: Option<Prerelease>,
105 build: Option<BuildMetadata>,
106 force: bool,
107 },
108 SetWorkspace {
109 version: Version,
110 },
111 DeleteGitTag(Version),
112}
113
114impl Display for Task {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 let text = match self {
117 Task::Push(remote) => &format!("Push to {remote}"),
118 Task::Publish => "Publish",
119 Task::Print => "Print",
120 Task::Tree => "Tree",
121 Task::Set { version, package } => {
122 &format!("Set {}: {}", package.name(), version.to_string())
123 }
124 Task::Bump { package, bump, .. } => &format!("Bump {bump}: {}", package.name()),
125 Task::BumpWorkspace { bump, .. } => &format!("Bump Workspace Package: {}", bump),
126 Task::SetWorkspace { version } => &format!("Set Workspace: {}", version.to_string()),
127 Task::DeleteGitTag(version) => &format!("Delete Git Tag: {}", version.to_string()),
128 };
129 write!(f, "{}", text)
130 }
131}
132
133impl Task {
134 pub fn is_version_change(&self) -> bool {
135 match self {
136 Task::Push(_) | Task::Publish | Task::Print | Task::DeleteGitTag(_) | Task::Tree => {
137 false
138 }
139 Task::Set { .. }
140 | Task::Bump { .. }
141 | Task::BumpWorkspace { .. }
142 | Task::SetWorkspace { .. } => true,
143 }
144 }
145
146 #[must_use]
150 pub fn is_delete_git_tag(&self) -> bool {
151 matches!(self, Self::DeleteGitTag(..))
152 }
153}
154
155impl Task {
157 pub fn from_action(
158 action: Action,
159 package: Package<ReadToml>,
160 pre: Option<Prerelease>,
161 build: Option<BuildMetadata>,
162 new_version: Option<Version>,
163 force: bool,
164 ) -> Result<Task> {
165 match action {
166 Action::Pre | Action::Patch | Action::Minor | Action::Major => Ok(Task::Bump {
167 package: package,
168 bump: action,
169 pre,
170 build,
171 force,
172 }),
173 Action::Set => Ok(Task::Set {
174 version: new_version.ok_or(miette::miette!(
175 "Expected a new version for Task::from_action when action is Set"
176 ))?,
177 package,
178 }),
179 Action::Print => Ok(Task::Tree),
180 Action::Tree => Ok(Task::Print),
181 }
182 }
183}
184
185impl Task {
186 pub fn run(&mut self) -> Option<Child> {
187 None
188 }
189}
190
191impl std::ops::Deref for Tasks {
192 type Target = HashMap<Task, Option<Child>>;
193
194 fn deref(&self) -> &Self::Target {
195 &self.tasks
196 }
197}
198
199impl std::ops::DerefMut for Tasks {
200 fn deref_mut(&mut self) -> &mut Self::Target {
201 &mut self.tasks
202 }
203}
204
205impl AsRef<HashMap<Task, Option<Child>>> for Tasks {
206 fn as_ref(&self) -> &HashMap<Task, Option<Child>> {
207 &self.tasks
208 }
209}
210
211impl AsMut<HashMap<Task, Option<Child>>> for Tasks {
212 fn as_mut(&mut self) -> &mut HashMap<Task, Option<Child>> {
213 &mut self.tasks
214 }
215}
216
217impl AsRef<HashSet<Task>> for Tasks {
218 fn as_ref(&self) -> &HashSet<Task> {
219 &self.completed
220 }
221}
222
223impl AsMut<HashSet<Task>> for Tasks {
224 fn as_mut(&mut self) -> &mut HashSet<Task> {
225 &mut self.completed
226 }
227}
228
229impl Tasks {
230 #[allow(clippy::result_large_err)]
231 #[instrument(skip_all, fields(remaining_tasks), name = "Tasks::join_all")]
232 pub fn join_all(mut self) -> miette::Result<Tasks, TaskError> {
233 tracing::debug!("Starting to join tasks: {}", self.remaining_tasks_left());
234 let span = current_span!();
235 while self.remaining_tasks() {
236 'tasks: for task in self.incomplete_tasks() {
237 let child_option = match self.get_mut(&task) {
238 Some(c) => c,
239 None => {
240 span.record("remaining_tasks", self.remaining_tasks_left());
241 tracing::info!("No child process existed for: {}", task);
242 self.completed.insert(task);
243 continue 'tasks;
244 }
245 };
246
247 let exit_status = if let Some(child) = child_option {
248 match child.try_wait() {
249 Ok(Some(exit_status)) => exit_status,
250 Ok(None) => {
251 continue 'tasks;
253 }
254 Err(e) => {
255 span.record("remaining_tasks", self.remaining_tasks_left());
256 let msg = format!("Error occured while running {task:?}: {}", e);
257 tracing::error!(msg);
258 return Err(TaskError::from_tasks(self, task, None, ""));
259 }
260 }
261 } else {
262 span.record("remaining_tasks", self.remaining_tasks_left());
263 tracing::info!("No child process existed for: {}", task);
264 self.completed.insert(task);
265 continue 'tasks;
266 };
267 let output = child_option
268 .take()
269 .expect("Already contuned if none.")
270 .wait_with_output()
271 .expect("Already checked in try_wait.");
272
273 if !exit_status.success() {
274 let msg = format!(
275 "{task:?} exited with code: {:?}",
276 output.status.code().unwrap_or_default()
277 );
278 span.record("remaining_tasks", self.remaining_tasks_left());
279 tracing::error!("{msg}");
280 return Err(TaskError::from_tasks(self, task, Some(output), msg));
281 }
282 self.completed.insert(task.clone());
283 span.record("remaining_tasks", self.remaining_tasks_left());
284 tracing::info!("{task:?} Complete");
285 }
286 }
287
288 span.record("remaining_tasks", self.remaining_tasks_left());
289 assert!(
290 self.all_tasks_but_delete_tag().len() == self.completed_tasks().len(),
291 "Tasks is not equal to completed tasks"
292 );
293 info!("All {} task/s complete!", self.completed_tasks().len());
294 Ok(self)
295 }
296}
297
298impl TaskError {
299 pub fn from_tasks(
300 tasks: Tasks,
301 errored_task: Task,
302 output: Option<Output>,
303 msg: impl Into<String>,
304 ) -> Self {
305 Self {
306 completed_tasks: tasks.completed_tasks(),
307 incomplete_tasks: tasks.incomplete_tasks(),
308 errored_task,
309 output: output
310 .as_ref()
311 .map(|o| o.stderr())
312 .unwrap_or("Unknown Ourput".into()),
313 status_code: output.as_ref().map(|o| o.status),
314 msg: msg.into(),
315 }
316 }
317}
318
319#[derive(Debug, thiserror::Error, miette::Diagnostic)]
320#[error("{output}")]
321#[diagnostic(code(TaskError))]
322pub struct TaskError {
323 pub completed_tasks: Vec<Task>,
324 pub incomplete_tasks: Vec<Task>,
325 pub errored_task: Task,
326 pub output: String,
327 pub status_code: Option<ExitStatus>,
328 #[help]
329 pub msg: String,
330}
331
332