use git_spawn::command::notes::NotesCommand;
use git_spawn::{GitCommand, Repository};
mod common;
async fn make_repo_with_commit() -> (tempfile::TempDir, Repository) {
let (tmp, repo) = common::init_repo().await;
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)
}
async fn head_sha(repo: &Repository) -> String {
repo.rev_parse().arg_str("HEAD").execute().await.unwrap()
}
#[tokio::test]
async fn add_show_append_list_round_trip() {
let (_tmp, repo) = make_repo_with_commit().await;
let sha = head_sha(&repo).await;
repo.notes(NotesCommand::add())
.ref_namespace("refs/notes/test")
.object("HEAD")
.message("reviewed")
.execute()
.await
.unwrap();
let shown = repo
.notes(NotesCommand::show())
.ref_namespace("refs/notes/test")
.object("HEAD")
.execute()
.await
.unwrap();
assert_eq!(shown.stdout_trimmed(), "reviewed");
repo.notes(NotesCommand::append())
.ref_namespace("refs/notes/test")
.object("HEAD")
.message("shipped")
.execute()
.await
.unwrap();
let shown = repo
.notes(NotesCommand::show())
.ref_namespace("refs/notes/test")
.object("HEAD")
.execute()
.await
.unwrap();
let text = shown.stdout_str();
assert!(text.contains("reviewed"), "missing original: {text}");
assert!(text.contains("shipped"), "missing appended: {text}");
let listed = repo
.notes(NotesCommand::list())
.ref_namespace("refs/notes/test")
.execute()
.await
.unwrap();
assert!(
listed.stdout_str().lines().any(|l| l.ends_with(&sha)),
"list missing object {sha}: {}",
listed.stdout_str()
);
}
#[tokio::test]
async fn binary_payload_round_trips_via_file() {
let (_tmp, repo) = make_repo_with_commit().await;
let payload: Vec<u8> = (128u16..256)
.flat_map(|b| std::iter::repeat_n(b as u8, 16))
.collect();
let payload_path = repo.path().join("payload.bin");
std::fs::write(&payload_path, &payload).unwrap();
repo.notes(NotesCommand::add())
.ref_namespace("refs/notes/blobs")
.object("HEAD")
.message_file(&payload_path)
.no_stripspace()
.execute()
.await
.unwrap();
let shown = repo
.notes(NotesCommand::show())
.ref_namespace("refs/notes/blobs")
.object("HEAD")
.execute()
.await
.unwrap();
assert_eq!(
shown.stdout_bytes(),
payload.as_slice(),
"binary note did not round-trip byte-for-byte"
);
}
#[tokio::test]
async fn remove_then_show_fails_cleanly() {
let (_tmp, repo) = make_repo_with_commit().await;
repo.notes(NotesCommand::add())
.ref_namespace("refs/notes/test")
.object("HEAD")
.message("temp")
.execute()
.await
.unwrap();
repo.notes(NotesCommand::remove())
.ref_namespace("refs/notes/test")
.object("HEAD")
.execute()
.await
.unwrap();
let err = repo
.notes(NotesCommand::show())
.ref_namespace("refs/notes/test")
.object("HEAD")
.execute()
.await
.unwrap_err();
assert!(
matches!(err, git_spawn::Error::CommandFailed { .. }),
"expected CommandFailed, got: {err:?}"
);
repo.notes(NotesCommand::remove())
.ref_namespace("refs/notes/test")
.object("HEAD")
.ignore_missing()
.execute()
.await
.unwrap();
}
#[tokio::test]
async fn notes_push_fetch_between_repos() {
let tmp = tempfile::tempdir().unwrap();
let bare_path = tmp.path().join("remote.git");
std::fs::create_dir_all(&bare_path).unwrap();
let mut init = git_spawn::InitCommand::in_directory(&bare_path);
init.bare().initial_branch("main").quiet();
init.execute().await.unwrap();
let a_path = tmp.path().join("a");
std::fs::create_dir_all(&a_path).unwrap();
let mut init_a = git_spawn::InitCommand::in_directory(&a_path);
init_a.initial_branch("main").quiet();
let repo_a = init_a.execute().await.unwrap();
common::configure_identity(&repo_a);
std::fs::write(repo_a.path().join("f.txt"), "hi\n").unwrap();
repo_a.add().path("f.txt").execute().await.unwrap();
repo_a.commit().message("init").execute().await.unwrap();
let sha = head_sha(&repo_a).await;
repo_a
.remote(git_spawn::RemoteCommand::add(
"origin",
bare_path.display().to_string(),
))
.execute()
.await
.unwrap();
repo_a
.push()
.remote("origin")
.refspec("main")
.execute()
.await
.unwrap();
repo_a
.notes(NotesCommand::add())
.ref_namespace("refs/notes/test")
.object("HEAD")
.message("from A")
.execute()
.await
.unwrap();
repo_a
.push()
.remote("origin")
.refspec("refs/notes/test:refs/notes/test")
.execute()
.await
.unwrap();
let b_path = tmp.path().join("b");
let repo_b = Repository::clone(bare_path.display().to_string(), &b_path)
.await
.unwrap();
common::configure_identity(&repo_b);
repo_b
.fetch()
.remote("origin")
.refspec("refs/notes/test:refs/notes/test")
.execute()
.await
.unwrap();
let shown = repo_b
.notes(NotesCommand::show())
.ref_namespace("refs/notes/test")
.object(&sha)
.execute()
.await
.unwrap();
assert_eq!(shown.stdout_trimmed(), "from A");
}
#[cfg(feature = "parse")]
#[tokio::test]
async fn parse_notes_list_pairs_note_and_object() {
use git_spawn::parse::parse_notes_list;
let (_tmp, repo) = make_repo_with_commit().await;
let sha = head_sha(&repo).await;
repo.notes(NotesCommand::add())
.ref_namespace("refs/notes/test")
.object("HEAD")
.message("note")
.execute()
.await
.unwrap();
let listed = repo
.notes(NotesCommand::list())
.ref_namespace("refs/notes/test")
.execute()
.await
.unwrap();
let pairs = parse_notes_list(&listed.stdout_str());
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0].1, sha, "object SHA should match HEAD");
assert_eq!(pairs[0].0.len(), 40, "note SHA should be a full object id");
}