cargo_actions/
git.rs

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    // 创建一个 RemoteCallbacks 对象
33    let mut cb = git2::RemoteCallbacks::new();
34    // 打开默认的 git 配置
35    let git_config = git2::Config::open_default()?;
36    // 创建一个 CredentialHandler 对象
37    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    // 设置凭证回调函数
49    cb.credentials(move |url, username, allowed| ch.try_next_credential(url, username, allowed));
50
51    // 创建一个 FetchOptions 对象
52    let mut fo = git2::FetchOptions::new();
53    // 设置远程回调函数
54    fo.remote_callbacks(cb)
55        // 下载所有标签
56        .download_tags(git2::AutotagOption::All)
57        // 更新 fetchhead
58        .update_fetchhead(true);
59
60    // 创建一个 RepoBuilder 对象
61    git2::build::RepoBuilder::new()
62        // 设置分支为 "master"
63        .branch("master")
64        // 设置 fetch options
65        .fetch_options(fo)
66        // 克隆仓库
67        .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                // https://mirror.ghproxy.com/https://github.com/
100                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}