Skip to main content

changepacks_cli/commands/
changepacks.rs

1use changepacks_core::{ChangePackLog, Project, UpdateType};
2use std::{collections::HashMap, path::PathBuf};
3use tokio::fs::write;
4
5use changepacks_utils::{get_changepacks_dir, get_relative_path};
6
7use anyhow::Result;
8
9use crate::{
10    CommandContext,
11    options::FilterOptions,
12    prompter::{InquirePrompter, Prompter},
13};
14
15#[derive(Debug)]
16pub struct ChangepackArgs {
17    pub filter: Option<FilterOptions>,
18    pub remote: bool,
19    pub yes: bool,
20    pub message: Option<String>,
21    pub update_type: Option<UpdateType>,
22}
23
24/// # Errors
25/// Returns error if command context creation or changepack creation fails.
26pub async fn handle_changepack(args: &ChangepackArgs) -> Result<()> {
27    handle_changepack_with_prompter(args, &InquirePrompter).await
28}
29
30/// # Errors
31/// Returns error if project discovery, prompting, or changepack file creation fails.
32pub async fn handle_changepack_with_prompter(
33    args: &ChangepackArgs,
34    prompter: &dyn Prompter,
35) -> Result<()> {
36    let ctx = CommandContext::new(args.remote).await?;
37
38    let mut projects = ctx
39        .project_finders
40        .iter()
41        .flat_map(|finder| finder.projects())
42        .collect::<Vec<_>>();
43
44    // Hide packages that inherit their version from workspace root.
45    // They are updated automatically when the workspace version bumps.
46    projects.retain(|p| {
47        if let Project::Package(pkg) = p {
48            !pkg.inherits_workspace_version()
49        } else {
50            true
51        }
52    });
53
54    if let Some(filter) = &args.filter {
55        projects.retain(|p| filter.matches(p));
56    }
57
58    println!("Found {} projects", projects.len());
59    // workspace first
60    projects.sort();
61
62    let mut update_map = HashMap::<PathBuf, UpdateType>::new();
63
64    for update_type in if let Some(update_type) = &args.update_type {
65        vec![*update_type]
66    } else {
67        vec![UpdateType::Major, UpdateType::Minor, UpdateType::Patch]
68    } {
69        if projects.is_empty() {
70            break;
71        }
72
73        let selected_projects = if args.yes {
74            projects.clone()
75        } else if update_type == UpdateType::Patch && projects.len() == 1 {
76            vec![projects[0]]
77        } else {
78            let message = format!("Select projects to update for {update_type}");
79            let defaults = projects
80                .iter()
81                .enumerate()
82                .filter_map(|(index, project)| {
83                    if project.is_changed() {
84                        Some(index)
85                    } else {
86                        None
87                    }
88                })
89                .collect::<Vec<_>>();
90            prompter.multi_select(&message, projects.clone(), defaults)?
91        };
92
93        // remove selected projects from projects by index
94        for project in selected_projects {
95            update_map.insert(
96                get_relative_path(&ctx.repo_root_path, project.path())?,
97                update_type,
98            );
99        }
100
101        let project_with_relpath: Vec<_> = projects
102            .iter()
103            .map(|project| {
104                get_relative_path(&ctx.repo_root_path, project.path()).map(|rel| (project, rel))
105            })
106            .collect::<Result<Vec<_>>>()?;
107
108        let keep_projects: Vec<_> = project_with_relpath
109            .into_iter()
110            .filter(|(_, rel_path)| !update_map.contains_key(rel_path))
111            .map(|(project, _)| *project)
112            .collect();
113
114        projects = keep_projects;
115    }
116
117    if update_map.is_empty() {
118        println!("No projects selected");
119        return Ok(());
120    }
121
122    let notes = if let Some(message) = &args.message {
123        message.clone()
124    } else {
125        prompter.text("write notes here")?
126    };
127
128    if notes.is_empty() {
129        println!("Notes are empty");
130        return Ok(());
131    }
132    let changepack_log = ChangePackLog::new(update_map, notes);
133    // random uuid
134    let changepack_log_id = nanoid::nanoid!();
135    let changepack_log_file = get_changepacks_dir(&CommandContext::current_dir()?)?
136        .join(format!("changepack_log_{changepack_log_id}.json"));
137    write(changepack_log_file, serde_json::to_string(&changepack_log)?).await?;
138
139    Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_changepack_args_debug() {
148        let args = ChangepackArgs {
149            filter: None,
150            remote: false,
151            yes: true,
152            message: Some("Test".to_string()),
153            update_type: Some(UpdateType::Patch),
154        };
155
156        // Test Debug trait
157        let debug_str = format!("{:?}", args);
158        assert!(debug_str.contains("ChangepackArgs"));
159    }
160
161    #[test]
162    fn test_changepack_args_with_filter() {
163        let args = ChangepackArgs {
164            filter: Some(FilterOptions::Package),
165            remote: true,
166            yes: false,
167            message: None,
168            update_type: None,
169        };
170
171        assert!(args.filter.is_some());
172        assert!(args.remote);
173        assert!(!args.yes);
174        assert!(args.message.is_none());
175        assert!(args.update_type.is_none());
176    }
177
178    #[test]
179    fn test_changepack_args_workspace_filter() {
180        let args = ChangepackArgs {
181            filter: Some(FilterOptions::Workspace),
182            remote: false,
183            yes: true,
184            message: Some("msg".to_string()),
185            update_type: Some(UpdateType::Major),
186        };
187
188        assert!(matches!(args.filter, Some(FilterOptions::Workspace)));
189        assert!(matches!(args.update_type, Some(UpdateType::Major)));
190    }
191
192    #[test]
193    fn test_changepack_args_minor_update() {
194        let args = ChangepackArgs {
195            filter: None,
196            remote: false,
197            yes: true,
198            message: Some("feature".to_string()),
199            update_type: Some(UpdateType::Minor),
200        };
201
202        assert!(matches!(args.update_type, Some(UpdateType::Minor)));
203    }
204}