omnivore_cli/git/
source.rs

1use 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),  // New variant for non-git directories
14}
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            // Try to resolve the path - could be relative or absolute
27            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                    // Ask for confirmation to proceed with non-git directory
37                    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}