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::{GitInfo, CommitInfo};
use anyhow::Result;
use chrono::{Utc, TimeZone};
use git2::{Repository, Sort};
use std::path::Path;
use tracing::{debug, info};

pub struct GitAnalyzer;

impl GitAnalyzer {
    pub fn new() -> Self {
        Self
    }

    pub async fn analyze(&self, path: &Path) -> Result<GitInfo> {
        let repo = match Repository::open(path) {
            Ok(repo) => repo,
            Err(_) => {
                return Ok(GitInfo {
                    is_git_repo: false,
                    ..Default::default()
                });
            }
        };

        info!("Analyzing git repository at: {}", path.display());

        let mut git_info = GitInfo {
            is_git_repo: true,
            ..Default::default()
        };

        // Get current branch
        git_info.current_branch = self.get_current_branch(&repo);

        // Get remote URL
        git_info.remote_url = self.get_remote_url(&repo);

        // Get recent commits
        git_info.recent_commits = self.get_recent_commits(&repo, 50)?;

        // Get total commit count
        git_info.total_commits = self.get_total_commits(&repo)?;

        // Get contributors
        git_info.contributors = self.get_contributors(&repo)?;

        debug!("Git analysis complete: {} commits, {} contributors", 
               git_info.total_commits, 
               git_info.contributors.len());

        Ok(git_info)
    }

    fn get_current_branch(&self, repo: &Repository) -> Option<String> {
        match repo.head() {
            Ok(head) => {
                if let Some(name) = head.shorthand() {
                    Some(name.to_string())
                } else {
                    None
                }
            }
            Err(_) => None,
        }
    }

    fn get_remote_url(&self, repo: &Repository) -> Option<String> {
        match repo.find_remote("origin") {
            Ok(remote) => {
                remote.url().map(|s| s.to_string())
            }
            Err(_) => None,
        }
    }

    fn get_recent_commits(&self, repo: &Repository, limit: usize) -> Result<Vec<CommitInfo>> {
        let mut revwalk = repo.revwalk()?;
        revwalk.set_sorting(Sort::TIME)?;
        revwalk.push_head()?;

        let mut commits = Vec::new();

        for (i, oid) in revwalk.enumerate() {
            if i >= limit {
                break;
            }

            let oid = oid?;
            let commit = repo.find_commit(oid)?;

            let author = commit.author();
            let committer = commit.committer();

            let message = commit.message()
                .unwrap_or("No message")
                .to_string();

            let author_name = author.name()
                .unwrap_or("Unknown")
                .to_string();

            let time = committer.when();
            let timestamp = time.seconds();
            let date = Utc.timestamp_opt(timestamp, 0)
                .single()
                .unwrap_or_else(Utc::now);

            // Get changed files
            let files_changed = self.get_changed_files(repo, &commit)?;

            commits.push(CommitInfo {
                hash: oid.to_string(),
                message,
                author: author_name,
                date,
                files_changed,
            });
        }

        Ok(commits)
    }

    fn get_changed_files(&self, repo: &Repository, commit: &git2::Commit) -> Result<Vec<String>> {
        let mut files = Vec::new();

        // Get the tree for this commit
        let tree = commit.tree()?;

        // Get parent commit's tree
        let parent_tree = if commit.parent_count() > 0 {
            let parent = commit.parent(0)?;
            Some(parent.tree()?)
        } else {
            None
        };

        // Get diff
        let diff = if let Some(parent) = parent_tree {
            repo.diff_tree_to_tree(Some(&parent), Some(&tree), None)?
        } else {
            repo.diff_tree_to_tree(None, Some(&tree), None)?
        };

        // Collect changed files
        diff.foreach(
            &mut |delta, _| {
                if let Some(path) = delta.new_file().path() {
                    files.push(path.to_string_lossy().to_string());
                }
                true
            },
            None,
            None,
            None,
        )?;

        Ok(files)
    }

    fn get_total_commits(&self, repo: &Repository) -> Result<i64> {
        let mut revwalk = repo.revwalk()?;
        revwalk.push_head()?;
        
        let count = revwalk.count() as i64;
        Ok(count)
    }

    fn get_contributors(&self, repo: &Repository) -> Result<Vec<String>> {
        let mut revwalk = repo.revwalk()?;
        revwalk.push_head()?;

        let mut contributors = std::collections::HashSet::new();

        for oid in revwalk {
            let oid = oid?;
            let commit = repo.find_commit(oid)?;
            
            // Extract the name immediately while commit is in scope
            let name = commit.author().name()
                .map(|n| n.to_string());
            
            if let Some(name) = name {
                contributors.insert(name);
            }
        }

        Ok(contributors.into_iter().collect())
    }

    /// Get commit statistics for a time period
    pub fn get_commit_stats(&self, path: &Path, days: i64) -> Result<CommitStats> {
        let repo = Repository::open(path)?;
        let since = Utc::now() - chrono::Duration::days(days);
        
        let mut revwalk = repo.revwalk()?;
        revwalk.push_head()?;

        let mut stats = CommitStats::default();

        for oid in revwalk {
            let oid = oid?;
            let commit = repo.find_commit(oid)?;
            
            let commit_time = commit.committer().when().seconds();
            let commit_date = Utc.timestamp_opt(commit_time, 0).single();

            if let Some(date) = commit_date {
                if date > since {
                    stats.total_commits += 1;
                    
                    // Track commit time patterns - use time() method instead
                    let hour = ((commit_time / 3600) % 24) as u8;
                    stats.commits_by_hour.entry(hour).and_modify(|e| *e += 1).or_insert(1);
                    
                    // Track day of week
                    let day = date.format("%A").to_string();
                    stats.commits_by_day.entry(day).and_modify(|e| *e += 1).or_insert(1);
                }
            }
        }

        Ok(stats)
    }
}

#[derive(Debug, Clone, Default)]
pub struct CommitStats {
    pub total_commits: i64,
    pub commits_by_hour: std::collections::HashMap<u8, i64>,
    pub commits_by_day: std::collections::HashMap<String, i64>,
}