use std::io::Write;
pub(crate) fn check_generated_paths(
cwd: Option<&std::path::Path>,
paths: &[String],
) -> std::collections::HashSet<String> {
if paths.is_empty() {
return std::collections::HashSet::new();
}
let mut command = std::process::Command::new("git");
command
.args(["check-attr", "-z", "diff", "linguist-generated", "--"])
.args(paths);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let Ok(output) = command.output() else {
return std::collections::HashSet::new();
};
if !output.status.success() {
return std::collections::HashSet::new();
}
let Ok(stdout) = String::from_utf8(output.stdout) else {
return std::collections::HashSet::new();
};
parse_generated_paths(&stdout)
}
pub(crate) fn check_generated_paths_batch(
cwd: Option<&std::path::Path>,
paths: &[String],
) -> std::collections::HashSet<String> {
if paths.is_empty() {
return std::collections::HashSet::new();
}
let mut command = std::process::Command::new("git");
command
.args(["check-attr", "--stdin", "-z", "diff", "linguist-generated"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let Ok(mut child) = command.spawn() else {
return std::collections::HashSet::new();
};
let Some(mut stdin) = child.stdin.take() else {
return std::collections::HashSet::new();
};
let Some(mut stdout) = child.stdout.take() else {
return std::collections::HashSet::new();
};
let stderr = child.stderr.take();
let stderr_reader = stderr.map(|mut stderr| {
std::thread::spawn(move || {
let mut buf = Vec::new();
let _ = std::io::Read::read_to_end(&mut stderr, &mut buf);
buf
})
});
let paths_owned: Vec<String> = paths.to_vec();
let writer = std::thread::spawn(move || -> std::io::Result<()> {
for path in &paths_owned {
stdin.write_all(path.as_bytes())?;
stdin.write_all(b"\0")?;
}
Ok(())
});
let mut stdout_bytes = Vec::new();
if std::io::Read::read_to_end(&mut stdout, &mut stdout_bytes).is_err() {
return std::collections::HashSet::new();
}
let Ok(Ok(())) = writer.join() else {
return std::collections::HashSet::new();
};
let status = child.wait();
if let Some(reader) = stderr_reader {
let _ = reader.join();
}
let Ok(status) = status else {
return std::collections::HashSet::new();
};
if !status.success() {
return std::collections::HashSet::new();
}
let Ok(stdout_text) = String::from_utf8(stdout_bytes) else {
return std::collections::HashSet::new();
};
parse_generated_paths(&stdout_text)
}
fn parse_generated_paths(output: &str) -> std::collections::HashSet<String> {
let fields: Vec<&str> = output
.split('\0')
.filter(|field| !field.is_empty())
.collect();
let mut generated = std::collections::HashSet::new();
for triple in fields.chunks_exact(3) {
let [path, attribute, value] = triple else {
continue;
};
let is_generated = (*attribute == "diff" && *value == "unset")
|| (*attribute == "linguist-generated" && matches!(*value, "set" | "true"));
if is_generated {
generated.insert((*path).to_string());
}
}
generated
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::run_git;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
#[test]
fn should_mark_path_generated_when_diff_attribute_is_unset() {
let output = "Cargo.lock\0diff\0unset\0Cargo.lock\0linguist-generated\0unspecified\0";
let expected: HashSet<String> = ["Cargo.lock".to_string()].into_iter().collect();
let actual = parse_generated_paths(output);
assert_eq!(expected, actual);
}
#[test]
fn should_mark_path_generated_when_linguist_generated_attribute_value_is_true() {
let output = "gen/foo.go\0diff\0unspecified\0gen/foo.go\0linguist-generated\0true\0";
let expected: HashSet<String> = ["gen/foo.go".to_string()].into_iter().collect();
let actual = parse_generated_paths(output);
assert_eq!(expected, actual);
}
#[test]
fn should_mark_path_generated_when_linguist_generated_attribute_is_bare_set() {
let output = "gen/foo.go\0diff\0unspecified\0gen/foo.go\0linguist-generated\0set\0";
let expected: HashSet<String> = ["gen/foo.go".to_string()].into_iter().collect();
let actual = parse_generated_paths(output);
assert_eq!(expected, actual);
}
#[test]
fn should_not_mark_path_generated_when_both_attributes_are_unspecified() {
let output = "normal.rs\0diff\0unspecified\0normal.rs\0linguist-generated\0unspecified\0";
let expected: HashSet<String> = HashSet::new();
let actual = parse_generated_paths(output);
assert_eq!(expected, actual);
}
#[test]
fn should_mark_only_matching_path_when_multiple_paths_are_queried() {
let output = "\
Cargo.lock\0diff\0unset\0Cargo.lock\0linguist-generated\0unspecified\0normal.rs\0diff\0unspecified\0normal.rs\0linguist-generated\0unspecified\0";
let expected: HashSet<String> = ["Cargo.lock".to_string()].into_iter().collect();
let actual = parse_generated_paths(output);
assert_eq!(expected, actual);
}
#[test]
fn should_return_empty_set_when_output_is_empty() {
let expected: HashSet<String> = HashSet::new();
let actual = parse_generated_paths("");
assert_eq!(expected, actual);
}
#[test]
fn should_report_paths_marked_generated_in_gitattributes() {
let dir = tempfile::TempDir::new().expect("create tempdir");
run_git(dir.path(), &["init", "--initial-branch=main"]);
std::fs::write(
dir.path().join(".gitattributes"),
"Cargo.lock -diff\ngen/*.go linguist-generated=true\n",
)
.expect("write .gitattributes");
std::fs::write(dir.path().join("Cargo.lock"), "").expect("write Cargo.lock");
std::fs::write(dir.path().join("normal.rs"), "").expect("write normal.rs");
std::fs::create_dir_all(dir.path().join("gen")).expect("create gen dir");
std::fs::write(dir.path().join("gen/foo.go"), "").expect("write gen/foo.go");
let paths = vec![
"Cargo.lock".to_string(),
"normal.rs".to_string(),
"gen/foo.go".to_string(),
];
let actual = check_generated_paths(Some(dir.path()), &paths);
let expected: HashSet<String> = ["Cargo.lock".to_string(), "gen/foo.go".to_string()]
.into_iter()
.collect();
assert_eq!(expected, actual);
}
#[test]
fn should_return_empty_set_when_cwd_is_not_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
std::fs::write(dir.path().join("Cargo.lock"), "").expect("write Cargo.lock");
let actual = check_generated_paths(Some(dir.path()), &["Cargo.lock".to_string()]);
let expected: HashSet<String> = HashSet::new();
assert_eq!(expected, actual);
}
#[test]
fn should_return_empty_set_when_paths_is_empty() {
let dir = tempfile::TempDir::new().expect("create tempdir");
run_git(dir.path(), &["init", "--initial-branch=main"]);
let actual = check_generated_paths(Some(dir.path()), &[]);
let expected: HashSet<String> = HashSet::new();
assert_eq!(expected, actual);
}
#[test]
fn should_report_paths_marked_generated_via_stdin_batch() {
let dir = tempfile::TempDir::new().expect("create tempdir");
run_git(dir.path(), &["init", "--initial-branch=main"]);
std::fs::write(
dir.path().join(".gitattributes"),
"Cargo.lock -diff\ngen/*.go linguist-generated=true\n",
)
.expect("write .gitattributes");
std::fs::write(dir.path().join("Cargo.lock"), "").expect("write Cargo.lock");
std::fs::write(dir.path().join("normal.rs"), "").expect("write normal.rs");
std::fs::create_dir_all(dir.path().join("gen")).expect("create gen dir");
std::fs::write(dir.path().join("gen/foo.go"), "").expect("write gen/foo.go");
let paths = vec![
"Cargo.lock".to_string(),
"normal.rs".to_string(),
"gen/foo.go".to_string(),
];
let actual = check_generated_paths_batch(Some(dir.path()), &paths);
let expected: HashSet<String> = ["Cargo.lock".to_string(), "gen/foo.go".to_string()]
.into_iter()
.collect();
assert_eq!(expected, actual);
}
#[test]
fn should_return_empty_set_when_batch_cwd_is_not_a_git_repository() {
let dir = tempfile::TempDir::new().expect("create tempdir");
std::fs::write(dir.path().join("Cargo.lock"), "").expect("write Cargo.lock");
let actual = check_generated_paths_batch(Some(dir.path()), &["Cargo.lock".to_string()]);
let expected: HashSet<String> = HashSet::new();
assert_eq!(expected, actual);
}
#[test]
fn should_return_empty_set_when_batch_paths_is_empty() {
let dir = tempfile::TempDir::new().expect("create tempdir");
run_git(dir.path(), &["init", "--initial-branch=main"]);
let actual = check_generated_paths_batch(Some(dir.path()), &[]);
let expected: HashSet<String> = HashSet::new();
assert_eq!(expected, actual);
}
#[test]
fn should_handle_many_paths_via_stdin_without_hitting_arg_limits() {
let dir = tempfile::TempDir::new().expect("create tempdir");
run_git(dir.path(), &["init", "--initial-branch=main"]);
std::fs::write(
dir.path().join(".gitattributes"),
"gen/*.go linguist-generated=true\n",
)
.expect("write .gitattributes");
std::fs::create_dir_all(dir.path().join("gen")).expect("create gen dir");
let mut paths = Vec::new();
for i in 0..5000 {
let path = format!("gen/file{i}.go");
std::fs::write(dir.path().join(&path), "").expect("write generated file");
paths.push(path);
}
let actual = check_generated_paths_batch(Some(dir.path()), &paths);
assert_eq!(paths.len(), actual.len());
}
}