Add Git inspection forms, find pruning, and WASI tail stdin (T76)

This commit is contained in:
2026-11-08 03:45:36 +00:00
parent de351919dd
commit 3930c6de1a
28 changed files with 4363 additions and 92 deletions
+47 -46
View File
@@ -61,6 +61,7 @@ mod history;
mod local;
mod inspect;
mod plumbing;
mod pretty;
mod stash;
use clap::{Parser, Subcommand};
#[derive(Parser)]
@@ -78,6 +79,12 @@ pub struct Cli {
#[derive(Subcommand)]
enum Command {
Show {
#[arg(short = 's', long = "no-patch")]
no_patch: bool,
#[arg(long)]
format: Option<String>,
#[arg(long)]
pretty: Option<String>,
#[arg(long)]
stat: bool,
#[arg(long)]
@@ -224,6 +231,8 @@ enum Command {
no_gpg_sign: bool,
},
Log {
#[arg(long, num_args = 0..=1, default_missing_value = "short", require_equals = true, value_parser = ["short"])]
decorate: Option<String>,
#[arg(long)]
name_only: bool,
#[arg(long)]
@@ -988,6 +997,7 @@ pub fn run(cli: Cli) -> Result<i32> {
}
}
Command::Log {
decorate,
name_only,
no_renames,
diff_merges,
@@ -1063,49 +1073,7 @@ pub fn run(cli: Cli) -> Result<i32> {
.as_deref()
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
{
let author = c.author().map_err(|e| error("log author", e))?;
let date = author
.time()
.map_err(|e| error("log date", e))?
.format_or_unix(gix::date::time::format::DEFAULT);
let mut labels = Vec::new();
for reference in repository
.references()
.map_err(|e| error("log refs", e))?
.all()
.map_err(|e| error("log refs", e))?
{
let reference = reference.map_err(|e| error("log refs", e))?;
let name = reference.name().as_bstr().to_str_lossy();
if repository
.rev_parse_single(name.as_ref())
.map(|id| id.to_string() == c.id.to_string())
.unwrap_or(false)
{
let short = reference.name().shorten().to_string();
labels.push(
if head_info(&repository)?.branch.as_deref() == Some(name.as_ref())
{
format!("HEAD -> {short}")
} else {
short
},
);
}
}
let decorations = if labels.is_empty() {
String::new()
} else {
format!(" ({})", labels.join(", "))
};
let text = plumbing::format(template, &message)
.replace("%H", &c.id.to_string())
.replace("%h", &c.id.to_string()[..7])
.replace("%s", message.lines().next().unwrap_or(""))
.replace("%an", &author.name.to_string())
.replace("%ae", &author.email.to_string())
.replace("%ad", &date)
.replace("%d", &decorations);
let text = pretty::format(&c, template)?;
if pretty.is_some() {
if !first {
println!();
@@ -1117,15 +1085,24 @@ pub fn run(cli: Cli) -> Result<i32> {
first = false;
} else if oneline {
println!(
"{} {}",
"{}{} {}",
&c.id.to_string()[..7],
if decorate.is_some() {
pretty::decorations(&c)?
} else {
String::new()
},
message.lines().next().unwrap_or("")
);
} else {
if !first {
println!();
}
output::commit(&c)?;
if decorate.is_some() {
output::decorated_commit(&c, &pretty::decorations(&c)?)?;
} else {
output::commit(&c)?;
}
first = false;
}
if name_only && !diffs.is_empty() {
@@ -1548,6 +1525,9 @@ pub fn run(cli: Cli) -> Result<i32> {
)?;
}
Command::Show {
no_patch,
format,
pretty,
revision,
stat,
oneline,
@@ -1565,6 +1545,22 @@ pub fn run(cli: Cli) -> Result<i32> {
.map_err(|e| error("show", e))?;
} else {
let commit = object.peel_to_commit().map_err(|e| error("show", e))?;
if no_patch {
if let Some(template) = format.as_deref().or(pretty
.as_deref()
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
{
let text = pretty::format(&commit, template)?;
if pretty.is_some() && format.is_none() {
print!("{text}");
} else {
println!("{text}");
}
} else {
output::commit(&commit)?;
}
return Ok(0);
}
let merge_commit = commit.parent_ids().count() > 1;
if merge_commit && !stat {
let mut common: Option<std::collections::BTreeSet<String>> = None;
@@ -1592,7 +1588,12 @@ pub fn run(cli: Cli) -> Result<i32> {
return Ok(129);
}
}
if oneline {
if let Some(template) = format.as_deref().or(pretty
.as_deref()
.map(|s| s.strip_prefix("format:").unwrap_or(s)))
{
println!("{}\n", pretty::format(&commit, template)?);
} else if oneline {
let decoded = commit.decode().map_err(|e| error("show", e))?;
println!(
"{} {}",
+4 -1
View File
@@ -2,10 +2,13 @@
use crate::changes::{FileDiff, Kind, Status};
use gix::bstr::ByteSlice;
pub(crate) fn commit(commit: &gix::Commit<'_>) -> crate::Result<()> {
decorated_commit(commit, "")
}
pub(crate) fn decorated_commit(commit: &gix::Commit<'_>, decorations: &str) -> crate::Result<()> {
let decoded = commit
.decode()
.map_err(|e| crate::repo::error("commit output", e))?;
println!("commit {}", commit.id);
println!("commit {}{}", commit.id, decorations);
if decoded.parents.len() > 1 {
println!(
"Merge: {}",
+157
View File
@@ -0,0 +1,157 @@
//! Shared commit formats and short reference decorations for log and show.
use crate::{Result, error};
use gix::bstr::ByteSlice;
pub(crate) fn decorations(commit: &gix::Commit<'_>) -> Result<String> {
let repo = commit.repo;
let head = crate::head_info(repo)?;
let mut labels = Vec::new();
for reference in repo
.references()
.map_err(|e| error("refs", e))?
.all()
.map_err(|e| error("refs", e))?
{
let mut reference = reference.map_err(|e| error("refs", e))?;
let name = reference.name().as_bstr().to_str_lossy().into_owned();
if reference
.peel_to_id()
.is_ok_and(|id| id.detach() == commit.id)
{
let short = reference.name().shorten().to_string();
labels.push(if head.branch.as_deref() == Some(name.as_ref()) {
format!("HEAD -> {short}")
} else if name.starts_with("refs/tags/") {
format!("tag: {short}")
} else {
short
});
}
}
if head.branch.is_none()
&& repo
.rev_parse_single("HEAD")
.is_ok_and(|id| id.detach() == commit.id)
{
labels.push("HEAD".into());
}
labels.sort_by(|a, b| {
b.starts_with("HEAD")
.cmp(&a.starts_with("HEAD"))
.then_with(|| b.starts_with("tag: ").cmp(&a.starts_with("tag: ")))
.then_with(|| b.cmp(a))
});
Ok(if labels.is_empty() {
String::new()
} else {
format!(" ({})", labels.join(", "))
})
}
fn relative(seconds: i64) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let mut diff = now - seconds;
if diff < 0 {
return "in the future".into();
}
let (count, unit) = if diff < 90 {
(diff, "second")
} else {
diff = (diff + 30) / 60;
if diff < 90 {
(diff, "minute")
} else {
diff = (diff + 30) / 60;
if diff < 36 {
(diff, "hour")
} else {
diff = (diff + 12) / 24;
if diff < 14 {
(diff, "day")
} else if diff < 70 {
((diff + 3) / 7, "week")
} else if diff < 365 {
((diff + 15) / 30, "month")
} else if diff < 1825 {
let total_months = (diff * 12 + 183) / 365;
let years = total_months / 12;
let months = total_months % 12;
if months != 0 {
return format!(
"{years} year{}, {months} month{} ago",
if years == 1 { "" } else { "s" },
if months == 1 { "" } else { "s" }
);
}
(years, "year")
} else {
((diff + 183) / 365, "year")
}
}
}
};
format!("{count} {unit}{} ago", if count == 1 { "" } else { "s" })
}
pub(crate) fn format(commit: &gix::Commit<'_>, template: &str) -> Result<String> {
let decoded = commit.decode().map_err(|e| error("format", e))?;
let message = decoded.message.to_str_lossy();
let author = commit.author().map_err(|e| error("author", e))?;
let committer = commit.committer().map_err(|e| error("committer", e))?;
let at = author.time().map_err(|e| error("date", e))?;
let ct = committer.time().map_err(|e| error("date", e))?;
let parents = decoded
.parents
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
let tree = decoded.tree.to_string();
let id = commit.id.to_string();
let fields = [
("%H", id.clone()),
("%h", id[..7].into()),
("%P", parents.join(" ")),
(
"%p",
parents
.iter()
.map(|p| &p[..7])
.collect::<Vec<_>>()
.join(" "),
),
("%T", tree.clone()),
("%t", tree[..7].into()),
("%an", author.name.to_string()),
("%ae", author.email.to_string()),
("%ad", at.format_or_unix(gix::date::time::format::DEFAULT)),
("%ar", relative(at.seconds)),
("%cn", committer.name.to_string()),
("%ce", committer.email.to_string()),
("%cd", ct.format_or_unix(gix::date::time::format::DEFAULT)),
("%cr", relative(ct.seconds)),
("%s", message.lines().next().unwrap_or("").into()),
(
"%b",
message.split_once("\n\n").map_or("", |(_, b)| b).into(),
),
("%B", message.to_string()),
("%d", decorations(commit)?),
("%n", "\n".into()),
("%%", "%".into()),
];
let expanded = crate::plumbing::format(template, &message);
let mut rest = expanded.as_str();
let mut text = String::new();
while !rest.is_empty() {
if let Some((key, value)) = fields.iter().find(|(key, _)| rest.starts_with(key)) {
text.push_str(value);
rest = &rest[key.len()..];
} else {
let ch = rest.chars().next().unwrap();
text.push(ch);
rest = &rest[ch.len_utf8()..];
}
}
Ok(text)
}