changepacks_cli/commands/
changepacks.rs1use 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 let repo = find_current_git_repo(¤t_dir)?;
40 let repo_root_path = repo.work_dir().context("Not a working directory")?;
41 let config = get_changepacks_config(¤t_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 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 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 let changepack_log_id = nanoid::nanoid!();
135 let changepack_log_file = get_changepacks_dir(¤t_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 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}