workspace_node_tools/
git.rs

1//! # Git
2//!
3//! This module provides a set of functions to interact with git.
4#![allow(clippy::all)]
5use execute::Execute;
6use icu::collator::{Collator, CollatorOptions, Numeric, Strength};
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::io::Write;
10use std::path::PathBuf;
11use std::{
12    env::temp_dir,
13    fs::{remove_file, File},
14    path::Path,
15    process::{Command, Stdio},
16};
17use version_compare::{Cmp, Version};
18
19use super::packages::PackageInfo;
20use super::paths::get_project_root_path;
21use super::utils::{package_scope_name_version, strip_trailing_newline};
22
23#[cfg(feature = "napi")]
24#[napi(object)]
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct Commit {
27    pub hash: String,
28    pub author_name: String,
29    pub author_email: String,
30    pub author_date: String,
31    pub message: String,
32}
33
34#[cfg(not(feature = "napi"))]
35#[derive(Debug, Clone, Deserialize, Serialize)]
36/// A struct that represents a commit information
37pub struct Commit {
38    pub hash: String,
39    pub author_name: String,
40    pub author_email: String,
41    pub author_date: String,
42    pub message: String,
43}
44
45#[cfg(feature = "napi")]
46#[napi(object)]
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct RemoteTags {
49    pub hash: String,
50    pub tag: String,
51}
52
53#[cfg(not(feature = "napi"))]
54#[derive(Debug, Clone, Deserialize, Serialize)]
55/// A struct that represents a remote tag information
56pub struct RemoteTags {
57    pub hash: String,
58    pub tag: String,
59}
60
61#[cfg(feature = "napi")]
62#[napi(object)]
63#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct PublishTagInfo {
65    pub hash: String,
66    pub tag: String,
67    pub package: String,
68}
69
70#[cfg(not(feature = "napi"))]
71#[derive(Debug, Clone, Deserialize, Serialize)]
72/// A struct that represents a publish tag information
73pub struct PublishTagInfo {
74    pub hash: String,
75    pub tag: String,
76    pub package: String,
77}
78
79/// Stage all uncommitted changes
80pub fn git_add_all(cwd: &String) -> Result<bool, std::io::Error> {
81    let mut git_add = Command::new("git");
82
83    git_add.current_dir(cwd.to_string()).arg("add").arg(".");
84
85    git_add.stdout(Stdio::piped());
86    git_add.stderr(Stdio::piped());
87
88    let output = git_add.execute_output().unwrap();
89
90    if output.status.success() {
91        Ok(true)
92    } else {
93        Ok(false)
94    }
95}
96
97/// Add a file to the git stage
98pub fn git_add(cwd: &String, file: &String) -> Result<bool, std::io::Error> {
99    let mut git_add = Command::new("git");
100
101    git_add.current_dir(cwd.to_string()).arg("add").arg(file);
102
103    git_add.stdout(Stdio::piped());
104    git_add.stderr(Stdio::piped());
105
106    let output = git_add.execute_output().unwrap();
107
108    if output.status.success() {
109        Ok(true)
110    } else {
111        Ok(false)
112    }
113}
114
115/// Configure git user name and email
116pub fn git_config(username: &String, email: &String, cwd: &String) -> Result<bool, std::io::Error> {
117    let mut git_config_user = Command::new("git");
118
119    git_config_user
120        .current_dir(cwd.to_string())
121        .arg("config")
122        .arg("user.name")
123        .arg(username);
124
125    git_config_user.stdout(Stdio::piped());
126    git_config_user.stderr(Stdio::piped());
127
128    let output_user = git_config_user.execute_output().unwrap();
129
130    let mut git_config_email = Command::new("git");
131    git_config_email
132        .current_dir(cwd.to_string())
133        .arg("config")
134        .arg("user.email")
135        .arg(email);
136
137    git_config_email.stdout(Stdio::piped());
138    git_config_email.stderr(Stdio::piped());
139
140    let output_email = git_config_email.execute_output().unwrap();
141    let status = output_user.status.success() == output_email.status.success();
142
143    if status {
144        Ok(true)
145    } else {
146        Ok(false)
147    }
148}
149
150/// Fetch everything from origin including tags
151pub fn git_fetch_all(
152    cwd: Option<String>,
153    fetch_tags: Option<bool>,
154) -> Result<bool, std::io::Error> {
155    let current_working_dir = match cwd {
156        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
157        None => get_project_root_path(None).unwrap(),
158    };
159
160    let mut command = Command::new("git");
161    command.arg("fetch").arg("origin");
162
163    if fetch_tags.unwrap_or(false) {
164        command.arg("--tags").arg("--force");
165    }
166
167    command.current_dir(&current_working_dir);
168
169    command.stdout(Stdio::piped());
170    command.stderr(Stdio::piped());
171
172    let output = command.execute_output().unwrap();
173
174    if output.status.success() {
175        Ok(true)
176    } else {
177        Ok(false)
178    }
179}
180
181/// Get the diverged commit from a particular git SHA or tag.
182pub fn get_diverged_commit(refer: String, cwd: Option<String>) -> Option<String> {
183    let current_working_dir = match cwd {
184        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
185        None => get_project_root_path(None).unwrap(),
186    };
187
188    let mut command = Command::new("git");
189    command.arg("merge-base").arg(&refer).arg("HEAD");
190    command.current_dir(&current_working_dir);
191
192    command.stdout(Stdio::piped());
193    command.stderr(Stdio::piped());
194
195    let output = command.execute_output().unwrap();
196
197    if !output.status.success() {
198        return None;
199    }
200
201    let output = String::from_utf8(output.stdout).unwrap();
202
203    Some(strip_trailing_newline(&output))
204}
205
206/// Get the current commit id
207pub fn git_current_sha(cwd: Option<String>) -> String {
208    let current_working_dir = match cwd {
209        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
210        None => get_project_root_path(None).unwrap(),
211    };
212
213    let mut command = Command::new("git");
214    command.arg("rev-parse").arg("--short").arg("HEAD");
215
216    command.current_dir(&current_working_dir);
217
218    command.stdout(Stdio::piped());
219    command.stderr(Stdio::piped());
220
221    let output = command.execute_output().unwrap();
222
223    let hash = String::from_utf8(output.stdout).unwrap();
224    strip_trailing_newline(&hash)
225}
226
227/// Get the previous commit id
228pub fn git_previous_sha(cwd: Option<String>) -> String {
229    let current_working_dir = match cwd {
230        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
231        None => get_project_root_path(None).unwrap(),
232    };
233
234    let mut command = Command::new("git");
235    command.arg("rev-parse").arg("--short").arg("HEAD~1");
236
237    command.current_dir(&current_working_dir);
238
239    command.stdout(Stdio::piped());
240    command.stderr(Stdio::piped());
241
242    let output = command.execute_output().unwrap();
243
244    let hash = String::from_utf8(output.stdout).unwrap();
245
246    strip_trailing_newline(&hash)
247}
248
249/// Get the first commit in a branch
250pub fn git_first_sha(cwd: Option<String>, branch: Option<String>) -> String {
251    let current_working_dir = match cwd {
252        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
253        None => get_project_root_path(None).unwrap(),
254    };
255
256    let branch = match branch {
257        Some(branch) => branch,
258        None => String::from("main"),
259    };
260
261    let mut command = Command::new("git");
262    command
263        .arg("log")
264        .arg(format!("{}..HEAD", branch))
265        .arg("--online")
266        .arg("--pretty=format:%h")
267        .arg("|")
268        .arg("tail")
269        .arg("-1");
270
271    command.current_dir(&current_working_dir);
272
273    command.stdout(Stdio::piped());
274    command.stderr(Stdio::piped());
275
276    let output = command.execute_output().unwrap();
277
278    let hash = String::from_utf8(output.stdout).unwrap();
279
280    strip_trailing_newline(&hash)
281}
282
283/// Verify if as uncommited changes in the current working directory
284pub fn git_workdir_unclean(cwd: Option<String>) -> bool {
285    let current_working_dir = match cwd {
286        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
287        None => get_project_root_path(None).unwrap(),
288    };
289
290    let mut command = Command::new("git");
291    command.arg("status").arg("--porcelain");
292
293    command.current_dir(&current_working_dir);
294
295    command.stdout(Stdio::piped());
296    command.stderr(Stdio::piped());
297
298    let output = command.execute_output().unwrap();
299
300    let output = String::from_utf8(output.stdout).unwrap();
301    let result = strip_trailing_newline(&output);
302
303    if result.is_empty() {
304        return false;
305    }
306
307    true
308}
309
310/// Get the current branch name
311pub fn git_current_branch(cwd: Option<String>) -> Option<String> {
312    let current_working_dir = match cwd {
313        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
314        None => get_project_root_path(None).unwrap(),
315    };
316
317    let mut command = Command::new("git");
318    command.arg("rev-parse").arg("--abbrev-ref").arg("HEAD");
319
320    command.current_dir(&current_working_dir);
321
322    command.stdout(Stdio::piped());
323    command.stderr(Stdio::piped());
324
325    let output = command.execute_output().unwrap();
326
327    let output = String::from_utf8(output.stdout).unwrap();
328    let result = strip_trailing_newline(&output);
329
330    if result.is_empty() {
331        return None;
332    }
333
334    Some(result)
335}
336
337/// Get the branch (last) name for a commit
338pub fn git_branch_from_commit(commit: String, cwd: Option<String>) -> Option<String> {
339    let current_working_dir = match cwd {
340        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
341        None => get_project_root_path(None).unwrap(),
342    };
343
344    // git --no-pager branch --no-color --no-column --format "%(refname:lstrip=2)" --contains <commit>
345    let mut command = Command::new("git");
346    command
347        .arg("--no-pager")
348        .arg("branch")
349        .arg("--no-color")
350        .arg("--no-column")
351        .arg("--format")
352        .arg(r#""%(refname:lstrip=2)""#)
353        .arg("--contains")
354        .arg(&commit);
355
356    command.current_dir(&current_working_dir);
357
358    command.stdout(Stdio::piped());
359    command.stderr(Stdio::piped());
360
361    let output = command.execute_output().unwrap();
362
363    let output = String::from_utf8(output.stdout).unwrap();
364    let result = strip_trailing_newline(&output);
365
366    if result.is_empty() {
367        return None;
368    }
369
370    Some(result)
371}
372
373/// Tags the current commit with a message
374pub fn git_tag(
375    tag: String,
376    message: Option<String>,
377    cwd: Option<String>,
378) -> Result<bool, std::io::Error> {
379    let current_working_dir = match cwd {
380        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
381        None => get_project_root_path(None).unwrap(),
382    };
383
384    let default_message = &tag;
385    let msg = message.or(Some(default_message.to_string())).unwrap();
386
387    let mut command = Command::new("git");
388    command.arg("tag").arg("-a").arg(&tag).arg("-m").arg(&msg);
389
390    command.current_dir(&current_working_dir);
391
392    command.stdout(Stdio::piped());
393    command.stderr(Stdio::piped());
394
395    let output = command.execute_output().unwrap();
396
397    if output.status.success() {
398        Ok(true)
399    } else {
400        Ok(false)
401    }
402}
403
404/// Pushes all changes in the monorepo without verification and follow tags
405pub fn git_push(cwd: Option<String>, follow_tags: Option<bool>) -> Result<bool, std::io::Error> {
406    let current_working_dir = match cwd {
407        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
408        None => get_project_root_path(None).unwrap(),
409    };
410
411    let mut command = Command::new("git");
412    command.arg("push");
413
414    if follow_tags.unwrap_or(false) {
415        command.arg("--follow-tags");
416    }
417
418    command.arg("--no-verify");
419    command.current_dir(&current_working_dir);
420
421    command.stdout(Stdio::piped());
422    command.stderr(Stdio::piped());
423
424    let output = command.execute_output().unwrap();
425
426    if output.status.success() {
427        Ok(true)
428    } else {
429        Ok(false)
430    }
431}
432
433// Commit all changes in the monorepo
434pub fn git_commit(
435    mut message: String,
436    body: Option<String>,
437    footer: Option<String>,
438    cwd: Option<String>,
439) -> Result<bool, std::io::Error> {
440    let current_working_dir = match cwd {
441        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
442        None => get_project_root_path(None).unwrap(),
443    };
444
445    if body.is_some() {
446        message.push_str("\n\n");
447        message.push_str(body.unwrap().as_str());
448    }
449
450    if footer.is_some() {
451        message.push_str("\n\n");
452        message.push_str(footer.unwrap().as_str());
453    }
454
455    let temp_dir = temp_dir();
456    let temp_file_path = temp_dir.join("commit_message.txt");
457
458    let mut file = File::create(&temp_file_path).unwrap();
459    file.write_all(message.as_bytes()).unwrap();
460
461    let file_path = temp_file_path.as_path();
462
463    let mut command = Command::new("git");
464    command
465        .arg("commit")
466        .arg("-F")
467        .arg(&file_path.to_str().unwrap())
468        .arg("--no-verify");
469
470    command.current_dir(&current_working_dir);
471
472    command.stdout(Stdio::piped());
473    command.stderr(Stdio::piped());
474
475    let output = command.execute_output().unwrap();
476
477    remove_file(file_path).expect("Commit file not deleted");
478
479    if output.status.success() {
480        Ok(true)
481    } else {
482        Ok(false)
483    }
484}
485
486/// Given a specific git sha, finds all files that have been modified
487/// since the sha and returns the absolute filepaths.
488pub fn git_all_files_changed_since_sha(sha: String, cwd: Option<String>) -> Vec<String> {
489    let current_working_dir = match cwd {
490        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
491        None => get_project_root_path(None).unwrap(),
492    };
493
494    let mut command = Command::new("git");
495    command
496        .arg("--no-pager")
497        .arg("diff")
498        .arg("--name-only")
499        .arg(format!("{}", sha));
500    command.current_dir(&current_working_dir);
501
502    command.stdout(Stdio::piped());
503    command.stderr(Stdio::piped());
504
505    let output = command.execute_output().unwrap();
506
507    if !output.status.success() {
508        return vec![];
509    }
510
511    let output = String::from_utf8(output.stdout).unwrap();
512    let root = Path::new(&current_working_dir);
513
514    output
515        .split("\n")
516        .filter(|item| !item.trim().is_empty())
517        .map(|item| root.join(item))
518        .filter(|item| item.exists())
519        .map(|item| item.to_str().unwrap().to_string())
520        .collect::<Vec<String>>()
521}
522
523/// Returns commits since a particular git SHA or tag.
524/// If the "since" parameter isn't provided, all commits
525/// from the dawn of man are returned
526pub fn get_commits_since(
527    cwd: Option<String>,
528    since: Option<String>,
529    relative: Option<String>,
530) -> Vec<Commit> {
531    let current_working_dir = match cwd {
532        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
533        None => get_project_root_path(None).unwrap(),
534    };
535
536    const DELIMITER: &str = r#"#=#"#;
537    const BREAK_LINE: &str = r#"#+#"#;
538
539    let mut command = Command::new("git");
540    command
541        .arg("--no-pager")
542        .arg("log")
543        .arg(format!(
544            "--format={}%H{}%an{}%ae{}%ad{}%B{}",
545            DELIMITER, DELIMITER, DELIMITER, DELIMITER, DELIMITER, BREAK_LINE
546        ))
547        .arg("--date=rfc2822");
548
549    if let Some(since) = since {
550        command.arg(format!("{}..", since));
551    }
552
553    if let Some(relative) = relative {
554        command.arg("--");
555        command.arg(&relative);
556    }
557
558    command.current_dir(&current_working_dir);
559
560    command.stdout(Stdio::piped());
561    command.stderr(Stdio::piped());
562
563    let output = command.execute_output().unwrap();
564
565    if !output.status.success() {
566        return vec![];
567    }
568
569    let output = String::from_utf8(output.stdout).unwrap();
570
571    output
572        .split(BREAK_LINE)
573        .filter(|item| !item.trim().is_empty())
574        .map(|item| {
575            let item_trimmed = item.trim();
576            let items = item_trimmed.split(DELIMITER).collect::<Vec<&str>>();
577
578            Commit {
579                hash: items.get(1).unwrap().to_string(),
580                author_name: items.get(2).unwrap().to_string(),
581                author_email: items.get(3).unwrap().to_string(),
582                author_date: items.get(4).unwrap().to_string(),
583                message: items.get(5).unwrap().to_string(),
584            }
585        })
586        .collect::<Vec<Commit>>()
587}
588
589/// Grabs the full list of all tags available on upstream or local
590pub fn get_remote_or_local_tags(cwd: Option<String>, local: Option<bool>) -> Vec<RemoteTags> {
591    let current_working_dir = match cwd {
592        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
593        None => get_project_root_path(None).unwrap(),
594    };
595
596    let mut command = Command::new("git");
597
598    match local {
599        Some(true) => command.arg("show-ref").arg("--tags"),
600        Some(false) => command.arg("ls-remote").arg("--tags").arg("origin"),
601        None => command.arg("ls-remote").arg("--tags").arg("origin"),
602    };
603
604    command.current_dir(&current_working_dir);
605
606    command.stdout(Stdio::piped());
607    command.stderr(Stdio::piped());
608
609    let output = command.execute_output().unwrap();
610
611    if !output.status.success() {
612        return vec![];
613    }
614
615    let output = String::from_utf8(output.stdout).unwrap();
616
617    #[cfg(windows)]
618    const LINE_ENDING: &'static str = "\r\n";
619    #[cfg(not(windows))]
620    const LINE_ENDING: &'static str = "\n";
621
622    output
623        .trim()
624        .split(LINE_ENDING)
625        .filter(|tags| !tags.trim().is_empty())
626        .map(|tags| {
627            let hash_tags = Regex::new(r"\s+")
628                .unwrap()
629                .split(tags)
630                .collect::<Vec<&str>>();
631
632            RemoteTags {
633                hash: hash_tags.get(0).unwrap().to_string(),
634                tag: hash_tags.get(1).unwrap().to_string(),
635            }
636        })
637        .collect::<Vec<RemoteTags>>()
638}
639
640/// Given an input of the "main" branch name,
641/// returns all the files that have changed since the current branch was created
642pub fn get_all_files_changed_since_branch(
643    package_info: &Vec<PackageInfo>,
644    branch: &String,
645    cwd: Option<String>,
646) -> Vec<String> {
647    let current_working_dir = match cwd {
648        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
649        None => get_project_root_path(None).unwrap(),
650    };
651
652    let mut all_files = vec![];
653
654    package_info.iter().for_each(|item| {
655        let files = git_all_files_changed_since_sha(
656            branch.to_string(),
657            Some(current_working_dir.to_string()),
658        );
659
660        let pkg_files = files
661            .iter()
662            .filter(|file| file.starts_with(item.package_path.as_str()))
663            .collect::<Vec<&String>>();
664
665        all_files.append(
666            &mut pkg_files
667                .iter()
668                .map(|file| file.to_string())
669                .collect::<Vec<String>>(),
670        );
671    });
672
673    all_files
674}
675
676/// Grabs the last known publish tag info for a package
677pub fn get_last_known_publish_tag_info_for_package(
678    package_info: &PackageInfo,
679    cwd: Option<String>,
680) -> Option<PublishTagInfo> {
681    let current_working_dir = match cwd {
682        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
683        None => get_project_root_path(None).unwrap(),
684    };
685
686    let mut remote_tags =
687        get_remote_or_local_tags(Some(current_working_dir.to_string()), Some(false));
688    let mut local_tags =
689        get_remote_or_local_tags(Some(current_working_dir.to_string()), Some(true));
690
691    /*let mut remote_tags = vec![
692        RemoteTags {
693            hash: String::from("ddd1fa69be3e6c6a8b2f18af8f8f5607106188db"),
694            tag: String::from("refs/tags/@b2x/workspace-node@1.0.4")
695        },
696        RemoteTags {
697            hash: String::from("c5353e1f3c9385c35f64e838a0a09dc4bb8f7b07"),
698            tag: String::from("refs/tags/@b2x/workspace-node@1.0.2")
699        }
700    ];
701
702    let mut local_tags = vec![
703        RemoteTags {
704            hash: String::from("4a16b15bb5cfeca493c79231452e94e56487d6b4"),
705            tag: String::from("refs/tags/@b2x/workspace-node@0.9.9")
706        },
707        RemoteTags {
708            hash: String::from("ee5f8209e6d3b06fbf5712e424652e909a4cb5c2"),
709            tag: String::from("refs/tags/@b2x/workspace-node@1.0.5")
710        }
711    ];*/
712
713    remote_tags.append(&mut local_tags);
714
715    let mut options = CollatorOptions::new();
716    options.strength = Some(Strength::Secondary);
717    options.numeric = Some(Numeric::On);
718
719    let collator = Collator::try_new(&Default::default(), options).unwrap();
720
721    remote_tags.sort_by(|a, b| {
722        let tag_a = a.tag.replace("refs/tags/", "");
723        let tag_b = b.tag.replace("refs/tags/", "");
724
725        collator.compare(&tag_b, &tag_a)
726    });
727
728    let package_tag = format!("{}@{}", package_info.name, package_info.version);
729
730    let mut match_tag = remote_tags.iter().find(|item| {
731        let tag = item.tag.replace("refs/tags/", "");
732        let matches: Vec<&str> = tag.matches(&package_tag).collect();
733
734        if matches.len() > 0 {
735            return true;
736        } else {
737            return false;
738        }
739    });
740
741    if match_tag.is_none() {
742        let mut highest_tag = None;
743
744        remote_tags.iter().for_each(|item| {
745            let tag = &item.tag.replace("refs/tags/", "");
746
747            if tag.contains(&package_info.name) {
748                if highest_tag.is_none() {
749                    highest_tag = Some(String::from(tag));
750                }
751
752                let high_tag = highest_tag.as_ref().unwrap();
753                let current_tag_meta = package_scope_name_version(tag).unwrap();
754                let highest_tag_meta = package_scope_name_version(high_tag).unwrap();
755
756                let current_version = Version::from(&current_tag_meta.version).unwrap();
757                let highest_version = Version::from(&highest_tag_meta.version).unwrap();
758
759                if current_version.compare_to(&highest_version, Cmp::Gt) {
760                    highest_tag = Some(String::from(tag));
761                }
762            }
763        });
764
765        if highest_tag.is_some() {
766            let highest_tag = highest_tag.unwrap();
767            let highest_tag_meta = package_scope_name_version(&highest_tag).unwrap();
768
769            match_tag = remote_tags.iter().find(|item| {
770                let tag = item.tag.replace("refs/tags/", "");
771                let matches: Vec<&str> = tag.matches(&highest_tag_meta.full).collect();
772
773                if matches.len() > 0 {
774                    return true;
775                } else {
776                    return false;
777                }
778            });
779        }
780    }
781
782    if match_tag.is_some() {
783        let hash = &match_tag.unwrap().hash;
784        let tag = &match_tag.unwrap().tag;
785        let package = &package_info.name;
786
787        return Some(PublishTagInfo {
788            hash: hash.to_string(),
789            tag: tag.to_string(),
790            package: package.to_string(),
791        });
792    }
793
794    None
795}
796
797/// Grabs the last known publish tag info for all packages in the monorepo
798pub fn get_last_known_publish_tag_info_for_all_packages(
799    package_info: &Vec<PackageInfo>,
800    cwd: Option<String>,
801) -> Vec<Option<PublishTagInfo>> {
802    let root = match cwd {
803        Some(dir) => get_project_root_path(Some(PathBuf::from(dir))).unwrap(),
804        None => get_project_root_path(None).unwrap(),
805    };
806
807    git_fetch_all(Some(root.to_string()), Some(true)).expect("Fetch all tags");
808
809    package_info
810        .iter()
811        .map(|item| get_last_known_publish_tag_info_for_package(&item, Some(root.to_string())))
812        .filter(|item| item.is_some())
813        .collect::<Vec<Option<PublishTagInfo>>>()
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819    use crate::{
820        manager::PackageManager, paths::get_project_root_path, utils::create_test_monorepo,
821    };
822    use std::fs::{remove_dir_all, File};
823
824    #[test]
825    fn test_git_fetch_all() -> Result<(), std::io::Error> {
826        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
827        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
828
829        let result = git_fetch_all(project_root, None)?;
830        assert_eq!(result, false);
831        remove_dir_all(&monorepo_dir)?;
832        Ok(())
833    }
834
835    #[test]
836    fn test_get_diverged_commit() -> Result<(), std::io::Error> {
837        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
838        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
839
840        let result = get_diverged_commit(String::from("@scope/package-a@1.0.0"), project_root);
841
842        assert!(result.is_some());
843        remove_dir_all(&monorepo_dir)?;
844        Ok(())
845    }
846
847    #[test]
848    fn test_git_current_sha() -> Result<(), std::io::Error> {
849        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
850        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
851
852        let result = git_current_sha(project_root);
853        assert_eq!(result.is_empty(), false);
854        remove_dir_all(&monorepo_dir)?;
855        Ok(())
856    }
857
858    #[test]
859    fn test_git_previous_sha() -> Result<(), std::io::Error> {
860        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
861        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
862
863        let result = git_previous_sha(project_root);
864        assert_eq!(result.is_empty(), true);
865        remove_dir_all(&monorepo_dir)?;
866        Ok(())
867    }
868
869    #[test]
870    fn test_git_workdir_unclean() -> Result<(), std::io::Error> {
871        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
872        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
873        let js_path = monorepo_dir.join("packages/package-a/index.js");
874
875        let mut js_file = File::create(&js_path)?;
876        js_file.write_all(r#"export const message = "hello";"#.as_bytes())?;
877
878        let result = git_workdir_unclean(project_root);
879        assert_eq!(result, true);
880        remove_dir_all(&monorepo_dir)?;
881        Ok(())
882    }
883
884    #[test]
885    fn test_git_branch_from_commit() -> Result<(), std::io::Error> {
886        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
887        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
888
889        let commit = git_current_sha(Some(project_root.as_ref().unwrap().to_string()));
890        let result = git_branch_from_commit(commit, project_root);
891        assert_eq!(result.is_some(), true);
892        remove_dir_all(&monorepo_dir)?;
893        Ok(())
894    }
895
896    #[test]
897    fn test_get_commits_since() -> Result<(), std::io::Error> {
898        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
899        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
900
901        let result = get_commits_since(
902            project_root,
903            Some(String::from("main")),
904            Some(String::from("packages/package-a")),
905        );
906        let count = result.len();
907
908        assert_eq!(count, 0);
909        remove_dir_all(&monorepo_dir)?;
910        Ok(())
911    }
912
913    #[test]
914    fn test_get_local_tags() -> Result<(), std::io::Error> {
915        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
916        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
917
918        let result = get_remote_or_local_tags(project_root, Some(true));
919        let count = result.len();
920
921        assert_eq!(count, 3);
922        remove_dir_all(&monorepo_dir)?;
923        Ok(())
924    }
925
926    #[test]
927    fn test_git_all_files_changed_since_sha() -> Result<(), std::io::Error> {
928        let ref monorepo_dir = create_test_monorepo(&PackageManager::Npm)?;
929        let project_root = get_project_root_path(Some(monorepo_dir.to_path_buf()));
930
931        let result = git_all_files_changed_since_sha(String::from("main"), project_root);
932        let count = result.len();
933
934        assert_eq!(count, 0);
935        remove_dir_all(&monorepo_dir)?;
936        Ok(())
937    }
938}