git_mover/
utils.rs

1//! Utility functions
2use std::collections::HashSet;
3use std::{fmt::Debug, sync::Arc};
4
5use serde::{Deserialize, Serialize};
6use std::process::Stdio;
7use tokio::join;
8use tokio::process::Command;
9use tokio::time::{timeout, Duration};
10
11use crate::errors::GitMoverError;
12use crate::platform::{Platform, PlatformType};
13use crate::sync::{delete_repos, sync_repos};
14use crate::{
15    codeberg::config::CodebergConfig, config::GitMoverConfig, github::config::GithubConfig,
16    gitlab::config::GitlabConfig,
17};
18
19/// Repository information
20#[derive(Deserialize, Serialize, Debug, Default, PartialEq, Eq, Hash, Clone)]
21pub struct Repo {
22    /// Name of the repository
23    pub name: String,
24
25    /// Path of the repository
26    pub path: String,
27
28    /// Description of the repository
29    pub description: String,
30
31    /// Whether the repository is private
32    pub private: bool,
33
34    /// Whether the repository is a fork
35    pub fork: bool,
36}
37
38impl Repo {
39    /// Show the full name of the repo, including its path (if different from the name)
40    pub fn show_full_name(&self) -> String {
41        let fmt_path = if self.name != self.path {
42            format!(" at path '{}'", self.path)
43        } else {
44            "".into()
45        };
46        format!("{}{}", self.name, fmt_path)
47    }
48}
49
50/// GIT direction
51pub enum Direction {
52    /// Source platform
53    Source,
54    /// Destination platform
55    Destination,
56}
57
58/// Get a number from the user
59pub fn input_number() -> Result<usize, GitMoverError> {
60    loop {
61        match input()?.parse::<usize>() {
62            Ok(i) => return Ok(i),
63            Err(_) => {
64                println!("Invalid input");
65            }
66        }
67    }
68}
69
70/// check git access
71pub(crate) async fn check_ssh_access<S: AsRef<str>>(
72    ssh_url: S,
73) -> Result<(String, String), GitMoverError> {
74    let ssh_url = ssh_url.as_ref();
75    let result = timeout(Duration::from_secs(5), async {
76        Command::new("ssh")
77            .arg("-T")
78            .arg(ssh_url)
79            .stdin(Stdio::null())
80            .stdout(Stdio::piped())
81            .stderr(Stdio::piped())
82            .output()
83            .await
84    })
85    .await;
86
87    match result {
88        Ok(Ok(output)) => {
89            let stdout_str = str::from_utf8(&output.stdout)?.to_string();
90            let stderr_str = str::from_utf8(&output.stderr)?.to_string();
91            Ok((stdout_str, stderr_str))
92        }
93        Ok(Err(e)) => Err(e.into()),
94        Err(e) => Err(e.into()),
95    }
96}
97
98/// Get the platform to use
99pub(crate) fn get_plateform(
100    config: &mut GitMoverConfig,
101    direction: Direction,
102) -> Result<Box<dyn Platform>, GitMoverError> {
103    let plateform_from_cli: Option<PlatformType> = match direction {
104        Direction::Source => config.cli_args.source.clone(),
105        Direction::Destination => config.cli_args.destination.clone(),
106    };
107    let chosen_platform = match plateform_from_cli {
108        Some(platform) => platform,
109        None => {
110            println!(
111                "Choose a platform {}",
112                match direction {
113                    Direction::Source => "for source",
114                    Direction::Destination => "for destination",
115                }
116            );
117            let platforms = [
118                PlatformType::Github,
119                PlatformType::Gitlab,
120                PlatformType::Codeberg,
121            ];
122            for (i, platform) in platforms.iter().enumerate() {
123                println!("{i}: {platform}");
124            }
125            let plateform = loop {
126                let plateform = input_number()?;
127                if platforms.get(plateform).is_none() {
128                    println!("Wrong number");
129                    continue;
130                } else {
131                    break plateform;
132                }
133            };
134            platforms[plateform].clone()
135        }
136    };
137    let plateform: Box<dyn Platform> = match chosen_platform {
138        PlatformType::Gitlab => Box::new(GitlabConfig::get_plateform(config)?),
139        PlatformType::Github => Box::new(GithubConfig::get_plateform(config)?),
140        PlatformType::Codeberg => Box::new(CodebergConfig::get_plateform(config)?),
141    };
142    Ok(plateform)
143}
144
145/// Main function to sync repositories
146/// # Errors
147/// Error if an error happens
148pub async fn main_sync(config: GitMoverConfig) -> Result<(), GitMoverError> {
149    let mut config = config;
150    let source_platform = get_plateform(&mut config, Direction::Source)?;
151    println!("Chosen {} as source", source_platform.get_remote_url());
152
153    let destination_platform = get_plateform(&mut config, Direction::Destination)?;
154    println!(
155        "Chosen {} as destination",
156        destination_platform.get_remote_url()
157    );
158    if source_platform.get_remote_url() == destination_platform.get_remote_url() {
159        return Err("Source and destination can't be the same".into());
160    }
161    println!("Checking the git access for each plateform");
162    let (acc, acc2) = join!(
163        source_platform.check_git_access(),
164        destination_platform.check_git_access()
165    );
166    match acc {
167        Ok(_) => {
168            println!("Checked access to {}", source_platform.get_remote_url());
169        }
170        Err(e) => return Err(e),
171    }
172    match acc2 {
173        Ok(_) => {
174            println!(
175                "Checked access to {}",
176                destination_platform.get_remote_url()
177            );
178        }
179        Err(e) => return Err(e),
180    }
181    let source_platform = Arc::new(source_platform);
182    let destination_platform = Arc::new(destination_platform);
183    let (repos_source, repos_destination) = join!(
184        source_platform.get_all_repos(),
185        destination_platform.get_all_repos()
186    );
187
188    let repos_source = match repos_source {
189        Ok(repos) => repos,
190        Err(e) => {
191            return Err(format!("Error getting repositories for source: {e}").into());
192        }
193    };
194
195    let repos_destination = match repos_destination {
196        Ok(repos) => repos,
197        Err(e) => {
198            return Err(format!("Error getting repositories for destination: {e}").into());
199        }
200    };
201
202    let repos_source_without_fork = repos_source
203        .clone()
204        .into_iter()
205        .filter(|repo| !repo.fork)
206        .collect::<Vec<_>>();
207    let repos_source_forks = repos_source
208        .clone()
209        .into_iter()
210        .filter(|repo| repo.fork)
211        .collect::<Vec<_>>();
212    println!("Number of repos in source: {}", repos_source.len());
213    println!(
214        "- Number of forked repos in source: {}",
215        repos_source_forks.len()
216    );
217    println!(
218        "- Number of (non-forked) repos in source: {}",
219        repos_source_without_fork.len()
220    );
221    println!(
222        "Number of repos in destination: {}",
223        repos_destination.len()
224    );
225    let cloned_repos_source_without_fork = repos_source_without_fork.clone();
226    let cloned_repos_destination = repos_destination.clone();
227    let item_source_set: HashSet<_> = cloned_repos_source_without_fork.iter().collect();
228    let item_destination_set: HashSet<_> = repos_destination.iter().collect();
229    let missing_dest: Vec<Repo> = cloned_repos_destination
230        .into_iter()
231        .filter(|item| !item_source_set.contains(item))
232        .collect();
233    let resync = config.cli_args.resync;
234    let difference: Vec<Repo> = if resync {
235        cloned_repos_source_without_fork
236    } else {
237        cloned_repos_source_without_fork
238            .into_iter()
239            .filter(|item| !item_destination_set.contains(item))
240            .collect()
241    };
242    println!("Number of repos to sync: {}", difference.len());
243    println!("Number of repos to delete: {}", missing_dest.len());
244    if !difference.is_empty() && yes_no_input("Do you want to start syncing ? (y/n)")? {
245        match sync_repos(
246            &config,
247            source_platform.clone(),
248            destination_platform.clone(),
249            difference,
250        )
251        .await
252        {
253            Ok(_) => {
254                println!("All repos synced");
255            }
256            Err(e) => return Err(format!("Error syncing repos: {e}").into()),
257        }
258    }
259    if config.cli_args.no_forks {
260        println!("Not syncing forks");
261    } else if repos_source_forks.is_empty() {
262        println!("No forks found");
263    } else if yes_no_input(format!(
264        "Do you want to sync forks ({})? (y/n)",
265        repos_source_forks.len()
266    ))? {
267        match sync_repos(
268            &config,
269            source_platform,
270            destination_platform.clone(),
271            repos_source_forks,
272        )
273        .await
274        {
275            Ok(_) => {
276                println!("All forks synced");
277            }
278            Err(e) => {
279                return Err(format!("Error syncing forks: {e}").into());
280            }
281        }
282    }
283    if config.cli_args.no_delete {
284        println!("Not prompting for deletion");
285    } else if missing_dest.is_empty() {
286        println!("Nothing to delete");
287    } else if yes_no_input(format!(
288        "Do you want to delete the missing ({}) repos (manually)? (y/n)",
289        missing_dest.len()
290    ))? {
291        match delete_repos(destination_platform, missing_dest).await {
292            Ok(_) => {
293                println!("All repos deleted");
294            }
295            Err(e) => {
296                return Err(format!("Error deleting repos: {e}").into());
297            }
298        }
299    }
300    Ok(())
301}
302
303/// Get input from the user
304pub(crate) fn input() -> Result<String, GitMoverError> {
305    use std::io::{stdin, stdout, Write};
306    let mut s = String::new();
307    let _ = stdout().flush();
308    stdin()
309        .read_line(&mut s)
310        .map_err(|e| GitMoverError::new_with_source("Did not enter a correct string", e))?;
311    if let Some('\n') = s.chars().next_back() {
312        s.pop();
313    }
314    if let Some('\r') = s.chars().next_back() {
315        s.pop();
316    }
317    Ok(s)
318}
319
320/// Get a yes/no input from the user
321pub(crate) fn yes_no_input<S: AsRef<str>>(msg: S) -> Result<bool, GitMoverError> {
322    let msg = msg.as_ref();
323    loop {
324        println!("{msg}");
325        let input = input()?;
326        match input.to_lowercase().as_str() {
327            "yes" | "y" | "Y" | "YES" | "Yes " => return Ok(true),
328            "no" | "n" | "N" | "NO" | "No" => return Ok(false),
329            _ => println!("Invalid input"),
330        }
331    }
332}
333
334/// Get password from the user
335pub(crate) fn get_password() -> Result<String, GitMoverError> {
336    rpassword::read_password()
337        .map_err(|e| GitMoverError::new_with_source("Error reading password", e))
338}
339
340#[cfg(test)]
341mod test {
342
343    use super::*;
344
345    #[test]
346    fn compare_repo() {
347        let repo1 = Repo {
348            name: "test".to_string(),
349            path: "test".to_string(),
350            description: "test".to_string(),
351            private: false,
352            fork: false,
353        };
354        let repo2 = Repo {
355            name: "test".to_string(),
356            path: "test".to_string(),
357            description: "test".to_string(),
358            private: false,
359            fork: false,
360        };
361        let repo3 = Repo {
362            name: "test".to_string(),
363            path: "test".to_string(),
364            description: "test".to_string(),
365            private: true,
366            fork: false,
367        };
368        assert!(repo1 == repo2);
369        assert!(repo1 != repo3);
370        assert_eq!(repo1, repo2);
371    }
372}