changepacks_cli/commands/
changepacks.rs1use changepacks_core::{ChangePackLog, Language, 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::{CliLanguage, 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 pub language: Vec<CliLanguage>,
23}
24
25pub async fn handle_changepack(args: &ChangepackArgs) -> Result<()> {
28 handle_changepack_with_prompter(args, &InquirePrompter).await
29}
30
31pub async fn handle_changepack_with_prompter(
34 args: &ChangepackArgs,
35 prompter: &dyn Prompter,
36) -> Result<()> {
37 let ctx = CommandContext::new(args.remote).await?;
38
39 let mut projects = ctx
40 .project_finders
41 .iter()
42 .flat_map(|finder| finder.projects())
43 .collect::<Vec<_>>();
44
45 projects.retain(|p| {
48 if let Project::Package(pkg) = p {
49 !pkg.inherits_workspace_version()
50 } else {
51 true
52 }
53 });
54
55 if let Some(filter) = &args.filter {
56 projects.retain(|p| filter.matches(p));
57 }
58 if !args.language.is_empty() {
59 let allowed_languages: Vec<Language> = args
60 .language
61 .iter()
62 .map(|&lang| Language::from(lang))
63 .collect();
64 projects.retain(|project| allowed_languages.contains(&project.language()));
65 }
66
67 println!("Found {} projects", projects.len());
68 projects.sort();
70
71 let mut update_map = HashMap::<PathBuf, UpdateType>::new();
72
73 for update_type in if let Some(update_type) = &args.update_type {
74 vec![*update_type]
75 } else {
76 vec![UpdateType::Major, UpdateType::Minor, UpdateType::Patch]
77 } {
78 if projects.is_empty() {
79 break;
80 }
81
82 let selected_projects = if args.yes {
83 projects.clone()
84 } else if update_type == UpdateType::Patch && projects.len() == 1 {
85 vec![projects[0]]
86 } else {
87 let message = format!("Select projects to update for {update_type}");
88 let defaults = projects
89 .iter()
90 .enumerate()
91 .filter_map(|(index, project)| {
92 if project.is_changed() {
93 Some(index)
94 } else {
95 None
96 }
97 })
98 .collect::<Vec<_>>();
99 prompter.multi_select(&message, projects.clone(), defaults)?
100 };
101
102 for project in selected_projects {
104 update_map.insert(
105 get_relative_path(&ctx.repo_root_path, project.path())?,
106 update_type,
107 );
108 }
109
110 let project_with_relpath: Vec<_> = projects
111 .iter()
112 .map(|project| {
113 get_relative_path(&ctx.repo_root_path, project.path()).map(|rel| (project, rel))
114 })
115 .collect::<Result<Vec<_>>>()?;
116
117 let keep_projects: Vec<_> = project_with_relpath
118 .into_iter()
119 .filter(|(_, rel_path)| !update_map.contains_key(rel_path))
120 .map(|(project, _)| *project)
121 .collect();
122
123 projects = keep_projects;
124 }
125
126 if update_map.is_empty() {
127 println!("No projects selected");
128 return Ok(());
129 }
130
131 let notes = if let Some(message) = &args.message {
132 message.clone()
133 } else {
134 prompter.text("write notes here")?
135 };
136
137 if notes.is_empty() {
138 println!("Notes are empty");
139 return Ok(());
140 }
141 let changepack_log = ChangePackLog::new(update_map, notes);
142 let changepack_log_id = nanoid::nanoid!();
144 let changepack_log_file = get_changepacks_dir(&CommandContext::current_dir()?)?
145 .join(format!("changepack_log_{changepack_log_id}.json"));
146 write(changepack_log_file, serde_json::to_string(&changepack_log)?).await?;
147
148 Ok(())
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_changepack_args_debug() {
157 let args = ChangepackArgs {
158 filter: None,
159 remote: false,
160 yes: true,
161 message: Some("Test".to_string()),
162 update_type: Some(UpdateType::Patch),
163 language: vec![],
164 };
165
166 let debug_str = format!("{:?}", args);
168 assert!(debug_str.contains("ChangepackArgs"));
169 }
170
171 #[test]
172 fn test_changepack_args_with_filter() {
173 let args = ChangepackArgs {
174 filter: Some(FilterOptions::Package),
175 remote: true,
176 yes: false,
177 message: None,
178 update_type: None,
179 language: vec![],
180 };
181
182 assert!(args.filter.is_some());
183 assert!(args.remote);
184 assert!(!args.yes);
185 assert!(args.message.is_none());
186 assert!(args.update_type.is_none());
187 }
188
189 #[test]
190 fn test_changepack_args_workspace_filter() {
191 let args = ChangepackArgs {
192 filter: Some(FilterOptions::Workspace),
193 remote: false,
194 yes: true,
195 message: Some("msg".to_string()),
196 update_type: Some(UpdateType::Major),
197 language: vec![],
198 };
199
200 assert!(matches!(args.filter, Some(FilterOptions::Workspace)));
201 assert!(matches!(args.update_type, Some(UpdateType::Major)));
202 }
203
204 #[test]
205 fn test_changepack_args_minor_update() {
206 let args = ChangepackArgs {
207 filter: None,
208 remote: false,
209 yes: true,
210 message: Some("feature".to_string()),
211 update_type: Some(UpdateType::Minor),
212 language: vec![],
213 };
214
215 assert!(matches!(args.update_type, Some(UpdateType::Minor)));
216 }
217
218 #[test]
219 fn test_changepack_args_with_language() {
220 let args = ChangepackArgs {
221 filter: None,
222 remote: false,
223 yes: true,
224 message: None,
225 update_type: None,
226 language: vec![CliLanguage::Node, CliLanguage::Rust],
227 };
228
229 assert_eq!(args.language.len(), 2);
230 }
231}