gitfetch_rs/fetcher/
gitlab.rs

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