Merge v3-t70: git log -N/diff --check, xargs -I/-0/-n/-r, /dev devices, /bin aliases (T70)

# Conflicts:
#	userland/git/README.md
This commit is contained in:
2026-11-08 01:15:01 +00:00
12 changed files with 476 additions and 33 deletions
+107
View File
@@ -618,6 +618,113 @@ fn unified(a: &str, b: &str, context: usize) -> String {
out
}
/// Git's default whitespace checks apply only to added lines.
pub fn check(
repo: &gix::Repository,
diffs: &[FileDiff],
revision: Option<&str>,
cached: bool,
) -> Result<i32> {
let snapshot = if let Some(rev) = revision {
Some(tree(repo, rev)?)
} else if cached {
Some(index(repo)?.0)
} else {
None
};
let mut failed = false;
for diff in diffs.iter().filter(|diff| !diff.binary) {
let mut number = 0;
let mut added = BTreeSet::new();
for line in diff.patch.split_terminator('\n') {
if line.starts_with("@@ ") {
number = line
.split_whitespace()
.nth(2)
.unwrap()
.trim_start_matches('+')
.split(',')
.next()
.unwrap()
.parse::<usize>()
.unwrap();
} else if number > 0 && line.starts_with('+') {
let text = &line[1..];
if text.trim_ascii().is_empty() {
added.insert(number);
}
let indent = text
.chars()
.take_while(|ch| *ch == ' ' || *ch == '\t')
.collect::<String>();
let mut errors = Vec::new();
if text.ends_with(|ch: char| ch.is_ascii_whitespace()) {
errors.push("trailing whitespace");
}
if !text.trim_ascii().is_empty() && indent.contains(" \t") {
errors.push("space before tab in indent");
}
if text.starts_with("<<<<<<< ") || text == "=======" || text.starts_with(">>>>>>> ")
{
println!("{}:{number}: leftover conflict marker", diff.path);
failed = true;
}
if !errors.is_empty() {
println!("{}:{number}: {}.", diff.path, errors.join(", "));
println!("{line}");
failed = true;
}
number += 1;
} else if number > 0 && line.starts_with(' ') {
number += 1;
}
}
if added.is_empty() {
continue;
}
let bytes = if let Some(ref snapshot) = snapshot {
blob(repo, snapshot.get(diff.path.as_bytes()))?
} else if diff.status == Kind::Deleted {
Vec::new()
} else {
let file = repo.workdir().unwrap().join(&diff.path);
if std::fs::symlink_metadata(&file)
.map_err(|e| error("diff check", e))?
.file_type()
.is_symlink()
{
gix::path::into_bstr(std::fs::read_link(&file).map_err(|e| error("diff check", e))?)
.map_err(|e| error("diff check", e))?
.into_owned()
.to_vec()
} else {
std::fs::read(file).map_err(|e| error("diff check", e))?
}
};
let text = String::from_utf8_lossy(&bytes);
let mut lines: Vec<_> = text.lines().collect();
if !text.ends_with('\n')
&& lines
.last()
.is_some_and(|line| line.trim_ascii().is_empty())
{
lines.pop();
}
let first = lines
.iter()
.enumerate()
.rev()
.take_while(|(_, line)| line.trim_ascii().is_empty())
.filter_map(|(i, _)| added.contains(&(i + 1)).then_some(i + 1))
.min();
if let Some(line) = first {
println!("{}:{line}: new blank line at EOF.", diff.path);
failed = true;
}
}
Ok(if failed { 2 } else { 0 })
}
#[cfg(test)]
mod tests {
use super::*;
+8 -2
View File
@@ -207,7 +207,7 @@ enum Command {
diff_merges: Option<String>,
#[arg(long)]
oneline: bool,
#[arg(short = 'n')]
#[arg(short = 'n', long = "max-count")]
limit: Option<usize>,
#[arg(short = '1')]
one: bool,
@@ -224,6 +224,8 @@ enum Command {
paths: Vec<String>,
},
Diff {
#[arg(long)]
check: bool,
#[arg(long)]
numstat: bool,
#[arg(long)]
@@ -894,6 +896,7 @@ pub fn run(cli: Cli) -> Result<i32> {
}
}
Command::Diff {
check,
numstat,
no_renames,
diff_filter,
@@ -921,13 +924,16 @@ pub fn run(cli: Cli) -> Result<i32> {
changes::DiffParams {
repo: path,
from,
to,
to: to.clone(),
staged: Some(cached),
path: paths.first().cloned(),
context: None,
},
!no_renames,
)?;
if check {
return changes::check(&repository, &diffs, to.as_deref(), cached);
}
if name_only || name_status {
for diff in diffs {
if name_status {
+29 -2
View File
@@ -1,11 +1,38 @@
//! Parse Git CLI arguments and return Git-compatible usage and operation exit codes.
use clap::{Parser, error::ErrorKind};
fn main() {
if matches!(std::env::args().nth(1).as_deref(), Some("--version" | "version")) {
if matches!(
std::env::args().nth(1).as_deref(),
Some("--version" | "version")
) {
println!("git version {}.sausage", env!("CARGO_PKG_VERSION"));
exit(0);
}
let cli = match git::Cli::try_parse() {
let mut args: Vec<_> = std::env::args_os().collect();
let mut command = 1;
while command < args.len() && args[command].to_string_lossy().starts_with('-') {
command += if args[command] == "-C" || args[command] == "-c" {
2
} else {
1
};
}
if args.get(command).is_some_and(|arg| arg == "log") {
let mut i = command + 1;
while i < args.len() && args[i] != "--" {
let arg = args[i].to_string_lossy();
if let Some(count) = arg
.strip_prefix('-')
.filter(|s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()))
{
let count = count.to_owned();
args.splice(i..=i, ["-n".into(), count.into()]);
i += 1;
}
i += 1;
}
}
let cli = match git::Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) => {
let code = if !error.use_stderr() {