#![cfg(feature = "git")]
#![allow(missing_docs)]
#![allow(unsafe_code)]
use std::path::Path;
use std::process::Command;
use rtb_credentials::CredentialRef;
use rtb_forge::git::{CloneOptions, PushOptions, Repo, RepoError};
#[test]
fn p1_push_options_with_credential_builder() {
let cref = CredentialRef { env: Some("RTB_TEST_TOKEN".to_string()), ..Default::default() };
let opts = PushOptions::default().with_credential(cref);
assert!(opts.credential.is_some(), "credential should be Some after builder");
}
#[tokio::test]
async fn p2_push_anonymous_to_bare_remote_succeeds() {
let upstream = build_three_commit_repo();
let bare = build_bare_remote();
let bare_url = format!("file://{}", bare.path().display());
Command::new("git")
.args(["remote", "add", "bare", &bare_url])
.current_dir(upstream.path())
.output()
.expect("add remote");
let repo = Repo::open(upstream.path()).await.expect("open");
repo.push("bare", "main", PushOptions::default()).await.expect("push");
let upstream_head = git_command(upstream.path(), &["rev-parse", "HEAD"]).trim().to_string();
let bare_main = git_command(bare.path(), &["rev-parse", "main"]).trim().to_string();
assert_eq!(bare_main, upstream_head, "pushed ref should match local HEAD");
}
#[tokio::test]
async fn p3_push_unknown_remote_errors() {
let upstream = build_three_commit_repo();
let repo = Repo::open(upstream.path()).await.expect("open");
let err =
repo.push("no-such-remote", "main", PushOptions::default()).await.expect_err("must fail");
assert!(
matches!(err, RepoError::PushFailed { ref remote, .. } if remote == "no-such-remote"),
"got {err:?}"
);
}
#[tokio::test]
async fn p4_push_unresolvable_credential_errors() {
let upstream = build_three_commit_repo();
let bare = build_bare_remote();
let bare_url = format!("file://{}", bare.path().display());
Command::new("git")
.args(["remote", "add", "bare", &bare_url])
.current_dir(upstream.path())
.output()
.expect("add remote");
let repo = Repo::open(upstream.path()).await.expect("open");
let cref = CredentialRef { env: Some("RTB_UNSET_TOKEN_P4".to_string()), ..Default::default() };
unsafe {
std::env::remove_var("RTB_UNSET_TOKEN_P4");
}
let opts = PushOptions::default().with_credential(cref);
let err = repo.push("bare", "main", opts).await.expect_err("must fail");
assert!(matches!(err, RepoError::Auth(_)), "got {err:?}");
}
#[tokio::test]
async fn p5_push_with_resolved_credential_against_file_succeeds() {
let upstream = build_three_commit_repo();
let bare = build_bare_remote();
let bare_url = format!("file://{}", bare.path().display());
Command::new("git")
.args(["remote", "add", "bare", &bare_url])
.current_dir(upstream.path())
.output()
.expect("add remote");
let repo = Repo::open(upstream.path()).await.expect("open");
unsafe {
std::env::set_var("RTB_TEST_TOKEN_P5", "dummy-token-value");
}
let cref = CredentialRef { env: Some("RTB_TEST_TOKEN_P5".to_string()), ..Default::default() };
let opts = PushOptions::default().with_credential(cref);
repo.push("bare", "main", opts).await.expect("push with auth env set");
unsafe {
std::env::remove_var("RTB_TEST_TOKEN_P5");
}
let upstream_head = git_command(upstream.path(), &["rev-parse", "HEAD"]).trim().to_string();
let bare_main = git_command(bare.path(), &["rev-parse", "main"]).trim().to_string();
assert_eq!(bare_main, upstream_head);
}
#[tokio::test]
async fn p6_clone_then_commit_then_push_roundtrip() {
let bare = build_bare_remote();
let bare_url = format!("file://{}", bare.path().display());
let scratch = build_three_commit_repo();
Command::new("git")
.args(["remote", "add", "bare", &bare_url])
.current_dir(scratch.path())
.output()
.expect("add remote");
Command::new("git")
.args(["push", "bare", "main"])
.current_dir(scratch.path())
.output()
.expect("seed push");
let dst_holder = tempfile::tempdir().expect("dst tempdir");
let dst = dst_holder.path().join("worker");
let clone = Repo::clone(&bare_url, &dst, CloneOptions::default()).await.expect("clone");
Command::new("git")
.args(["config", "--local", "user.name", "carol"])
.current_dir(&dst)
.output()
.expect("user.name");
Command::new("git")
.args(["config", "--local", "user.email", "carol@example.test"])
.current_dir(&dst)
.output()
.expect("user.email");
std::fs::write(dst.join("hello.txt"), b"hi\n").expect("write");
let local_oid = clone.commit(&[Path::new("hello.txt")], "add hello").await.expect("commit");
clone.push("origin", "main", PushOptions::default()).await.expect("push back");
let bare_main = git_command(bare.path(), &["rev-parse", "main"]).trim().to_string();
assert_eq!(bare_main, local_oid, "bare ref should match the locally-made commit");
}
fn build_three_commit_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
git_init(path);
write(path, "README.md", "v1\n");
git_add_all(path);
git_commit(path, "alice", "initial");
write(path, "README.md", "v2\n");
git_add_all(path);
git_commit(path, "alice", "second");
write(path, "README.md", "v3\n");
git_add_all(path);
git_commit(path, "alice", "third");
dir
}
fn build_bare_remote() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("bare tempdir");
let path = dir.path();
Command::new("git")
.args(["init", "--bare", "-b", "main"])
.current_dir(path)
.output()
.expect("git init --bare");
dir
}
fn git_init(path: &Path) {
Command::new("git").args(["init", "-b", "main"]).current_dir(path).output().expect("git init");
}
fn git_add_all(path: &Path) {
Command::new("git").args(["add", "-A"]).current_dir(path).output().expect("git add");
}
fn git_commit(path: &Path, author: &str, message: &str) {
Command::new("git")
.args(["commit", "--allow-empty", "-m", message])
.env("GIT_AUTHOR_NAME", author)
.env("GIT_AUTHOR_EMAIL", format!("{author}@example.test"))
.env("GIT_COMMITTER_NAME", author)
.env("GIT_COMMITTER_EMAIL", format!("{author}@example.test"))
.env("GIT_AUTHOR_DATE", "2026-05-19T00:00:00Z")
.env("GIT_COMMITTER_DATE", "2026-05-19T00:00:00Z")
.current_dir(path)
.output()
.expect("git commit");
}
fn write(path: &Path, file: &str, contents: &str) {
std::fs::write(path.join(file), contents).expect("write");
}
fn git_command(path: &Path, args: &[&str]) -> String {
let out = Command::new("git").args(args).current_dir(path).output().expect("git");
assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr));
String::from_utf8(out.stdout).expect("utf8")
}