use crate::client::Client;
use crate::config;
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct CommitArgs {
#[command(subcommand)]
pub command: CommitCommand,
}
#[derive(Subcommand)]
pub enum CommitCommand {
List {
repo: Option<String>,
#[arg(long, default_value = "HEAD")]
ref_name: String,
#[arg(long, default_value = "20")]
limit: u32,
},
View {
sha: String,
#[arg(long)]
diff: bool,
},
}
#[derive(Debug, Deserialize)]
struct Commit {
oid: Option<String>,
message: Option<String>,
author: Option<CommitAuthor>,
time: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CommitAuthor {
name: Option<String>,
}
pub async fn run(args: CommitArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
CommitCommand::List {
repo,
ref_name,
limit,
} => list(repo.as_deref(), &ref_name, limit).await,
CommitCommand::View { sha, diff } => view(&sha, diff).await,
}
}
async fn view(sha: &str, diff: bool) -> Result<(), Box<dyn std::error::Error>> {
let mut args: Vec<&str> = vec!["show"];
if !diff {
args.push("--stat");
args.push("--no-patch");
}
args.push(sha);
let status = std::process::Command::new("git").args(&args).status()?;
if !status.success() {
return Err(format!("git show {sha} failed").into());
}
Ok(())
}
async fn list(
repo: Option<&str>,
ref_name: &str,
limit: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client
.get_with_query(
&format!("/{org}/{name}/commits"),
&[("ref", ref_name), ("per_page", &limit.to_string())],
)
.await?;
if output::is_json() {
output::print_json(&resp);
return Ok(());
}
let commits: Vec<Commit> = if let Some(arr) = resp.get("commits") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else {
serde_json::from_value(resp).unwrap_or_default()
};
output::header(&format!("Commits ({org}/{name}) — {ref_name}"));
let rows: Vec<Vec<String>> = commits
.iter()
.map(|c| {
let sha = c
.oid
.as_deref()
.map(|s| if s.len() > 8 { &s[..8] } else { s })
.unwrap_or("?")
.to_string();
let msg = c
.message
.as_deref()
.unwrap_or("")
.lines()
.next()
.unwrap_or("")
.to_string();
let author = c
.author
.as_ref()
.and_then(|a| a.name.clone())
.unwrap_or_default();
let time = c
.time
.as_deref()
.map(output::format_time)
.unwrap_or_default();
vec![sha, msg, author, time]
})
.collect();
output::print_table(&["SHA", "MESSAGE", "AUTHOR", "DATE"], &rows);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
#[command(no_binary_name = true)]
struct CommitCli {
#[command(subcommand)]
command: CommitCommand,
}
fn parse(args: &[&str]) -> CommitCommand {
CommitCli::try_parse_from(args).expect("parse").command
}
#[test]
fn view_parses_without_diff() {
match parse(&["view", "abc123"]) {
CommitCommand::View { sha, diff } => {
assert_eq!(sha, "abc123");
assert!(!diff);
}
_ => panic!("expected View"),
}
}
#[test]
fn view_parses_with_diff() {
match parse(&["view", "abc123", "--diff"]) {
CommitCommand::View { diff, .. } => assert!(diff),
_ => panic!("expected View"),
}
}
}