1#![allow(clippy::all)]
2
3use serde::{Deserialize, Serialize};
24use std::io::BufWriter;
25use std::{
26 collections::BTreeMap,
27 fs::File,
28 io::BufReader,
29 path::{Path, PathBuf},
30};
31
32use crate::bumps::Bump;
33
34use super::git::git_current_branch;
35use super::paths::get_project_root_path;
36
37type ChangesData = BTreeMap<String, Vec<Change>>;
39
40#[cfg(not(feature = "napi"))]
41#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
42pub struct ChangesOptions {
44 pub message: Option<String>,
45 pub git_user_name: Option<String>,
46 pub git_user_email: Option<String>,
47}
48
49#[cfg(feature = "napi")]
50#[napi(object)]
51#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
52pub struct ChangesOptions {
53 pub message: Option<String>,
54 pub git_user_name: Option<String>,
55 pub git_user_email: Option<String>,
56}
57
58#[cfg(not(feature = "napi"))]
59#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
60pub struct ChangesFileData {
62 pub message: Option<String>,
63 pub git_user_name: Option<String>,
64 pub git_user_email: Option<String>,
65 pub changes: ChangesData,
66}
67
68#[cfg(feature = "napi")]
69#[napi(object)]
70#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
71pub struct ChangesFileData {
72 pub message: Option<String>,
73 pub git_user_name: Option<String>,
74 pub git_user_email: Option<String>,
75 pub changes: ChangesData,
76}
77
78#[cfg(not(feature = "napi"))]
79#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
80pub struct Changes {
82 pub changes: ChangesData,
83}
84
85#[cfg(feature = "napi")]
86#[napi(object)]
87#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
88pub struct Changes {
89 pub changes: ChangesData,
90}
91
92#[cfg(not(feature = "napi"))]
93#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
94pub struct Change {
96 pub package: String,
97 pub release_as: Bump,
98 pub deploy: Vec<String>,
99}
100
101#[cfg(feature = "napi")]
102#[napi(object)]
103#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
104pub struct Change {
105 pub package: String,
106 pub release_as: Bump,
107 pub deploy: Vec<String>,
108}
109
110pub fn init_changes(
113 cwd: Option<String>,
114 change_options: &Option<ChangesOptions>,
115) -> ChangesFileData {
116 let ref root = match cwd {
117 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
118 None => get_project_root_path(None).unwrap(),
119 };
120
121 let root_path = Path::new(root);
122 let ref changes_path = root_path.join(String::from(".changes.json"));
123
124 if changes_path.exists() {
125 let changes_file = File::open(changes_path).unwrap();
126 let changes_reader = BufReader::new(changes_file);
127
128 let changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
129 return changes;
130 } else {
131 let message = match &change_options {
132 Some(options) => match &options.message {
133 Some(msg) => msg.to_string(),
134 None => String::from("chore(release): release new version"),
135 },
136 None => String::from("chore(release): release new version"),
137 };
138
139 let username = match &change_options {
140 Some(options) => match &options.git_user_name {
141 Some(name) => name.to_string(),
142 None => String::from("Git Bot"),
143 },
144 None => String::from("Git Bot"),
145 };
146
147 let email = match &change_options {
148 Some(options) => match &options.git_user_email {
149 Some(email) => email.to_string(),
150 None => String::from("git.bot@domain.com"),
151 },
152 None => String::from("git.bot@domain.com"),
153 };
154
155 let changes = ChangesFileData {
156 message: Some(message),
157 git_user_name: Some(username),
158 git_user_email: Some(email),
159 changes: ChangesData::new(),
160 };
161
162 let changes_file = File::create(changes_path).unwrap();
163 let changes_writer = BufWriter::new(changes_file);
164
165 serde_json::to_writer_pretty(changes_writer, &changes).unwrap();
166
167 return changes;
168 }
169}
170
171pub fn add_change(change: &Change, cwd: Option<String>) -> bool {
173 let ref root = match cwd {
174 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
175 None => get_project_root_path(None).unwrap(),
176 };
177
178 let root_path = Path::new(root);
179 let ref changes_path = root_path.join(String::from(".changes.json"));
180
181 if changes_path.exists() {
182 let changes_file = File::open(changes_path).unwrap();
183 let changes_reader = BufReader::new(changes_file);
184
185 let mut changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
186
187 let current_branch = git_current_branch(Some(root.to_string()));
188
189 let branch = match current_branch {
190 Some(branch) => branch,
191 None => String::from("main"),
192 };
193
194 if changes.changes.contains_key(&branch) {
195 let branch_changes = changes.changes.get_mut(&branch).unwrap();
196
197 let pkg_already_added = branch_changes
198 .iter()
199 .any(|branch_change| branch_change.package.as_str() == change.package.as_str());
200
201 if !pkg_already_added {
202 branch_changes.push(Change {
203 package: change.package.to_string(),
204 release_as: change.release_as,
205 deploy: change.deploy.to_vec(),
206 });
207 }
208 } else {
209 changes.changes.insert(
210 branch,
211 vec![Change {
212 package: change.package.to_string(),
213 release_as: change.release_as,
214 deploy: change.deploy.to_vec(),
215 }],
216 );
217 }
218
219 let changes_file = File::create(changes_path).unwrap();
220 let changes_writer = BufWriter::new(changes_file);
221
222 serde_json::to_writer_pretty(changes_writer, &changes).unwrap();
223
224 return true;
225 }
226
227 false
228}
229
230pub fn remove_change(branch_name: String, cwd: Option<String>) -> bool {
232 let ref root = match cwd {
233 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
234 None => get_project_root_path(None).unwrap(),
235 };
236
237 let root_path = Path::new(root);
238 let ref changes_path = root_path.join(String::from(".changes.json"));
239
240 if changes_path.exists() {
241 let changes_file = File::open(changes_path).unwrap();
242 let changes_reader = BufReader::new(changes_file);
243
244 let mut changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
245
246 if changes.changes.contains_key(&branch_name) {
247 changes.changes.remove(&branch_name);
248
249 let changes_file = File::create(changes_path).unwrap();
250 let changes_writer = BufWriter::new(changes_file);
251
252 serde_json::to_writer_pretty(changes_writer, &changes).unwrap();
253
254 return true;
255 }
256 }
257
258 false
259}
260
261pub fn get_changes(cwd: Option<String>) -> Changes {
263 let ref root = match cwd {
264 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
265 None => get_project_root_path(None).unwrap(),
266 };
267
268 let root_path = Path::new(root);
269 let ref changes_path = root_path.join(String::from(".changes.json"));
270
271 if changes_path.exists() {
272 let changes_file = File::open(changes_path).unwrap();
273 let changes_reader = BufReader::new(changes_file);
274
275 let changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
276
277 return Changes {
278 changes: changes.changes,
279 };
280 }
281
282 Changes {
283 changes: ChangesData::new(),
284 }
285}
286
287pub fn get_change(branch: String, cwd: Option<String>) -> Vec<Change> {
289 let ref root = match cwd {
290 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
291 None => get_project_root_path(None).unwrap(),
292 };
293
294 let root_path = Path::new(root);
295 let ref changes_path = root_path.join(String::from(".changes.json"));
296
297 if changes_path.exists() {
298 let changes_file = File::open(changes_path).unwrap();
299 let changes_reader = BufReader::new(changes_file);
300
301 let changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
302
303 if changes.changes.contains_key(&branch) {
304 return changes.changes.get(&branch).unwrap().to_vec();
305 } else {
306 return vec![];
307 }
308 }
309
310 vec![]
311}
312
313pub fn get_package_change(
315 package_name: String,
316 branch: String,
317 cwd: Option<String>,
318) -> Option<Change> {
319 let ref root = match cwd {
320 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
321 None => get_project_root_path(None).unwrap(),
322 };
323
324 let root_path = Path::new(root);
325 let ref changes_path = root_path.join(String::from(".changes.json"));
326
327 if changes_path.exists() {
328 let changes_file = File::open(changes_path).unwrap();
329 let changes_reader = BufReader::new(changes_file);
330
331 let changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
332
333 if changes.changes.contains_key(&branch) {
334 let branch_changes = changes.changes.get(&branch).unwrap();
335
336 let package_change = branch_changes
337 .iter()
338 .find(|change| change.package == package_name);
339
340 if let Some(change) = package_change {
341 return Some(change.clone());
342 }
343
344 return None;
345 }
346
347 return None;
348 }
349
350 None
351}
352
353pub fn change_exist(branch: String, packages_name: Vec<String>, cwd: Option<String>) -> bool {
355 let ref root = match cwd {
356 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
357 None => get_project_root_path(None).unwrap(),
358 };
359
360 let root_path = Path::new(root);
361 let ref changes_path = root_path.join(String::from(".changes.json"));
362
363 if changes_path.exists() {
364 let changes_file = File::open(changes_path).unwrap();
365 let changes_reader = BufReader::new(changes_file);
366
367 let changes: ChangesFileData = serde_json::from_reader(changes_reader).unwrap();
368
369 if changes.changes.contains_key(&branch) {
370 let branch_changes = changes.changes.get(&branch).unwrap();
371
372 let existing_packages_changes = branch_changes
373 .iter()
374 .map(|change| change.package.to_string())
375 .collect::<Vec<String>>();
376
377 let package_names_diff = packages_name
378 .iter()
379 .filter_map(|p| {
380 if existing_packages_changes.contains(&p) {
381 None
382 } else {
383 Some(p.to_string())
384 }
385 })
386 .collect::<Vec<String>>();
387
388 match package_names_diff.len() {
389 0 => return true,
390 _ => return false,
391 };
392 }
393 }
394
395 false
396}
397
398pub fn changes_file_exist(cwd: Option<String>) -> bool {
400 let ref root = match cwd {
401 Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
402 None => get_project_root_path(None).unwrap(),
403 };
404
405 let root_path = Path::new(root);
406 let ref changes_path = root_path.join(String::from(".changes.json"));
407
408 changes_path.exists()
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 use crate::manager::PackageManager;
416 use crate::paths::get_project_root_path;
417 use crate::utils::create_test_monorepo;
418 use std::fs::remove_dir_all;
419
420 #[test]
421 fn test_init_changes() -> Result<(), Box<dyn std::error::Error>> {
422 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
423 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
424
425 let ref root = project_root.unwrap().to_string();
426
427 let changes_data_file = init_changes(Some(root.to_string()), &None);
428 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
429
430 assert_eq!(changes_data_file.message.is_some(), true);
431 assert_eq!(changes_path.is_file(), true);
432 remove_dir_all(&monorepo_dir)?;
433 Ok(())
434 }
435
436 #[test]
437 fn test_add_change() -> Result<(), Box<dyn std::error::Error>> {
438 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
439 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
440
441 let ref root = project_root.unwrap().to_string();
442
443 let change = Change {
444 package: String::from("test-package"),
445 release_as: Bump::Major,
446 deploy: vec![String::from("production")],
447 };
448
449 init_changes(Some(root.to_string()), &None);
450
451 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
452 let result = add_change(&change, Some(root.to_string()));
453
454 assert_eq!(result, true);
455 assert_eq!(changes_path.is_file(), true);
456 remove_dir_all(&monorepo_dir)?;
457 Ok(())
458 }
459
460 #[test]
461 fn test_duplicate_add_change() -> Result<(), Box<dyn std::error::Error>> {
462 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
463 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
464
465 let ref root = project_root.unwrap().to_string();
466
467 let change = Change {
468 package: String::from("test-package"),
469 release_as: Bump::Major,
470 deploy: vec![String::from("production")],
471 };
472
473 init_changes(Some(root.to_string()), &None);
474
475 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
476 add_change(&change, Some(root.to_string()));
477 add_change(&change, Some(root.to_string()));
478
479 let changes = get_changes(Some(root.to_string()));
480 let length = changes.changes["main"].len();
481
482 assert_eq!(length, 1);
483 assert_eq!(changes_path.is_file(), true);
484 remove_dir_all(&monorepo_dir)?;
485 Ok(())
486 }
487
488 #[test]
489 fn test_remove_change() -> Result<(), Box<dyn std::error::Error>> {
490 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
491 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
492
493 let ref root = project_root.unwrap().to_string();
494
495 let change = Change {
496 package: String::from("test-package"),
497 release_as: Bump::Major,
498 deploy: vec![String::from("production")],
499 };
500
501 init_changes(Some(root.to_string()), &None);
502
503 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
504 add_change(&change, Some(root.to_string()));
505
506 let result = remove_change(String::from("main"), Some(root.to_string()));
507
508 assert_eq!(result, true);
509 assert_eq!(changes_path.is_file(), true);
510 remove_dir_all(&monorepo_dir)?;
511 Ok(())
512 }
513
514 #[test]
515 fn test_get_changes() -> Result<(), Box<dyn std::error::Error>> {
516 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
517 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
518
519 let ref root = project_root.unwrap().to_string();
520
521 let change = Change {
522 package: String::from("test-package"),
523 release_as: Bump::Major,
524 deploy: vec![String::from("production")],
525 };
526
527 init_changes(Some(root.to_string()), &None);
528
529 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
530 add_change(&change, Some(root.to_string()));
531
532 let changes = get_changes(Some(root.to_string()));
533
534 assert_eq!(changes.changes.contains_key(&String::from("main")), true);
535 assert_eq!(changes.changes.get(&String::from("main")).unwrap().len(), 1);
536 assert_eq!(changes_path.is_file(), true);
537 remove_dir_all(&monorepo_dir)?;
538 Ok(())
539 }
540
541 #[test]
542 fn test_get_change() -> Result<(), Box<dyn std::error::Error>> {
543 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
544 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
545
546 let ref root = project_root.unwrap().to_string();
547
548 let change = Change {
549 package: String::from("test-package"),
550 release_as: Bump::Major,
551 deploy: vec![String::from("production")],
552 };
553
554 init_changes(Some(root.to_string()), &None);
555
556 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
557 add_change(&change, Some(root.to_string()));
558
559 let changes = get_change(String::from("main"), Some(root.to_string()));
560
561 assert_eq!(changes.len(), 1);
562 assert_eq!(changes_path.is_file(), true);
563 remove_dir_all(&monorepo_dir)?;
564 Ok(())
565 }
566
567 #[test]
568 fn test_change_exist() -> Result<(), Box<dyn std::error::Error>> {
569 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
570 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
571
572 let ref root = project_root.unwrap().to_string();
573
574 let change = Change {
575 package: String::from("test-package"),
576 release_as: Bump::Major,
577 deploy: vec![String::from("production")],
578 };
579
580 init_changes(Some(root.to_string()), &None);
581
582 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
583 add_change(&change, Some(root.to_string()));
584
585 let result = change_exist(
586 String::from("main"),
587 vec!["test-package".to_string()],
588 Some(root.to_string()),
589 );
590
591 assert_eq!(result, true);
592 assert_eq!(changes_path.is_file(), true);
593 remove_dir_all(&monorepo_dir)?;
594 Ok(())
595 }
596
597 #[test]
598 fn test_multiple_change_exist() {
599 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm).unwrap();
600 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf())).unwrap();
601
602 let ref root = project_root.to_string();
603
604 let change_package_a = Change {
605 package: String::from("@scope/package-a"),
606 release_as: Bump::Major,
607 deploy: vec![String::from("production")],
608 };
609
610 let change_package_b = Change {
611 package: String::from("@scope/package-b"),
612 release_as: Bump::Major,
613 deploy: vec![String::from("production")],
614 };
615
616 init_changes(Some(root.to_string()), &None);
617
618 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
619 add_change(&change_package_a, Some(root.to_string()));
620 add_change(&change_package_b, Some(root.to_string()));
621
622 let result = change_exist(
623 String::from("main"),
624 vec![
625 "@scope/package-a".to_string(),
626 "@scope/package-b".to_string(),
627 ],
628 Some(root.to_string()),
629 );
630
631 assert_eq!(result, true);
632 assert_eq!(changes_path.is_file(), true);
633 remove_dir_all(&monorepo_dir).unwrap();
634 }
635
636 #[test]
637 fn test_change_exist_with_new_package() -> Result<(), Box<dyn std::error::Error>> {
638 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
639 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
640
641 let ref root = project_root.unwrap().to_string();
642
643 let change = Change {
644 package: String::from("test-package"),
645 release_as: Bump::Major,
646 deploy: vec![String::from("production")],
647 };
648
649 init_changes(Some(root.to_string()), &None);
650
651 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
652 add_change(&change, Some(root.to_string()));
653
654 let result = change_exist(
655 String::from("main"),
656 vec!["test-package".to_string(), "@scope/package-a".to_string()],
657 Some(root.to_string()),
658 );
659
660 assert_eq!(result, false);
661 assert_eq!(changes_path.is_file(), true);
662 remove_dir_all(&monorepo_dir)?;
663 Ok(())
664 }
665
666 #[test]
667 fn test_change_exist_with_empty_packages() -> Result<(), Box<dyn std::error::Error>> {
668 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
669 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
670
671 let ref root = project_root.unwrap().to_string();
672
673 init_changes(Some(root.to_string()), &None);
674
675 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
676
677 let result = change_exist(
678 String::from("main"),
679 vec!["test-package".to_string(), "@scope/package-a".to_string()],
680 Some(root.to_string()),
681 );
682
683 assert_eq!(result, false);
684 assert_eq!(changes_path.is_file(), true);
685 remove_dir_all(&monorepo_dir)?;
686 Ok(())
687 }
688
689 #[test]
690 fn test_changes_file_exist() -> Result<(), Box<dyn std::error::Error>> {
691 let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
692 let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
693
694 let ref root = project_root.unwrap().to_string();
695
696 let ref changes_path = monorepo_dir.join(String::from(".changes.json"));
697 let result = changes_file_exist(Some(root.to_string()));
698
699 assert_eq!(result, false);
700 assert_eq!(changes_path.is_file(), false);
701 remove_dir_all(&monorepo_dir)?;
702 Ok(())
703 }
704}