1use anyhow::Result;
2use dialoguer::{Input, Password};
3use git2::Progress;
4use std::{error::Error, fmt::Display, path::Path};
5
6struct CredentialUI;
7
8impl git2_credentials::CredentialUI for CredentialUI {
9 fn ask_user_password(&self, username: &str) -> Result<(String, String), Box<dyn Error>> {
10 let user: String = Input::with_theme(&dialoguer::theme::ColorfulTheme::default())
11 .default(username.to_owned())
12 .with_prompt("username")
13 .interact()?;
14 let password: String = Password::with_theme(&dialoguer::theme::ColorfulTheme::default())
15 .with_prompt("password (hidden)")
16 .allow_empty_password(true)
17 .interact()?;
18 Ok((user, password))
19 }
20
21 fn ask_ssh_passphrase(&self, passphrase_prompt: &str) -> Result<String, Box<dyn Error>> {
22 let passphrase: String = Password::with_theme(&dialoguer::theme::ColorfulTheme::default())
23 .with_prompt(passphrase_prompt)
24 .allow_empty_password(true)
25 .interact()?;
26 Ok(passphrase)
27 }
28}
29type ProgressCallback<'a> = Option<Box<dyn for<'b> FnMut(Progress<'b>) + 'a>>;
30
31pub fn clone_repo(url: &str, path: &Path, progress_cb: ProgressCallback) -> Result<()> {
32 let mut cb = git2::RemoteCallbacks::new();
34 let git_config = git2::Config::open_default()?;
36 let mut ch =
38 git2_credentials::CredentialHandler::new_with_ui(git_config, Box::new(CredentialUI));
39
40 if progress_cb.is_some() {
41 let mut progress_cb = progress_cb.unwrap();
42 cb.transfer_progress(move |progress| {
43 progress_cb(progress);
44 true
45 });
46 }
47
48 cb.credentials(move |url, username, allowed| ch.try_next_credential(url, username, allowed));
50
51 let mut fo = git2::FetchOptions::new();
53 fo.remote_callbacks(cb)
55 .download_tags(git2::AutotagOption::All)
57 .update_fetchhead(true);
59
60 git2::build::RepoBuilder::new()
62 .branch("master")
64 .fetch_options(fo)
66 .clone(url, path)?;
68
69 Ok(())
70}
71
72pub enum GitUrl {
73 Http(String),
74 Ssh(String),
75 Unknown(String),
76}
77impl Display for GitUrl {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 GitUrl::Http(url) => f.write_str(url),
81 GitUrl::Ssh(url) => f.write_str(url),
82 GitUrl::Unknown(url) => f.write_str(url),
83 }
84 }
85}
86
87impl<T: AsRef<str>> From<T> for GitUrl {
88 fn from(value: T) -> Self {
89 let value = value.as_ref().trim();
90 if value.starts_with("https://github.com/") {
91 GitUrl::Http(value.to_string())
92 } else if value.starts_with("git@github.com:") {
93 GitUrl::Ssh(value.to_string())
94 } else {
95 if value.starts_with("git@") || value.starts_with("https://") {
96 return GitUrl::Unknown(value.to_string());
97 }
98 if value.ends_with(".git") {
99 return GitUrl::Unknown(format!("git@github.com:{value}"));
101 }
102 GitUrl::Unknown(format!("git@github.com:{value}.git"))
103 }
104 }
105}
106
107impl GitUrl {
108 pub fn clone(&self, path: &Path) -> anyhow::Result<()> {
109 match self {
110 GitUrl::Http(url) => clone_repo(url, path, None),
111 GitUrl::Ssh(url) => clone_repo(url, path, None),
112 GitUrl::Unknown(url) => clone_repo(url, path, None),
113 }
114 }
115}