#![cfg(unix)]
use std::path::Path;
use std::process::Command;
use grit_lib::fetch::{fetch_remote, NoProgress};
use grit_lib::objects::ObjectId;
use grit_lib::odb::Odb;
use grit_lib::refs::resolve_ref;
use grit_lib::transfer::{FetchOptions, TagMode, UpdateMode};
use grit_lib::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 open_odb(git_dir: &Path) -> Odb {
Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf())
}
fn build_source(dir: &Path) {
git(dir, &["init", "-q", "-b", "main", "."]);
std::fs::write(dir.join("a.txt"), "one\n").unwrap();
git(dir, &["add", "a.txt"]);
git(dir, &["commit", "-q", "-m", "c1"]);
std::fs::write(dir.join("b.txt"), "two\n").unwrap();
git(dir, &["add", "b.txt"]);
git(dir, &["commit", "-q", "-m", "c2"]);
git(dir, &["tag", "-a", "v1", "-m", "release one"]);
git(dir, &["branch", "topic"]);
}
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
# Fake ssh: ignore host/options, run the remote command locally.
# The remote command is always the last argument.
cmd=
for cmd in "$@"; do :; done
# Rewrite the dashed transport name to the `git upload-pack` subcommand form.
case "$cmd" in
"git-upload-pack "*) cmd="git upload-pack ${cmd#git-upload-pack }" ;;
"git-receive-pack "*) cmd="git receive-pack ${cmd#git-receive-pack }" ;;
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 run_fetch_and_assert(transport: &SshTransport, url: &str, source: &Path, local_root: &Path) {
let main_oid = rev_parse(source, "refs/heads/main");
let topic_oid = rev_parse(source, "refs/heads/topic");
let tag_oid = rev_parse(source, "refs/tags/v1");
std::fs::create_dir_all(local_root).unwrap();
git(local_root, &["init", "-q", "-b", "main", "."]);
let local_git = local_root.join(".git");
let mut conn = match transport.connect(url, Service::UploadPack, &ConnectOptions::default()) {
Ok(c) => c,
Err(e) => panic!("SshTransport::connect failed for {url}: {e}"),
};
assert!(
conn.advertised_refs()
.iter()
.any(|(n, o)| n == "refs/heads/main" && *o == main_oid),
"advertisement missing refs/heads/main = {}",
main_oid.to_hex()
);
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::All,
..Default::default()
};
let outcome = fetch_remote(&local_git, &mut *conn, &opts, &mut NoProgress)
.expect("fetch_remote over ssh");
let got_main = resolve_ref(&local_git, "refs/remotes/origin/main").expect("origin/main");
let got_topic = resolve_ref(&local_git, "refs/remotes/origin/topic").expect("origin/topic");
assert_eq!(got_main, main_oid, "origin/main oid mismatch vs source");
assert_eq!(got_topic, topic_oid, "origin/topic oid mismatch vs source");
let got_tag = resolve_ref(&local_git, "refs/tags/v1").expect("tag v1 written");
assert_eq!(got_tag, tag_oid, "tag v1 oid mismatch vs source");
let local_odb = open_odb(&local_git);
for oid in [main_oid, topic_oid, tag_oid] {
assert!(
local_odb.exists(&oid),
"object {} missing from local odb after fetch",
oid.to_hex()
);
local_odb
.read(&oid)
.unwrap_or_else(|e| panic!("read {}: {e}", oid.to_hex()));
}
let main_update = outcome
.updates
.iter()
.find(|u| u.remote_ref == "refs/heads/main")
.expect("update for main");
assert_eq!(main_update.mode, UpdateMode::New);
assert_eq!(main_update.new_oid, Some(main_oid));
assert_eq!(outcome.default_branch.as_deref(), Some("main"));
assert_eq!(
got_main.to_hex(),
git(source, &["rev-parse", "refs/heads/main"]).trim()
);
let fsck = Command::new("git")
.current_dir(local_root)
.args(["fsck", "--no-dangling"])
.output()
.expect("run git fsck");
assert!(
fsck.status.success(),
"git fsck failed after ssh fetch: {}",
String::from_utf8_lossy(&fsck.stderr)
);
}
#[test]
fn fetch_over_ssh_shell_command_lands_refs_and_objects() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh available");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let source = tmp.path().join("source");
std::fs::create_dir_all(&source).unwrap();
build_source(&source);
git(&source, &["symbolic-ref", "HEAD", "refs/heads/main"]);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: could not create executable fake-ssh script");
return;
};
let transport = SshTransport::with_shell_command(fake_ssh.as_os_str());
let abs_path = source.to_str().expect("utf8 path");
let url = format!("fakehost:{abs_path}");
run_fetch_and_assert(&transport, &url, &source, &tmp.path().join("local-shell"));
}
#[test]
fn fetch_over_ssh_v2_lands_refs_and_objects() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh available");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let source = tmp.path().join("source");
std::fs::create_dir_all(&source).unwrap();
build_source(&source);
git(&source, &["symbolic-ref", "HEAD", "refs/heads/main"]);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: could not create executable fake-ssh script");
return;
};
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let abs_path = source.to_str().expect("utf8 path");
let url = format!("ssh://git@fakehost{abs_path}");
let main_oid = rev_parse(&source, "refs/heads/main");
let topic_oid = rev_parse(&source, "refs/heads/topic");
let tag_oid = rev_parse(&source, "refs/tags/v1");
let local_root = tmp.path().join("local-v2");
std::fs::create_dir_all(&local_root).unwrap();
git(&local_root, &["init", "-q", "-b", "main", "."]);
let local_git = local_root.join(".git");
let opts_v2 = ConnectOptions {
protocol_version: 2,
..Default::default()
};
let mut conn = transport
.connect(&url, Service::UploadPack, &opts_v2)
.expect("SshTransport::connect v2");
if conn.protocol_version() != 2 {
eprintln!(
"SKIP: server negotiated v{} (GIT_PROTOCOL not honored by upload-pack)",
conn.protocol_version()
);
return;
}
assert!(
conn.advertised_refs().is_empty(),
"v2 connection must advertise no refs on connect"
);
assert!(
conn.capabilities()
.iter()
.any(|c| c == "ls-refs" || c.starts_with("ls-refs=") || c.starts_with("fetch=")),
"v2 capability block missing ls-refs/fetch: {:?}",
conn.capabilities()
);
let opts = FetchOptions {
refspecs: vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()],
tags: TagMode::All,
..Default::default()
};
let outcome =
fetch_remote(&local_git, &mut *conn, &opts, &mut NoProgress).expect("v2 fetch over ssh");
let got_main = resolve_ref(&local_git, "refs/remotes/origin/main").expect("origin/main");
let got_topic = resolve_ref(&local_git, "refs/remotes/origin/topic").expect("origin/topic");
assert_eq!(got_main, main_oid);
assert_eq!(got_topic, topic_oid);
let got_tag = resolve_ref(&local_git, "refs/tags/v1").expect("tag v1");
assert_eq!(got_tag, tag_oid);
assert_eq!(outcome.default_branch.as_deref(), Some("main"));
let local_odb = open_odb(&local_git);
for oid in [main_oid, topic_oid, tag_oid] {
assert!(local_odb.exists(&oid), "object {} missing", oid.to_hex());
}
let fsck = Command::new("git")
.current_dir(&local_root)
.args(["fsck", "--no-dangling"])
.output()
.expect("run git fsck");
assert!(
fsck.status.success(),
"git fsck failed after v2 ssh fetch: {}",
String::from_utf8_lossy(&fsck.stderr)
);
}
#[test]
fn fetch_over_ssh_program_lands_refs_and_objects() {
if Command::new("sh").arg("-c").arg("exit 0").status().is_err() {
eprintln!("SKIP: no POSIX sh available");
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let source = tmp.path().join("source");
std::fs::create_dir_all(&source).unwrap();
build_source(&source);
git(&source, &["symbolic-ref", "HEAD", "refs/heads/main"]);
let Some(fake_ssh) = write_fake_ssh(tmp.path()) else {
eprintln!("SKIP: could not create executable fake-ssh script");
return;
};
let transport = SshTransport::with_program(fake_ssh.as_os_str());
let abs_path = source.to_str().expect("utf8 path");
let url = format!("ssh://git@fakehost{abs_path}");
run_fetch_and_assert(&transport, &url, &source, &tmp.path().join("local-prog"));
}