securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::cli::UI;
use crate::ops::utils::{format_timestamp, short_oid};
use anyhow::Result;
use std::path::Path;

pub struct LogOptions<'a> {
    pub max_count: usize,
    pub oneline: bool,
    pub author_filter: Option<&'a str>,
    pub since: Option<&'a str>,
    pub until: Option<&'a str>,
    pub all: bool,
    pub verbose: bool,
}

pub fn execute(path: &Path, opts: &LogOptions, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let mut revwalk = repo.revwalk()?;

    if opts.all {
        // Walk all refs (branches, tags, remotes)
        revwalk.push_glob("refs/*")?;
    } else {
        revwalk.push_head()?;
    }
    revwalk.set_sorting(git2::Sort::TIME)?;

    let since_secs = opts.since.and_then(parse_date_approx);
    let until_secs = opts.until.and_then(parse_date_approx);

    let mut shown = 0;
    for oid in revwalk {
        if shown >= opts.max_count {
            break;
        }
        let oid = oid?;
        let commit = repo.find_commit(oid)?;

        // Apply time filters
        let commit_time = commit.time().seconds();
        if let Some(s) = since_secs {
            if commit_time < s {
                continue;
            }
        }
        if let Some(u) = until_secs {
            if commit_time > u {
                continue;
            }
        }

        // Apply author filter
        if let Some(author_pat) = opts.author_filter {
            let author = commit.author();
            let name = author.name().unwrap_or("");
            let email = author.email().unwrap_or("");
            let pat_lower = author_pat.to_lowercase();
            if !name.to_lowercase().contains(&pat_lower)
                && !email.to_lowercase().contains(&pat_lower)
            {
                continue;
            }
        }

        if opts.oneline {
            let short_id = short_oid(&oid);
            let msg = commit.summary().unwrap_or("");
            ui.log_oneline(&short_id, msg);
        } else {
            let short_id = short_oid(&oid);
            let summary = commit.summary().unwrap_or("");
            let author = commit.author();
            let author_str = format!(
                "{} <{}>",
                author.name().unwrap_or(""),
                author.email().unwrap_or("")
            );
            let time = commit.time();
            let time_ago = format_timestamp(time.seconds(), time.offset_minutes());
            let is_last = shown + 1 >= opts.max_count;
            ui.log_entry(&short_id, summary, &author_str, &time_ago, is_last);
        }

        if opts.verbose {
            // Compare to parent
            let parent = commit.parent(0).ok();
            let parent_tree = parent.as_ref().and_then(|p| p.tree().ok());
            let current_tree = commit.tree().ok();

            if let (Some(t1), Some(t2)) = (parent_tree, current_tree) {
                let mut diff_opts = git2::DiffOptions::new();
                let diff = repo.diff_tree_to_tree(Some(&t1), Some(&t2), Some(&mut diff_opts))?;
                crate::ops::diff::display_diff(&diff, ui)?;
            }
        }
        shown += 1;
    }

    Ok(())
}

pub fn execute_compact(
    path: &Path,
    max_count: usize,
    author_filter: Option<&str>,
    since: Option<&str>,
    until: Option<&str>,
    all: bool,
) -> Result<String> {
    use crate::cli::compact::truncate_line;

    let repo = crate::ops::open_repo(path)?;
    let mut revwalk = repo.revwalk()?;

    if all {
        revwalk.push_glob("refs/*")?;
    } else {
        revwalk.push_head()?;
    }
    revwalk.set_sorting(git2::Sort::TIME)?;

    let since_secs = since.and_then(parse_date_approx);
    let until_secs = until.and_then(parse_date_approx);

    // Default to 10 entries for compact output
    let limit = if max_count > 100 { 10 } else { max_count };

    let mut lines = Vec::new();
    let mut shown = 0;

    for oid in revwalk {
        if shown >= limit {
            break;
        }
        let oid = oid?;
        let commit = repo.find_commit(oid)?;

        let commit_time = commit.time().seconds();
        if let Some(s) = since_secs {
            if commit_time < s {
                continue;
            }
        }
        if let Some(u) = until_secs {
            if commit_time > u {
                continue;
            }
        }

        if let Some(author_pat) = author_filter {
            let author = commit.author();
            let name = author.name().unwrap_or("");
            let email = author.email().unwrap_or("");
            let pat_lower = author_pat.to_lowercase();
            if !name.to_lowercase().contains(&pat_lower)
                && !email.to_lowercase().contains(&pat_lower)
            {
                continue;
            }
        }

        let short_id = short_oid(&oid);
        let summary = commit.summary().unwrap_or("");
        let author = commit.author();
        let author_name = author.name().unwrap_or("");
        let time = commit.time();
        let timestamp = format_timestamp(time.seconds(), time.offset_minutes());

        let line = format!("{} {} ({}) <{}>", short_id, summary, timestamp, author_name);
        lines.push(truncate_line(&line, 120));
        shown += 1;
    }

    Ok(lines.join("\n"))
}

/// Parse a date string into unix timestamp. Supports YYYY-MM-DD and common formats.
fn parse_date_approx(s: &str) -> Option<i64> {
    // Try YYYY-MM-DD
    let parts: Vec<&str> = s.split('-').collect();
    if parts.len() == 3 {
        let year: i64 = parts[0].parse().ok()?;
        let month: u32 = parts[1].parse().ok()?;
        let day: u32 = parts[2].parse().ok()?;
        if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
            return None;
        }
        // Approximate: compute days from epoch
        let mut total_days: i64 = 0;
        for y in 1970..year {
            total_days += if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) {
                366
            } else {
                365
            };
        }
        let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
        let month_days = [
            31,
            if is_leap { 29 } else { 28 },
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31,
        ];
        for &md in month_days.iter().take(month as usize - 1) {
            total_days += md as i64;
        }
        total_days += (day - 1) as i64;
        return Some(total_days * 86400);
    }
    None
}