use super::*;
use crate::test_support::{executable, fixture, remove};
use std::path::PathBuf;
fn snapshot(entries: &[(&str, &str, &str)]) -> ConfigSnapshot {
let mut listing: Vec<u8> = Vec::new();
for (scope, key, value) in entries {
listing.extend_from_slice(scope.as_bytes());
listing.push(0);
listing.extend_from_slice(key.as_bytes());
listing.push(b'\n');
listing.extend_from_slice(value.as_bytes());
listing.push(0);
}
return ConfigSnapshot::parse(listing.as_slice(), true).expect("listing parses");
}
#[test]
fn lfs_keys_of_the_repository_mark_it_as_using_lfs() {
assert!(uses_lfs(&snapshot(&[("local", "lfs.repositoryformatversion", "0")])));
assert!(uses_lfs(&snapshot(&[("local", "lfs.https://example.invalid/x.git/info/lfs.access", "basic")])));
assert!(uses_lfs(&snapshot(&[("local", "filter.lfs.process", "git-lfs filter-process")])));
assert!(uses_lfs(&snapshot(&[("worktree", "lfs.url", "http://127.0.0.1:1")])));
assert!(uses_lfs(&snapshot(&[("command", "lfs.url", "http://127.0.0.1:1")])));
assert!(uses_lfs(&snapshot(&[("system", "core.autocrlf", "false"), ("local", "lfs.url", "x")])));
}
#[test]
fn lfs_keys_of_the_machine_or_account_do_not_mark_a_repository_as_using_lfs() {
assert!(!uses_lfs(&snapshot(&[("global", "filter.lfs.process", "git-lfs filter-process")])));
assert!(!uses_lfs(&snapshot(&[("global", "lfs.customtransfer.xet.path", "git-xet")])));
assert!(!uses_lfs(&snapshot(&[("system", "filter.lfs.clean", "git-lfs clean")])));
assert!(!uses_lfs(&snapshot(&[
("global", "filter.lfs.smudge", "git-lfs smudge"),
("local", "core.bare", "false"),
])));
}
#[test]
fn other_configuration_does_not_mark_the_repository_as_using_lfs() {
assert!(!uses_lfs(&snapshot(&[])));
assert!(!uses_lfs(&snapshot(&[("local", "core.hookspath", "hooks")])));
assert!(!uses_lfs(&snapshot(&[("local", "hook.lfs.event", "pre-push")])));
assert!(!uses_lfs(&snapshot(&[("local", "filter.lfsx.process", "x")])));
assert!(!uses_lfs(&snapshot(&[("local", "lfsextra.url", "x")])));
assert!(!uses_lfs(&snapshot(&[("local", "filter.other.clean", "x")])));
}
#[test]
fn shell_quoting_keeps_one_literal_word() {
assert_eq!(shell_quote(OsStr::new("/usr/bin/x")), OsString::from("'/usr/bin/x'"));
assert_eq!(shell_quote(OsStr::new("/a b/x")), OsString::from("'/a b/x'"));
assert_eq!(shell_quote(OsStr::new("it's")), OsString::from("'it'\\''s'"));
assert_eq!(shell_quote(OsStr::new("")), OsString::from("''"));
}
#[test]
fn the_hook_definition_names_the_event_and_the_wrapper_entry() {
let tokens: Vec<OsString> = hook_arguments(Path::new("/opt/cli git/cli-git-native"));
assert_eq!(
tokens,
vec![
OsString::from("-c"),
OsString::from("hook.cli-git-lfs-pre-push.event=pre-push"),
OsString::from("-c"),
OsString::from("hook.cli-git-lfs-pre-push.command='/opt/cli git/cli-git-native' cli-git lfs-pre-push"),
]
);
}
#[test]
fn the_hook_precedes_the_callers_arguments() {
let combined: Vec<OsString> = with_lfs_hook(
&[OsString::from("-c"), OsString::from("a=b"), OsString::from("push")],
Path::new("/x"),
);
assert_eq!(combined.len(), 7);
assert_eq!(combined[0], OsString::from("-c"));
assert_eq!(combined[3].to_string_lossy(), "hook.cli-git-lfs-pre-push.command='/x' cli-git lfs-pre-push");
assert_eq!(&combined[4..], &[OsString::from("-c"), OsString::from("a=b"), OsString::from("push")]);
}
#[test]
fn only_the_exact_entry_words_start_the_hook_entry() {
let entry: Vec<OsString> = ["cli-git", "lfs-pre-push", "origin", "https://example.invalid/r.git"]
.iter()
.map(OsString::from)
.collect();
assert_eq!(
hook_entry_arguments(entry.as_slice()),
Some(&entry[2..])
);
let bare: Vec<OsString> = ["cli-git", "lfs-pre-push"].iter().map(OsString::from).collect();
assert_eq!(hook_entry_arguments(bare.as_slice()), Some(&bare[2..]));
for refused in [
vec!["cli-git"],
vec!["cli-git", "doctor"],
vec!["-c", "x=y", "cli-git", "lfs-pre-push"],
vec!["lfs-pre-push", "cli-git"],
vec!["push", "origin"],
vec![],
] {
let arguments: Vec<OsString> = refused.iter().map(OsString::from).collect();
assert_eq!(hook_entry_arguments(arguments.as_slice()), None, "{refused:?}");
}
}
#[test]
fn the_missing_program_message_names_the_cause_and_every_way_forward() {
let message: String = missing_program_message(&std::io::Error::from(std::io::ErrorKind::NotFound));
for needle in [
"stopped before it sent anything",
"`git-lfs` could not be started",
"install Git LFS",
"git lfs uninstall",
"git push --no-verify",
"git lfs push --all <remote>",
] {
assert!(message.contains(needle), "{needle}: {message}");
}
assert!(!message.contains('\u{2014}') && !message.contains('\u{2013}'));
}
#[test]
fn the_failure_message_says_nothing_was_published_and_names_every_way_forward() {
let message: String = failed_message("exited with status 2");
for needle in [
"`git-lfs pre-push` exited with status 2",
"before any ref changed and nothing was published",
"`lfs.url`",
"git lfs push --all <remote>",
"git push --no-verify",
"GIT_LFS_SKIP_PUSH=1",
] {
assert!(message.contains(needle), "{needle}: {message}");
}
}
fn recording_program(root: &Path, status: i32) -> PathBuf {
let program: PathBuf = root.join("fake-git-lfs");
let source: String = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > '{root}/arguments.txt'\ncat > '{root}/input.txt'\nexit {status}\n",
root = root.display()
);
executable(program.as_path(), source.as_bytes());
return program;
}
fn run_entry(program: &Path, arguments: &[&str]) -> (i32, String) {
let owned: Vec<OsString> = arguments.iter().map(OsString::from).collect();
let mut diagnostics: Vec<u8> = Vec::new();
let status: i32 = run_with_program(program.as_os_str(), owned.as_slice(), &mut diagnostics);
return (status, String::from_utf8_lossy(diagnostics.as_slice()).into_owned());
}
#[test]
fn the_entry_passes_the_hook_arguments_to_pre_push_and_succeeds_silently() {
let root: PathBuf = fixture("lfs-entry-pass");
let program: PathBuf = recording_program(root.as_path(), 0);
let (status, diagnostics) = run_entry(program.as_path(), &["origin", "https://example.invalid/r.git"]);
assert_eq!(status, 0);
assert_eq!(diagnostics, "");
let recorded: String = std::fs::read_to_string(root.join("arguments.txt")).expect("arguments");
assert_eq!(recorded, "pre-push\norigin\nhttps://example.invalid/r.git\n");
remove(root.as_path());
}
#[test]
fn the_entry_returns_the_programs_status_and_explains_a_failure() {
let root: PathBuf = fixture("lfs-entry-fail");
let program: PathBuf = recording_program(root.as_path(), 7);
let (status, diagnostics) = run_entry(program.as_path(), &["origin", "u"]);
assert_eq!(status, 7);
assert!(diagnostics.contains("`git-lfs pre-push` exited with status 7"), "{diagnostics}");
assert!(diagnostics.contains("nothing was published"), "{diagnostics}");
remove(root.as_path());
}
#[test]
fn the_entry_blocks_when_the_program_cannot_be_started() {
let root: PathBuf = fixture("lfs-entry-missing");
let (status, diagnostics) = run_entry(root.join("absent").as_path(), &["origin", "u"]);
assert_eq!(status, 1);
assert!(diagnostics.contains("could not be started"), "{diagnostics}");
assert!(diagnostics.contains("install Git LFS"), "{diagnostics}");
remove(root.as_path());
}
#[test]
fn the_entry_blocks_when_the_program_is_ended_by_a_signal() {
let root: PathBuf = fixture("lfs-entry-signal");
let program: PathBuf = root.join("killed");
executable(program.as_path(), b"#!/bin/sh\nkill -TERM $$\nsleep 5\n");
let (status, diagnostics) = run_entry(program.as_path(), &["origin", "u"]);
assert_eq!(status, 128 + 15);
assert!(diagnostics.contains("was ended by signal 15"), "{diagnostics}");
remove(root.as_path());
}
#[test]
fn the_hook_command_survives_a_shell_with_an_awkward_executable_path() {
let root: PathBuf = fixture("lfs-entry-quoting");
let directory: PathBuf = root.join("it's a dir");
std::fs::create_dir(&directory).expect("directory");
let program: PathBuf = directory.join("cli git");
let record: PathBuf = root.join("record.txt");
let source: String = format!("#!/bin/sh\nprintf '%s|' \"$@\" > '{}'\n", record.display());
executable(program.as_path(), source.as_bytes());
let tokens: Vec<OsString> = hook_arguments(program.as_path());
let command: String = tokens[3].to_string_lossy().replacen("hook.cli-git-lfs-pre-push.command=", "", 1);
let status = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(format!("{command} \"$@\""))
.arg("sh")
.arg("origin")
.arg("a url")
.status()
.expect("sh starts");
assert!(status.success());
assert_eq!(std::fs::read_to_string(record).expect("record"), "cli-git|lfs-pre-push|origin|a url|");
remove(root.as_path());
}