choreo-daemon 0.1.0

Agentic coding assistant — daemon, TUI, and bridges
use crate::tools::{ToolError, truncate_tool_output};
use gix::bstr::ByteSlice;
use schemars::JsonSchema;
use serde::Deserialize;
use std::{fmt::Write as _, io};

use super::{describe_head, open_repo, repo_work_dir_display};

#[derive(Debug, Deserialize, JsonSchema)]
pub struct GitLogArgs {
    pub repo_path: Option<String>,
    pub limit: Option<usize>,
}

pub fn execute_git_log_tool(
    args: &GitLogArgs,
    working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
    let output = git_log_impl(
        args.repo_path.as_deref(),
        args.limit.unwrap_or(10).clamp(1, 100),
        working_dir,
    )?;
    Ok(truncate_tool_output(&output))
}

pub(crate) fn git_log_impl(
    repo_path: Option<&str>,
    limit: usize,
    working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
    let repo = open_repo(repo_path, working_dir)?;
    let head = match repo.head_id() {
        Ok(head) => head,
        Err(_) => return Ok("repository has no commits yet".to_string()),
    };

    let mut walk = repo
        .rev_walk([head.detach()])
        .all()
        .map_err(io::Error::other)?;

    let mut out = String::new();
    writeln!(&mut out, "repository: {}", repo_work_dir_display(&repo)).ok();
    writeln!(&mut out, "head: {}", describe_head(&repo)?).ok();

    let mut count = 0usize;
    for info in (&mut walk).take(limit) {
        let info = info.map_err(io::Error::other)?;
        let commit = info.object().map_err(io::Error::other)?;
        let short_id = commit.short_id().map_err(io::Error::other)?;
        let decoded = commit.decode().map_err(io::Error::other)?;
        let author = commit.author().map_err(io::Error::other)?;
        let title = decoded
            .message
            .lines()
            .next()
            .map(|line| String::from_utf8_lossy(line.trim()).into_owned())
            .unwrap_or_default();
        writeln!(
            &mut out,
            "{} {} <{}> {}",
            short_id, author.name, author.email, title
        )
        .ok();
        count += 1;
    }

    if count == 0 {
        writeln!(&mut out, "repository has no commits yet").ok();
    }

    Ok(out.trim_end().to_string())
}

pub fn describe_git_log_invocation(args: &GitLogArgs) -> String {
    let limit = args.limit.unwrap_or(10).clamp(1, 100);
    match &args.repo_path {
        Some(p) => format!("Showing git log for `{}` (last {} commits).", p, limit),
        None => format!("Showing git log (last {} commits).", limit),
    }
}

pub(crate) struct GitLog;

define_tool!(
    GitLog,
    "git_log",
    "Show recent Git commits for the repository containing the given path.",
    GitLogArgs,
    execute_git_log_tool,
    "git",
    describe_git_log_invocation
);