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::{
6    find_current_git_repo, find_project_dirs, get_changepacks_config, get_changepacks_dir,
7    get_relative_path,
8};
9
10use anyhow::{Context, Result};
11
12use crate::{
13    finders::get_finders,
14    options::FilterOptions,
15    prompter::{InquirePrompter, Prompter},
16};
17
18#[derive(Debug)]
19pub struct ChangepackArgs {
20    pub filter: Option<FilterOptions>,
21    pub remote: bool,
22    pub yes: bool,
23    pub message: Option<String>,
24    pub update_type: Option<UpdateType>,
25}
26
27pub async fn handle_changepack(args: &ChangepackArgs) -> Result<()> {
28    handle_changepack_with_prompter(args, &InquirePrompter).await
29}
30
31pub async fn handle_changepack_with_prompter(
32    args: &ChangepackArgs,
33    prompter: &dyn Prompter,
34) -> Result<()> {
35    let mut project_finders = get_finders();
36    let current_dir = std::env::current_dir()?;
37
38    // collect all projects
39    let repo = find_current_git_repo(&current_dir)?;
40    let repo_root_path = repo.work_dir().context("Not a working directory")?;
41    let config = get_changepacks_config(&current_dir).await?;
42    find_project_dirs(&repo, &mut project_finders, &config, args.remote).await?;
43
44    let mut projects = project_finders
45        .iter()
46        .flat_map(|finder| finder.projects())
47        .collect::<Vec<_>>();
48
49    if let Some(filter) = &args.filter {
50        projects.retain(|project| match filter {
51            FilterOptions::Workspace => matches!(project, Project::Workspace(_)),
52            FilterOptions::Package => matches!(project, Project::Package(_)),
53        });
54    }
55
56    println!("Found {} projects", projects.len());
57    // workspace first
58    projects.sort();
59
60    let mut update_map = HashMap::<PathBuf, UpdateType>::new();
61
62    for update_type in if let Some(update_type) = &args.update_type {
63        vec![update_type.clone()]
64    } else {
65        vec![UpdateType::Major, UpdateType::Minor, UpdateType::Patch]
66    } {
67        if projects.is_empty() {
68            break;
69        }
70
71        let selected_projects = if !args.yes {
72            if update_type == UpdateType::Patch && projects.len() == 1 {
73                vec![projects[0]]
74            } else {
75                let message = format!("Select projects to update for {}", update_type);
76                let defaults = projects
77                    .iter()
78                    .enumerate()
79                    .filter_map(|(index, project)| {
80                        if project.is_changed() {
81                            Some(index)
82                        } else {
83                            None
84                        }
85                    })
86                    .collect::<Vec<_>>();
87                prompter.multi_select(&message, projects.clone(), defaults)?
88            }
89        } else {
90            projects.clone()
91        };
92
93        // remove selected projects from projects by index
94        for project in selected_projects {
95            update_map.insert(
96                get_relative_path(repo_root_path, project.path())?,
97                update_type.clone(),
98            );
99        }
100
101        let project_with_relpath: Vec<_> = projects
102            .iter()
103            .map(|project| {
104                get_relative_path(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(&current_dir)?
136        .join(format!("changepack_log_{}.json", changepack_log_id));
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}