gitfetch_rs/fetcher/
gitea.rs

1use super::Fetcher;
2use anyhow::Result;
3use async_trait::async_trait;
4use serde_json::Value;
5
6pub struct GiteaFetcher {
7  client: reqwest::Client,
8  api_base: String,
9  token: Option<String>,
10}
11
12impl GiteaFetcher {
13  pub fn new(base_url: &str, token: Option<&str>) -> Result<Self> {
14    let base = base_url.trim_end_matches('/').to_string();
15    let api_base = format!("{}/api/v1", base);
16
17    Ok(Self {
18      client: reqwest::Client::new(),
19      api_base,
20      token: token.map(String::from),
21    })
22  }
23
24  fn api_request(&self, endpoint: &str) -> Result<Value> {
25    let url = format!("{}{}", self.api_base, endpoint);
26
27    let mut req = self.client.get(&url);
28
29    if let Some(token) = &self.token {
30      req = req.header("Authorization", format!("token {}", token));
31    }
32
33    let rt = tokio::runtime::Runtime::new()?;
34    let response =
35      rt.block_on(async { req.timeout(std::time::Duration::from_secs(30)).send().await })?;
36
37    if !response.status().is_success() {
38      return Err(anyhow::anyhow!(
39        "Gitea API request failed: {}",
40        response.status()
41      ));
42    }
43
44    let rt = tokio::runtime::Runtime::new()?;
45    let data = rt.block_on(async { response.json::<Value>().await })?;
46
47    Ok(data)
48  }
49}
50
51#[async_trait]
52impl Fetcher for GiteaFetcher {
53  async fn get_authenticated_user(&self) -> Result<String> {
54    if self.token.is_none() {
55      return Err(anyhow::anyhow!("Token required for Gitea authentication"));
56    }
57
58    let data = self.api_request("/user")?;
59    data["login"]
60      .as_str()
61      .map(String::from)
62      .ok_or_else(|| anyhow::anyhow!("Could not get authenticated user"))
63  }
64
65  async fn fetch_user_data(&self, username: &str) -> Result<Value> {
66    self.api_request(&format!("/users/{}", username))
67  }
68
69  async fn fetch_user_stats(&self, username: &str, user_data: Option<&Value>) -> Result<Value> {
70    let _user = if let Some(data) = user_data {
71      data.clone()
72    } else {
73      self.fetch_user_data(username).await?
74    };
75
76    // Fetch user's repositories
77    let mut repos = Vec::new();
78    let mut page = 1;
79    let per_page = 50;
80
81    loop {
82      let endpoint = format!("/users/{}/repos?page={}&limit={}", username, page, per_page);
83      let data = self.api_request(&endpoint)?;
84
85      let data_array = match data.as_array() {
86        Some(arr) if !arr.is_empty() => arr,
87        _ => break,
88      };
89
90      repos.extend(data_array.clone());
91      page += 1;
92
93      if data_array.len() < per_page {
94        break;
95      }
96    }
97
98    // Calculate statistics
99    let total_stars: i64 = repos.iter().filter_map(|r| r["stars_count"].as_i64()).sum();
100
101    let total_forks: i64 = repos.iter().filter_map(|r| r["forks_count"].as_i64()).sum();
102
103    // Calculate language statistics
104    let languages = self.calculate_language_stats(&repos);
105
106    // Gitea doesn't have contribution graphs
107    Ok(serde_json::json!({
108      "total_stars": total_stars,
109      "total_forks": total_forks,
110      "total_repos": repos.len(),
111      "languages": languages,
112      "contribution_graph": [],
113      "current_streak": 0,
114      "longest_streak": 0,
115      "total_contributions": 0,
116      "pull_requests": {
117        "open": 0,
118        "awaiting_review": 0,
119        "mentions": 0
120      },
121      "issues": {
122        "assigned": 0,
123        "created": 0,
124        "mentions": 0
125      },
126    }))
127  }
128}
129
130impl GiteaFetcher {
131  fn calculate_language_stats(&self, repos: &[Value]) -> Value {
132    use std::collections::HashMap;
133
134    let mut language_counts: HashMap<String, i32> = HashMap::new();
135
136    for repo in repos {
137      if let Some(language) = repo["language"].as_str() {
138        if !language.is_empty() {
139          let normalized = language.to_lowercase();
140          *language_counts.entry(normalized).or_insert(0) += 1;
141        }
142      }
143    }
144
145    let total: i32 = language_counts.values().sum();
146    if total == 0 {
147      return serde_json::json!({});
148    }
149
150    let mut language_percentages: HashMap<String, f64> = HashMap::new();
151    for (lang, count) in language_counts {
152      let percentage = (count as f64 / total as f64) * 100.0;
153      let display_name = lang
154        .chars()
155        .enumerate()
156        .map(|(i, c)| {
157          if i == 0 {
158            c.to_uppercase().to_string()
159          } else {
160            c.to_string()
161          }
162        })
163        .collect::<String>();
164      language_percentages.insert(display_name, percentage);
165    }
166
167    serde_json::to_value(language_percentages).unwrap_or_else(|_| serde_json::json!({}))
168  }
169}