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 BitbucketClient {
    client: Client,
    username: String,
    app_password: String,
}

#[derive(Deserialize)]
struct BitbucketRepo {
    slug: String,
    full_name: String,
    description: Option<String>,
    #[serde(rename = "mainbranch")]
    main_branch: Option<BitbucketBranchRef>,
    #[serde(rename = "type")]
    repo_type: String,
    links: BitbucketLinks,
}

#[derive(Deserialize)]
struct BitbucketBranchRef {
    name: String,
}

#[derive(Deserialize)]
struct BitbucketLinks {
    html: BitbucketLink,
}

#[derive(Deserialize)]
struct BitbucketLink {
    href: String,
}

#[derive(Deserialize)]
struct BitbucketBranch {
    name: String,
    target: BitbucketCommitRef,
}

#[derive(Deserialize)]
struct BitbucketCommitRef {
    hash: String,
}

#[derive(Deserialize)]
struct BitbucketCommit {
    hash: String,
    message: String,
    author: BitbucketAuthor,
    date: String,
}

#[derive(Deserialize)]
struct BitbucketAuthor {
    raw: String,
}

#[derive(Deserialize)]
struct BitbucketPR {
    id: u32,
    title: String,
    state: String,
    author: BitbucketAuthor,
    created_on: String,
    merged_on: Option<String>,
}

impl BitbucketClient {
    pub fn new(username: String, app_password: String) -> Self {
        Self {
            client: Client::new(),
            username,
            app_password,
        }
    }

    async fn get(&self, endpoint: &str) -> Result<reqwest::Response> {
        let url = format!("https://api.bitbucket.org/2.0{}", endpoint);
        Ok(self.client.get(&url)
            .basic_auth(&self.username, Some(&self.app_password))
            .send().await?)
    }
}

impl VCSClient for BitbucketClient {
    fn provider(&self) -> VCSProvider {
        VCSProvider::Bitbucket
    }

    async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>> {
        let response = self.get(&format!("/repositories/{}", self.username)).await?;
        let data: serde_json::Value = response.json().await?;
        let repos: Vec<BitbucketRepo> = serde_json::from_value(data["values"].clone())?;

        Ok(repos.into_iter().map(|r| RepositoryInfo {
            name: r.slug,
            full_name: r.full_name,
            description: r.description,
            language: None,
            stars: 0,
            forks: 0,
            is_private: r.repo_type == "repository",
            default_branch: r.main_branch.map(|b| b.name).unwrap_or_else(|| "main".to_string()),
            url: r.links.html.href,
        }).collect())
    }

    async fn get_repository(&self, name: &str) -> Result<RepositoryInfo> {
        let response = self.get(&format!("/repositories/{}/{}", self.username, name)).await?;
        let r: BitbucketRepo = response.json().await?;

        Ok(RepositoryInfo {
            name: r.slug,
            full_name: r.full_name,
            description: r.description,
            language: None,
            stars: 0,
            forks: 0,
            is_private: r.repo_type == "repository",
            default_branch: r.main_branch.map(|b| b.name).unwrap_or_else(|| "main".to_string()),
            url: r.links.html.href,
        })
    }

    async fn list_branches(&self, repo: &str) -> Result<Vec<BranchInfo>> {
        let response = self.get(&format!("/repositories/{}/{}/refs/branches", self.username, repo)).await?;
        let data: serde_json::Value = response.json().await?;
        let branches: Vec<BitbucketBranch> = serde_json::from_value(data["values"].clone())?;

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

    async fn list_commits(&self, repo: &str, limit: usize) -> Result<Vec<CommitInfo>> {
        let response = self.get(&format!("/repositories/{}/{}/commits?pagelen={}", self.username, repo, limit)).await?;
        let data: serde_json::Value = response.json().await?;
        let commits: Vec<BitbucketCommit> = serde_json::from_value(data["values"].clone())?;

        Ok(commits.into_iter().map(|c| CommitInfo {
            sha: c.hash,
            message: c.message,
            author: c.author.raw,
            date: c.date,
            additions: 0,
            deletions: 0,
        }).collect())
    }

    async fn get_pull_requests(&self, repo: &str) -> Result<Vec<PullRequestInfo>> {
        let response = self.get(&format!("/repositories/{}/{}/pullrequests", self.username, repo)).await?;
        let data: serde_json::Value = response.json().await?;
        let prs: Vec<BitbucketPR> = serde_json::from_value(data["values"].clone())?;

        Ok(prs.into_iter().map(|p| PullRequestInfo {
            number: p.id,
            title: p.title,
            state: p.state,
            author: p.author.raw,
            created_at: p.created_on,
            merged_at: p.merged_on,
        }).collect())
    }

    async fn get_languages(&self, _repo: &str) -> Result<Vec<LanguageStats>> {
        Ok(Vec::new())
    }
}