Provide world Git identity, reflog fallback, object inspection, and which (T73)

This commit is contained in:
2026-11-08 02:28:34 +00:00
parent 1892642d8e
commit fdda49394f
11 changed files with 450 additions and 30 deletions
+44 -20
View File
@@ -57,30 +57,54 @@ pub(crate) struct BranchDeleted {
pub reference: String,
}
/// Reflogs require an identity, but switching branches must not require configuring one.
/// With no repository identity, leave reflogs untouched and report that explicitly; never invent an author.
/// Reflog-only writes use a fallback identity when the repository has none.
pub(crate) fn reflog_signature(repo: &gix::Repository) -> Result<gix::actor::Signature> {
if let Some(signature) = repo
.committer()
.transpose()
.map_err(|e| error("reflog identity", e))?
{
return signature
.to_owned()
.map_err(|e| error("reflog identity", e));
}
let config = repo.config_snapshot();
let name = std::env::var("GIT_COMMITTER_NAME")
.ok()
.or_else(|| config.string("user.name").map(|s| s.to_string()))
.or_else(|| std::env::var("USER").ok())
.unwrap_or_else(|| "Sausage".into());
let email = std::env::var("GIT_COMMITTER_EMAIL")
.ok()
.or_else(|| config.string("user.email").map(|s| s.to_string()))
.unwrap_or_else(|| {
format!(
"{}@{}",
std::env::var("USER").unwrap_or_else(|_| "sausage".into()),
std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".into())
)
});
let seconds = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
Ok(gix::actor::Signature {
name: name.into(),
email: email.into(),
time: gix::date::Time::new(seconds, 0),
})
}
/// Publish references and their reflog entries without requiring a commit identity.
pub(crate) fn edit_refs(
repo: &gix::Repository,
edits: Vec<gix::refs::transaction::RefEdit>,
) -> Result<Vec<String>> {
let committer = repo
.committer()
.transpose()
.map_err(|e| error("reflog identity", e))?;
if let Some(committer) = committer {
repo.edit_references_as(edits, Some(committer))
.map_err(|e| error("checkout refs", e))?;
Ok(Vec::new())
} else {
let mut local = repo.clone();
local.refs.write_reflog = gix::refs::store::WriteReflog::Disable;
local
.edit_references_as(edits, None)
.map_err(|e| error("checkout refs", e))?;
Ok(vec![
"reflog not written: no repository committer identity configured".into(),
])
}
let signature = reflog_signature(repo)?;
let mut time = Default::default();
repo.edit_references_as(edits, Some(signature.to_ref(&mut time)))
.map_err(|e| error("checkout refs", e))?;
Ok(Vec::new())
}
pub(crate) fn branch_name(name: &str) -> Result<String> {
+92
View File
@@ -0,0 +1,92 @@
//! Read Git object types, sizes, contents, and tree entries without changing repository state.
use crate::{Result, repo::error};
use std::io::Write;
pub(crate) fn cat_file(
repo: &gix::Repository,
revision: &str,
kind: bool,
size: bool,
exists: bool,
) -> Result<()> {
let object = repo
.rev_parse_single(revision)
.map_err(|e| error("cat-file", e))?
.object()
.map_err(|e| error("cat-file", e))?;
if exists {
return Ok(());
}
if kind {
println!("{}", object.kind);
} else if size {
println!("{}", object.data.len());
} else if object.kind == gix::objs::Kind::Tree {
ls_tree(repo, revision, false, false, None)?;
} else {
std::io::stdout()
.write_all(&object.data)
.map_err(|e| error("cat-file", e))?;
}
Ok(())
}
pub(crate) fn ls_tree(
repo: &gix::Repository,
revision: &str,
recursive: bool,
names: bool,
path: Option<&str>,
) -> Result<()> {
let id = crate::local::tree(repo, revision)?;
fn walk(
repo: &gix::Repository,
id: gix::ObjectId,
prefix: &str,
recursive: bool,
names: bool,
filter: Option<&str>,
) -> Result<()> {
let tree = repo.find_tree(id).map_err(|e| error("ls-tree", e))?;
for entry in tree.iter() {
let entry = entry.map_err(|e| error("ls-tree", e))?;
let filename = format!("{prefix}{}", entry.filename());
let directory = entry.mode().is_tree();
let descend = directory
&& (recursive
|| filter.is_some_and(|path| path.starts_with(&format!("{filename}/"))));
if descend {
walk(
repo,
entry.oid().to_owned(),
&format!("{filename}/"),
recursive,
names,
filter,
)?;
} else if filter.is_none_or(|path| {
filename == path
|| filename.starts_with(&format!("{}/", path.trim_end_matches('/')))
}) {
if names {
println!("{filename}");
} else {
let kind = if directory {
"tree"
} else if entry.mode().is_commit() {
"commit"
} else {
"blob"
};
println!(
"{:06o} {kind} {}\t{filename}",
entry.mode().kind() as u32,
entry.oid()
);
}
}
}
Ok(())
}
walk(repo, id, "", recursive, names, path)
}
+82 -5
View File
@@ -59,6 +59,7 @@ fn identity(name: Option<String>, email: Option<String>) -> Result<Identity> {
mod cherry_pick;
mod history;
mod local;
mod inspect;
mod plumbing;
mod stash;
use clap::{Parser, Subcommand};
@@ -79,8 +80,32 @@ enum Command {
Show {
#[arg(long)]
stat: bool,
#[arg(long)]
oneline: bool,
#[arg(long)]
name_only: bool,
revision: Option<String>,
},
#[command(group(clap::ArgGroup::new("mode").required(true).multiple(false)))]
CatFile {
#[arg(short = 't', group = "mode")]
kind: bool,
#[arg(short = 'p', group = "mode")]
pretty: bool,
#[arg(short = 's', group = "mode")]
size: bool,
#[arg(short = 'e', group = "mode")]
exists: bool,
revision: String,
},
LsTree {
#[arg(short = 'r')]
recursive: bool,
#[arg(long)]
name_only: bool,
revision: String,
path: Option<String>,
},
Clone {
url: String,
path: Option<String>,
@@ -257,6 +282,8 @@ enum Command {
delete: bool,
#[arg(short = 'D')]
force_delete: bool,
#[arg(short = 'f', long)]
force: bool,
#[arg(short = 'm')]
rename: bool,
name: Option<String>,
@@ -1124,6 +1151,7 @@ pub fn run(cli: Cli) -> Result<i32> {
list,
delete,
force_delete,
force,
show_current,
all,
rename,
@@ -1164,6 +1192,14 @@ pub fn run(cli: Cli) -> Result<i32> {
if !quiet {
println!("Deleted branch {name} (was {}).", &id[..7]);
}
} else if force {
let full = branches::branch_name(&name)?;
worktrees::ensure_available(&repository, &full)?;
if head_info(&repository)?.branch.as_deref() == Some(&full) {
return Err(error("branch", "cannot force update current branch"));
}
let id = branches::resolve(&repository, start.as_deref().unwrap_or("HEAD"))?;
plumbing::update_ref(&repository, &full, &id.to_string(), None)?;
} else {
branches::create(
&repository,
@@ -1488,7 +1524,35 @@ pub fn run(cli: Cli) -> Result<i32> {
)?;
}
},
Command::Show { revision, stat } => {
Command::CatFile {
kind,
pretty: _,
size,
exists,
revision,
} => {
inspect::cat_file(&repository, &revision, kind, size, exists)?;
}
Command::LsTree {
recursive,
name_only,
revision,
path,
} => {
inspect::ls_tree(
&repository,
&revision,
recursive,
name_only,
path.as_deref(),
)?;
}
Command::Show {
revision,
stat,
oneline,
name_only,
} => {
let object = repository
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
.map_err(|e| error("show", e))?
@@ -1528,9 +1592,18 @@ pub fn run(cli: Cli) -> Result<i32> {
return Ok(129);
}
}
output::commit(&commit)?;
println!();
if merge_commit && !stat {
if oneline {
let decoded = commit.decode().map_err(|e| error("show", e))?;
println!(
"{} {}",
&commit.id.to_string()[..7],
decoded.message.to_str_lossy().lines().next().unwrap_or("")
);
} else {
output::commit(&commit)?;
println!();
}
if merge_commit && !stat && !name_only {
return Ok(0);
}
let old = match commit.parent_ids().next() {
@@ -1548,7 +1621,11 @@ pub fn run(cli: Cli) -> Result<i32> {
path: None,
context: None,
})?;
if stat {
if name_only {
for diff in diffs {
println!("{}", diff.path);
}
} else if stat {
output::stat(&diffs, false);
} else {
for diff in diffs {
+1 -1
View File
@@ -118,7 +118,7 @@ pub(crate) fn reset(
repo,
Some(gix::ObjectId::from_hex(old.as_bytes()).map_err(|e| error("reset HEAD", e))?),
id,
&staging::signature(None, None)?,
&branches::reflog_signature(repo)?,
"reset",
)?;
}