use git2::Signature;
use ngit::git_events::KIND_PULL_REQUEST;
use rstest::*;
use super::*;
#[tokio::test]
#[serial]
async fn new_branch_when_no_state_event_exists() -> Result<()> {
generate_repo_with_state_event().await?;
Ok(())
}
mod two_branches_in_batch_one_added_one_updated {
use super::*;
#[fixture]
async fn scenario() -> TwoBranchesScenario {
let git_repo = prep_git_repo().expect("failed to prep git repo");
let source_git_repo =
GitTestRepo::recreate_as_bare(&git_repo).expect("failed to create bare git repo");
std::fs::write(git_repo.dir.join("commit.md"), "some content")
.expect("failed to write commit.md");
let main_commit_id = git_repo
.stage_and_commit("commit.md")
.expect("failed to commit main");
git_repo
.create_branch("vnext")
.expect("failed to create vnext branch");
git_repo
.checkout("vnext")
.expect("failed to checkout vnext");
std::fs::write(git_repo.dir.join("vnext.md"), "some content")
.expect("failed to write vnext.md");
let vnext_commit_id = git_repo
.stage_and_commit("vnext.md")
.expect("failed to commit vnext");
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let main_commit_id_clone = main_commit_id;
let vnext_commit_id_clone = vnext_commit_id;
let cli_tester_handle = std::thread::spawn(move || -> Result<(bool, bool, bool, bool)> {
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push refs/heads/main:refs/heads/main")?;
p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
p.send_line("")?;
p.expect_eventually("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
let main_on_server =
source_git_repo.get_tip_of_local_branch("main")? == main_commit_id_clone;
let vnext_on_server =
source_git_repo.get_tip_of_local_branch("vnext")? == vnext_commit_id_clone;
let main_remote_ref_matches = git_repo
.git_repo
.find_reference("refs/remotes/nostr/main")?
.peel_to_commit()?
.id()
== main_commit_id_clone;
let vnext_remote_ref_matches = git_repo
.git_repo
.find_reference("refs/remotes/nostr/vnext")?
.peel_to_commit()?
.id()
== vnext_commit_id_clone;
Ok((
main_on_server,
vnext_on_server,
main_remote_ref_matches,
vnext_remote_ref_matches,
))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (main_on_server, vnext_on_server, main_remote_ref_matches, vnext_remote_ref_matches) =
cli_tester_handle
.join()
.unwrap()
.expect("cli tester failed");
TwoBranchesScenario {
main_commit_id: main_commit_id.to_string(),
vnext_commit_id: vnext_commit_id.to_string(),
main_on_server,
vnext_on_server,
main_remote_ref_matches,
vnext_remote_ref_matches,
}
}
#[rstest]
#[tokio::test]
#[serial]
async fn updates_branch_on_git_server(#[future] scenario: TwoBranchesScenario) -> Result<()> {
let s = scenario.await;
assert!(
s.main_on_server,
"main branch should be updated on git server"
);
assert!(
s.vnext_on_server,
"vnext branch should be updated on git server"
);
Ok(())
}
#[rstest]
#[tokio::test]
#[serial]
async fn remote_refs_updated_in_local_git(
#[future] scenario: TwoBranchesScenario,
) -> Result<()> {
let s = scenario.await;
assert!(
s.main_remote_ref_matches,
"main remote ref should match commit"
);
assert!(
s.vnext_remote_ref_matches,
"vnext remote ref should match commit"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn prints_git_helper_ok_respose() -> Result<()> {
let git_repo = prep_git_repo()?;
let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;
std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
let main_commit_id = git_repo.stage_and_commit("commit.md")?;
git_repo.create_branch("vnext")?;
git_repo.checkout("vnext")?;
std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
git_repo.stage_and_commit("vnext.md")?;
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
assert_ne!(
source_git_repo.get_tip_of_local_branch("main")?,
main_commit_id
);
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push refs/heads/main:refs/heads/main")?;
p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
p.send_line("")?;
p.expect_eventually("ok ")?;
p.expect("refs/heads/main\r\n")?;
p.expect("ok refs/heads/vnext\r\n")?;
p.expect_eventually("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
Ok(())
}
#[tokio::test]
#[serial]
async fn when_no_existing_state_event_state_on_git_server_published_in_nostr_state_event()
-> Result<()> {
let git_repo = prep_git_repo()?;
let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?;
std::fs::write(git_repo.dir.join("commit.md"), "some content")?;
let main_commit_id = git_repo.stage_and_commit("commit.md")?;
git_repo.create_branch("vnext")?;
git_repo.checkout("vnext")?;
std::fs::write(git_repo.dir.join("vnext.md"), "some content")?;
let vnext_commit_id = git_repo.stage_and_commit("vnext.md")?;
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push refs/heads/main:refs/heads/main")?;
p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
p.send_line("")?;
p.expect_eventually_and_print("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
let state_event = r56
.events
.iter()
.find(|e| e.kind.eq(&STATE_KIND))
.context("state event not created")?;
assert_eq!(
state_event.tags.identifier(),
generate_repo_ref_event().tags.identifier(),
);
assert_eq!(
state_event
.tags
.iter()
.filter(|t| t.kind().to_string().as_str().ne("d"))
.map(|t| t.as_slice().to_vec())
.collect::<HashSet<Vec<String>>>(),
HashSet::from([
vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
vec!["refs/heads/vnext".to_string(), vnext_commit_id.to_string()],
]),
);
Ok(())
}
#[tokio::test]
#[serial]
async fn existing_state_event_published_in_nostr_state_event() -> Result<()> {
let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
let git_repo = prep_git_repo()?;
let example_branch_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string();
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
let main_commit_id = git_repo.stage_and_commit("new.md")?;
git_repo.create_branch("vnext")?;
git_repo.checkout("vnext")?;
std::fs::write(git_repo.dir.join("more.md"), "some content")?;
let vnext_commit_id = git_repo.stage_and_commit("more.md")?;
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
state_event.clone(),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push refs/heads/main:refs/heads/main")?;
p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
p.send_line("")?;
p.expect_eventually_and_print("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
assert_eq!(
git_repo
.git_repo
.find_reference("refs/remotes/nostr/main")?
.peel_to_commit()?
.id(),
main_commit_id,
);
assert_eq!(
git_repo
.git_repo
.find_reference("refs/remotes/nostr/vnext")?
.peel_to_commit()?
.id(),
vnext_commit_id
);
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
assert_eq!(
source_git_repo.get_tip_of_local_branch("main")?,
main_commit_id
);
assert_eq!(
source_git_repo.get_tip_of_local_branch("vnext")?,
vnext_commit_id
);
let state_event = r56
.events
.iter()
.find(|e| e.kind.eq(&STATE_KIND))
.context("state event not created")?;
assert_eq!(
state_event
.tags
.iter()
.filter(|t| t.kind().to_string().as_str().ne("d"))
.map(|t| t.as_slice().to_vec())
.collect::<HashSet<Vec<String>>>(),
HashSet::from([
vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
vec![
"refs/heads/example-branch".to_string(),
example_branch_commit_id.to_string()
],
vec!["refs/heads/vnext".to_string(), vnext_commit_id.to_string()],
]),
);
Ok(())
}
}
mod delete_one_branch {
use super::*;
#[derive(Clone)]
struct DeleteBranchScenario {
vnext_commit_id_str: String,
branch_was_deleted_on_server: bool,
remote_ref_was_deleted_locally: bool,
}
#[fixture]
async fn scenario() -> DeleteBranchScenario {
let git_repo = prep_git_repo().unwrap();
git_repo.create_branch("vnext").unwrap();
git_repo.checkout("vnext").unwrap();
std::fs::write(git_repo.dir.join("vnext.md"), "some content").unwrap();
let vnext_commit_id = git_repo.stage_and_commit("vnext.md").unwrap();
let vnext_commit_id_str = vnext_commit_id.to_string();
let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo).unwrap();
git_repo
.git_repo
.reference("refs/remotes/nostr/vnext", vnext_commit_id, true, "")
.unwrap();
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let (server_deleted, local_deleted) = {
let cli_tester_handle = std::thread::spawn(move || -> Result<(bool, bool)> {
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)
.unwrap();
p.send_line("push :refs/heads/vnext").unwrap();
p.send_line("").unwrap();
p.expect_eventually_and_print("\r\n\r\n").unwrap();
p.exit().unwrap();
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p).unwrap();
}
let server_deleted = source_git_repo
.git_repo
.find_reference("refs/heads/vnext")
.is_err();
let local_deleted = git_repo
.git_repo
.find_reference("refs/remotes/nostr/vnext")
.is_err();
Ok((server_deleted, local_deleted))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap().unwrap()
};
DeleteBranchScenario {
vnext_commit_id_str,
branch_was_deleted_on_server: server_deleted,
remote_ref_was_deleted_locally: local_deleted,
}
}
#[rstest]
#[tokio::test]
#[serial]
async fn deletes_branch_on_git_server(#[future] scenario: DeleteBranchScenario) -> Result<()> {
let s = scenario.await;
assert!(
s.branch_was_deleted_on_server,
"Branch should be deleted on git server"
);
Ok(())
}
#[rstest]
#[tokio::test]
#[serial]
async fn remote_refs_updated_in_local_git(
#[future] scenario: DeleteBranchScenario,
) -> Result<()> {
let s = scenario.await;
assert!(
s.remote_ref_was_deleted_locally,
"Remote ref should be deleted locally"
);
Ok(())
}
#[rstest]
#[tokio::test]
#[serial]
async fn verify_commit_id_captured(#[future] scenario: DeleteBranchScenario) -> Result<()> {
let s = scenario.await;
assert_eq!(
s.vnext_commit_id_str.len(),
40,
"Should have valid commit SHA"
);
Ok(())
}
mod when_existing_state_event {
use super::*;
#[tokio::test]
#[serial]
async fn state_event_updated_and_branch_deleted_and_ok_printed() -> Result<()> {
let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
let git_repo = prep_git_repo()?;
let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string();
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
state_event.clone(),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
let mut p =
cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push :refs/heads/example-branch")?;
p.send_line("")?;
p.expect_eventually("ok ")?;
p.expect("refs/heads/example-branch\r\n")?;
p.expect_eventually("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
let state_event = r56
.events
.iter()
.find(|e| e.kind.eq(&STATE_KIND))
.context("state event not created")?;
assert_eq!(
state_event
.tags
.iter()
.filter(|t| t.kind().to_string().as_str().ne("d"))
.map(|t| t.as_slice().to_vec())
.collect::<HashSet<Vec<String>>>(),
HashSet::from([
vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
]),
);
Ok(())
}
mod already_deleted_on_git_server {
use super::*;
#[tokio::test]
#[serial]
async fn existing_state_event_updated_and_ok_printed() -> Result<()> {
let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
{
let tmp_repo = GitTestRepo::clone_repo(&source_git_repo)?;
let mut remote = tmp_repo.git_repo.find_remote("origin")?;
remote.push(&[":refs/heads/example-branch"], None)?;
}
let git_repo = prep_git_repo()?;
let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string();
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
state_event.clone(),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
let mut p =
cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push :refs/heads/example-branch")?;
p.send_line("")?;
p.expect_eventually("ok ")?;
p.expect("refs/heads/example-branch\r\n")?;
p.expect_eventually("\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
let state_event = r56
.events
.iter()
.find(|e| e.kind.eq(&STATE_KIND))
.context("state event not created")?;
assert_eq!(
state_event
.tags
.iter()
.filter(|t| t.kind().to_string().as_str().ne("d"))
.map(|t| t.as_slice().to_vec())
.collect::<HashSet<Vec<String>>>(),
HashSet::from([
vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
]),
);
Ok(())
}
}
}
}
#[tokio::test]
#[serial]
async fn pushes_to_all_git_servers_listed_and_ok_printed() -> Result<()> {
let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
let second_source_git_repo = GitTestRepo::duplicate(&source_git_repo)?;
let git_repo = prep_git_repo()?;
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
let main_commit_id = git_repo.stage_and_commit("new.md")?;
let events = vec![
generate_test_key_1_metadata_event("fred"),
generate_test_key_1_relay_list_event(),
generate_repo_ref_event_with_git_server(vec![
source_git_repo.dir.to_str().unwrap().to_string(),
second_source_git_repo.dir.to_str().unwrap().to_string(),
]),
generate_repo_ref_event_with_git_server_with_keys(
vec![source_git_repo.dir.to_str().unwrap().to_string()],
&TEST_KEY_2_KEYS,
),
state_event.clone(),
];
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events;
let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
p.send_line("push refs/heads/main:refs/heads/main")?;
p.send_line("")?;
p.expect_eventually("ok ")?;
p.expect("refs/heads/main\r\n")?;
p.expect_eventually("\r\n\r\n")?;
p.exit()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(())
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
cli_tester_handle.join().unwrap()?;
assert_eq!(
source_git_repo.get_tip_of_local_branch("main")?,
main_commit_id
);
assert_eq!(
second_source_git_repo.get_tip_of_local_branch("main")?,
main_commit_id
);
Ok(())
}
#[tokio::test]
#[serial]
async fn proposal_three_way_merge_commit_pushed_to_main_leads_to_status_event_issued() -> Result<()>
{
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid)> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
git_repo.checkout_remote_branch(&branch_name)?;
git_repo.checkout("refs/heads/main")?;
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
git_repo.stage_and_commit("new.md")?;
CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
["merge", &branch_name, "-m", "proposal merge commit message"],
)
.expect_end_eventually_and_print()?;
let oid = git_repo.get_tip_of_local_branch("main")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually("merge commit ")?;
p.expect_eventually(": create nostr proposal status event\r\n")?;
p.expect_eventually(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok((output, oid))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (output, merge_oid) = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(
" 431b84e..{} main -> main\r\n",
&merge_oid.to_string()[..7]
)
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 2, "{new_events:?}");
let proposal_cover_letter_event = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
})
.unwrap();
let merge_status = new_events
.iter()
.find(|e| e.kind.eq(&Kind::GitStatusApplied))
.unwrap();
assert_eq!(
vec!["merge-commit-id".to_string(), merge_oid.to_string()],
merge_status
.tags
.iter()
.find(|t| t.as_slice()[0].eq("merge-commit-id"))
.unwrap()
.clone()
.to_vec(),
"status sets correct merge-commit-id tag {merge_status:?}"
);
let proposal_tip = r55
.events
.iter()
.filter(|e| {
e.tags
.iter()
.any(|t| t.as_slice()[1].eq(&proposal_cover_letter_event.id.to_string()))
&& e.kind.eq(&Kind::GitPatch)
})
.next_back()
.unwrap();
assert_eq!(
proposal_tip.id.to_string(),
merge_status
.tags
.iter()
.find(|t| t.as_slice()[0].eq("q"))
.unwrap()
.as_slice()[1],
"status mentions proposal tip event \r\nmerge status:\r\n{}\r\nproposal tip:\r\n{}",
merge_status.as_json(),
proposal_tip.as_json(),
);
assert_eq!(
proposal_cover_letter_event.id.to_string(),
merge_status
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
merge_status.as_json(),
proposal_cover_letter_event.as_json(),
);
Ok(())
}
#[tokio::test]
#[serial]
async fn proposal_fast_forward_merge_commits_pushed_to_main_leads_to_status_event_issued()
-> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid)> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
git_repo.checkout_remote_branch(&branch_name)?;
git_repo.checkout("refs/heads/main")?;
CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
["merge", &branch_name, "-m", "proposal merge commit message"],
)
.expect_end_eventually_and_print()?;
let oid = git_repo.get_tip_of_local_branch("main")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually(format!(
"fast-forward merge: create nostr proposal status event for {branch_name}\r\n"
))?;
p.expect_eventually(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok((output, oid))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (output, tip_oid) = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(
" 431b84e..{} main -> main\r\n",
&tip_oid.to_string()[..7]
)
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 2, "{new_events:?}");
let proposal_cover_letter_event = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
})
.unwrap();
let proposal_patches: Vec<&Event> = r55
.events
.iter()
.filter(|e| {
e.kind == Kind::GitPatch
&& e.tags
.iter()
.any(|t| t.as_slice()[1].eq(&proposal_cover_letter_event.id.to_string()))
})
.collect();
let merge_status = new_events
.iter()
.find(|e| e.kind.eq(&Kind::GitStatusApplied))
.unwrap();
let patch_commit_ids_parents_first = proposal_patches
.iter()
.map(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("commit"))
.unwrap()
.as_slice()[1]
.to_string()
})
.collect::<Vec<String>>();
assert_eq!(
HashSet::from_iter(
[
vec!["merge-commit-id".to_string()],
patch_commit_ids_parents_first
]
.concat()
.iter()
.cloned()
),
HashSet::<String>::from_iter(
merge_status
.tags
.iter()
.find(|t| t.as_slice()[0].eq("merge-commit-id"))
.unwrap()
.clone()
.to_vec()
.iter()
.cloned()
),
"status sets correct merge-commit-id tag {merge_status:?}"
);
for patch_id in proposal_patches
.iter()
.map(|e| e.id.to_string())
.collect::<Vec<String>>()
{
assert!(
merge_status
.tags
.iter()
.any(|t| t.as_slice()[0].eq("q") && t.as_slice()[1] == patch_id),
"merge status doesnt mention proposal patch {patch_id} \r\nmerge status:\r\n{}",
merge_status.as_json(),
);
}
assert_eq!(
proposal_cover_letter_event.id.to_string(),
merge_status
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
merge_status.as_json(),
proposal_cover_letter_event.as_json(),
);
Ok(())
}
#[tokio::test]
#[serial]
async fn proposal_commits_applied_and_pushed_to_main_leads_to_status_event_issued() -> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid, Oid)> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
create_and_populate_branch(
&git_repo,
"tmptmp",
"a",
false,
Some(&Signature::now("Different User", "other.user@nostr.com")?),
)?;
let applied_proposal_tip = git_repo.get_tip_of_local_branch("tmptmp")?;
git_repo.git_repo.branch(
"main",
&git_repo.git_repo.find_commit(applied_proposal_tip)?,
true,
)?;
let main_oid = git_repo.checkout("refs/heads/main")?;
assert_eq!(applied_proposal_tip, main_oid);
let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually(format!(
"applied commits from proposal: create nostr proposal status event for {branch_name}\r\n" ))?;
p.expect_eventually(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
let first_applied_commit_oid = git_repo
.git_repo
.find_commit(applied_proposal_tip)?
.parent(0)?
.id();
Ok((output, first_applied_commit_oid, applied_proposal_tip))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (output, first_oid, tip_oid) = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(
" 431b84e..{} main -> main\r\n",
&tip_oid.to_string()[..7]
)
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 2, "{new_events:?}");
let proposal_cover_letter_event = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
})
.unwrap();
let proposal_patches: Vec<&Event> = r55
.events
.iter()
.filter(|e| {
e.kind == Kind::GitPatch
&& e.tags
.iter()
.any(|t| t.as_slice()[1].eq(&proposal_cover_letter_event.id.to_string()))
})
.collect();
let merge_status = new_events
.iter()
.find(|e| e.kind.eq(&Kind::GitStatusApplied))
.unwrap();
assert_eq!(
HashSet::<String>::from_iter(vec![
"applied-as-commits".to_string(),
first_oid.to_string(),
tip_oid.to_string(),
]),
HashSet::<String>::from_iter(
merge_status
.tags
.iter()
.find(|t| t.as_slice()[0].eq("applied-as-commits"))
.unwrap()
.clone()
.to_vec()
.iter()
.cloned(),
),
"status sets correct applied-as-commits tag {merge_status:?}"
);
for patch_id in proposal_patches
.iter()
.map(|e| e.id.to_string())
.collect::<Vec<String>>()
{
assert!(
merge_status
.tags
.iter()
.any(|t| t.as_slice()[0].eq("q") && t.as_slice()[1] == patch_id),
"merge status doesnt mention proposal patch {patch_id} \r\nmerge status:\r\n{}",
merge_status.as_json(),
);
}
assert_eq!(
proposal_cover_letter_event.id.to_string(),
merge_status
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
merge_status.as_json(),
proposal_cover_letter_event.as_json(),
);
Ok(())
}
#[tokio::test]
#[serial]
async fn push_2_commits_to_existing_proposal() -> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<(String, String)> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
git_repo.checkout_remote_branch(&branch_name)?;
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
git_repo.stage_and_commit("new.md")?;
std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
git_repo.stage_and_commit("new2.md")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok((output, branch_name))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (output, branch_name) = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(" 2d1b467..9d83ff4 {branch_name} -> {branch_name}\r\n").as_str(),
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 2);
let first_new_patch = new_events
.iter()
.find(|e| e.content.contains("new.md"))
.unwrap();
let second_new_patch = new_events
.iter()
.find(|e| e.content.contains("new2.md"))
.unwrap();
assert!(
first_new_patch.content.contains("[PATCH 3/4]"),
"first patch labeled with [PATCH 3/4]"
);
assert!(
second_new_patch.content.contains("[PATCH 4/4]"),
"second patch labeled with [PATCH 4/4]"
);
let proposal = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
})
.unwrap();
assert_eq!(
proposal.id.to_string(),
first_new_patch
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"first patch sets proposal id as root"
);
assert_eq!(
first_new_patch.id.to_string(),
second_new_patch
.tags
.iter()
.find(|t| t.is_reply())
.unwrap()
.as_slice()[1],
"second new patch replies to the first new patch"
);
let previous_proposal_tip_event = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.any(|t| t.as_slice()[1].eq(&proposal.id.to_string()))
&& e.content.contains("[PATCH 2/2]")
})
.unwrap();
assert_eq!(
previous_proposal_tip_event.id.to_string(),
first_new_patch
.tags
.iter()
.find(|t| t.is_reply())
.unwrap()
.as_slice()[1],
"first patch replies to the previous tip of proposal"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn force_push_creates_proposal_revision() -> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<(String, String)> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
let oid = git_repo.checkout_remote_branch(&branch_name)?;
git_repo.checkout("main")?;
git_repo.git_repo.branch(
&branch_name,
&git_repo.git_repo.find_commit(oid)?.parent(0)?,
true,
)?;
git_repo.checkout(&branch_name)?;
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
git_repo.stage_and_commit("new.md")?;
std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
git_repo.stage_and_commit("new2.md")?;
let mut p =
CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push", "--force"]);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok((output, branch_name))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let (output, branch_name) = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(" + 2d1b467...ead85e0 {branch_name} -> {branch_name} (forced update)\r\n").as_str(),
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 3);
let proposal = r55
.events
.iter()
.find(|e| {
e.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
})
.unwrap();
let revision_root_patch = new_events
.iter()
.find(|e| {
e.tags
.iter()
.any(|t| ["revision-root", "root-revision"].contains(&t.as_slice()[1].as_str()))
})
.unwrap();
assert_eq!(
proposal.id.to_string(),
revision_root_patch
.tags
.iter()
.find(|t| t.is_reply())
.unwrap()
.as_slice()[1],
"revision root patch replies to original proposal"
);
assert!(
revision_root_patch.content.contains("[PATCH 1/3]"),
"revision root labeled with [PATCH 1/3] event: {revision_root_patch:?}",
);
let second_patch = new_events
.iter()
.find(|e| e.content.contains("new.md"))
.unwrap();
let third_patch = new_events
.iter()
.find(|e| e.content.contains("new2.md"))
.unwrap();
assert!(
second_patch.content.contains("[PATCH 2/3]"),
"second patch labeled with [PATCH 2/3]"
);
assert!(
third_patch.content.contains("[PATCH 3/3]"),
"third patch labeled with [PATCH 3/3]"
);
assert_eq!(
revision_root_patch.id.to_string(),
second_patch
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"second patch sets revision id as root"
);
assert_eq!(
second_patch.id.to_string(),
third_patch
.tags
.iter()
.find(|t| t.is_reply())
.unwrap()
.as_slice()[1],
"third patch replies to the second new patch"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn push_new_pr_branch_creates_proposal() -> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let branch_name = "pr/my-new-proposal";
let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
let mut git_repo = clone_git_repo_with_nostr_url()?;
git_repo.delete_dir_on_drop = false;
git_repo.create_branch(branch_name)?;
git_repo.checkout(branch_name)?;
std::fs::write(git_repo.dir.join("new.md"), "some content")?;
git_repo.stage_and_commit("new.md")?;
std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
git_repo.stage_and_commit("new2.md")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
["push", "-u", "origin", branch_name],
);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(output)
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let output = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(" * [new branch] {branch_name} -> {branch_name}\r\nbranch '{branch_name}' set up to track 'origin/{branch_name}'.\r\n").as_str(),
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 2);
let proposal = new_events
.iter()
.find(|e| e.tags.iter().any(|t| t.as_slice()[1].eq("root")))
.unwrap();
assert!(
proposal.content.contains("new.md"),
"first patch is proposal root"
);
assert!(
proposal.content.contains("[PATCH 1/2]"),
"proposal root labeled with[PATCH 1/2] event: {proposal:?}",
);
assert_eq!(
proposal
.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"))
.unwrap()
.as_slice()[1],
branch_name.replace("pr/", ""),
);
let second_patch = new_events
.iter()
.find(|e| e.content.contains("new2.md"))
.unwrap();
assert!(
second_patch.content.contains("[PATCH 2/2]"),
"second patch labeled with [PATCH 2/2]"
);
assert_eq!(
proposal.id.to_string(),
second_patch
.tags
.iter()
.find(|t| t.is_root())
.unwrap()
.as_slice()[1],
"second patch sets proposal id as root"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn push_new_pr_branch_with_title_description_options_creates_pr_with_custom_title_description()
-> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let branch_name = "pr/my-pr-with-title";
let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
let mut git_repo = clone_git_repo_with_nostr_url()?;
git_repo.delete_dir_on_drop = false;
git_repo.create_branch(branch_name)?;
git_repo.checkout(branch_name)?;
let large_content = "x".repeat(70 * 1024);
std::fs::write(git_repo.dir.join("large_file.txt"), large_content)?;
git_repo.stage_and_commit("large_file.txt")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
[
"push",
"--push-option=title=Custom PR Title",
"--push-option=description=Custom PR description here",
"-u",
"origin",
branch_name,
],
);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(output)
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let output = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(" * [new branch] {branch_name} -> {branch_name}\r\nbranch '{branch_name}' set up to track 'origin/{branch_name}'.\r\n").as_str(),
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 1, "should create exactly 1 PR event");
let pr_event = new_events.first().unwrap();
assert!(
pr_event.kind.eq(&KIND_PULL_REQUEST),
"event should be a PR event"
);
let title_tag = pr_event.tags.iter().find(|t| t.as_slice()[0].eq("subject"));
assert!(
title_tag.is_some(),
"PR event should have a subject tag for title"
);
assert_eq!(
title_tag.unwrap().as_slice()[1],
"Custom PR Title",
"title should match push-option"
);
assert_eq!(
pr_event.content, "Custom PR description here",
"description should match push-option"
);
let branch_name_tag = pr_event
.tags
.iter()
.find(|t| t.as_slice()[0].eq("branch-name"));
assert!(
branch_name_tag.is_some(),
"PR event should have a branch-name tag"
);
assert_eq!(
branch_name_tag.unwrap().as_slice()[1],
branch_name.replace("pr/", ""),
"branch-name tag should match"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn push_with_escaped_newlines_in_description_creates_pr_with_multiline_description()
-> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let branch_name = "pr/my-pr-multiline";
let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
let mut git_repo = clone_git_repo_with_nostr_url()?;
git_repo.delete_dir_on_drop = false;
git_repo.create_branch(branch_name)?;
git_repo.checkout(branch_name)?;
let large_content = "x".repeat(70 * 1024);
std::fs::write(git_repo.dir.join("large_file.txt"), large_content)?;
git_repo.stage_and_commit("large_file.txt")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
[
"push",
"--push-option=title=Multiline PR",
r"--push-option=description=First line\n\nSecond paragraph\nThird line",
"-u",
"origin",
branch_name,
],
);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(output)
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let output = cli_tester_handle.join().unwrap()?;
assert_eq!(
output,
format!(" * [new branch] {branch_name} -> {branch_name}\r\nbranch '{branch_name}' set up to track 'origin/{branch_name}'.\r\n").as_str(),
);
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 1, "should create exactly 1 PR event");
let pr_event = new_events.first().unwrap();
assert!(
pr_event.kind.eq(&KIND_PULL_REQUEST),
"event should be a PR event"
);
let title_tag = pr_event.tags.iter().find(|t| t.as_slice()[0].eq("subject"));
assert!(
title_tag.is_some(),
"PR event should have a subject tag for title"
);
assert_eq!(
title_tag.unwrap().as_slice()[1],
"Multiline PR",
"title should match push-option"
);
assert_eq!(
pr_event.content, "First line\n\nSecond paragraph\nThird line",
"description should contain real newlines from escaped \\n sequences"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn force_push_to_existing_patch_series_with_title_description_options_creates_patches_with_custom_cover_letter()
-> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
let git_repo = clone_git_repo_with_nostr_url()?;
git_repo.checkout_remote_branch(&branch_name)?;
std::fs::write(git_repo.dir.join("new1.txt"), "content 1")?;
git_repo.stage_and_commit("add new1")?;
std::fs::write(git_repo.dir.join("new2.txt"), "content 2")?;
git_repo.stage_and_commit("add new2")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
[
"push",
"--force",
"--push-option=title=Custom Patch Title",
"--push-option=description=Custom patch series description",
"origin",
&branch_name,
],
);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(output)
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let output = cli_tester_handle.join().unwrap()?;
assert!(!output.is_empty(), "should have output from push");
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(
new_events.len(),
5,
"should create 1 cover letter + 4 patch events"
);
let cover_letter = new_events
.iter()
.find(|e| e.kind.eq(&Kind::GitPatch) && e.content.contains("[PATCH 0/4]"))
.expect("should have a cover letter event");
assert!(
cover_letter.content.contains("Custom Patch Title"),
"cover letter should contain custom title"
);
assert!(
cover_letter
.content
.contains("Custom patch series description"),
"cover letter content should contain custom description"
);
let patches: Vec<&Event> = new_events
.iter()
.filter(|e| {
e.kind.eq(&Kind::GitPatch) && !e.content.contains("[PATCH 0/4]") })
.collect();
assert_eq!(patches.len(), 4, "should have 4 patch events");
Ok(())
}
#[tokio::test]
#[serial]
async fn push_new_pr_branch_with_multiple_commits_sets_merge_base_to_main_tip() -> Result<()> {
let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
let _source_path = source_git_repo.dir.to_str().unwrap().to_string();
let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
Relay::new(8051, None, None),
Relay::new(8052, None, None),
Relay::new(8053, None, None),
Relay::new(8055, None, None),
Relay::new(8056, None, None),
Relay::new(8057, None, None),
);
r51.events = events.clone();
r55.events = events.clone();
#[allow(clippy::mutable_key_type)]
let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
let branch_name = "pr/multi-commit-pr";
let cli_tester_handle = std::thread::spawn(move || -> Result<String> {
let mut git_repo = clone_git_repo_with_nostr_url()?;
git_repo.delete_dir_on_drop = false;
let main_tip = git_repo.get_tip_of_local_branch("main")?.to_string();
git_repo.create_branch(branch_name)?;
git_repo.checkout(branch_name)?;
let large_content = "x".repeat(70 * 1024);
std::fs::write(git_repo.dir.join("large1.txt"), &large_content)?;
git_repo.stage_and_commit("add large1")?;
std::fs::write(git_repo.dir.join("large2.txt"), &large_content)?;
git_repo.stage_and_commit("add large2")?;
let mut p = CliTester::new_git_with_remote_helper_from_dir(
&git_repo.dir,
["push", "-u", "origin", branch_name],
);
cli_expect_nostr_fetch(&mut p)?;
p.expect("git servers: listing refs...\r\n")?;
p.expect_eventually_and_print(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
let output = p.expect_end_eventually()?;
for p in [51, 52, 53, 55, 56, 57] {
relay::shutdown_relay(8000 + p)?;
}
Ok(format!("{main_tip}\n{output}"))
});
let _ = join!(
r51.listen_until_close(),
r52.listen_until_close(),
r53.listen_until_close(),
r55.listen_until_close(),
r56.listen_until_close(),
r57.listen_until_close(),
);
let result = cli_tester_handle.join().unwrap()?;
let (main_tip, _output) = result.split_once('\n').unwrap();
let new_events = r55
.events
.iter()
.cloned()
.collect::<HashSet<Event>>()
.difference(&before)
.cloned()
.collect::<Vec<Event>>();
assert_eq!(new_events.len(), 1, "should create exactly 1 PR event");
let pr_event = new_events.first().unwrap();
assert!(
pr_event.kind.eq(&KIND_PULL_REQUEST),
"event should be a PR event"
);
let merge_base_tag = pr_event
.tags
.iter()
.find(|t| t.as_slice()[0].eq("merge-base"));
assert!(
merge_base_tag.is_some(),
"PR event should have a merge-base tag"
);
assert_eq!(
merge_base_tag.unwrap().as_slice()[1],
main_tip,
"merge-base should be the main branch tip at the time of branching, not the parent of the PR tip"
);
Ok(())
}
mod push_from_another_maintainer {
}