Skip to main content

cargo_uv/
task.rs

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    /// Unordered [Vec] of all task keys as [Task].
32    pub fn vec_keys(&self) -> Vec<Task> {
33        self.keys().cloned().collect()
34    }
35
36    /// [Vec<Task>] of incomplete tasks.
37    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    /// [Vec<Task>] of completed [Task].
46    ///
47    /// The underlying hashset can be accessed with [AsRef] and [AsMut]
48    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    /// [bool] if remaining tasks exist based on [self.incomplete_tasks].
64    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    /// Returns `true` if the task is [`DeleteGitTag`].
147    ///
148    /// [`DeleteGitTag`]: Task::DeleteGitTag
149    #[must_use]
150    pub fn is_delete_git_tag(&self) -> bool {
151        matches!(self, Self::DeleteGitTag(..))
152    }
153}
154
155/// TODO: Make a reference.
156impl 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                            // Task still going
252                            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// TODO: Add tests to tasks.