omnivore_cli/git/
source.rs1use anyhow::{anyhow, Context, Result};
2use git2::{build::RepoBuilder, Cred, FetchOptions, RemoteCallbacks};
3use std::path::{Path, PathBuf};
4use tempfile::TempDir;
5use url::Url;
6use colored::*;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone)]
10pub enum SourceType {
11 Remote(String),
12 Local(PathBuf),
13 LocalNonGit(PathBuf), }
15
16impl SourceType {
17 pub fn from_string(source: &str) -> Result<Self> {
18 if source.starts_with("http://")
19 || source.starts_with("https://")
20 || source.starts_with("git@")
21 || source.starts_with("ssh://")
22 {
23 Ok(SourceType::Remote(source.to_string()))
24 } else {
25 let path = PathBuf::from(source);
26 let resolved_path = if path.is_absolute() {
28 path
29 } else {
30 std::env::current_dir()?.join(path)
31 };
32
33 if resolved_path.is_dir() {
34 let git_dir = resolved_path.join(".git");
35 if !git_dir.exists() {
36 if Self::confirm_non_git_directory(&resolved_path)? {
38 Ok(SourceType::LocalNonGit(resolved_path.canonicalize()?))
39 } else {
40 Err(anyhow!("Operation cancelled by user"))
41 }
42 } else {
43 Ok(SourceType::Local(resolved_path.canonicalize()?))
44 }
45 } else {
46 Err(anyhow!("'{}' is not a valid directory", source))
47 }
48 }
49 }
50
51 fn confirm_non_git_directory(path: &Path) -> Result<bool> {
52 println!();
53 println!(
54 "{}",
55 format!(
56 "⚠️ '{}' is not a Git repository (no .git directory found)",
57 path.display()
58 )
59 .yellow()
60 .bold()
61 );
62 print!(
63 "{}",
64 "Do you want to continue and analyze this directory anyway? [y/N]: "
65 .bright_white()
66 );
67 io::stdout().flush()?;
68
69 let mut input = String::new();
70 io::stdin().read_line(&mut input)?;
71 let input = input.trim().to_lowercase();
72
73 Ok(input == "y" || input == "yes")
74 }
75}
76
77pub struct SourceAcquisition {
78 source_type: SourceType,
79 depth: u32,
80 keep_temp: bool,
81 temp_dir: Option<TempDir>,
82}
83
84impl SourceAcquisition {
85 pub fn new(source_type: SourceType, depth: u32, keep_temp: bool) -> Self {
86 Self {
87 source_type,
88 depth,
89 keep_temp,
90 temp_dir: None,
91 }
92 }
93
94 pub async fn acquire(&mut self) -> Result<PathBuf> {
95 match self.source_type.clone() {
96 SourceType::Remote(url) => self.clone_remote(&url).await,
97 SourceType::Local(path) => Ok(path),
98 SourceType::LocalNonGit(path) => Ok(path),
99 }
100 }
101
102 async fn clone_remote(&mut self, url: &str) -> Result<PathBuf> {
103 let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
104 let repo_path = temp_dir.path().to_path_buf();
105
106 let url_str = url.to_string();
107 let repo_path_clone = repo_path.clone();
108 let depth = self.depth;
109
110 let clone_result = tokio::task::spawn_blocking(move || {
111 let mut callbacks = RemoteCallbacks::new();
112 callbacks.credentials(|_url, username_from_url, _allowed_types| {
113 if let Ok(home) = std::env::var("HOME") {
114 let ssh_key = PathBuf::from(&home).join(".ssh/id_rsa");
115 if ssh_key.exists() {
116 return Cred::ssh_key(
117 username_from_url.unwrap_or("git"),
118 None,
119 &ssh_key,
120 None,
121 );
122 }
123
124 let ssh_key = PathBuf::from(&home).join(".ssh/id_ed25519");
125 if ssh_key.exists() {
126 return Cred::ssh_key(
127 username_from_url.unwrap_or("git"),
128 None,
129 &ssh_key,
130 None,
131 );
132 }
133 }
134
135 Cred::default()
136 });
137
138 callbacks.certificate_check(|_cert, _host| {
139 Ok(git2::CertificateCheckStatus::CertificateOk)
140 });
141
142 let mut fetch_options = FetchOptions::new();
143 fetch_options.remote_callbacks(callbacks);
144 fetch_options.depth(depth as i32);
145
146 let mut builder = RepoBuilder::new();
147 builder.fetch_options(fetch_options);
148 builder.clone(&url_str, &repo_path_clone)
149 })
150 .await
151 .context("Failed to spawn blocking task")?;
152
153 match clone_result {
154 Ok(_) => {
155 self.temp_dir = Some(temp_dir);
156 Ok(self.temp_dir.as_ref().unwrap().path().to_path_buf())
157 }
158 Err(e) => {
159 if e.message().contains("authentication") || e.message().contains("401") {
160 Err(anyhow!(
161 "Authentication failed. Please ensure your Git credentials (SSH key, etc.) are configured correctly.\nError: {}",
162 e
163 ))
164 } else if e.message().contains("not found") || e.message().contains("404") {
165 Err(anyhow!("Repository not found: {}", url))
166 } else {
167 Err(anyhow!("Failed to clone repository: {}", e))
168 }
169 }
170 }
171 }
172
173 pub async fn cleanup(&mut self) -> Result<()> {
174 if self.keep_temp {
175 if let Some(temp_dir) = &self.temp_dir {
176 println!(
177 "Temporary clone kept at: {}",
178 temp_dir.path().display()
179 );
180 std::mem::forget(temp_dir.path().to_path_buf());
181 }
182 }
183 Ok(())
184 }
185}
186
187#[allow(dead_code)]
188pub fn is_git_repository(path: &Path) -> bool {
189 path.join(".git").exists()
190}
191
192#[allow(dead_code)]
193pub fn validate_url(url_str: &str) -> Result<Url> {
194 if url_str.starts_with("git@") {
195 let ssh_url = url_str.replace(':', "/").replace("git@", "ssh://git@");
196 Url::parse(&ssh_url).context("Invalid SSH URL format")
197 } else {
198 Url::parse(url_str).context("Invalid URL format")
199 }
200}