Skip to main content

githem_core/
lib.rs

1pub mod cache;
2pub mod filtering;
3pub mod ingester;
4pub mod parser;
5
6pub use cache::{
7    CacheCommitStatus, CacheEntry, CacheManager, CacheStats, CachedFile, RepositoryCache,
8};
9pub use filtering::{get_default_excludes, get_excludes_for_preset, FilterConfig, FilterPreset};
10pub use ingester::{FilterStats, IngestOptions, Ingester, IngestionCallback};
11pub use parser::{
12    normalize_source_url, parse_github_url, validate_github_name, GitHubUrlType, ParsedGitHubUrl,
13};
14
15use anyhow::Result;
16use git2::Repository;
17use serde::{Deserialize, Serialize};
18use std::io::IsTerminal;
19use std::path::Path;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RepositoryMetadata {
24    pub url: String,
25    pub default_branch: String,
26    pub branches: Vec<String>,
27    pub size: Option<u64>,
28    pub last_commit: Option<String>,
29    pub remote_url: Option<String>,
30}
31
32pub fn is_remote_url(source: &str) -> bool {
33    source.starts_with("https://github.com/")
34        || source.starts_with("https://gitlab.com/")
35        || source.starts_with("https://gist.github.com/")
36        || source.starts_with("https://raw.githubusercontent.com/")
37        || source.starts_with("https://gist.githubusercontent.com/")
38}
39
40pub fn clone_repository(url: &str, branch: Option<&str>) -> Result<Repository> {
41    if !is_remote_url(url) {
42        return Err(anyhow::anyhow!("Invalid or unsafe URL"));
43    }
44
45    let temp_id = SystemTime::now()
46        .duration_since(UNIX_EPOCH)
47        .unwrap()
48        .as_millis();
49    let path = std::env::temp_dir().join(format!("githem-{temp_id}"));
50
51    let mut fetch_opts = git2::FetchOptions::new();
52    let mut callbacks = git2::RemoteCallbacks::new();
53
54    callbacks.credentials(|url, username_from_url, allowed_types| {
55        if !is_remote_url(url) {
56            return Err(git2::Error::from_str(
57                "Invalid URL for credential authentication",
58            ));
59        }
60
61        if allowed_types.contains(git2::CredentialType::SSH_KEY) {
62            if let Ok(cred) = git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git")) {
63                return Ok(cred);
64            }
65
66            if let Ok(home) = std::env::var("HOME") {
67                let ssh_dir = Path::new(&home).join(".ssh");
68                if ssh_dir.exists() {
69                    let private_key = ssh_dir.join("id_ed25519");
70                    let public_key = ssh_dir.join("id_ed25519.pub");
71
72                    if private_key.exists() && public_key.exists() {
73                        return git2::Cred::ssh_key(
74                            username_from_url.unwrap_or("git"),
75                            Some(&public_key),
76                            &private_key,
77                            None,
78                        );
79                    }
80                }
81            }
82        }
83
84        if allowed_types.contains(git2::CredentialType::DEFAULT) && url.starts_with("https://") {
85            return git2::Cred::default();
86        }
87
88        Err(git2::Error::from_str(
89            "No secure authentication method available",
90        ))
91    });
92
93    if std::io::stderr().is_terminal() {
94        callbacks.transfer_progress(|stats| {
95            if stats.total_objects() > 0 {
96                eprint!(
97                    "\rReceiving objects: {}% ({}/{})",
98                    (100 * stats.received_objects()) / stats.total_objects(),
99                    stats.received_objects(),
100                    stats.total_objects()
101                );
102            }
103            true
104        });
105    }
106
107    fetch_opts.remote_callbacks(callbacks);
108    fetch_opts.depth(1);
109    fetch_opts.download_tags(git2::AutotagOption::None);
110
111    let mut builder = git2::build::RepoBuilder::new();
112    builder.fetch_options(fetch_opts);
113
114    if let Some(branch) = branch {
115        builder.branch(branch);
116    }
117
118    let repo = builder.clone(url, &path)?;
119
120    if std::io::stderr().is_terminal() {
121        eprintln!();
122    }
123
124    Ok(repo)
125}
126
127pub fn checkout_branch(repo: &Repository, branch_name: &str) -> Result<()> {
128    let (object, reference) = repo.revparse_ext(branch_name)?;
129    repo.checkout_tree(&object, None)?;
130
131    match reference {
132        Some(gref) => repo.set_head(gref.name().unwrap())?,
133        None => repo.set_head_detached(object.id())?,
134    }
135
136    Ok(())
137}
138
139pub fn glob_match(pattern: &str, path: &str) -> bool {
140    if pattern.starts_with("*.") {
141        return path.ends_with(&pattern[1..]);
142    }
143
144    if let Some(prefix) = pattern.strip_suffix("/*") {
145        return path.starts_with(prefix) && path.len() > prefix.len();
146    }
147
148    if pattern.contains('*') {
149        let parts: Vec<&str> = pattern.split('*').collect();
150        if parts.len() == 2 {
151            return path.starts_with(parts[0]) && path.ends_with(parts[1]);
152        }
153    }
154
155    path == pattern || path.starts_with(&format!("{pattern}/"))
156}
157
158pub fn estimate_tokens(content: &str) -> usize {
159    let chars = content.len();
160    let words = content.split_whitespace().count();
161    let lines = content.lines().count();
162    ((chars as f32 / 3.3 + words as f32 * 0.75) / 2.0 + lines as f32 * 0.1) as usize
163}
164
165pub fn count_files(content: &str) -> usize {
166    content.matches("=== ").count()
167}
168
169pub fn generate_tree(content: &str) -> String {
170    let mut tree = String::new();
171    tree.push_str("Repository structure:\n");
172    for line in content.lines() {
173        if line.starts_with("=== ") && line.ends_with(" ===") {
174            let path = &line[4..line.len() - 4];
175            tree.push_str(&format!("📄 {path}\n"));
176        }
177    }
178    tree
179}