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 + 6 files changed, 225 insertions(+), 39 deletions(-) (limited to 'src') 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?; -- cgit v1.2.3