git_conform/core/
api.rs

1//! Public interface of the core module
2
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5
6use crate::core::backend::{
7    search_for_repos,
8    exec_async_check
9};
10use crate::utils::{
11    APP_NAME,
12    TrackingFile,
13    repo_is_tracked,
14    repos_valid
15};
16
17use std::fs::{self, OpenOptions};
18use std::io::Write as _;
19use std::path::Path;
20
21/// Scans only specified directories
22pub fn scan_dirs(mut dirs: Vec<String>, tracking_file: &TrackingFile, scan_hidden: bool) -> Result<(), String> {
23    // Remove duplicates
24    dirs.sort_unstable();
25    dirs.dedup();
26
27    // Directories validation
28
29    let mut dirs_ok = true;
30
31    for dir in &mut dirs {
32        let path = Path::new(&dir);
33
34        // Check if the path exists
35        if let Ok(p) = path.try_exists() {
36            if !p {
37                eprintln!("{APP_NAME}: Directory '{dir}' does not exist");
38                dirs_ok = false;
39                continue;
40            }
41        }
42        else {
43            eprintln!("{APP_NAME}: Cannot check the existance of directory '{dir}'");
44            dirs_ok = false;
45            continue;
46        }
47
48        // Check if the path leads to a file
49        if path.is_file() {
50            eprintln!("{APP_NAME}: '{dir}' is not a directory");
51            dirs_ok = false;
52        }
53
54        // Check if the path contains valid UTF-8 characters
55        // and make it absolute, if it does
56        if let Some(s) = fs::canonicalize(&dir)
57            .map_err(|e| format!("{dir}: {e}"))?
58            .to_str() {
59            *dir = s.to_string();
60        }
61        else {
62            eprintln!("{APP_NAME}: {dir}: The path contains invalid UTF-8 characters");
63            dirs_ok = false;
64        }
65    }
66
67    if !dirs_ok {
68        return Err(String::from("Directories validation failed"));
69    }
70
71    search_for_repos(dirs.as_slice(), tracking_file, scan_hidden)?;
72
73    Ok(())
74}
75
76/// Scans all directories in user's /home
77pub fn scan_all(home_dir: String, tracking_file: &TrackingFile, scan_hidden: bool) -> Result<(), String> {
78    search_for_repos(&[home_dir], tracking_file, scan_hidden)?;
79
80    Ok(())
81}
82
83/// Prints the paths of all tracked git repositories to the standard output
84pub fn list(track_file_contents: &str) -> Result<(), String> {
85    if track_file_contents.is_empty() {
86        return Err(String::from("No repository is being tracked"));
87    }
88
89    print!("{track_file_contents}");
90
91    Ok(())
92}
93
94/// Writes the paths of the specified repos to the tracking file
95pub fn add(mut repos: Vec<String>, tracking_file: &TrackingFile) -> Result<(), String> {
96    // Remove duplicates
97    repos.sort_unstable();
98    repos.dedup();
99
100    repos = repos_valid(repos.as_slice())?;
101
102    // Open/create the tracking file for writing
103    let mut track_file = OpenOptions::new()
104        .create(true)
105        .append(true)
106        .open(tracking_file.path.clone())
107        .map_err(|e| format!("{}: {e}", tracking_file.path))?;
108
109    for repo in repos {
110        // Check if the tracking file already
111        // contains the git repository path
112        if repo_is_tracked(repo.as_str(), tracking_file.contents.as_str()) {
113            println!("{APP_NAME}: '{repo}' is already being tracked");
114            continue;
115        }
116
117        // Add the path of the git repository to the tracking file
118        track_file.write_all(
119            format!("{repo}\n").as_bytes())
120            .map_err(|e| format!("{}: {e}", tracking_file.path))?;
121    }
122
123    Ok(())
124}
125
126/// Removes only specified repositories from the tracking file
127pub fn remove_repos(mut repos: Vec<String>, tracking_file: &TrackingFile) -> Result<(), String> {
128    if tracking_file.contents.is_empty() {
129        return Err(String::from("No repository is being tracked"));
130    }
131
132    // Remove duplicates
133    repos.sort_unstable();
134    repos.dedup();
135
136    let mut repos_ok = true;
137
138    // Repositories validation
139    for repo in &repos {
140        // Check if the tracking file contains the git repository
141        if !repo_is_tracked(repo.as_str(), tracking_file.contents.as_str()) {
142            eprintln!("{APP_NAME}: '{repo}' is not being tracked");
143            repos_ok = false;
144        }
145    }
146
147    if !repos_ok {
148        return Err(String::from("Repositories validation failed"));
149    }
150
151    let mut track_file_lines: Vec<&str> = tracking_file.contents.lines().collect();
152
153    // Open/create the tracking file for writing
154    let mut track_file = OpenOptions::new()
155        .write(true)
156        .truncate(true)
157        .open(tracking_file.path.clone())
158        .map_err(|e| format!("{}: {e}", tracking_file.path))?;
159
160    for repo in repos {
161        // Remove specified repositories from the vector
162        if let Some(last) = track_file_lines.last() {
163            if repo.trim() == last.trim() {
164                track_file_lines.pop();
165            }
166            else {
167                track_file_lines.retain(|&x| x.trim() != repo.trim());
168            }
169        }
170    }
171
172    // Write the final changes to the tracking file
173    track_file.write_all(track_file_lines.join("\n").as_bytes())
174        .map_err(|e| format!("{}: {e}", tracking_file.path))?;
175
176    Ok(())
177}
178
179/// Removes the tracking file
180pub fn remove_all(tracking_file: &TrackingFile) -> Result<(), String> {
181    if tracking_file.contents.is_empty() {
182        return Err(String::from("No repository is being tracked"));
183    }
184
185    fs::remove_file(tracking_file.path.clone()).map_err(|e| format!("{}: {e}", tracking_file.path))?;
186
187    Ok(())
188}
189
190/// Asynchronously retrieves important details about each repo
191/// in the repos Vec and prints them to the standard output
192pub async fn check_repos(mut repos: Vec<String>, flags: &[bool]) -> Result<(), String> {
193    // Remove duplicates
194    repos.sort_unstable();
195    repos.dedup();
196
197    repos = repos_valid(repos.as_slice())?;
198
199    exec_async_check(repos, flags.to_vec()).await?;
200
201    Ok(())
202}
203
204/// Asynchronously retrieves important details about each repo
205/// in the tracking file and prints them to the standard output
206pub async fn check_all(tracking_file: &TrackingFile, flags: &[bool]) -> Result<(), String> {
207    if tracking_file.contents.is_empty() {
208        return Err(String::from("No repository is being tracked"));
209    }
210
211    // Put all the tracking file entries in a Vec to
212    // avoid lifetime constraints on async tasks
213    let track_file_lines: Vec<String> = tracking_file.contents
214        .lines()
215        .map(String::from)
216        .collect();
217
218    exec_async_check(track_file_lines, flags.to_vec()).await?;
219
220    Ok(())
221}