i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
#![allow(dead_code)]

use super::{VCSClient, VCSProvider, BranchInfo, CommitInfo, LanguageStats, PullRequestInfo, RepositoryInfo};
use anyhow::Result;
use reqwest::Client;
use serde::Deserialize;

pub struct GitLabClient {
    client: Client,
    base_url: String,
    token: String,
}

#[derive(Deserialize)]
struct GitLabProject {
    id: u64,
    name: String,
    #[serde(rename = "path_with_namespace")]
    full_name: String,
    description: Option<String>,
    #[serde(rename = "default_branch")]
    default_branch: String,
    #[serde(rename = "http_url_to_repo")]
    url: String,
    #[serde(rename = "visibility")]
    is_private: bool,
    #[serde(rename = "star_count")]
    stars: u32,
    #[serde(rename = "forks_count")]
    forks: u32,
}

#[derive(Deserialize)]
struct GitLabBranch {
    name: String,
    #[serde(rename = "commit")]
    commit_info: GitLabCommitRef,
    #[serde(rename = "protected")]
    is_protected: bool,
}

#[derive(Deserialize)]
struct GitLabCommitRef {
    id: String,
}

#[derive(Deserialize)]
struct GitLabCommit {
    id: String,
    message: String,
    author_name: String,
    authored_date: String,
    stats: Option<GitLabCommitStats>,
}

#[derive(Deserialize)]
struct GitLabCommitStats {
    additions: i32,
    deletions: i32,
}

#[derive(Deserialize)]
struct GitLabMR {
    iid: u32,
    title: String,
    state: String,
    author: GitLabUser,
    created_at: String,
    merged_at: Option<String>,
}

#[derive(Deserialize)]
struct GitLabUser {
    username: String,
}

#[derive(Deserialize)]
struct GitLabLanguage {
    name: String,
    share: f64,
}

impl GitLabClient {
    pub fn new(token: String, base_url: Option<String>) -> Self {
        Self {
            client: Client::new(),
            base_url: base_url.unwrap_or_else(|| "https://gitlab.com".to_string()),
            token,
        }
    }

    async fn get(&self, endpoint: &str) -> Result<reqwest::Response> {
        let url = format!("{}{}", self.base_url, endpoint);
        Ok(self.client.get(&url)
            .header("PRIVATE-TOKEN", &self.token)
            .send().await?)
    }
}

impl VCSClient for GitLabClient {
    fn provider(&self) -> VCSProvider {
        VCSProvider::GitLab
    }

    async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>> {
        let response = self.get("/api/v4/projects?membership=true&per_page=100").await?;
        let projects: Vec<GitLabProject> = response.json().await?;

        Ok(projects.into_iter().map(|p| RepositoryInfo {
            name: p.name,
            full_name: p.full_name,
            description: p.description,
            language: None,
            stars: p.stars,
            forks: p.forks,
            is_private: p.is_private,
            default_branch: p.default_branch,
            url: p.url,
        }).collect())
    }

    async fn get_repository(&self, name: &str) -> Result<RepositoryInfo> {
        let encoded = urlencoding::encode(name);
        let response = self.get(&format!("/api/v4/projects/{}", encoded)).await?;
        let p: GitLabProject = response.json().await?;

        Ok(RepositoryInfo {
            name: p.name,
            full_name: p.full_name,
            description: p.description,
            language: None,
            stars: p.stars,
            forks: p.forks,
            is_private: p.is_private,
            default_branch: p.default_branch,
            url: p.url,
        })
    }

    async fn list_branches(&self, repo: &str) -> Result<Vec<BranchInfo>> {
        let encoded = urlencoding::encode(repo);
        let response = self.get(&format!("/api/v4/projects/{}/repository/branches", encoded)).await?;
        let branches: Vec<GitLabBranch> = response.json().await?;

        Ok(branches.into_iter().map(|b| BranchInfo {
            name: b.name,
            sha: b.commit_info.id,
            is_default: false,
        }).collect())
    }

    async fn list_commits(&self, repo: &str, limit: usize) -> Result<Vec<CommitInfo>> {
        let encoded = urlencoding::encode(repo);
        let response = self.get(&format!("/api/v4/projects/{}/repository/commits?per_page={}", encoded, limit)).await?;
        let commits: Vec<GitLabCommit> = response.json().await?;

        Ok(commits.into_iter().map(|c| CommitInfo {
            sha: c.id,
            message: c.message,
            author: c.author_name,
            date: c.authored_date,
            additions: c.stats.as_ref().map(|s| s.additions).unwrap_or(0),
            deletions: c.stats.as_ref().map(|s| s.deletions).unwrap_or(0),
        }).collect())
    }

    async fn get_pull_requests(&self, repo: &str) -> Result<Vec<PullRequestInfo>> {
        let encoded = urlencoding::encode(repo);
        let response = self.get(&format!("/api/v4/projects/{}/merge_requests", encoded)).await?;
        let mrs: Vec<GitLabMR> = response.json().await?;

        Ok(mrs.into_iter().map(|m| PullRequestInfo {
            number: m.iid,
            title: m.title,
            state: m.state,
            author: m.author.username,
            created_at: m.created_at,
            merged_at: m.merged_at,
        }).collect())
    }

    async fn get_languages(&self, repo: &str) -> Result<Vec<LanguageStats>> {
        let encoded = urlencoding::encode(repo);
        let response = self.get(&format!("/api/v4/projects/{}/languages", encoded)).await?;
        let langs: std::collections::HashMap<String, GitLabLanguage> = response.json().await?;

        let _total: u64 = langs.values().map(|l| l.share as u64).sum();
        
        Ok(langs.into_iter().map(|(name, l)| LanguageStats {
            name,
            bytes: l.share as u64,
            percentage: l.share,
        }).collect())
    }
}