use git_spawn::{
CatFileCommand, DescribeCommand, ForEachRefCommand, GitCommand, HashObjectCommand,
LsFilesCommand, LsTreeCommand, Repository, RevParseCommand, ShowRefCommand, SymbolicRefCommand,
UpdateRefCommand,
};
fn configure_identity(repo: &Repository) {
for (k, v) in [
("user.email", "test@example.com"),
("user.name", "Test"),
("commit.gpgsign", "false"),
("core.autocrlf", "false"),
] {
let status = std::process::Command::new("git")
.args(["config", "--local", k, v])
.current_dir(repo.path())
.status()
.expect("git config");
assert!(status.success());
}
}
async fn make_repo_with_commit() -> (tempfile::TempDir, Repository) {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("repo");
std::fs::create_dir_all(&path).unwrap();
let mut init = git_spawn::InitCommand::in_directory(&path);
init.initial_branch("main").quiet();
let repo = init.execute().await.expect("init");
configure_identity(&repo);
std::fs::write(repo.path().join("hello.txt"), "hi\n").unwrap();
repo.add().path("hello.txt").execute().await.unwrap();
repo.commit().message("init").execute().await.unwrap();
(tmp, repo)
}
#[tokio::test]
async fn rev_parse_resolves_head() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut cmd = RevParseCommand::new();
cmd.current_dir(repo.path()).arg_str("HEAD");
let sha = cmd.execute().await.unwrap();
assert_eq!(sha.len(), 40, "unexpected SHA: {sha}");
}
#[tokio::test]
async fn rev_parse_show_toplevel() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut cmd = RevParseCommand::new();
cmd.current_dir(repo.path()).show_toplevel();
let top = cmd.execute().await.unwrap();
let want = std::fs::canonicalize(repo.path()).unwrap();
let got = std::fs::canonicalize(&top).unwrap();
assert_eq!(got, want);
}
#[tokio::test]
async fn ls_files_sees_tracked_file() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut cmd = LsFilesCommand::new();
cmd.current_dir(repo.path()).cached();
let out = cmd.execute().await.unwrap();
assert!(out.stdout.lines().any(|l| l == "hello.txt"));
}
#[tokio::test]
async fn ls_tree_head_name_only() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut cmd = LsTreeCommand::new("HEAD");
cmd.current_dir(repo.path()).name_only();
let out = cmd.execute().await.unwrap();
assert!(out.stdout.contains("hello.txt"));
}
#[tokio::test]
async fn cat_file_type_and_pretty_print() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut t = CatFileCommand::object_type("HEAD");
t.current_dir(repo.path());
assert_eq!(t.execute().await.unwrap(), "commit");
let mut p = CatFileCommand::pretty_print("HEAD:hello.txt");
p.current_dir(repo.path());
assert_eq!(p.execute().await.unwrap(), "hi");
}
#[tokio::test]
async fn hash_object_write_and_read_back() {
let (_tmp, repo) = make_repo_with_commit().await;
let blob_path = repo.path().join("blobby.txt");
std::fs::write(&blob_path, "some bytes\n").unwrap();
let mut h = HashObjectCommand::new();
h.current_dir(repo.path()).write().path(&blob_path);
let sha = h.execute().await.unwrap();
assert_eq!(sha.len(), 40);
let mut c = CatFileCommand::pretty_print(&sha);
c.current_dir(repo.path());
assert_eq!(c.execute().await.unwrap(), "some bytes");
}
#[tokio::test]
async fn update_ref_creates_and_deletes() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut rp = RevParseCommand::new();
rp.current_dir(repo.path()).arg_str("HEAD");
let head = rp.execute().await.unwrap();
let mut up = UpdateRefCommand::new();
up.current_dir(repo.path())
.ref_name("refs/heads/shadow")
.new_value(&head);
up.execute().await.unwrap();
let mut fe = ForEachRefCommand::new();
fe.current_dir(repo.path())
.pattern("refs/heads/*")
.format("%(refname:short)");
let out = fe.execute().await.unwrap();
assert!(out.stdout.lines().any(|l| l == "shadow"));
let mut rm = UpdateRefCommand::new();
rm.current_dir(repo.path())
.ref_name("refs/heads/shadow")
.delete();
rm.execute().await.unwrap();
let out2 = fe.execute().await.unwrap();
assert!(!out2.stdout.lines().any(|l| l == "shadow"));
}
#[tokio::test]
async fn describe_always_returns_sha_when_no_tag() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut d = DescribeCommand::new();
d.current_dir(repo.path()).always().commit("HEAD");
let out = d.execute().await.unwrap();
assert!(!out.is_empty());
}
#[tokio::test]
async fn describe_finds_tag() {
let (_tmp, repo) = make_repo_with_commit().await;
repo.tag().name("v0.1.0").execute().await.unwrap();
let mut d = DescribeCommand::new();
d.current_dir(repo.path()).tags();
let out = d.execute().await.unwrap();
assert!(out.starts_with("v0.1.0"), "unexpected describe: {out}");
}
#[tokio::test]
async fn show_ref_lists_heads() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut s = ShowRefCommand::new();
s.current_dir(repo.path()).heads();
let out = s.execute().await.unwrap();
assert!(out.stdout.contains("refs/heads/main"));
}
#[tokio::test]
async fn symbolic_ref_reads_head() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut s = SymbolicRefCommand::read("HEAD");
s.current_dir(repo.path());
let target = s.execute().await.unwrap();
assert_eq!(target, "refs/heads/main");
}
#[tokio::test]
async fn symbolic_ref_short_returns_branch_name() {
let (_tmp, repo) = make_repo_with_commit().await;
let mut s = SymbolicRefCommand::read("HEAD").short();
s.current_dir(repo.path());
assert_eq!(s.execute().await.unwrap(), "main");
}
#[cfg(feature = "parse")]
mod parsers {
use super::*;
use git_spawn::command::status::StatusFormat;
use git_spawn::parse::{DiffKind, StatusKind, parse_diff_name_status, parse_log, parse_status};
#[tokio::test]
async fn status_parser_captures_modification() {
let (_tmp, repo) = make_repo_with_commit().await;
std::fs::write(repo.path().join("hello.txt"), "changed\n").unwrap();
std::fs::write(repo.path().join("new.txt"), "fresh\n").unwrap();
let out = repo
.status()
.format(StatusFormat::PorcelainV1)
.null_terminate()
.execute()
.await
.unwrap();
let entries = parse_status(&out.stdout).unwrap();
let hello = entries.iter().find(|e| e.path == "hello.txt").unwrap();
assert_eq!(hello.worktree, StatusKind::Modified);
let fresh = entries.iter().find(|e| e.path == "new.txt").unwrap();
assert_eq!(fresh.index, StatusKind::Untracked);
}
#[tokio::test]
async fn log_parser_reads_structured_entries() {
let (_tmp, repo) = make_repo_with_commit().await;
std::fs::write(repo.path().join("second.txt"), "s").unwrap();
repo.add().path("second.txt").execute().await.unwrap();
repo.commit()
.message("second commit")
.execute()
.await
.unwrap();
let out = repo
.log()
.format(git_spawn::parse::LOG_FORMAT)
.execute()
.await
.unwrap();
let commits = parse_log(&out.stdout).unwrap();
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].subject, "second commit");
assert_eq!(commits[1].subject, "init");
assert_eq!(commits[0].author_name, "Test");
}
#[tokio::test]
async fn diff_name_status_parser() {
let (_tmp, repo) = make_repo_with_commit().await;
std::fs::write(repo.path().join("hello.txt"), "changed\n").unwrap();
std::fs::write(repo.path().join("brand-new.txt"), "new\n").unwrap();
repo.add().all().execute().await.unwrap();
let out = repo
.diff()
.cached()
.name_status()
.arg("-z")
.execute()
.await
.unwrap();
let entries = parse_diff_name_status(&out.stdout).unwrap();
assert!(
entries
.iter()
.any(|e| e.kind == DiffKind::Modified && e.path == "hello.txt")
);
assert!(
entries
.iter()
.any(|e| e.kind == DiffKind::Added && e.path == "brand-new.txt")
);
}
}