workspace_node_tools/
bumps.rs

1#![warn(dead_code)]
2#![allow(unused_imports)]
3#![allow(clippy::all)]
4
5//! # Bumps
6//!
7//! This module is responsible for managing the bumps in the monorepo.
8use semver::{BuildMetadata, Prerelease, Version as SemVersion};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use std::collections::HashMap;
13use std::fs::OpenOptions;
14use std::io::{BufWriter, Write};
15use std::path::PathBuf;
16
17use crate::conventional::ConventionalPackage;
18
19use super::changes::{get_package_change, init_changes, Change};
20use super::conventional::{get_conventional_for_package, ConventionalPackageOptions};
21use super::git::{
22    git_add_all, git_all_files_changed_since_sha, git_commit, git_config, git_current_branch,
23    git_current_sha, git_fetch_all, git_push, git_tag,
24};
25use super::packages::PackageInfo;
26use super::packages::{get_package_info, get_packages};
27use super::paths::get_project_root_path;
28
29#[cfg(feature = "napi")]
30#[napi(string_enum)]
31#[derive(Debug, Deserialize, Serialize, PartialEq)]
32pub enum Bump {
33    Major,
34    Minor,
35    Patch,
36    Snapshot,
37}
38
39#[cfg(not(feature = "napi"))]
40#[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq)]
41/// Enum representing the type of bump to be performed.
42pub enum Bump {
43    Major,
44    Minor,
45    Patch,
46    Snapshot,
47}
48
49#[cfg(feature = "napi")]
50#[napi(object)]
51#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
52pub struct BumpOptions {
53    pub changes: Vec<Change>,
54    pub since: Option<String>,
55    pub release_as: Option<Bump>,
56    pub fetch_all: Option<bool>,
57    pub fetch_tags: Option<bool>,
58    pub sync_deps: Option<bool>,
59    pub push: Option<bool>,
60    pub cwd: Option<String>,
61}
62
63#[cfg(not(feature = "napi"))]
64#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
65/// Struct representing the options for the bump operation.
66pub struct BumpOptions {
67    pub changes: Vec<Change>,
68    pub since: Option<String>,
69    pub release_as: Option<Bump>,
70    pub fetch_all: Option<bool>,
71    pub fetch_tags: Option<bool>,
72    pub sync_deps: Option<bool>,
73    pub push: Option<bool>,
74    pub cwd: Option<String>,
75}
76
77#[cfg(not(feature = "napi"))]
78#[derive(Debug, Clone, Deserialize, Serialize)]
79/// Struct representing the bump package.
80pub struct BumpPackage {
81    pub from: String,
82    pub to: String,
83    pub package_info: PackageInfo,
84    pub conventional_commits: Value,
85}
86
87#[cfg(feature = "napi")]
88#[napi(object)]
89#[derive(Debug, Clone, Deserialize, Serialize)]
90pub struct BumpPackage {
91    pub from: String,
92    pub to: String,
93    pub package_info: PackageInfo,
94    pub conventional_commits: Value,
95}
96
97#[cfg(not(feature = "napi"))]
98#[derive(Debug, Clone, Deserialize, Serialize)]
99/// Struct representing the bump package.
100pub struct RecommendBumpPackage {
101    pub from: String,
102    pub to: String,
103    pub package_info: PackageInfo,
104    pub conventional: ConventionalPackage,
105    pub changed_files: Vec<String>,
106    pub deploy_to: Vec<String>,
107}
108
109#[cfg(feature = "napi")]
110#[napi(object)]
111#[derive(Debug, Clone, Deserialize, Serialize)]
112/// Struct representing the bump package.
113pub struct RecommendBumpPackage {
114    pub from: String,
115    pub to: String,
116    pub package_info: PackageInfo,
117    pub conventional: ConventionalPackage,
118    pub changed_files: Vec<String>,
119    pub deploy_to: Vec<String>,
120}
121
122impl Bump {
123    /// Bumps the version of the package to major.
124    fn bump_major(version: String) -> SemVersion {
125        let mut sem_version = SemVersion::parse(&version).unwrap();
126        sem_version.major += 1;
127        sem_version.minor = 0;
128        sem_version.patch = 0;
129        sem_version.pre = Prerelease::EMPTY;
130        sem_version.build = BuildMetadata::EMPTY;
131        sem_version
132    }
133
134    /// Bumps the version of the package to minor.
135    fn bump_minor(version: String) -> SemVersion {
136        let mut sem_version = SemVersion::parse(&version).unwrap();
137        sem_version.minor += 1;
138        sem_version.patch = 0;
139        sem_version.pre = Prerelease::EMPTY;
140        sem_version.build = BuildMetadata::EMPTY;
141        sem_version
142    }
143
144    /// Bumps the version of the package to patch.
145    fn bump_patch(version: String) -> SemVersion {
146        let mut sem_version = SemVersion::parse(&version).unwrap();
147        sem_version.patch += 1;
148        sem_version.pre = Prerelease::EMPTY;
149        sem_version.build = BuildMetadata::EMPTY;
150        sem_version
151    }
152
153    /// Bumps the version of the package to snapshot appending the sha to the version.
154    fn bump_snapshot(version: String) -> SemVersion {
155        let sha = git_current_sha(None);
156        let alpha = format!("alpha.{}.{}", 0, sha);
157
158        let mut sem_version = SemVersion::parse(&version).unwrap();
159        sem_version.pre = Prerelease::new(alpha.as_str()).unwrap_or(Prerelease::EMPTY);
160        sem_version.build = BuildMetadata::EMPTY;
161        sem_version
162    }
163}
164
165pub fn get_package_recommend_bump(
166    package_info: &PackageInfo,
167    root: &String,
168    options: Option<BumpOptions>,
169) -> RecommendBumpPackage {
170    let ref current_branch =
171        git_current_branch(Some(root.to_string())).unwrap_or(String::from("origin/main"));
172
173    let package_version = &package_info.version.to_string();
174    let package_name = &package_info.name.to_string();
175    let package_change = get_package_change(
176        package_name.to_string(),
177        current_branch.to_string(),
178        Some(root.to_string()),
179    );
180
181    let settings = options.unwrap_or_else(|| BumpOptions {
182        changes: vec![],
183        since: None,
184        release_as: None,
185        fetch_all: None,
186        fetch_tags: None,
187        sync_deps: None,
188        push: None,
189        cwd: None,
190    });
191
192    let ref since = settings.since.unwrap_or(String::from("origin/main"));
193
194    let release_as = settings
195        .release_as
196        .unwrap_or_else(|| match package_change.to_owned() {
197            Some(change) => change.release_as,
198            None => Bump::Patch,
199        });
200
201    let deploy_to = match package_change.to_owned() {
202        Some(change) => change.deploy,
203        None => vec![String::from("production")],
204    };
205
206    let fetch_all = settings.fetch_all.unwrap_or(false);
207
208    let semversion = match release_as {
209        Bump::Major => Bump::bump_major(package_version.to_string()),
210        Bump::Minor => Bump::bump_minor(package_version.to_string()),
211        Bump::Patch => Bump::bump_patch(package_version.to_string()),
212        Bump::Snapshot => Bump::bump_snapshot(package_version.to_string()),
213    };
214
215    let changed_files = git_all_files_changed_since_sha(since.to_string(), Some(root.to_string()));
216    let ref version = semversion.to_string();
217
218    let conventional = get_conventional_for_package(
219        &package_info,
220        Some(fetch_all),
221        Some(root.to_string()),
222        &Some(ConventionalPackageOptions {
223            version: Some(version.to_string()),
224            title: Some("# What changed?".to_string()),
225        }),
226    );
227
228    RecommendBumpPackage {
229        from: package_version.to_string(),
230        to: version.to_string(),
231        package_info: package_info.to_owned(),
232        conventional: conventional.to_owned(),
233        changed_files: changed_files.to_owned(),
234        deploy_to: deploy_to.to_owned(),
235    }
236}
237
238/// Get bumps version of the package. If sync_deps is true, it will also sync the dependencies and dev-dependencies.
239pub fn get_bumps(options: &BumpOptions) -> Vec<BumpPackage> {
240    let ref root = match options.cwd {
241        Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
242        None => get_project_root_path(None).unwrap(),
243    };
244
245    if options.fetch_tags.is_some() {
246        git_fetch_all(Some(root.to_string()), options.fetch_tags)
247            .expect("No possible to fetch tags");
248    }
249
250    let since = match options.since {
251        Some(ref since) => since.to_string(),
252        None => String::from("origin/main"),
253    };
254
255    let current_branch = git_current_branch(Some(root.to_string())).unwrap_or(String::from("main"));
256
257    let ref packages = get_packages(Some(root.to_string()));
258    let changed_packages = packages
259        .iter()
260        .filter(|package| {
261            options
262                .changes
263                .iter()
264                .any(|change| change.package == package.name)
265        })
266        .map(|package| package.to_owned())
267        .collect::<Vec<PackageInfo>>();
268
269    if changed_packages.len() == 0 {
270        return vec![];
271    }
272
273    let mut bump_changes = HashMap::new();
274    let mut bump_dependencies = HashMap::new();
275
276    for changed_package in changed_packages.iter() {
277        let change = options
278            .changes
279            .iter()
280            .find(|change| change.package == changed_package.name);
281
282        if change.is_some() {
283            let release_as = match Some(current_branch.contains("main")) {
284                Some(true) => change.unwrap().release_as,
285                Some(false) | None => Bump::Snapshot,
286            };
287
288            let change = Change {
289                release_as,
290                ..change.unwrap().to_owned()
291            };
292
293            bump_changes.insert(changed_package.name.to_string(), change.to_owned());
294        }
295
296        if options.sync_deps.unwrap_or(false) {
297            packages.iter().for_each(|package| {
298                package.dependencies.iter().for_each(|dependency| {
299                    let release_as = match Some(current_branch.contains("main")) {
300                        Some(true) => Bump::Patch,
301                        Some(false) | None => Bump::Snapshot,
302                    };
303
304                    if dependency.name == changed_package.name {
305                        if change.is_some() && !bump_changes.contains_key(&package.name) {
306                            bump_changes.insert(
307                                package.name.to_string(),
308                                Change {
309                                    package: package.name.to_string(),
310                                    release_as,
311                                    deploy: change.unwrap().deploy.to_owned(),
312                                },
313                            );
314                        }
315                    }
316                });
317            });
318        }
319    }
320
321    let mut bumps = bump_changes
322        .iter()
323        .map(|(package_name, change)| {
324            let package = get_package_info(package_name.to_string(), Some(root.to_string()));
325
326            let release_as = match Some(current_branch.contains("main")) {
327                Some(true) => change.release_as.to_owned(),
328                Some(false) | None => Bump::Snapshot,
329            };
330
331            let recommended_bump = get_package_recommend_bump(
332                &package.unwrap(),
333                root,
334                Some(BumpOptions {
335                    changes: vec![change.to_owned()],
336                    since: Some(since.to_string()),
337                    release_as: Some(release_as.to_owned()),
338                    fetch_all: options.fetch_all.to_owned(),
339                    fetch_tags: options.fetch_tags.to_owned(),
340                    sync_deps: options.sync_deps.to_owned(),
341                    push: options.push.to_owned(),
342                    cwd: Some(root.to_string()),
343                }),
344            );
345
346            let bump = BumpPackage {
347                from: recommended_bump.from.to_string(),
348                to: recommended_bump.to.to_string(),
349                conventional_commits: recommended_bump
350                    .conventional
351                    .conventional_commits
352                    .to_owned(),
353                package_info: recommended_bump.package_info.to_owned(),
354            };
355
356            if bump.package_info.dependencies.len() > 0 {
357                bump_dependencies.insert(
358                    package_name.to_string(),
359                    bump.package_info.dependencies.to_owned(),
360                );
361            }
362
363            return bump;
364        })
365        .collect::<Vec<BumpPackage>>();
366
367    bumps.iter_mut().for_each(|bump| {
368        let version = bump.to.to_string();
369        bump.package_info.update_version(version.to_string());
370        bump.package_info
371            .extend_changed_files(vec![String::from("package.json")]);
372        bump.package_info.write_package_json();
373    });
374
375    if options.sync_deps.unwrap_or(false) {
376        bump_dependencies.iter().for_each(|(package_name, deps)| {
377            let temp_bumps = bumps.clone();
378            let bump = bumps
379                .iter_mut()
380                .find(|b| b.package_info.name == package_name.to_string())
381                .unwrap();
382
383            for dep in deps {
384                let bump_dep = temp_bumps.iter().find(|b| b.package_info.name == dep.name);
385
386                if bump_dep.is_some() {
387                    bump.package_info.update_dependency_version(
388                        dep.name.to_string(),
389                        bump_dep.unwrap().to.to_string(),
390                    );
391                    bump.package_info.update_dev_dependency_version(
392                        dep.name.to_string(),
393                        bump_dep.unwrap().to.to_string(),
394                    );
395                    bump.package_info.write_package_json();
396                }
397            }
398        });
399    }
400
401    bumps
402}
403
404/// Apply version bumps, commit and push changes. Returns a list of packages that have been updated.
405/// Also generate changelog file and update dependencies and devDependencies in package.json.
406pub fn apply_bumps(options: &BumpOptions) -> Vec<BumpPackage> {
407    let ref root = match options.cwd {
408        Some(ref dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
409        None => get_project_root_path(None).unwrap(),
410    };
411
412    let ref changes_data = init_changes(Some(root.to_string()), &None);
413    let git_user_name = changes_data.git_user_name.to_owned();
414    let git_user_email = changes_data.git_user_email.to_owned();
415
416    git_config(
417        &git_user_name.unwrap_or(String::from("")),
418        &git_user_email.unwrap_or(String::from("")),
419        &root.to_string(),
420    )
421    .expect("Failed to set git user name and email");
422
423    let bumps = get_bumps(options);
424
425    if bumps.len() != 0 {
426        for bump in &bumps {
427            let git_message = changes_data.message.to_owned();
428
429            let ref bump_pkg_json_file_path =
430                PathBuf::from(bump.package_info.package_json_path.to_string());
431            let ref bump_changelog_file_path =
432                PathBuf::from(bump.package_info.package_path.to_string())
433                    .join(String::from("CHANGELOG.md"));
434
435            // Write bump_pkg_json_file_path
436            let bump_pkg_json_file = OpenOptions::new()
437                .write(true)
438                .append(false)
439                .open(bump_pkg_json_file_path)
440                .unwrap();
441            let pkg_json_writer = BufWriter::new(bump_pkg_json_file);
442            serde_json::to_writer_pretty(pkg_json_writer, &bump.package_info.pkg_json).unwrap();
443
444            let conventional = get_conventional_for_package(
445                &bump.package_info,
446                options.fetch_all.to_owned(),
447                Some(root.to_string()),
448                &Some(ConventionalPackageOptions {
449                    version: Some(bump.to.to_string()),
450                    title: Some("# What changed?".to_string()),
451                }),
452            );
453
454            // Write bump_changelog_file_path
455            let mut bump_changelog_file = OpenOptions::new()
456                .write(true)
457                .create(true)
458                .append(false)
459                .open(bump_changelog_file_path)
460                .unwrap();
461
462            bump_changelog_file
463                .write_all(conventional.changelog_output.as_bytes())
464                .unwrap();
465
466            let ref package_tag = format!("{}@{}", bump.package_info.name, bump.to);
467
468            git_add_all(&root.to_string()).expect("Failed to add all files to git");
469            git_commit(
470                git_message.unwrap_or(String::from("chore: release version")),
471                None,
472                None,
473                Some(root.to_string()),
474            )
475            .unwrap();
476            git_tag(
477                package_tag.to_string(),
478                Some(format!(
479                    "chore: release {} to version {}",
480                    bump.package_info.name, bump.to
481                )),
482                Some(root.to_string()),
483            )
484            .unwrap();
485
486            if options.push.unwrap_or(false) {
487                git_push(Some(root.to_string()), Some(true)).unwrap();
488            }
489        }
490    }
491
492    bumps
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::changes::{add_change, get_change, init_changes};
499    use crate::manager::PackageManager;
500    use crate::packages::get_changed_packages;
501    use crate::paths::get_project_root_path;
502    use crate::utils::create_test_monorepo;
503    use std::fs::remove_dir_all;
504    use std::fs::File;
505    use std::io::Write;
506    use std::process::Command;
507    use std::process::Stdio;
508
509    fn create_single_changes(root: &String) -> Result<(), Box<dyn std::error::Error>> {
510        let change_package_a = Change {
511            package: String::from("@scope/package-a"),
512            release_as: Bump::Major,
513            deploy: vec![String::from("production")],
514        };
515
516        init_changes(Some(root.to_string()), &None);
517
518        add_change(&change_package_a, Some(root.to_string()));
519
520        Ok(())
521    }
522
523    fn create_single_package(monorepo_dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
524        let js_path = monorepo_dir.join("packages/package-a/index.js");
525
526        let branch = Command::new("git")
527            .current_dir(&monorepo_dir)
528            .arg("checkout")
529            .arg("-b")
530            .arg("feat/message")
531            .stdout(Stdio::piped())
532            .spawn()
533            .expect("Git branch problem");
534
535        branch.wait_with_output()?;
536
537        let mut js_file = File::create(&js_path)?;
538        js_file
539            .write_all(r#"export const message = "hello package-a";"#.as_bytes())
540            .unwrap();
541
542        let add = Command::new("git")
543            .current_dir(&monorepo_dir)
544            .arg("add")
545            .arg(".")
546            .stdout(Stdio::piped())
547            .spawn()
548            .expect("Git add problem");
549
550        add.wait_with_output()?;
551
552        let commit = Command::new("git")
553            .current_dir(&monorepo_dir)
554            .arg("commit")
555            .arg("-m")
556            .arg("feat: message to the world")
557            .stdout(Stdio::piped())
558            .spawn()
559            .expect("Git commit problem");
560
561        commit.wait_with_output()?;
562
563        Ok(())
564    }
565
566    fn create_multiple_changes(root: &String) -> Result<(), Box<dyn std::error::Error>> {
567        let change_package_a = Change {
568            package: String::from("@scope/package-a"),
569            release_as: Bump::Major,
570            deploy: vec![String::from("production")],
571        };
572
573        let change_package_c = Change {
574            package: String::from("@scope/package-c"),
575            release_as: Bump::Minor,
576            deploy: vec![String::from("production")],
577        };
578
579        init_changes(Some(root.to_string()), &None);
580
581        add_change(&change_package_a, Some(root.to_string()));
582        add_change(&change_package_c, Some(root.to_string()));
583
584        Ok(())
585    }
586
587    fn create_multiple_packages(monorepo_dir: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
588        let js_path_package_a = monorepo_dir.join("packages/package-a/index.js");
589        let js_path_package_c = monorepo_dir.join("packages/package-c/index.js");
590
591        let branch = Command::new("git")
592            .current_dir(&monorepo_dir)
593            .arg("checkout")
594            .arg("-b")
595            .arg("feat/message")
596            .stdout(Stdio::piped())
597            .spawn()
598            .expect("Git branch problem");
599
600        branch.wait_with_output()?;
601
602        let mut js_file_package_a = File::create(&js_path_package_a)?;
603        js_file_package_a
604            .write_all(r#"export const message = "hello package-a";"#.as_bytes())
605            .unwrap();
606
607        let mut js_file_package_c = File::create(&js_path_package_c)?;
608        js_file_package_c
609            .write_all(r#"export const message = "hello package-c";"#.as_bytes())
610            .unwrap();
611
612        let add = Command::new("git")
613            .current_dir(&monorepo_dir)
614            .arg("add")
615            .arg(".")
616            .stdout(Stdio::piped())
617            .spawn()
618            .expect("Git add problem");
619
620        add.wait_with_output()?;
621
622        let commit = Command::new("git")
623            .current_dir(&monorepo_dir)
624            .arg("commit")
625            .arg("-m")
626            .arg("feat: message to the world")
627            .stdout(Stdio::piped())
628            .spawn()
629            .expect("Git commit problem");
630
631        commit.wait_with_output()?;
632
633        Ok(())
634    }
635
636    fn create_single_dependency_changes(root: &String) -> Result<(), Box<dyn std::error::Error>> {
637        let change_package_a = Change {
638            package: String::from("@scope/package-b"),
639            release_as: Bump::Snapshot,
640            deploy: vec![String::from("production")],
641        };
642
643        init_changes(Some(root.to_string()), &None);
644
645        add_change(&change_package_a, Some(root.to_string()));
646
647        Ok(())
648    }
649
650    fn create_single_dependency_package(
651        monorepo_dir: &PathBuf,
652    ) -> Result<(), Box<dyn std::error::Error>> {
653        let js_path = monorepo_dir.join("packages/package-b/index.js");
654
655        let branch = Command::new("git")
656            .current_dir(&monorepo_dir)
657            .arg("checkout")
658            .arg("-b")
659            .arg("feat/message")
660            .stdout(Stdio::piped())
661            .spawn()
662            .expect("Git branch problem");
663
664        branch.wait_with_output()?;
665
666        let mut js_file = File::create(&js_path)?;
667        js_file
668            .write_all(r#"export const message = "hello package-b";"#.as_bytes())
669            .unwrap();
670
671        let add = Command::new("git")
672            .current_dir(&monorepo_dir)
673            .arg("add")
674            .arg(".")
675            .stdout(Stdio::piped())
676            .spawn()
677            .expect("Git add problem");
678
679        add.wait_with_output()?;
680
681        let commit = Command::new("git")
682            .current_dir(&monorepo_dir)
683            .arg("commit")
684            .arg("-m")
685            .arg("feat: message to the world")
686            .stdout(Stdio::piped())
687            .spawn()
688            .expect("Git commit problem");
689
690        commit.wait_with_output()?;
691
692        Ok(())
693    }
694
695    fn create_multiple_dependency_changes(root: &String) -> Result<(), Box<dyn std::error::Error>> {
696        let change_package_a = Change {
697            package: String::from("@scope/package-a"),
698            release_as: Bump::Major,
699            deploy: vec![String::from("production")],
700        };
701
702        let change_package_b = Change {
703            package: String::from("@scope/package-b"),
704            release_as: Bump::Major,
705            deploy: vec![String::from("production")],
706        };
707
708        init_changes(Some(root.to_string()), &None);
709
710        add_change(&change_package_a, Some(root.to_string()));
711        add_change(&change_package_b, Some(root.to_string()));
712
713        Ok(())
714    }
715
716    fn create_multiple_dependency_packages(
717        monorepo_dir: &PathBuf,
718    ) -> Result<(), Box<dyn std::error::Error>> {
719        let js_path = monorepo_dir.join("packages/package-b/index.js");
720        let js_path_no_depend = monorepo_dir.join("packages/package-a/index.js");
721
722        let branch = Command::new("git")
723            .current_dir(&monorepo_dir)
724            .arg("checkout")
725            .arg("-b")
726            .arg("feat/message")
727            .stdout(Stdio::piped())
728            .spawn()
729            .expect("Git branch problem");
730
731        branch.wait_with_output()?;
732
733        let mut js_file_no_depend = File::create(&js_path_no_depend)?;
734        js_file_no_depend
735            .write_all(r#"export const message = "hello package-a";"#.as_bytes())
736            .unwrap();
737
738        let mut js_file = File::create(&js_path)?;
739        js_file
740            .write_all(r#"export const message = "hello package-b";"#.as_bytes())
741            .unwrap();
742
743        let add = Command::new("git")
744            .current_dir(&monorepo_dir)
745            .arg("add")
746            .arg(".")
747            .stdout(Stdio::piped())
748            .spawn()
749            .expect("Git add problem");
750
751        add.wait_with_output()?;
752
753        let commit = Command::new("git")
754            .current_dir(&monorepo_dir)
755            .arg("commit")
756            .arg("-m")
757            .arg("feat: message to the world")
758            .stdout(Stdio::piped())
759            .spawn()
760            .expect("Git commit problem");
761
762        commit.wait_with_output()?;
763
764        Ok(())
765    }
766
767    #[test]
768    fn test_single_get_bumps() -> Result<(), Box<dyn std::error::Error>> {
769        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm).unwrap();
770        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf())).unwrap();
771
772        let ref root = project_root.to_string();
773
774        create_single_package(monorepo_dir)?;
775        create_single_changes(&root)?;
776
777        let changes = get_change(String::from("feat/message"), Some(root.to_string()));
778
779        let bumps = get_bumps(&BumpOptions {
780            changes,
781            since: Some(String::from("main")),
782            release_as: Some(Bump::Major),
783            fetch_all: None,
784            fetch_tags: None,
785            sync_deps: Some(false),
786            push: Some(false),
787            cwd: Some(root.to_string()),
788        });
789
790        dbg!(&bumps);
791
792        assert_eq!(bumps.len(), 1);
793
794        let first_bump = bumps.get(0);
795
796        assert_eq!(first_bump.is_some(), true);
797
798        remove_dir_all(&monorepo_dir)?;
799        Ok(())
800    }
801
802    #[test]
803    fn test_multiple_get_bumps() -> Result<(), Box<dyn std::error::Error>> {
804        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm).unwrap();
805        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf())).unwrap();
806
807        let ref root = project_root.to_string();
808
809        create_multiple_packages(monorepo_dir)?;
810        create_multiple_changes(&root)?;
811
812        let changes = get_change(String::from("feat/message"), Some(root.to_string()));
813
814        let bumps = get_bumps(&BumpOptions {
815            changes,
816            since: Some(String::from("main")),
817            release_as: None,
818            fetch_all: None,
819            fetch_tags: None,
820            sync_deps: Some(false),
821            push: Some(false),
822            cwd: Some(root.to_string()),
823        });
824
825        assert_eq!(bumps.len(), 2);
826
827        let first_bump = bumps.get(0);
828        let second_bump = bumps.get(1);
829
830        assert_eq!(first_bump.is_some(), true);
831        assert_eq!(second_bump.is_some(), true);
832
833        remove_dir_all(&monorepo_dir)?;
834        Ok(())
835    }
836
837    #[test]
838    fn test_single_dependency_get_bumps() -> Result<(), Box<dyn std::error::Error>> {
839        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm).unwrap();
840        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf())).unwrap();
841
842        let ref root = project_root.to_string();
843
844        create_single_dependency_package(monorepo_dir)?;
845        create_single_dependency_changes(&root)?;
846
847        let changes = get_change(String::from("feat/message"), Some(root.to_string()));
848
849        let bumps = get_bumps(&BumpOptions {
850            changes,
851            since: Some(String::from("main")),
852            release_as: None,
853            fetch_all: None,
854            fetch_tags: None,
855            sync_deps: Some(true),
856            push: Some(false),
857            cwd: Some(root.to_string()),
858        });
859
860        assert_eq!(bumps.len(), 2);
861
862        let first_bump = bumps.get(0);
863        let second_bump = bumps.get(1);
864
865        assert_eq!(first_bump.is_some(), true);
866        assert_eq!(second_bump.is_some(), true);
867
868        remove_dir_all(&monorepo_dir)?;
869        Ok(())
870    }
871
872    #[test]
873    fn test_multiple_dependency_get_bumps() -> Result<(), Box<dyn std::error::Error>> {
874        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm).unwrap();
875        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf())).unwrap();
876
877        let ref root = project_root.to_string();
878
879        create_multiple_dependency_packages(monorepo_dir)?;
880        create_multiple_dependency_changes(&root)?;
881
882        let changes = get_change(String::from("feat/message"), Some(root.to_string()));
883
884        let bumps = get_bumps(&BumpOptions {
885            changes,
886            since: Some(String::from("main")),
887            release_as: None,
888            fetch_all: None,
889            fetch_tags: None,
890            sync_deps: Some(true),
891            push: Some(false),
892            cwd: Some(root.to_string()),
893        });
894
895        assert_eq!(bumps.len(), 3);
896
897        let first_bump = bumps.get(0);
898        let second_bump = bumps.get(1);
899        let third_bump = bumps.get(2);
900
901        assert_eq!(first_bump.is_some(), true);
902        assert_eq!(second_bump.is_some(), true);
903        assert_eq!(third_bump.is_some(), true);
904
905        remove_dir_all(&monorepo_dir)?;
906        Ok(())
907    }
908
909    #[test]
910    fn test_apply_bumps() -> Result<(), Box<dyn std::error::Error>> {
911        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
912        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
913
914        create_multiple_dependency_packages(monorepo_dir)?;
915
916        let ref root = project_root.unwrap().to_string();
917
918        let packages = get_changed_packages(Some(String::from("main")), Some(root.to_string()))
919            .iter()
920            .map(|package| package.name.to_string())
921            .collect::<Vec<String>>();
922
923        init_changes(Some(root.to_string()), &None);
924
925        for package in packages {
926            let change_package = Change {
927                package: package.to_string(),
928                release_as: Bump::Major,
929                deploy: vec![String::from("production")],
930            };
931
932            add_change(&change_package, Some(root.to_string()));
933        }
934
935        let changes = get_change(String::from("feat/message"), Some(root.to_string()));
936
937        let main_branch = Command::new("git")
938            .current_dir(&monorepo_dir)
939            .arg("checkout")
940            .arg("main")
941            .stdout(Stdio::piped())
942            .spawn()
943            .expect("Git checkout main problem");
944
945        main_branch.wait_with_output()?;
946
947        let merge_branch = Command::new("git")
948            .current_dir(&monorepo_dir)
949            .arg("merge")
950            .arg("feat/message")
951            .stdout(Stdio::piped())
952            .spawn()
953            .expect("Git merge problem");
954
955        merge_branch.wait_with_output()?;
956
957        let bump_options = BumpOptions {
958            changes,
959            since: Some(String::from("main")),
960            release_as: Some(Bump::Minor),
961            fetch_all: None,
962            fetch_tags: None,
963            sync_deps: Some(true),
964            push: Some(false),
965            cwd: Some(root.to_string()),
966        };
967
968        let bumps = apply_bumps(&bump_options);
969
970        assert_eq!(bumps.len(), 3);
971        remove_dir_all(&monorepo_dir)?;
972        Ok(())
973    }
974}