thoughts_tool/git/
clone.rs1use anyhow::{Context, Result};
2use colored::*;
3use git2::{FetchOptions, Progress, RemoteCallbacks};
4use std::path::PathBuf;
5
6pub struct CloneOptions {
7 pub url: String,
8 pub target_path: PathBuf,
9 pub branch: Option<String>,
10}
11
12pub fn clone_repository(options: &CloneOptions) -> Result<()> {
13 println!("{} {}", "Cloning".green(), options.url);
14 println!(" to: {}", options.target_path.display());
15
16 if let Some(parent) = options.target_path.parent() {
18 std::fs::create_dir_all(parent).context("Failed to create clone directory")?;
19 }
20
21 if options.target_path.exists() {
23 let entries = std::fs::read_dir(&options.target_path)?;
24 if entries.count() > 0 {
25 anyhow::bail!(
26 "Target directory is not empty: {}",
27 options.target_path.display()
28 );
29 }
30 }
31
32 let mut builder = git2::build::RepoBuilder::new();
34 let mut fetch_opts = FetchOptions::new();
35 let mut callbacks = RemoteCallbacks::new();
36
37 callbacks.credentials(|_url, username_from_url, allowed_types| {
39 if allowed_types.contains(git2::CredentialType::SSH_KEY) {
41 let username = username_from_url.unwrap_or("git");
42
43 if std::env::var("SSH_AUTH_SOCK").is_ok() {
48 if let Ok(cred) = git2::Cred::ssh_key_from_agent(username) {
50 return Ok(cred);
51 }
52 }
54
55 let home = dirs::home_dir()
57 .ok_or_else(|| git2::Error::from_str("Cannot find home directory"))?;
58 let ssh_dir = home.join(".ssh");
59
60 let key_files = [
62 "id_ed25519", "id_rsa", "id_ecdsa", ];
66
67 for key_name in &key_files {
68 let private_key = ssh_dir.join(key_name);
69 if private_key.exists() {
70 if let Ok(cred) = git2::Cred::ssh_key(
72 username,
73 None, private_key.as_path(),
75 None, ) {
77 return Ok(cred);
78 }
79
80 let public_key = ssh_dir.join(format!("{key_name}.pub"));
82 if public_key.exists()
83 && let Ok(cred) = git2::Cred::ssh_key(
84 username,
85 Some(public_key.as_path()),
86 private_key.as_path(),
87 None,
88 )
89 {
90 return Ok(cred);
91 }
92 }
93 }
94
95 Err(git2::Error::from_str(
96 "SSH authentication failed. No valid SSH keys found in ~/.ssh/\n\
97 Checked for: id_ed25519, id_rsa, id_ecdsa\n\
98 Note: Passphrase-protected keys are not currently supported",
99 ))
100 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
101 git2::Cred::default()
103 } else {
104 git2::Cred::default()
105 }
106 });
107
108 callbacks.transfer_progress(|stats: Progress| {
110 let received = stats.received_objects();
111 let total = stats.total_objects();
112
113 if total > 0 {
114 let percent = (received as f32 / total as f32) * 100.0;
115 print!(
116 "\r {}: {}/{} objects ({:.1}%)",
117 "Progress".cyan(),
118 received,
119 total,
120 percent
121 );
122 std::io::Write::flush(&mut std::io::stdout()).ok();
123 }
124 true
125 });
126
127 fetch_opts.remote_callbacks(callbacks);
128 builder.fetch_options(fetch_opts);
129
130 if let Some(branch) = &options.branch {
131 builder.branch(branch);
132 }
133
134 builder
136 .clone(&options.url, &options.target_path)
137 .context("Failed to clone repository")?;
138
139 println!("\n✓ Clone completed successfully");
140 Ok(())
141}