//! Git's text summaries for small, textual diffs. use crate::changes::{FileDiff, Kind, Status}; use gix::bstr::ByteSlice; pub(crate) fn commit(commit: &gix::Commit<'_>) -> crate::Result<()> { let decoded = commit .decode() .map_err(|e| crate::repo::error("commit output", e))?; println!("commit {}", commit.id); if decoded.parents.len() > 1 { println!( "Merge: {}", decoded .parents .iter() .map(|id| id.to_string()[..7].to_owned()) .collect::>() .join(" ") ); } let author = commit .author() .map_err(|e| crate::repo::error("commit author", e))?; let time = author .time() .map_err(|e| crate::repo::error("commit date", e))?; println!( "Author: {} <{}>\nDate: {}\n\n {}", author.name, author.email, time.format_or_unix(gix::date::time::format::DEFAULT), decoded .message .to_str_lossy() .trim_end() .replace('\n', "\n ") ); Ok(()) } pub(crate) fn author_date(commit: &gix::Commit<'_>) -> crate::Result<()> { let author = commit .author() .map_err(|e| crate::repo::error("author", e))?; println!( " Date: {}", author .time() .map_err(|e| crate::repo::error("date", e))? .format_or_unix(gix::date::time::format::DEFAULT) ); Ok(()) } pub(crate) fn long_status(s: &Status) { let branch = s .head .branch .as_deref() .unwrap_or("HEAD") .trim_start_matches("refs/heads/"); if s.head.detached { println!( "HEAD detached at {}", &s.head.id.as_deref().unwrap_or("HEAD")[..7] ); } else { println!("On branch {branch}"); } if s.head.unborn { println!("\nNo commits yet\n"); } if !s.staged.is_empty() { println!("Changes to be committed:"); println!( " (use \"git {} ...\" to unstage)", if s.head.unborn { "rm --cached" } else { "restore --staged" } ); for c in &s.staged { println!("\t{:12}{}", label(&c.status), c.path); } println!(); } if !s.unstaged.is_empty() { println!("Changes not staged for commit:"); println!(" (use \"git add ...\" to update what will be committed)"); println!(" (use \"git restore ...\" to discard changes in working directory)"); for c in &s.unstaged { println!("\t{:12}{}", label(&c.status), c.path); } println!(); } if !s.untracked.is_empty() { println!( "Untracked files:\n (use \"git add ...\" to include in what will be committed)" ); for path in &s.untracked { println!("\t{path}"); } println!(); } if s.staged.is_empty() { if !s.unstaged.is_empty() { println!("no changes added to commit (use \"git add\" and/or \"git commit -a\")"); } else if !s.untracked.is_empty() { println!( "nothing added to commit but untracked files present (use \"git add\" to track)" ); } else if s.head.unborn { println!("nothing to commit (create/copy files and use \"git add\" to track)"); } else { println!("nothing to commit, working tree clean"); } } } fn label(kind: &Kind) -> &'static str { match kind { Kind::Added => "new file:", Kind::Modified => "modified:", Kind::Deleted => "deleted:", Kind::Renamed => "renamed:", } } pub(crate) fn numstat(diff: &FileDiff, nul: bool) { let hunks = diff.patch.lines().skip_while(|l| !l.starts_with("@@")); let added = hunks.clone().filter(|l| l.starts_with('+')).count(); let removed = hunks.filter(|l| l.starts_with('-')).count(); let (added, removed) = if diff.binary { ("-".into(), "-".into()) } else { (added.to_string(), removed.to_string()) }; print!( "{added}\t{removed}\t{}{}", diff.path, if nul { "\0" } else { "\n" } ); } pub(crate) fn stat(diffs: &[FileDiff], summary: bool) { summarize(diffs, true, summary); } pub(crate) fn commit_stat(diffs: &[FileDiff]) { summarize(diffs, false, true); } fn summarize(diffs: &[FileDiff], names: bool, summary: bool) { let width = diffs.iter().map(|d| d.path.len()).max().unwrap_or(0); let mut insertions = 0; let mut deletions = 0; for diff in diffs { let added = diff .patch .lines() .filter(|l| l.starts_with('+') && !l.starts_with("+++")) .count(); let removed = diff .patch .lines() .filter(|l| l.starts_with('-') && !l.starts_with("---")) .count(); insertions += added; deletions += removed; if names { println!( " {:width$} | {} {}{}", diff.path, added + removed, "+".repeat(added), "-".repeat(removed) ); } } if diffs.is_empty() { return; } print!( " {} file{} changed", diffs.len(), if diffs.len() == 1 { "" } else { "s" } ); if insertions > 0 { print!( ", {insertions} insertion{}(+)", if insertions == 1 { "" } else { "s" } ); } if deletions > 0 { print!( ", {deletions} deletion{}(-)", if deletions == 1 { "" } else { "s" } ); } println!(); if summary { for d in diffs { match d.status { Kind::Added => println!(" create mode 100644 {}", d.path), Kind::Deleted => println!(" delete mode 100644 {}", d.path), _ => (), } } } } pub(crate) fn conflicts(paths: &[String], merge: bool) { for path in paths { println!("Auto-merging {path}"); println!("CONFLICT (content): Merge conflict in {path}"); } if merge && !paths.is_empty() { println!("Automatic merge failed; fix conflicts and then commit the result."); } }