From d5284b758661c491e6a206570763f2982424b70a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 23 Feb 2024 13:57:23 +0000 Subject: feat(init): add customisation and defaults - allow more cli input options - allow customisation of more fields in interface - change default identifer from shorthand root commit to short name - defaults to existing repo event (users or other) or maintainers.yaml --- src/repo_ref.rs | 4 + src/sub_commands/init.rs | 256 +++++++++++++++++++++++++++++++++++++++------- src/sub_commands/list.rs | 1 + src/sub_commands/pull.rs | 1 + src/sub_commands/push.rs | 1 + src/sub_commands/send.rs | 1 + tests/init.rs | 260 ++++++++--------------------------------------- 7 files changed, 266 insertions(+), 258 deletions(-) diff --git a/src/repo_ref.rs b/src/repo_ref.rs index 8e944d7..c7b42fa 100644 --- a/src/repo_ref.rs +++ b/src/repo_ref.rs @@ -152,6 +152,7 @@ pub async fn fetch( #[cfg(not(test))] client: &Client, // TODO: more rubust way of finding repo events fallback_relays: Vec, + prompt_for_nevent_if_cant_event: bool, ) -> Result { let repo_config = get_repo_config_from_yaml(git_repo); @@ -187,6 +188,9 @@ pub async fn fetch( { break event.clone(); } + if !prompt_for_nevent_if_cant_event { + bail!("cannot find repo event"); + } println!("cannot find repo event"); loop { let bech32 = Interactor::default() diff --git a/src/sub_commands/init.rs b/src/sub_commands/init.rs index 3a0ff55..54b6156 100644 --- a/src/sub_commands/init.rs +++ b/src/sub_commands/init.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use nostr::{secp256k1::XOnlyPublicKey, FromBech32, ToBech32}; use super::send::send_events; #[cfg(not(test))] @@ -10,7 +11,7 @@ use crate::{ client::Connect, git::{Repo, RepoActions}, login, - repo_ref::{extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, RepoRef}, + repo_ref::{self, extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, RepoRef}, Cli, }; @@ -22,14 +23,27 @@ pub struct SubCommandArgs { #[clap(short, long)] /// optional description description: Option, + #[clap(long)] + /// git server url users can clone from + clone_url: Option, #[clap(short, long, value_parser, num_args = 1..)] /// homepage web: Vec, #[clap(short, long, value_parser, num_args = 1..)] /// relays contributors push patches and comments to relays: Vec, + #[clap(short, long, value_parser, num_args = 1..)] + /// npubs of other maintainers + other_maintainers: Vec, + #[clap(long)] + /// usually root commit but will be more recent commit for forks + earliest_unique_commit: Option, + #[clap(short, long)] + /// shortname with no spaces or special characters + identifier: Option, } +#[allow(clippy::too_many_lines)] pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { let git_repo = Repo::discover().context("cannot find a git repository")?; @@ -40,29 +54,95 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { // TODO: check for empty repo // TODO: check for existing maintaiers file + #[cfg(not(test))] + let mut client = Client::default(); + #[cfg(test)] + let mut client = ::default(); + + let (keys, user_ref) = login::launch(&cli_args.nsec, &cli_args.password, Some(&client)).await?; + + client.set_keys(&keys).await; + + let repo_ref = if let Ok(rep_ref) = repo_ref::fetch( + &git_repo, + root_commit.to_string(), + &client, + user_ref.relays.write(), + false, + ) + .await + { + Some(rep_ref) + } else { + None + }; + let repo_config_result = get_repo_config_from_yaml(&git_repo); // TODO: check for other claims - let identifier = root_commit.to_string()[..7].to_string(); - let name = match &args.title { Some(t) => t.clone(), - None => Interactor::default().input(PromptInputParms::default().with_prompt("name"))?, + None => Interactor::default().input( + PromptInputParms::default() + .with_prompt("name") + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.name.clone() + } else { + String::new() + }), + )?, + }; + + let identifier = match &args.identifier { + Some(t) => t.clone(), + None => Interactor::default().input( + PromptInputParms::default() + .with_prompt("identifier") + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.identifier.clone() + } else { + name.clone() + .replace(' ', "-") + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c.eq(&'/') { + c + } else { + '-' + } + }) + .collect() + }), + )?, }; let description = match &args.description { Some(t) => t.clone(), - None => { - Interactor::default().input(PromptInputParms::default().with_prompt("description"))? - } + None => Interactor::default().input( + PromptInputParms::default() + .with_prompt("description") + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.description.clone() + } else { + String::new() + }), + )?, }; - let git_server = git_repo - .get_origin_url() - .context( - "to claim the repository it must be available on a publically accessable git server", - ) - .context("no git remote origin configured")?; + let git_server = match &args.clone_url { + Some(t) => t.clone(), + None => Interactor::default().input( + PromptInputParms::default() + .with_prompt("clone url") + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.git_server.clone() + } else if let Ok(git_repo) = git_repo.get_origin_url() { + git_repo + } else { + String::new() + }), + )?, + }; let web: Vec = if args.web.is_empty() { Interactor::default() @@ -70,7 +150,11 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { PromptInputParms::default() .with_prompt("web") .optional() - .with_default(format!("https://gitworkshop.dev/repo/{}", &identifier)), + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.web.clone().join(" ") + } else { + format!("https://gitworkshop.dev/repo/{}", &identifier) + }), )? .split(' ') .map(std::string::ToString::to_string) @@ -78,41 +162,132 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { } else { args.web.clone() }; - #[cfg(not(test))] - let mut client = Client::default(); - #[cfg(test)] - let mut client = ::default(); - let (keys, user_ref) = login::launch(&cli_args.nsec, &cli_args.password, Some(&client)).await?; - - client.set_keys(&keys).await; - - let mut maintainers = vec![keys.public_key()]; + let maintainers: Vec = { + let mut dont_ask = !args.other_maintainers.is_empty(); + let mut maintainers_string = if !args.other_maintainers.is_empty() { + [args.other_maintainers.clone()].concat().join(" ") + } else if repo_ref.is_none() && repo_config_result.is_err() { + keys.public_key().to_bech32()? + } else { + let maintainers = if let Ok(config) = &repo_config_result { + config.maintainers.clone() + } else if let Some(repo_ref) = &repo_ref { + repo_ref + .maintainers + .clone() + .iter() + .map(|k| k.to_bech32().unwrap()) + .collect() + } else { + //unreachable + vec![keys.public_key().to_bech32()?] + }; + // add current user if not present + if maintainers.iter().any(|m| { + if let Ok(m_pubkey) = XOnlyPublicKey::from_bech32(m) { + user_ref.public_key.eq(&m_pubkey) + } else { + false + } + }) { + maintainers.join(" ") + } else { + [maintainers, vec![keys.public_key().to_bech32()?]] + .concat() + .join(" ") + } + }; + 'outer: loop { + if !dont_ask { + maintainers_string = Interactor::default() + .input( + PromptInputParms::default() + .with_prompt("maintainers") + .with_default(maintainers_string), + )? + .split(' ') + .map(std::string::ToString::to_string) + .collect(); + } + let mut maintainers: Vec = vec![]; + for m in maintainers_string.split(' ') { + if let Ok(m_pubkey) = XOnlyPublicKey::from_bech32(m) { + maintainers.push(m_pubkey); + } else { + println!("not a valid set of npubs seperated by a space"); + dont_ask = false; + continue 'outer; + } + } + // add current user incase removed + if !maintainers.iter().any(|m| user_ref.public_key.eq(m)) { + maintainers.push(keys.public_key()); + } + break maintainers; + } + }; - let repo_relays: Vec = if !args.relays.is_empty() { - args.relays.clone() - } else if let Ok(config) = &repo_config_result { - config.relays.clone() + // TODO: check if relays are free to post to so contributors can submit patches + // TODO: recommend some reliable free ones + let relays: Vec = if args.relays.is_empty() { + Interactor::default() + .input( + PromptInputParms::default() + .with_prompt("relays") + .with_default(if let Ok(config) = &repo_config_result { + config.relays.clone().join(" ") + } else if let Some(repo_ref) = &repo_ref { + repo_ref.relays.clone().join(" ") + } else { + user_ref.relays.write().join(" ") + }), + )? + .split(' ') + .map(std::string::ToString::to_string) + .collect() } else { - // TODO: choice input defaulting to user relay list filtered by non paid relays - // TODO: allow manual input for more relays - // TODO: reccommend some free relays - user_ref.relays.write() + args.relays.clone() }; - if let Ok(config) = &repo_config_result { - maintainers = extract_pks(config.maintainers.clone())?; - } + let earliest_unique_commit = match &args.earliest_unique_commit { + Some(t) => t.clone(), + None => { + let mut earliest_unique_commit = if let Some(repo_ref) = &repo_ref { + repo_ref.root_commit.clone() + } else { + root_commit.to_string() + }; + loop { + earliest_unique_commit = Interactor::default().input( + PromptInputParms::default() + .with_prompt("earliest unique commit") + .with_default(earliest_unique_commit.clone()), + )?; + if let Ok(exists) = git_repo.does_commit_exist(&earliest_unique_commit) { + if exists { + break earliest_unique_commit; + } + println!("commit does not exist on current repository"); + } else { + println!("commit id not formatted correctly"); + } + if earliest_unique_commit.len().ne(&40) { + println!("commit id must be 40 characters long"); + } + } + } + }; // if yaml file doesnt exist or needs updating if match &repo_config_result { Ok(config) => { !(extract_pks(config.maintainers.clone())?.eq(&maintainers) - && config.relays.eq(&repo_relays)) + && config.relays.eq(&relays)) } Err(_) => true, } { - save_repo_config_to_yaml(&git_repo, maintainers.clone(), repo_relays.clone())?; + save_repo_config_to_yaml(&git_repo, maintainers.clone(), relays.clone())?; println!( "maintainers.yaml {}. commit and push.", if repo_config_result.is_err() { @@ -121,6 +296,9 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { "updated" } ); + println!( + "this enables existing contributors to automatically fetch your repo event (instead of one from a pubkey pretending to be the maintainer)" + ); } println!("publishing repostory reference..."); @@ -129,10 +307,10 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { identifier, name, description, - root_commit: root_commit.to_string(), + root_commit: earliest_unique_commit, git_server, web, - relays: repo_relays.clone(), + relays: relays.clone(), maintainers, } .to_event(&keys)?; @@ -141,7 +319,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { &client, vec![repo_event], user_ref.relays.write(), - repo_relays, + relays, !cli_args.disable_cli_spinners, ) .await?; diff --git a/src/sub_commands/list.rs b/src/sub_commands/list.rs index 666c4bf..f7397f1 100644 --- a/src/sub_commands/list.rs +++ b/src/sub_commands/list.rs @@ -48,6 +48,7 @@ pub async fn launch(_cli_args: &Cli, _args: &SubCommandArgs) -> Result<()> { root_commit.to_string(), &client, client.get_fallback_relays().clone(), + true, ) .await?; diff --git a/src/sub_commands/pull.rs b/src/sub_commands/pull.rs index 9b74719..daae37f 100644 --- a/src/sub_commands/pull.rs +++ b/src/sub_commands/pull.rs @@ -44,6 +44,7 @@ pub async fn launch() -> Result<()> { root_commit.to_string(), &client, client.get_fallback_relays().clone(), + true, ) .await?; diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index 06c3e50..bcac178 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs @@ -60,6 +60,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { root_commit.to_string(), &client, client.get_fallback_relays().clone(), + true, ) .await?; diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index b5ab78f..857bc60 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs @@ -151,6 +151,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { .to_string(), &client, user_ref.relays.write(), + true, ) .await?; diff --git a/tests/init.rs b/tests/init.rs index 1ad3810..d733643 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -8,10 +8,40 @@ fn expect_msgs_first(p: &mut CliTester) -> Result<()> { p.expect("logged in as fred\r\n")?; // // p.expect("searching for existing claims on repository...\r\n")?; p.expect("maintainers.yaml created. commit and push.\r\n")?; + p.expect("this enables existing contributors to automatically fetch your repo event (instead of one from a pubkey pretending to be the maintainer) publishing repostory reference...\r\n")?; p.expect("publishing repostory reference...\r\n")?; Ok(()) } +fn get_cli_args() -> Vec<&'static str> { + vec![ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "init", + "--title", + "example-name", + "--identifier", + "example-identifier", + "--description", + "example-description", + "--web", + "https://exampleproject.xyz", + "https://gitworkshop.dev/123", + "--relays", + "ws://localhost:8055", + "ws://localhost:8056", + "--clone-url", + "https://git.myhosting.com/my-repo.git", + "--earliest-unique-commit", + "9ee507fc4357d7ee16a5d8901bedcd103f23c17d", + "--other-maintainers", + TEST_KEY_1_NPUB, + ] +} + mod when_repo_not_previously_claimed { use super::*; @@ -29,27 +59,7 @@ mod when_repo_not_previously_claimed { } fn cli_tester_init(git_repo: &GitTestRepo) -> CliTester { - CliTester::new_from_dir( - &git_repo.dir, - [ - "--nsec", - TEST_KEY_1_NSEC, - "--password", - TEST_PASSWORD, - "--disable-cli-spinners", - "init", - "--title", - "example-name", - "--description", - "example-description", - "--web", - "https://exampleproject.xyz", - "https://gitworkshop.dev/123", - "--relays", - "ws://localhost:8055", - "ws://localhost:8056", - ], - ) + CliTester::new_from_dir(&git_repo.dir, get_cli_args()) } async fn prep_run_init() -> Result<( @@ -259,13 +269,12 @@ mod when_repo_not_previously_claimed { } } - mod tags { + mod tags_as_specified_in_args { use super::*; #[tokio::test] #[serial] - async fn d_replaceable_event_identifier_defaults_to_root_commit_id_shorthand() - -> Result<()> { + async fn d_replaceable_event_identifier() -> Result<()> { let (_, _, r53, r55, r56, r57) = prep_run_init().await?; for relay in [&r53, &r55, &r56, &r57] { let event: &nostr::Event = relay @@ -275,10 +284,9 @@ mod when_repo_not_previously_claimed { .unwrap(); assert!( - event - .tags - .iter() - .any(|t| t.as_vec()[0].eq("d") && t.as_vec()[1].eq("9ee507f")) + event.tags.iter().any( + |t| t.as_vec()[0].eq("d") && t.as_vec()[1].eq("example-identifier") + ) ); } Ok(()) @@ -286,7 +294,7 @@ mod when_repo_not_previously_claimed { #[tokio::test] #[serial] - async fn root_commit_as_reference() -> Result<()> { + async fn earliest_unique_commit_as_reference() -> Result<()> { let (_, _, r53, r55, r56, r57) = prep_run_init().await?; for relay in [&r53, &r55, &r56, &r57] { let event: &nostr::Event = relay @@ -352,7 +360,7 @@ mod when_repo_not_previously_claimed { assert!( event.tags.iter().any(|t| t.as_vec()[0].eq("clone") - && t.as_vec()[1].eq("https://localhost:1000")) + && t.as_vec()[1].eq("https://git.myhosting.com/my-repo.git")) /* todo check it defaults to origin */ ); } Ok(()) @@ -404,7 +412,7 @@ mod when_repo_not_previously_claimed { #[tokio::test] #[serial] - async fn current_user_in_maintainers() -> Result<()> { + async fn maintainers() -> Result<()> { let (_, _, r53, r55, r56, r57) = prep_run_init().await?; for relay in [&r53, &r55, &r56, &r57] { let event: &nostr::Event = relay @@ -498,192 +506,6 @@ mod when_repo_not_previously_claimed { } } } - - mod when_repo_relays_not_specified { - use futures::join; - use test_utils::relay::Relay; - - use super::*; - - fn prep_git_repo() -> Result { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - test_repo.add_remote("origin", "https://localhost:1000")?; - Ok(test_repo) - } - - fn cli_tester_init(git_repo: &GitTestRepo) -> CliTester { - CliTester::new_from_dir( - &git_repo.dir, - [ - "--nsec", - TEST_KEY_1_NSEC, - "--password", - TEST_PASSWORD, - "--disable-cli-spinners", - "init", - "--title", - "example-name", - "--description", - "example-description", - "--web", - "https://exampleproject.xyz", - "https://gitworkshop.dev/123", - ], - ) - } - - async fn prep_run_init() -> Result<( - Relay<'static>, - Relay<'static>, - Relay<'static>, - Relay<'static>, - Relay<'static>, - Relay<'static>, - )> { - let git_repo = prep_git_repo()?; - // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) - let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( - Relay::new( - 8051, - None, - Some(&|relay, client_id, subscription_id, _| -> Result<()> { - relay.respond_events( - client_id, - &subscription_id, - &vec![ - generate_test_key_1_metadata_event("fred"), - generate_test_key_1_relay_list_event(), - ], - )?; - Ok(()) - }), - ), - 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), - ); - - // // check relay had the right number of events - let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_init(&git_repo); - p.expect_end_eventually()?; - for p in [51, 52, 53, 55, 56, 57] { - relay::shutdown_relay(8000 + p)?; - } - Ok(()) - }); - - // launch relay - 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((r51, r52, r53, r55, r56, r57)) - } - - mod tags { - use super::*; - - #[tokio::test] - #[serial] - async fn relays_match_user_write_relays() -> Result<()> { - let (_, _, r53, r55, _, _) = prep_run_init().await?; - for relay in [&r53, &r55] { - let event: &nostr::Event = relay - .events - .iter() - .find(|e| e.kind.as_u64().eq(&REPOSITORY_KIND)) - .unwrap(); - - assert!(event.tags.iter().any(|t| t.as_vec()[0].eq("relays") - && t.as_vec()[1].eq("ws://localhost:8053") - && t.as_vec()[2].eq("ws://localhost:8055"))); - } - Ok(()) - } - } - - mod cli_ouput { - use super::*; - - async fn run_test_async() -> Result<()> { - let git_repo = prep_git_repo()?; - - // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) - let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( - Relay::new( - 8051, - None, - Some(&|relay, client_id, subscription_id, _| -> Result<()> { - relay.respond_events( - client_id, - &subscription_id, - &vec![ - generate_test_key_1_metadata_event("fred"), - generate_test_key_1_relay_list_event(), - ], - )?; - Ok(()) - }), - ), - 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), - ); - - // // check relay had the right number of events - let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_init(&git_repo); - expect_msgs_first(&mut p)?; - relay::expect_send_with_progress( - &mut p, - vec![ - (" [my-relay] [repo-relay] ws://localhost:8053", true, ""), - (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), - (" [default] ws://localhost:8051", true, ""), - (" [default] ws://localhost:8052", true, ""), - (" [default] ws://localhost:8057", true, ""), - ], - 1, - )?; - p.expect_end_with_whitespace()?; - for p in [51, 52, 53, 55, 56, 57] { - relay::shutdown_relay(8000 + p)?; - } - Ok(()) - }); - - // launch relay - 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 check_cli_output() -> Result<()> { - run_test_async().await?; - Ok(()) - } - } - } + // TODO: cli caputuring input } - -// TODO: when_updating_existing_repoistory +// TODO: when_updating_existing_repoistory correct defaults are used -- cgit v1.2.3