From 949c6459aa7683453a7160423b689ceadb08954b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 4 Sep 2024 08:04:48 +0100 Subject: refactor: organise into lib and bin structure the make the code more readable this commit just moves the files, the next commit should fix the imports --- src/bin/ngit/sub_commands/init.rs | 385 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 src/bin/ngit/sub_commands/init.rs (limited to 'src/bin/ngit/sub_commands/init.rs') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs new file mode 100644 index 0000000..5b7e03d --- /dev/null +++ b/src/bin/ngit/sub_commands/init.rs @@ -0,0 +1,385 @@ +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use nostr::{nips::nip01::Coordinate, FromBech32, PublicKey, ToBech32}; +use nostr_sdk::Kind; + +use super::send::send_events; +#[cfg(not(test))] +use crate::client::Client; +#[cfg(test)] +use crate::client::MockConnect; +use crate::{ + cli::Cli, + cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, + client::{fetching_with_report, get_repo_ref_from_cache, Connect}, + git::{convert_clone_url_to_https, Repo, RepoActions}, + login, + repo_ref::{ + extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, + try_and_get_repo_coordinates, RepoRef, + }, +}; + +#[derive(Debug, clap::Args)] +pub struct SubCommandArgs { + #[clap(short, long)] + /// name of repository + title: Option, + #[clap(short, long)] + /// optional description + description: Option, + #[clap(long)] + /// git server url users can clone from + clone_url: Vec, + #[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")?; + let git_repo_path = git_repo.get_path()?; + + let root_commit = git_repo + .get_root_commit() + .context("failed to get root commit of the repository")?; + + // 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 repo_coordinates = if let Ok(repo_coordinates) = + try_and_get_repo_coordinates(&git_repo, &client, false).await + { + Some(repo_coordinates) + } else { + None + }; + + let repo_ref = if let Some(repo_coordinates) = repo_coordinates { + fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; + Some(get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?) + } else { + None + }; + + let (signer, user_ref) = login::launch( + &git_repo, + &cli_args.bunker_uri, + &cli_args.bunker_app_key, + &cli_args.nsec, + &cli_args.password, + Some(&client), + false, + false, + ) + .await?; + + let repo_config_result = get_repo_config_from_yaml(&git_repo); + // TODO: check for other claims + + let name = match &args.title { + Some(t) => t.clone(), + 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 { + let fallback = name + .clone() + .replace(' ', "-") + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c.eq(&'/') { + c + } else { + '-' + } + }) + .collect(); + if let Ok(config) = &repo_config_result { + if let Some(identifier) = &config.identifier { + identifier.to_string() + } else { + fallback + } + } else { + fallback + } + }), + )?, + }; + + let description = match &args.description { + Some(t) => t.clone(), + 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 = if args.clone_url.is_empty() { + Interactor::default() + .input( + PromptInputParms::default() + .with_prompt("clone url (for fetch)") + .with_default(if let Some(repo_ref) = &repo_ref { + repo_ref.git_server.clone().join(" ") + } else if let Ok(url) = git_repo.get_origin_url() { + if let Ok(fetch_url) = convert_clone_url_to_https(&url) { + fetch_url + } else { + // local repo or custom protocol + url + } + } else { + String::new() + }), + )? + .split(' ') + .map(std::string::ToString::to_string) + .collect() + } else { + args.clone_url.clone() + }; + + let web: Vec = if args.web.is_empty() { + Interactor::default() + .input( + PromptInputParms::default() + .with_prompt("web") + .optional() + .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) + .collect() + } else { + args.web.clone() + }; + + 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() { + signer.public_key().await?.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![signer.public_key().await?.to_bech32()?] + }; + // add current user if not present + if maintainers.iter().any(|m| { + if let Ok(m_pubkey) = PublicKey::from_bech32(m) { + user_ref.public_key.eq(&m_pubkey) + } else { + false + } + }) { + maintainers.join(" ") + } else { + [maintainers, vec![signer.public_key().await?.to_bech32()?]] + .concat() + .join(" ") + } + }; + 'outer: loop { + if !dont_ask { + println!("{}", &maintainers_string); + maintainers_string = Interactor::default().input( + PromptInputParms::default() + .with_prompt("maintainers") + .with_default(maintainers_string), + )?; + } + let mut maintainers: Vec = vec![]; + for m in maintainers_string.split(' ') { + if let Ok(m_pubkey) = PublicKey::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(signer.public_key().await?); + } + break maintainers; + } + }; + + // 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 { + args.relays.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"); + } + } + } + }; + + println!("publishing repostory reference..."); + + let repo_ref = RepoRef { + identifier: identifier.clone(), + name, + description, + root_commit: earliest_unique_commit, + git_server, + web, + relays: relays.clone(), + maintainers: maintainers.clone(), + events: HashMap::new(), + }; + let repo_event = repo_ref.to_event(&signer).await?; + + client.set_signer(signer).await; + + send_events( + &client, + git_repo_path, + vec![repo_event], + user_ref.relays.write(), + relays.clone(), + !cli_args.disable_cli_spinners, + false, + ) + .await?; + + git_repo.save_git_config_item( + "nostr.repo", + &Coordinate { + kind: Kind::GitRepoAnnouncement, + public_key: user_ref.public_key, + identifier: identifier.clone(), + relays: vec![], + } + .to_bech32()?, + false, + )?; + + // if yaml file doesnt exist or needs updating + if match &repo_config_result { + Ok(config) => { + ! as Clone>::clone(&config.identifier) + .unwrap_or_default() + .eq(&identifier) + || !extract_pks(config.maintainers.clone())?.eq(&maintainers) + || !config.relays.eq(&relays) + } + Err(_) => true, + } { + save_repo_config_to_yaml( + &git_repo, + identifier.clone(), + maintainers.clone(), + relays.clone(), + )?; + println!( + "maintainers.yaml {}. commit and push.", + if repo_config_result.is_err() { + "created" + } else { + "updated" + } + ); + println!( + "this optional file helps in identifying who the maintainers are over time through the commit history" + ); + } + Ok(()) +} -- cgit v1.2.3