a73x

src/log.rs

Ref:   Size: 6.5 KiB

use git2::Repository;

use crate::dag;
use crate::error::Error;
use crate::event::{Action, Author};

/// A single entry in the collab log output.
#[derive(Debug, Clone)]
pub struct LogEntry {
    pub timestamp: String,
    pub event_type: String,
    pub entity_kind: String,
    pub entity_id: String,
    pub author: Author,
    pub summary: String,
}

/// Collect all events from every collab ref, sorted by timestamp.
/// If `limit` is provided, return at most that many entries (most recent last).
pub fn collect_events(repo: &Repository, limit: Option<usize>) -> Result<Vec<LogEntry>, Error> {
    let mut entries = Vec::new();

    // Walk issues
    let issue_refs = repo.references_glob("refs/collab/issues/*")?;
    for r in issue_refs {
        let r = r?;
        let ref_name = r.name().unwrap_or_default().to_string();
        let id = ref_name
            .strip_prefix("refs/collab/issues/")
            .unwrap_or_default()
            .to_string();
        if let Ok(events) = dag::walk_events(repo, &ref_name) {
            for (_oid, event) in events {
                entries.push(LogEntry {
                    timestamp: event.timestamp.clone(),
                    event_type: action_type_name(&event.action),
                    entity_kind: "issue".to_string(),
                    entity_id: id.clone(),
                    author: event.author,
                    summary: action_summary(&event.action),
                });
            }
        }
    }

    // Walk patches
    let patch_refs = repo.references_glob("refs/collab/patches/*")?;
    for r in patch_refs {
        let r = r?;
        let ref_name = r.name().unwrap_or_default().to_string();
        let id = ref_name
            .strip_prefix("refs/collab/patches/")
            .unwrap_or_default()
            .to_string();
        if let Ok(events) = dag::walk_events(repo, &ref_name) {
            for (_oid, event) in events {
                entries.push(LogEntry {
                    timestamp: event.timestamp.clone(),
                    event_type: action_type_name(&event.action),
                    entity_kind: "patch".to_string(),
                    entity_id: id.clone(),
                    author: event.author,
                    summary: action_summary(&event.action),
                });
            }
        }
    }

    // Sort by timestamp (chronological)
    entries.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));

    if let Some(n) = limit {
        entries.truncate(n);
    }

    Ok(entries)
}

/// Format log entries into a human-readable string.
pub fn format_log(entries: &[LogEntry]) -> String {
    let mut out = String::new();
    for entry in entries {
        out.push_str(&format!(
            "{} {} {:.8} {} <{}> {}\n",
            entry.timestamp,
            entry.event_type,
            entry.entity_kind,
            entry.entity_id.get(..8).unwrap_or(&entry.entity_id),
            entry.author.name,
            entry.summary,
        ));
    }
    out
}

/// Print the log to stdout.
pub fn print_log(repo: &Repository, limit: Option<usize>) -> Result<(), Error> {
    let entries = collect_events(repo, limit)?;
    if entries.is_empty() {
        println!("No collab events found.");
        return Ok(());
    }
    print!("{}", format_log(&entries));
    Ok(())
}

fn action_type_name(action: &Action) -> String {
    match action {
        Action::IssueOpen { .. } => "IssueOpen".to_string(),
        Action::IssueComment { .. } => "IssueComment".to_string(),
        Action::IssueClose { .. } => "IssueClose".to_string(),
        Action::IssueEdit { .. } => "IssueEdit".to_string(),
        Action::IssueLabel { .. } => "IssueLabel".to_string(),
        Action::IssueUnlabel { .. } => "IssueUnlabel".to_string(),
        Action::IssueAssign { .. } => "IssueAssign".to_string(),
        Action::IssueUnassign { .. } => "IssueUnassign".to_string(),
        Action::IssueReopen => "IssueReopen".to_string(),
        Action::IssueCommitLink { .. } => "IssueCommitLink".to_string(),
        Action::PatchCreate { .. } => "PatchCreate".to_string(),
        Action::PatchRevision { .. } => "PatchRevision".to_string(),
        Action::PatchReview { .. } => "PatchReview".to_string(),
        Action::PatchComment { .. } => "PatchComment".to_string(),
        Action::PatchInlineComment { .. } => "PatchInlineComment".to_string(),
        Action::PatchClose { .. } => "PatchClose".to_string(),
        Action::PatchMerge => "PatchMerge".to_string(),
        Action::Merge => "Merge".to_string(),
    }
}

fn action_summary(action: &Action) -> String {
    match action {
        Action::IssueOpen { title, .. } => format!("open \"{}\"", title),
        Action::IssueComment { body } => truncate(body, 60),
        Action::IssueClose { reason } => match reason {
            Some(r) => format!("close: {}", r),
            None => "close".to_string(),
        },
        Action::IssueEdit { title, body } => {
            let parts: Vec<&str> = [
                title.as_ref().map(|_| "title"),
                body.as_ref().map(|_| "body"),
            ]
            .into_iter()
            .flatten()
            .collect();
            format!("edit {}", parts.join(", "))
        }
        Action::IssueLabel { label } => format!("label \"{}\"", label),
        Action::IssueUnlabel { label } => format!("unlabel \"{}\"", label),
        Action::IssueAssign { assignee } => format!("assign \"{}\"", assignee),
        Action::IssueUnassign { assignee } => format!("unassign \"{}\"", assignee),
        Action::IssueReopen => "reopen".to_string(),
        Action::IssueCommitLink { commit } => {
            format!("commit link {}", &commit[..commit.len().min(7)])
        }
        Action::PatchCreate { title, .. } => format!("create \"{}\"", title),
        Action::PatchRevision { body, .. } => match body {
            Some(b) => format!("revision: {}", truncate(b, 50)),
            None => "revision".to_string(),
        },
        Action::PatchReview { verdict, .. } => format!("review: {}", verdict),
        Action::PatchComment { body } => truncate(body, 60),
        Action::PatchInlineComment { file, line, .. } => format!("comment on {}:{}", file, line),
        Action::PatchClose { reason } => match reason {
            Some(r) => format!("close: {}", r),
            None => "close".to_string(),
        },
        Action::PatchMerge => "merge".to_string(),
        Action::Merge => "dag merge".to_string(),
    }
}

fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max {
        s.to_string()
    } else {
        format!("{}...", &s[..max])
    }
}