use super::commit::remove_settled;
use super::support::{Fixture, Observed, REAL_GIT, direct, fixture, git, observe, repository, stderr_of};
use std::ffi::OsStr;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
#[derive(Clone, Debug)]
struct Upload {
oid: String,
bytes: Vec<u8>,
origin_main: String,
}
#[derive(Default)]
struct ServerState {
uploads: Vec<Upload>,
refuse_batch: bool,
}
struct LfsServer {
port: u16,
state: Arc<Mutex<ServerState>>,
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
hooks: Option<PathBuf>,
}
fn origin_main(origin: &Path) -> String {
let output = std::process::Command::new(REAL_GIT)
.arg(format!("--git-dir={}", origin.display()))
.args(["rev-parse", "--verify", "--quiet", "refs/heads/main"])
.output()
.expect("rev-parse starts");
let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
return if text.is_empty() { String::from("absent") } else { text };
}
fn read_request(stream: &TcpStream) -> Option<(String, Vec<u8>)> {
let mut reader: BufReader<&TcpStream> = BufReader::new(stream);
let mut request_line: String = String::new();
reader.read_line(&mut request_line).ok()?;
let mut length: usize = 0;
loop {
let mut line: String = String::new();
reader.read_line(&mut line).ok()?;
if line == "\r\n" || line.is_empty() {
break;
}
if let Some((name, value)) = line.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
length = value.trim().parse().ok()?;
}
}
let mut body: Vec<u8> = vec![0; length];
reader.read_exact(body.as_mut_slice()).ok()?;
return Some((request_line.trim_end().to_owned(), body));
}
fn respond(mut stream: &TcpStream, status: &str, body: &str) {
let text: String = format!(
"HTTP/1.1 {status}\r\nContent-Type: application/vnd.git-lfs+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
let _written = stream.write_all(text.as_bytes());
}
fn serve(stream: &TcpStream, port: u16, origin: &Path, state: &Mutex<ServerState>) {
let Some((request_line, body)) = read_request(stream) else {
return;
};
if request_line.starts_with("POST /lfs/objects/batch ") {
if state.lock().expect("state").refuse_batch {
respond(stream, "403 Forbidden", "{\"message\":\"the fixture server refuses every batch\"}");
return;
}
let asked: serde_json::Value = serde_json::from_slice(body.as_slice()).unwrap_or_default();
let known: Vec<String> = state.lock().expect("state").uploads.iter().map(|upload| { return upload.oid.clone(); }).collect();
let objects: Vec<String> = asked["objects"]
.as_array()
.cloned()
.unwrap_or_default()
.iter()
.map(|object| {
let oid: String = object["oid"].as_str().unwrap_or_default().to_owned();
let size: u64 = object["size"].as_u64().unwrap_or_default();
if known.contains(&oid) {
return format!("{{\"oid\":\"{oid}\",\"size\":{size}}}");
}
return format!(
"{{\"oid\":\"{oid}\",\"size\":{size},\"actions\":{{\"upload\":{{\"href\":\"http://127.0.0.1:{port}/lfs/objects/{oid}\"}}}}}}"
);
})
.collect();
respond(stream, "200 OK", format!("{{\"transfer\":\"basic\",\"objects\":[{}]}}", objects.join(",")).as_str());
} else if request_line.starts_with("POST /lfs/locks/verify ") {
respond(stream, "200 OK", "{\"ours\":[],\"theirs\":[]}");
} else if let Some(rest) = request_line.strip_prefix("PUT /lfs/objects/") {
let oid: String = rest.split(' ').next().unwrap_or_default().to_owned();
let seen: String = origin_main(origin);
state.lock().expect("state").uploads.push(Upload { oid, bytes: body, origin_main: seen });
respond(stream, "200 OK", "{}");
} else {
respond(stream, "404 Not Found", "{\"message\":\"unknown endpoint\"}");
}
}
impl LfsServer {
fn start(origin: PathBuf) -> Self {
let listener: TcpListener = TcpListener::bind("127.0.0.1:0").expect("bind loopback");
let port: u16 = listener.local_addr().expect("address").port();
let state: Arc<Mutex<ServerState>> = Arc::new(Mutex::new(ServerState::default()));
let stop: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
let thread_state: Arc<Mutex<ServerState>> = Arc::clone(&state);
let thread_stop: Arc<AtomicBool> = Arc::clone(&stop);
let thread: JoinHandle<()> = std::thread::spawn(move || {
for connection in listener.incoming() {
if thread_stop.load(Ordering::SeqCst) {
return;
}
let Ok(stream) = connection else {
continue;
};
let _timeout = stream.set_read_timeout(Some(Duration::from_secs(20)));
serve(&stream, port, origin.as_path(), &thread_state);
}
});
return Self { port, state, stop, thread: Some(thread), hooks: None };
}
fn uploads(&self) -> Vec<Upload> {
return self.state.lock().expect("state").uploads.clone();
}
}
impl Drop for LfsServer {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(hooks) = self.hooks.as_deref() {
let _restored = std::fs::set_permissions(hooks, std::fs::Permissions::from_mode(0o755));
}
let _connected = TcpStream::connect(("127.0.0.1", self.port));
if let Some(thread) = self.thread.take() {
let _joined = thread.join();
}
}
}
fn real_lfs_program() -> Option<PathBuf> {
let path: std::ffi::OsString = std::env::var_os("PATH")?;
return std::env::split_paths(&path)
.map(|directory| { return directory.join("git-lfs"); })
.find(|candidate| { return candidate.is_file(); });
}
fn lfs_world(fixture: &Fixture) -> Option<(PathBuf, PathBuf, LfsServer, Vec<u8>)> {
let Some(program) = real_lfs_program() else {
eprintln!("skipped: git-lfs is not on PATH, so the real upload was not exercised");
return None;
};
std::os::unix::fs::symlink(program, fixture.root.join("bin/git-lfs")).expect("git-lfs link");
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
let origin: PathBuf = fixture.root.join("origin.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", "origin.git"]);
git(fixture, repo.as_path(), &["remote", "add", "origin", origin.to_str().expect("UTF-8")]);
let mut server: LfsServer = LfsServer::start(origin.clone());
let settings: [(&str, String); 7] = [
("filter.lfs.clean", String::from("git-lfs clean -- %f")),
("filter.lfs.smudge", String::from("git-lfs smudge -- %f")),
("filter.lfs.process", String::from("git-lfs filter-process")),
("filter.lfs.required", String::from("true")),
("lfs.url", format!("http://127.0.0.1:{}/lfs", server.port)),
("lfs.transfer.maxretries", String::from("1")),
("maintenance.autoDetach", String::from("false")),
];
for (key, value) in settings {
git(fixture, repo.as_path(), &["config", key, value.as_str()]);
}
std::fs::write(repo.join(".gitattributes"), b"*.bin filter=lfs diff=lfs merge=lfs -text\n").expect("attributes");
let content: Vec<u8> = (0..4096_u32).map(|index| { return u8::try_from(index % 251).expect("below 256"); }).collect();
std::fs::write(repo.join("big.bin"), content.as_slice()).expect("big.bin");
let added: Observed = observe(
&mut super::commit::wrapped_commit(fixture, repo.as_path(), &["add", "--", ".gitattributes", "big.bin"]),
b"",
);
assert_eq!(added.code, Some(0), "{}", stderr_of(&added));
let mut path: std::ffi::OsString = fixture.root.join("bin").into_os_string();
path.push(":/usr/bin:/bin");
let committed = direct(fixture)
.current_dir(repo.as_path())
.env("PATH", path)
.args(["commit", "--quiet", "--message=lfs"])
.output()
.expect("commit starts");
assert!(committed.status.success(), "{}", String::from_utf8_lossy(committed.stderr.as_slice()));
let hooks: PathBuf = repo.join(".git/hooks");
for shim in ["pre-push", "post-checkout", "post-commit", "post-merge"] {
let _removed = std::fs::remove_file(hooks.join(shim));
}
std::fs::set_permissions(&hooks, std::fs::Permissions::from_mode(0o555)).expect("read-only hooks directory");
server.hooks = Some(hooks);
return Some((repo, origin, server, content));
}
fn pointer_oid(fixture: &Fixture, repo: &Path) -> String {
let pointer: String = super::commit::git_text(fixture, repo, &["cat-file", "blob", "HEAD:big.bin"]);
return pointer
.lines()
.find_map(|line| { return line.strip_prefix("oid sha256:"); })
.expect("a pointer file has an oid line")
.to_owned();
}
fn published_main(fixture: &Fixture, origin: &Path) -> String {
let output = direct(fixture)
.current_dir(origin)
.args(["rev-parse", "--verify", "--quiet", "refs/heads/main"])
.output()
.expect("rev-parse starts");
let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
return if text.is_empty() { String::from("absent") } else { text };
}
#[test]
fn the_real_upload_delivers_the_object_before_the_ref() {
let fixture: Fixture = fixture("lfs-real");
let Some((repo, origin, server, content)) = lfs_world(&fixture) else {
return;
};
let pushed: Observed = observe(
&mut super::commit::wrapped_commit(&fixture, repo.as_path(), &["push", "origin", "main"]),
b"",
);
let stderr: String = stderr_of(&pushed);
assert_eq!(pushed.code, Some(0), "{stderr}");
let uploads: Vec<Upload> = server.uploads();
assert_eq!(uploads.len(), 1, "one object was uploaded: {stderr}");
assert_eq!(uploads[0].oid, pointer_oid(&fixture, repo.as_path()));
assert_eq!(uploads[0].bytes, content, "the server holds the file's bytes");
assert_eq!(uploads[0].origin_main, "absent", "the object arrived before the ref moved");
assert_ne!(published_main(&fixture, origin.as_path()), "absent", "the ref moved after the upload: {stderr}");
drop(server);
remove_settled(&fixture);
}
#[test]
fn a_refused_upload_publishes_no_pointer() {
let fixture: Fixture = fixture("lfs-real-refused");
let Some((repo, origin, server, _content)) = lfs_world(&fixture) else {
return;
};
server.state.lock().expect("state").refuse_batch = true;
let pushed: Observed = observe(
&mut super::commit::wrapped_commit(&fixture, repo.as_path(), &["push", "origin", "main"]),
b"",
);
let stderr: String = stderr_of(&pushed);
assert_ne!(pushed.code, Some(0), "{stderr}");
assert!(stderr.contains("`git-lfs push` exited with status"), "{stderr}");
assert!(stderr.contains("nothing was published"), "{stderr}");
assert_eq!(published_main(&fixture, origin.as_path()), "absent", "{stderr}");
assert!(server.uploads().is_empty());
drop(server);
remove_settled(&fixture);
}
#[test]
fn the_real_upload_precedes_the_auto_push_of_a_commit() {
let fixture: Fixture = fixture("lfs-real-auto");
let Some((repo, origin, server, content)) = lfs_world(&fixture) else {
return;
};
std::fs::write(repo.join("second.bin"), [content.as_slice(), b"second"].concat()).expect("second.bin");
let added: Observed = observe(
&mut super::commit::wrapped_commit(&fixture, repo.as_path(), &["add", "--", "second.bin"]),
b"",
);
assert_eq!(added.code, Some(0), "{}", stderr_of(&added));
let landed: Observed = observe(
&mut super::commit::wrapped_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--message=second", "--", "second.bin"]),
b"",
);
let stderr: String = stderr_of(&landed);
assert_eq!(landed.code, Some(0), "{stderr}");
assert!(!stderr.contains("auto-push to origin failed"), "{stderr}");
let uploads: Vec<Upload> = server.uploads();
assert_eq!(uploads.len(), 2, "both LFS objects of the branch were uploaded: {stderr}");
assert!(uploads.iter().all(|upload| { return upload.origin_main == "absent"; }), "{uploads:?}");
assert_ne!(published_main(&fixture, origin.as_path()), "absent", "{stderr}");
drop(server);
remove_settled(&fixture);
}