changepacks_cli/commands/
changepacks.rs1use 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
24pub async fn handle_changepack(args: &ChangepackArgs) -> Result<()> {
27 handle_changepack_with_prompter(args, &InquirePrompter).await
28}
29
30pub 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 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 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 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 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 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}