From 771f944af447c202eba045936a36dee71ab797ac Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 4 Sep 2024 11:32:05 +0100 Subject: refactor: fix imports, etc based on restructure move some functions out of ngit and into lib/mod and lib/git_events remove MockConnect from binaries so it is only used in the library. this was done: * mainly because automocks were not being imported from lib into each binary * but also because the these functions were being tested with MockConnect --- src/bin/ngit/sub_commands/send.rs | 969 +------------------------------------- 1 file changed, 9 insertions(+), 960 deletions(-) (limited to 'src/bin/ngit/sub_commands/send.rs') diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 3c4df9d..a807305 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs @@ -1,35 +1,26 @@ -use std::{path::Path, str::FromStr, time::Duration}; +use std::path::Path; use anyhow::{bail, Context, Result}; use console::Style; -use futures::future::join_all; -use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; +use ngit::{client::send_events, git_events::generate_cover_letter_and_patch_events}; use nostr::{ - nips::{ - nip01::Coordinate, - nip10::Marker, - nip19::{Nip19, Nip19Event}, - }, - EventBuilder, FromBech32, Tag, TagKind, ToBech32, UncheckedUrl, + nips::{nip10::Marker, nip19::Nip19Event}, + ToBech32, }; -use nostr_sdk::{hashes::sha1::Hash as Sha1Hash, Kind, NostrSigner, TagStandard}; +use nostr_sdk::hashes::sha1::Hash as Sha1Hash; -use super::list::tag_value; -#[cfg(not(test))] -use crate::client::Client; -#[cfg(test)] -use crate::client::MockConnect; use crate::{ cli::Cli, cli_interactor::{ Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms, }, client::{ - fetching_with_report, get_events_from_cache, get_repo_ref_from_cache, sign_event, Connect, + fetching_with_report, get_events_from_cache, get_repo_ref_from_cache, Client, Connect, }, - git::{Repo, RepoActions}, + git::{identify_ahead_behind, Repo, RepoActions}, + git_events::{event_is_patch_set_root, event_tag_from_nip19_or_hex}, login, - repo_ref::{get_repo_coordinates, RepoRef}, + repo_ref::get_repo_coordinates, }; #[derive(Debug, clap::Args)] @@ -61,10 +52,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re .get_main_or_master_branch() .context("the default branches (main or master) do not exist")?; - #[cfg(not(test))] let mut client = Client::default(); - #[cfg(test)] - let mut client = ::default(); let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?; @@ -277,172 +265,6 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re Ok(()) } -#[allow(clippy::module_name_repetitions)] -#[allow(clippy::too_many_lines)] -pub async fn send_events( - #[cfg(test)] client: &crate::client::MockConnect, - #[cfg(not(test))] client: &Client, - git_repo_path: &Path, - events: Vec, - my_write_relays: Vec, - repo_read_relays: Vec, - animate: bool, - silent: bool, -) -> Result<()> { - let fallback = [ - client.get_fallback_relays().clone(), - if events - .iter() - .any(|e| e.kind().eq(&Kind::GitRepoAnnouncement)) - { - client.get_blaster_relays().clone() - } else { - vec![] - }, - ] - .concat(); - let mut relays: Vec<&String> = vec![]; - - let all = &[ - repo_read_relays.clone(), - my_write_relays.clone(), - fallback.clone(), - ] - .concat(); - // add duplicates first - for r in &repo_read_relays { - let r_clean = remove_trailing_slash(r); - if !my_write_relays - .iter() - .filter(|x| r_clean.eq(&remove_trailing_slash(x))) - .count() - > 1 - && !relays.iter().any(|x| r_clean.eq(&remove_trailing_slash(x))) - { - relays.push(r); - } - } - - for r in all { - let r_clean = remove_trailing_slash(r); - if !relays.iter().any(|x| r_clean.eq(&remove_trailing_slash(x))) { - relays.push(r); - } - } - - let m = if silent { - MultiProgress::with_draw_target(ProgressDrawTarget::hidden()) - } else { - MultiProgress::new() - }; - let pb_style = ProgressStyle::with_template(if animate { - " {spinner} {prefix} {bar} {pos}/{len} {msg}" - } else { - " - {prefix} {bar} {pos}/{len} {msg}" - })? - .progress_chars("##-"); - - let pb_after_style = - |symbol| ProgressStyle::with_template(format!(" {symbol} {}", "{prefix} {msg}",).as_str()); - let pb_after_style_succeeded = pb_after_style(if animate { - console::style("✔".to_string()) - .for_stderr() - .green() - .to_string() - } else { - "y".to_string() - })?; - - let pb_after_style_failed = pb_after_style(if animate { - console::style("✘".to_string()) - .for_stderr() - .red() - .to_string() - } else { - "x".to_string() - })?; - - #[allow(clippy::borrow_deref_ref)] - join_all(relays.iter().map(|&relay| async { - let relay_clean = remove_trailing_slash(&*relay); - let details = format!( - "{}{}{} {}", - if my_write_relays - .iter() - .any(|r| relay_clean.eq(&remove_trailing_slash(r))) - { - " [my-relay]" - } else { - "" - }, - if repo_read_relays - .iter() - .any(|r| relay_clean.eq(&remove_trailing_slash(r))) - { - " [repo-relay]" - } else { - "" - }, - if fallback - .iter() - .any(|r| relay_clean.eq(&remove_trailing_slash(r))) - { - " [default]" - } else { - "" - }, - relay_clean, - ); - let pb = m.add( - ProgressBar::new(events.len() as u64) - .with_prefix(details.to_string()) - .with_style(pb_style.clone()), - ); - if animate { - pb.enable_steady_tick(Duration::from_millis(300)); - } - pb.inc(0); // need to make pb display intially - let mut failed = false; - for event in &events { - match client - .send_event_to(git_repo_path, relay.as_str(), event.clone()) - .await - { - Ok(_) => pb.inc(1), - Err(e) => { - pb.set_style(pb_after_style_failed.clone()); - pb.finish_with_message( - console::style( - e.to_string() - .replace("relay pool error:", "error:") - .replace("event not published: ", "error: "), - ) - .for_stderr() - .red() - .to_string(), - ); - failed = true; - break; - } - }; - } - if !failed { - pb.set_style(pb_after_style_succeeded.clone()); - pb.finish_with_message(""); - } - })) - .await; - Ok(()) -} - -fn remove_trailing_slash(s: &String) -> String { - match s.as_str().strip_suffix('/') { - Some(s) => s, - None => s, - } - .to_string() -} - fn choose_commits(git_repo: &Repo, proposed_commits: Vec) -> Result> { let mut proposed_commits = if proposed_commits.len().gt(&10) { vec![] @@ -583,781 +405,8 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( Ok((root_proposal_id, mention_tags)) } -#[allow(clippy::too_many_lines)] -pub async fn generate_cover_letter_and_patch_events( - cover_letter_title_description: Option<(String, String)>, - git_repo: &Repo, - commits: &[Sha1Hash], - signer: &NostrSigner, - repo_ref: &RepoRef, - root_proposal_id: &Option, - mentions: &[nostr::Tag], -) -> Result> { - let root_commit = git_repo - .get_root_commit() - .context("failed to get root commit of the repository")?; - - let mut events = vec![]; - - if let Some((title, description)) = cover_letter_title_description { - events.push(sign_event(EventBuilder::new( - nostr::event::Kind::GitPatch, - format!( - "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}", - commits.last().unwrap(), - commits.len() - ), - [ - repo_ref.maintainers.iter().map(|m| Tag::coordinate(Coordinate { - kind: nostr::Kind::GitRepoAnnouncement, - public_key: *m, - identifier: repo_ref.identifier.to_string(), - relays: repo_ref.relays.clone(), - })).collect::>(), - vec![ - Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))), - Tag::hashtag("cover-letter"), - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![format!("git patch cover letter: {}", title.clone())], - ), - ], - if let Some(event_ref) = root_proposal_id.clone() { - vec![ - Tag::hashtag("root"), - Tag::hashtag("revision-root"), - // TODO check if id is for a root proposal (perhaps its for an issue?) - event_tag_from_nip19_or_hex(&event_ref,"proposal",Marker::Reply, false, false)?, - ] - } else { - vec![ - Tag::hashtag("root"), - ] - }, - mentions.to_vec(), - // this is not strictly needed but makes for prettier branch names - // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding - // a change like this, or the removal of this tag will require the actual branch name to be tracked - // so pulling and pushing still work - if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { - if !branch_name.eq("main") - && !branch_name.eq("master") - && !branch_name.eq("origin/main") - && !branch_name.eq("origin/master") - { - vec![ - Tag::custom( - nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), - vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") { - branch_name.to_string() - } else { - branch_name - }], - ), - ] - } - else { vec![] } - } else { - vec![] - }, - repo_ref.maintainers - .iter() - .map(|pk| Tag::public_key(*pk)) - .collect(), - ].concat(), - ), signer).await - .context("failed to create cover-letter event")?); - } - - for (i, commit) in commits.iter().enumerate() { - events.push( - generate_patch_event( - git_repo, - &root_commit, - commit, - events.first().map(|event| event.id), - signer, - repo_ref, - events.last().map(nostr::Event::id), - if events.is_empty() && commits.len().eq(&1) { - None - } else { - Some(((i + 1).try_into()?, commits.len().try_into()?)) - }, - if events.is_empty() { - if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { - if !branch_name.eq("main") - && !branch_name.eq("master") - && !branch_name.eq("origin/main") - && !branch_name.eq("origin/master") - { - Some(if let Some(branch_name) = branch_name.strip_prefix("pr/") { - branch_name.to_string() - } else { - branch_name - }) - } else { - None - } - } else { - None - } - } else { - None - }, - root_proposal_id, - if events.is_empty() { mentions } else { &[] }, - ) - .await - .context("failed to generate patch event")?, - ); - } - Ok(events) -} - -fn event_tag_from_nip19_or_hex( - reference: &str, - reference_name: &str, - marker: Marker, - allow_npub_reference: bool, - prompt_for_correction: bool, -) -> Result { - let mut bech32 = reference.to_string(); - loop { - if bech32.is_empty() { - bech32 = Interactor::default().input( - PromptInputParms::default().with_prompt(&format!("{reference_name} reference")), - )?; - } - if let Ok(nip19) = Nip19::from_bech32(bech32.clone()) { - match nip19 { - Nip19::Event(n) => { - break Ok(Tag::from_standardized(nostr_sdk::TagStandard::Event { - event_id: n.event_id, - relay_url: n.relays.first().map(UncheckedUrl::new), - marker: Some(marker), - public_key: None, - })); - } - Nip19::EventId(id) => { - break Ok(Tag::from_standardized(nostr_sdk::TagStandard::Event { - event_id: id, - relay_url: None, - marker: Some(marker), - public_key: None, - })); - } - Nip19::Coordinate(coordinate) => { - break Ok(Tag::coordinate(coordinate)); - } - Nip19::Profile(profile) => { - if allow_npub_reference { - break Ok(Tag::public_key(profile.public_key)); - } - } - Nip19::Pubkey(public_key) => { - if allow_npub_reference { - break Ok(Tag::public_key(public_key)); - } - } - _ => {} - } - } - if let Ok(id) = nostr::EventId::from_str(&bech32) { - break Ok(Tag::from_standardized(nostr_sdk::TagStandard::Event { - event_id: id, - relay_url: None, - marker: Some(marker), - public_key: None, - })); - } - if prompt_for_correction { - println!("not a valid {reference_name} event reference"); - } else { - bail!(format!("not a valid {reference_name} event reference")); - } - - bech32 = String::new(); - } -} - -pub struct CoverLetter { - pub title: String, - pub description: String, - pub branch_name: String, - pub event_id: Option, -} - -impl CoverLetter { - pub fn get_branch_name(&self) -> Result { - Ok(format!( - "pr/{}({})", - self.branch_name, - &self - .event_id - .context("proposal root event_id must be know to get it's branch name")? - .to_hex() - .as_str()[..8], - )) - } -} -pub fn event_is_cover_letter(event: &nostr::Event) -> bool { - // TODO: look for Subject:[ PATCH 0/n ] but watch out for: - // [PATCH v1 0/n ] or - // [PATCH subsystem v2 0/n ] - event.kind.eq(&Kind::GitPatch) - && event.tags().iter().any(|t| t.as_vec()[1].eq("root")) - && event - .tags() - .iter() - .any(|t| t.as_vec()[1].eq("cover-letter")) -} - -pub fn commit_msg_from_patch(patch: &nostr::Event) -> Result { - if let Ok(msg) = tag_value(patch, "description") { - Ok(msg) - } else { - let start_index = patch - .content - .find("] ") - .context("event is not formatted as a patch or cover letter")? - + 2; - let end_index = patch.content[start_index..] - .find("\ndiff --git") - .unwrap_or(patch.content.len()); - Ok(patch.content[start_index..end_index].to_string()) - } -} - -pub fn commit_msg_from_patch_oneliner(patch: &nostr::Event) -> Result { - Ok(commit_msg_from_patch(patch)? - .split('\n') - .collect::>()[0] - .to_string()) -} - -pub fn event_to_cover_letter(event: &nostr::Event) -> Result { - if !event_is_patch_set_root(event) { - bail!("event is not a patch set root event (root patch or cover letter)") - } - - let title = commit_msg_from_patch_oneliner(event)?; - let full = commit_msg_from_patch(event)?; - let description = full[title.len()..].trim().to_string(); - - Ok(CoverLetter { - title: title.clone(), - description, - // TODO should this be prefixed by format!("{}-"e.id.to_string()[..5]?) - branch_name: if let Ok(name) = match tag_value(event, "branch-name") { - Ok(name) => { - if !name.eq("main") && !name.eq("master") { - Ok(name) - } else { - Err(()) - } - } - _ => Err(()), - } { - name - } else { - let s = title - .replace(' ', "-") - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c.eq(&'/') { - c - } else { - '-' - } - }) - .collect(); - s - }, - event_id: Some(event.id()), - }) -} - -pub fn event_is_patch_set_root(event: &nostr::Event) -> bool { - event.kind.eq(&Kind::GitPatch) && event.tags().iter().any(|t| t.as_vec()[1].eq("root")) -} - -pub fn event_is_revision_root(event: &nostr::Event) -> bool { - event.kind.eq(&Kind::GitPatch) - && event - .tags() - .iter() - .any(|t| t.as_vec()[1].eq("revision-root")) -} - -pub fn patch_supports_commit_ids(event: &nostr::Event) -> bool { - event.kind.eq(&Kind::GitPatch) - && event - .tags() - .iter() - .any(|t| t.as_vec()[0].eq("commit-pgp-sig")) -} - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::too_many_lines)] -pub async fn generate_patch_event( - git_repo: &Repo, - root_commit: &Sha1Hash, - commit: &Sha1Hash, - thread_event_id: Option, - signer: &nostr_sdk::NostrSigner, - repo_ref: &RepoRef, - parent_patch_event_id: Option, - series_count: Option<(u64, u64)>, - branch_name: Option, - root_proposal_id: &Option, - mentions: &[nostr::Tag], -) -> Result { - let commit_parent = git_repo - .get_commit_parent(commit) - .context("failed to get parent commit")?; - let relay_hint = repo_ref.relays.first().map(nostr::UncheckedUrl::from); - - sign_event( - EventBuilder::new( - nostr::event::Kind::GitPatch, - git_repo - .make_patch_from_commit(commit, &series_count) - .context(format!("cannot make patch for commit {commit}"))?, - [ - repo_ref - .maintainers - .iter() - .map(|m| { - Tag::coordinate(Coordinate { - kind: nostr::Kind::GitRepoAnnouncement, - public_key: *m, - identifier: repo_ref.identifier.to_string(), - relays: repo_ref.relays.clone(), - }) - }) - .collect::>(), - vec![ - Tag::from_standardized(TagStandard::Reference(root_commit.to_string())), - // commit id reference is a trade-off. its now - // unclear which one is the root commit id but it - // enables easier location of code comments againt - // code that makes it into the main branch, assuming - // the commit id is correct - Tag::from_standardized(TagStandard::Reference(commit.to_string())), - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("alt")), - vec![format!( - "git patch: {}", - git_repo - .get_commit_message_summary(commit) - .unwrap_or_default() - )], - ), - ], - if let Some(thread_event_id) = thread_event_id { - vec![Tag::from_standardized(nostr_sdk::TagStandard::Event { - event_id: thread_event_id, - relay_url: relay_hint.clone(), - marker: Some(Marker::Root), - public_key: None, - })] - } else if let Some(event_ref) = root_proposal_id.clone() { - vec![ - Tag::hashtag("root"), - Tag::hashtag("revision-root"), - // TODO check if id is for a root proposal (perhaps its for an issue?) - event_tag_from_nip19_or_hex( - &event_ref, - "proposal", - Marker::Reply, - false, - false, - )?, - ] - } else { - vec![Tag::hashtag("root")] - }, - mentions.to_vec(), - if let Some(id) = parent_patch_event_id { - vec![Tag::from_standardized(nostr_sdk::TagStandard::Event { - event_id: id, - relay_url: relay_hint.clone(), - marker: Some(Marker::Reply), - public_key: None, - })] - } else { - vec![] - }, - // see comment on branch names in cover letter event creation - if let Some(branch_name) = branch_name { - if thread_event_id.is_none() { - vec![Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")), - vec![branch_name.to_string()], - )] - } else { - vec![] - } - } else { - vec![] - }, - // whilst it is in nip34 draft to tag the maintainers - // I'm not sure it is a good idea because if they are - // interested in all patches then their specialised - // client should subscribe to patches tagged with the - // repo reference. maintainers of large repos will not - // be interested in every patch. - repo_ref - .maintainers - .iter() - .map(|pk| Tag::public_key(*pk)) - .collect(), - vec![ - // a fallback is now in place to extract this from the patch - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("commit")), - vec![commit.to_string()], - ), - // this is required as patches cannot be relied upon to include the 'base - // commit' - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("parent-commit")), - vec![commit_parent.to_string()], - ), - // this is required to ensure the commit id matches - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("commit-pgp-sig")), - vec![ - git_repo - .extract_commit_pgp_signature(commit) - .unwrap_or_default(), - ], - ), - // removing description tag will not cause anything to break - Tag::from_standardized(nostr_sdk::TagStandard::Description( - git_repo.get_commit_message(commit)?.to_string(), - )), - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("author")), - git_repo.get_commit_author(commit)?, - ), - // this is required to ensure the commit id matches - Tag::custom( - TagKind::Custom(std::borrow::Cow::Borrowed("committer")), - git_repo.get_commit_comitter(commit)?, - ), - ], - ] - .concat(), - ), - signer, - ) - .await - .context("failed to sign event") -} // TODO // - find profile // - file relays // - find repo events // - - -/** - * returns `(from_branch,to_branch,ahead,behind)` - */ -pub fn identify_ahead_behind( - git_repo: &Repo, - from_branch: &Option, - to_branch: &Option, -) -> Result<(String, String, Vec, Vec)> { - let (from_branch, from_tip) = match from_branch { - Some(name) => ( - name.to_string(), - git_repo - .get_tip_of_branch(name) - .context(format!("cannot find from_branch '{name}'"))?, - ), - None => ( - if let Ok(name) = git_repo.get_checked_out_branch_name() { - name - } else { - "head".to_string() - }, - git_repo - .get_head_commit() - .context("failed to get head commit") - .context( - "checkout a commit or specify a from_branch. head does not reveal a commit", - )?, - ), - }; - - let (to_branch, to_tip) = match to_branch { - Some(name) => ( - name.to_string(), - git_repo - .get_tip_of_branch(name) - .context(format!("cannot find to_branch '{name}'"))?, - ), - None => { - let (name, commit) = git_repo - .get_main_or_master_branch() - .context("the default branches (main or master) do not exist")?; - (name.to_string(), commit) - } - }; - - match git_repo.get_commits_ahead_behind(&to_tip, &from_tip) { - Err(e) => { - if e.to_string().contains("is not an ancestor of") { - return Err(e).context(format!( - "'{from_branch}' is not branched from '{to_branch}'" - )); - } - Err(e).context(format!( - "failed to get commits ahead and behind from '{from_branch}' to '{to_branch}'" - )) - } - Ok((ahead, behind)) => Ok((from_branch, to_branch, ahead, behind)), - } -} - -#[cfg(test)] -mod tests { - use test_utils::git::GitTestRepo; - - use super::*; - mod identify_ahead_behind { - - use super::*; - use crate::git::oid_to_sha1; - - #[test] - fn when_from_branch_doesnt_exist_return_error() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - - test_repo.populate()?; - let branch_name = "doesnt_exist"; - assert_eq!( - identify_ahead_behind(&git_repo, &Some(branch_name.to_string()), &None) - .unwrap_err() - .to_string(), - format!("cannot find from_branch '{}'", &branch_name), - ); - Ok(()) - } - - #[test] - fn when_to_branch_doesnt_exist_return_error() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - - test_repo.populate()?; - let branch_name = "doesnt_exist"; - assert_eq!( - identify_ahead_behind(&git_repo, &None, &Some(branch_name.to_string())) - .unwrap_err() - .to_string(), - format!("cannot find to_branch '{}'", &branch_name), - ); - Ok(()) - } - - #[test] - fn when_to_branch_is_none_and_no_main_or_master_branch_return_error() -> Result<()> { - let test_repo = GitTestRepo::new("notmain")?; - let git_repo = Repo::from_path(&test_repo.dir)?; - - test_repo.populate()?; - - assert_eq!( - identify_ahead_behind(&git_repo, &None, &None) - .unwrap_err() - .to_string(), - "the default branches (main or master) do not exist", - ); - Ok(()) - } - - #[test] - fn when_from_branch_is_not_head_return_as_from_branch() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - - test_repo.populate()?; - // create feature branch with 1 commit ahead - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let head_oid = test_repo.stage_and_commit("add t3.md")?; - - // make feature branch 1 commit behind - test_repo.checkout("main")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let main_oid = test_repo.stage_and_commit("add t4.md")?; - - let (from_branch, to_branch, ahead, behind) = - identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?; - - assert_eq!(from_branch, "feature"); - assert_eq!(ahead, vec![oid_to_sha1(&head_oid)]); - assert_eq!(to_branch, "main"); - assert_eq!(behind, vec![oid_to_sha1(&main_oid)]); - Ok(()) - } - - #[test] - fn when_to_branch_is_not_main_return_as_to_branch() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - - test_repo.populate()?; - // create dev branch with 1 commit ahead - test_repo.create_branch("dev")?; - test_repo.checkout("dev")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let dev_oid_first = test_repo.stage_and_commit("add t3.md")?; - - // create feature branch with 1 commit ahead of dev - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let feature_oid = test_repo.stage_and_commit("add t4.md")?; - - // make feature branch 1 behind - test_repo.checkout("dev")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let dev_oid = test_repo.stage_and_commit("add t3.md")?; - - let (from_branch, to_branch, ahead, behind) = identify_ahead_behind( - &git_repo, - &Some("feature".to_string()), - &Some("dev".to_string()), - )?; - - assert_eq!(from_branch, "feature"); - assert_eq!(ahead, vec![oid_to_sha1(&feature_oid)]); - assert_eq!(to_branch, "dev"); - assert_eq!(behind, vec![oid_to_sha1(&dev_oid)]); - - let (from_branch, to_branch, ahead, behind) = - identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?; - - assert_eq!(from_branch, "feature"); - assert_eq!( - ahead, - vec![oid_to_sha1(&feature_oid), oid_to_sha1(&dev_oid_first)] - ); - assert_eq!(to_branch, "main"); - assert_eq!(behind, vec![]); - - Ok(()) - } - } - - mod event_to_cover_letter { - use super::*; - - fn generate_cover_letter(title: &str, description: &str) -> Result { - Ok(nostr::event::EventBuilder::new( - nostr::event::Kind::GitPatch, - format!("From ea897e987ea9a7a98e7a987e97987ea98e7a3334 Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/2] {title}\n\n{description}"), - [ - Tag::hashtag("cover-letter"), - Tag::hashtag("root"), - ], - ) - .to_event(&nostr::Keys::generate())?) - } - - #[test] - fn basic_title() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter("the title", "description here")?)? - .title, - "the title", - ); - Ok(()) - } - - #[test] - fn basic_description() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter("the title", "description here")?)? - .description, - "description here", - ); - Ok(()) - } - - #[test] - fn description_trimmed() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter( - "the title", - " \n \ndescription here\n\n " - )?)? - .description, - "description here", - ); - Ok(()) - } - - #[test] - fn multi_line_description() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter( - "the title", - "description here\n\nmore here\nmore" - )?)? - .description, - "description here\n\nmore here\nmore", - ); - Ok(()) - } - - #[test] - fn new_lines_in_title_forms_part_of_description() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter( - "the title\nwith new line", - "description here\n\nmore here\nmore" - )?)? - .title, - "the title", - ); - assert_eq!( - event_to_cover_letter(&generate_cover_letter( - "the title\nwith new line", - "description here\n\nmore here\nmore" - )?)? - .description, - "with new line\n\ndescription here\n\nmore here\nmore", - ); - Ok(()) - } - - mod blank_description { - use super::*; - - #[test] - fn title_correct() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter("the title", "")?)?.title, - "the title", - ); - Ok(()) - } - - #[test] - fn description_is_empty_string() -> Result<()> { - assert_eq!( - event_to_cover_letter(&generate_cover_letter("the title", "")?)?.description, - "", - ); - Ok(()) - } - } - } -} -- cgit v1.2.3