#![cfg(unix)]
use std::path::Path;
use std::process::Command;
use grit_lib::objects::ObjectId;
use grit_lib::prelude::Environment;
use grit_lib::progress::NeverCancel;
use grit_lib::remote::clone::{clone, CloneOptions};
use grit_lib::remote::fetch::{fetch_remote, NoProgress};
use grit_lib::remote::push::push_remote;
use grit_lib::remote::push_report::PushRefStatus;
use grit_lib::remote::transfer::{FetchOptions, PushOptions, PushRefSpec, TagMode};
use grit_lib::remote::transport::{ConnectOptions, Service, SshTransport, Transport};
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.output()
.expect("run git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).expect("utf8 git output")
}
fn rev_parse(dir: &Path, rev: &str) -> ObjectId {
ObjectId::from_hex(git(dir, &["rev-parse", rev]).trim()).expect("valid oid")
}
fn init_empty_bare(dir: &Path) {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", "--bare", "-b", "main", "."]);
}
fn write_fake_ssh(dir: &Path) -> Option<std::path::PathBuf> {
use std::os::unix::fs::PermissionsExt;
let script = dir.join("fake-ssh.sh");
let body = r#"#!/bin/sh
cmd=
for cmd in "$@"; do :; done
case "$cmd" in
"git-upload-pack "*) cmd="git upload-pack ${cmd#git-upload-pack }" ;;
"git-receive-pack "*)
rp="git receive-pack ${cmd#git-receive-pack }"
if [ -n "$GRIT_TEST_SERVER_STDERR" ]; then
eval "$rp" 2>>"$GRIT_TEST_SERVER_STDERR"
exit $?
fi
cmd="$rp"
;;
esac
eval "exec $cmd"
"#;
std::fs::write(&script, body).ok()?;
let mut perms = std::fs::metadata(&script).ok()?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).ok()?;
Some(script)
}
fn ssh_url(bare: &Path) -> String {
format!("ssh://git@fakehost{}", bare.display())
}
fn connect_opts_v2() -> ConnectOptions {
ConnectOptions {
protocol_version: 2,
..Default::default()
}
}
#[test]
fn ssh_connect_empty_bare_completes_handshake() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let bare = tmp.path().join("remote.git");
init_empty_bare(&bare);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: fake-ssh script");
return;
};
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let conn = transport
.connect(&ssh_url(&bare), Service::UploadPack, &connect_opts_v2())
.expect("connect to empty bare over ssh");
assert!(
conn.advertised_refs().is_empty(),
"unexpected refs: {:?}",
conn.advertised_refs()
);
assert!(
!conn.capabilities().is_empty(),
"empty repository must still advertise server capabilities (handshake complete)"
);
}
#[test]
fn fetch_empty_bare_over_ssh_is_noop() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let bare = tmp.path().join("remote.git");
init_empty_bare(&bare);
let local = tmp.path().join("local");
std::fs::create_dir_all(&local).unwrap();
git(&local, &["init", "-q", "-b", "main", "."]);
std::fs::write(local.join("a.txt"), "one\n").unwrap();
git(&local, &["add", "a.txt"]);
git(&local, &["commit", "-q", "-m", "c1"]);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: fake-ssh script");
return;
};
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let mut conn = transport
.connect(&ssh_url(&bare), Service::UploadPack, &connect_opts_v2())
.expect("connect");
let mut opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::None,
..Default::default()
};
fetch_remote(
&local.join(".git"),
&mut *conn,
&mut opts,
&mut NoProgress,
&grit_lib::progress::NeverCancel,
)
.expect("fetch against empty remote");
}
#[test]
fn clone_empty_bare_over_ssh_succeeds() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let bare = tmp.path().join("remote.git");
init_empty_bare(&bare);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: fake-ssh script");
return;
};
let url = ssh_url(&bare);
let dest = tmp.path().join("clone");
let mut opts = CloneOptions::new(url, dest.clone());
opts.environment = Environment::from_vars(
[(
std::ffi::OsString::from("GIT_SSH"),
fake_ssh.as_os_str().to_os_string(),
)],
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/")),
);
let outcome =
clone(opts, &mut NoProgress, &NeverCancel, None).expect("clone empty bare over ssh");
assert!(outcome.branches_with_commits.is_empty());
assert!(dest.join(".git").is_dir());
}
#[test]
fn push_to_empty_bare_over_ssh_without_hung_up_stderr() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let bare = tmp.path().join("remote.git");
init_empty_bare(&bare);
let local = tmp.path().join("local");
std::fs::create_dir_all(&local).unwrap();
git(&local, &["init", "-q", "-b", "main", "."]);
std::fs::write(local.join("a.txt"), "one\n").unwrap();
git(&local, &["add", "a.txt"]);
git(&local, &["commit", "-q", "-m", "c1"]);
let tip = rev_parse(&local, "HEAD");
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: fake-ssh script");
return;
};
let server_stderr = tmp.path().join("receive-pack.stderr");
std::env::set_var(
"GRIT_TEST_SERVER_STDERR",
server_stderr.to_str().expect("utf8 path"),
);
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let mut conn = transport
.connect(
&ssh_url(&bare),
Service::ReceivePack,
&ConnectOptions::default(),
)
.expect("receive-pack connect");
let spec = PushRefSpec {
src: Some(tip),
dst: "refs/heads/x".to_owned(),
..Default::default()
};
let outcome = push_remote(
&local.join(".git"),
&mut *conn,
&[spec],
&PushOptions::default(),
&mut NoProgress,
&grit_lib::progress::NeverCancel,
)
.expect("push to empty bare");
drop(conn);
assert_eq!(outcome.results.len(), 1);
assert_eq!(outcome.results[0].status, PushRefStatus::Ok);
let server_log = std::fs::read_to_string(&server_stderr).unwrap_or_default();
assert!(
!server_log.contains("hung up unexpectedly"),
"receive-pack stderr must not report hung up on a successful push: {server_log}"
);
}