From 8c763d0483309c85a32a7f4a20ba0279083ee40f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 29 Nov 2024 16:09:38 +0000 Subject: feat(init): set remote `origin` check whether remote `origin` is nostr url and if not attempt to set it. --- src/bin/ngit/sub_commands/init.rs | 90 +++++++++++++++++++++++- src/lib/client.rs | 18 +++-- src/lib/git/mod.rs | 4 +- src/lib/git/nostr_url.rs | 140 +++++++++++++++++++++++++++++++++++++- src/lib/repo_ref.rs | 76 +++++++++++++++------ 5 files changed, 297 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 32af619..ffee9bd 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use anyhow::{Context, Result}; -use ngit::cli_interactor::PromptConfirmParms; +use ngit::{cli_interactor::PromptConfirmParms, git::nostr_url::NostrUrlDecoded}; use nostr::{nips::nip01::Coordinate, FromBech32, PublicKey, ToBech32}; use nostr_sdk::{Kind, RelayUrl}; @@ -413,6 +413,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { git_server, web, relays: relays.clone(), + trusted_maintainer: user_ref.public_key, maintainers: maintainers.clone(), events: HashMap::new(), }; @@ -431,6 +432,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { ) .await?; + // TODO - does this git config item do more harm than good? git_repo.save_git_config_item( "nostr.repo", &Coordinate { @@ -448,6 +450,11 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { .map(std::string::ToString::to_string) .collect::>(); + prompt_to_set_nostr_url_as_origin(&repo_ref, &git_repo)?; + + // TODO: if no state event exists and there is currently a remote called + // "origin", automtically push rather than waiting for the next commit + // no longer create a new maintainers.yaml file - its too confusing for users // as it falls out of sync with data in nostr event . update if it already // exists @@ -481,3 +488,82 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { } Ok(()) } + +fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> { + println!( + "starting from your next commit, when you `git push` to a remote that uses your nostr url, it will store your repository state on nostr and update the state of the git server(s) you just listed." + ); + println!( + "in addition, any remote branches beginning with `pr/` are open PRs from contributors. they can submit these by simply pushing a branch with this `pr/` prefix." + ); + + if let Ok(origin_remote) = git_repo.git_repo.find_remote("origin") { + if let Some(origin_url) = origin_remote.url() { + if let Ok(nostr_url) = NostrUrlDecoded::from_str(origin_url) { + if let Some(c) = &nostr_url.coordinates.iter().next() { + if c.identifier == repo_ref.identifier { + if nostr_url + .coordinates + .iter() + .next() + .context( + "a decoded nostr url will always have at least one coordinate", + )? + .public_key + == repo_ref.trusted_maintainer + { + return Ok(()); + } + // origin is set to a different trusted maintainer + println!( + "warning: currently git remote 'origin' is set to a different trusted maintainer with the same identifier" + ); + ask_to_set_origin_remote(repo_ref, git_repo)?; + } else { + // origin is linked to a different identifier + println!( + "warning: currently git remote 'origin' is set to a different repository identifier" + ); + ask_to_set_origin_remote(repo_ref, git_repo)?; + } + } + } else { + // remote is non-nostr url + ask_to_set_origin_remote(repo_ref, git_repo)?; + } + } else { + // no origin remote + ask_to_create_new_origin_remote(repo_ref, git_repo)?; + } + } + println!("contributors can clone your repository by installing ngit and using this clone url:"); + println!("{}", repo_ref.to_nostr_git_url()); + + Ok(()) +} + +fn ask_to_set_origin_remote(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> { + if Interactor::default().confirm( + PromptConfirmParms::default() + .with_default(true) + .with_prompt("set remote \"origin\" to the nostr url of your repository?"), + )? { + git_repo + .git_repo + .remote_set_url("origin", &repo_ref.to_nostr_git_url())?; + } + Ok(()) +} + +fn ask_to_create_new_origin_remote(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> { + if Interactor::default().confirm( + PromptConfirmParms::default() + .with_default(true) + .with_prompt("set remote \"origin\" to the nostr url of your repository?"), + )? { + git_repo + .git_repo + .remote("origin", &repo_ref.to_nostr_git_url())?; + } + Ok(()) +} diff --git a/src/lib/client.rs b/src/lib/client.rs index 534eb9e..cd9a75c 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs @@ -832,7 +832,13 @@ pub async fn get_repo_ref_from_cache( ] .concat(); for e in events { - if let Ok(repo_ref) = RepoRef::try_from(e.clone()) { + if let Ok(repo_ref) = RepoRef::try_from(( + e.clone(), + repo_coordinates + .iter() + .next() + .map(|coordinate| coordinate.public_key), + )) { for m in repo_ref.maintainers { if maintainers.insert(m) { new_coordinate = true; @@ -846,12 +852,16 @@ pub async fn get_repo_ref_from_cache( } } repo_events.sort_by_key(|e| e.created_at); - let repo_ref = RepoRef::try_from( + let repo_ref = RepoRef::try_from(( repo_events .first() .context("no repo events at specified coordinates")? .clone(), - )?; + repo_coordinates + .iter() + .next() + .map(|coordinate| coordinate.public_key), + ))?; let mut events: HashMap = HashMap::new(); for m in &maintainers { @@ -1179,7 +1189,7 @@ async fn process_fetched_events( )); } // if contains new maintainer - if let Ok(repo_ref) = &RepoRef::try_from(event.clone()) { + if let Ok(repo_ref) = &RepoRef::try_from((event.clone(), None)) { for m in &repo_ref.maintainers { if !request .repo_coordinates_without_relays // prexisting maintainers diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index a49d306..0615213 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs @@ -1716,7 +1716,7 @@ mod tests { &oid_to_sha1(&original_oid), Some(nostr::EventId::all_zeros()), &TEST_KEY_1_SIGNER, - &RepoRef::try_from(generate_repo_ref_event()).unwrap(), + &RepoRef::try_from((generate_repo_ref_event(), None)).unwrap(), None, None, None, @@ -1869,7 +1869,7 @@ mod tests { &git_repo, &[oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], &TEST_KEY_1_SIGNER, - &RepoRef::try_from(generate_repo_ref_event()).unwrap(), + &RepoRef::try_from((generate_repo_ref_event(), None)).unwrap(), &None, &[], ) diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs index e4b6825..a501765 100644 --- a/src/lib/git/nostr_url.rs +++ b/src/lib/git/nostr_url.rs @@ -3,7 +3,7 @@ use std::{collections::HashSet, str::FromStr}; use anyhow::{anyhow, bail, Context, Error, Result}; use nostr::nips::nip01::Coordinate; -use nostr_sdk::{PublicKey, RelayUrl, Url}; +use nostr_sdk::{PublicKey, RelayUrl, ToBech32, Url}; #[derive(Debug, PartialEq, Default, Clone)] pub enum ServerProtocol { @@ -61,6 +61,33 @@ pub struct NostrUrlDecoded { pub user: Option, } +impl fmt::Display for NostrUrlDecoded { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "nostr://")?; + if let Some(user) = &self.user { + write!(f, "{}@", user)?; + } + if let Some(protocol) = &self.protocol { + write!(f, "{}/", protocol)?; + } + let c = self.coordinates.iter().next().unwrap(); + write!(f, "{}/", c.public_key.to_bech32().unwrap())?; + if let Some(relay) = c.relays.first() { + write!( + f, + "{}/", + urlencoding::encode( + relay + .as_str_without_trailing_slash() + .replace("wss://", "") + .as_str() + ) + )?; + } + write!(f, "{}", c.identifier) + } +} + static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo"; impl std::str::FromStr for NostrUrlDecoded { @@ -837,8 +864,117 @@ mod tests { assert!(result.is_err()); } } + mod nostr_git_url_format { + use std::collections::HashSet; + + use nostr::nips::nip01::Coordinate; + use nostr_sdk::PublicKey; + + use super::*; + use crate::git::nostr_url::NostrUrlDecoded; + + #[test] + fn standard() -> Result<()> { + assert_eq!( + format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + coordinates: HashSet::from_iter(vec![Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], + }]), + protocol: None, + user: None, + } + ), + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", + ); + Ok(()) + } + + #[test] + fn no_relay() -> Result<()> { + assert_eq!( + format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + coordinates: HashSet::from_iter(vec![Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![], + }]), + protocol: None, + user: None, + } + ), + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit", + ); + Ok(()) + } + + #[test] + fn with_protocol() -> Result<()> { + assert_eq!( + format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + coordinates: HashSet::from_iter(vec![Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], + }]), + protocol: Some(ServerProtocol::Ssh), + user: None, + } + ), + "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", + ); + Ok(()) + } + + #[test] + fn with_protocol_and_user() -> Result<()> { + assert_eq!( + format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + coordinates: HashSet::from_iter(vec![Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], + }]), + protocol: Some(ServerProtocol::Ssh), + user: Some("bla".to_string()), + } + ), + "nostr://bla@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", + ); + Ok(()) + } + } - mod nostr_git_url_paramemters_from_str { + mod nostr_url_decoded_paramemters_from_str { use std::str::FromStr; use super::*; diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index 2ef4b8c..d566b43 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs @@ -20,7 +20,7 @@ use crate::{ git::{nostr_url::NostrUrlDecoded, Repo, RepoActions}, }; -#[derive(Default, Clone)] +#[derive(Clone)] pub struct RepoRef { pub name: String, pub description: String, @@ -30,18 +30,30 @@ pub struct RepoRef { pub web: Vec, pub relays: Vec, pub maintainers: Vec, + pub trusted_maintainer: PublicKey, pub events: HashMap, - // code languages and hashtags } -impl TryFrom for RepoRef { +impl TryFrom<(nostr::Event, Option)> for RepoRef { type Error = anyhow::Error; - fn try_from(event: nostr::Event) -> Result { + fn try_from((event, trusted_maintainer): (nostr::Event, Option)) -> Result { if !event.kind.eq(&Kind::GitRepoAnnouncement) { bail!("incorrect kind"); } - let mut r = Self::default(); + + let mut r = Self { + name: String::new(), + description: String::new(), + identifier: String::new(), + root_commit: String::new(), + git_server: Vec::new(), + web: Vec::new(), + relays: Vec::new(), + maintainers: Vec::new(), + trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey), + events: HashMap::new(), + }; for tag in event.tags.iter() { match tag.as_slice() { @@ -183,11 +195,7 @@ impl RepoRef { pub fn coordinate_with_hint(&self) -> Coordinate { Coordinate { kind: Kind::GitRepoAnnouncement, - public_key: *self - .maintainers - .first() - .context("no maintainers in repo ref") - .unwrap(), + public_key: self.trusted_maintainer, identifier: self.identifier.clone(), relays: if let Some(relay) = self.relays.first() { vec![relay.clone()] @@ -204,6 +212,18 @@ impl RepoRef { .map(|c| (c.clone(), self.events.get(c).map(|e| e.created_at))) .collect::)>>() } + + pub fn to_nostr_git_url(&self) -> String { + format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + coordinates: HashSet::from_iter(vec![self.coordinate_with_hint()]), + protocol: None, + user: None, + } + ) + } } pub async fn get_repo_coordinates( @@ -469,6 +489,7 @@ mod tests { RelayUrl::parse("ws://relay1.io").unwrap(), RelayUrl::parse("ws://relay2.io").unwrap(), ], + trusted_maintainer: TEST_KEY_1_KEYS.public_key(), maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], events: HashMap::new(), } @@ -482,20 +503,27 @@ mod tests { #[tokio::test] async fn identifier() { assert_eq!( - RepoRef::try_from(create().await).unwrap().identifier, + RepoRef::try_from((create().await, None)) + .unwrap() + .identifier, "123412341", ) } #[tokio::test] async fn name() { - assert_eq!(RepoRef::try_from(create().await).unwrap().name, "test name",) + assert_eq!( + RepoRef::try_from((create().await, None)).unwrap().name, + "test name", + ) } #[tokio::test] async fn description() { assert_eq!( - RepoRef::try_from(create().await).unwrap().description, + RepoRef::try_from((create().await, None)) + .unwrap() + .description, "test description", ) } @@ -503,7 +531,9 @@ mod tests { #[tokio::test] async fn root_commit_is_r_tag() { assert_eq!( - RepoRef::try_from(create().await).unwrap().root_commit, + RepoRef::try_from((create().await, None)) + .unwrap() + .root_commit, "5e664e5a7845cd1373c79f580ca4fe29ab5b34d2", ) } @@ -526,7 +556,7 @@ mod tests { async fn less_than_40_characters() { let s = "5e664e5a7845cd1373"; assert_eq!( - RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await) + RepoRef::try_from((create_with_incorrect_first_commit_ref(s).await, None)) .unwrap() .root_commit, "", @@ -537,7 +567,7 @@ mod tests { async fn more_than_40_characters() { let s = "5e664e5a7845cd1373c79f580ca4fe29ab5b34d2111111111"; assert_eq!( - RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await) + RepoRef::try_from((create_with_incorrect_first_commit_ref(s).await, None)) .unwrap() .root_commit, "", @@ -548,7 +578,7 @@ mod tests { async fn not_hex_characters() { let s = "xxx64e5a7845cd1373c79f580ca4fe29ab5b34d2"; assert_eq!( - RepoRef::try_from(create_with_incorrect_first_commit_ref(s).await) + RepoRef::try_from((create_with_incorrect_first_commit_ref(s).await, None)) .unwrap() .root_commit, "", @@ -559,7 +589,9 @@ mod tests { #[tokio::test] async fn git_server() { assert_eq!( - RepoRef::try_from(create().await).unwrap().git_server, + RepoRef::try_from((create().await, None)) + .unwrap() + .git_server, vec!["https://localhost:1000"], ) } @@ -567,7 +599,7 @@ mod tests { #[tokio::test] async fn web() { assert_eq!( - RepoRef::try_from(create().await).unwrap().web, + RepoRef::try_from((create().await, None)).unwrap().web, vec![ "https://exampleproject.xyz".to_string(), "https://gitworkshop.dev/123".to_string() @@ -578,7 +610,7 @@ mod tests { #[tokio::test] async fn relays() { assert_eq!( - RepoRef::try_from(create().await).unwrap().relays, + RepoRef::try_from((create().await, None)).unwrap().relays, vec![ RelayUrl::parse("ws://relay1.io").unwrap(), RelayUrl::parse("ws://relay2.io").unwrap(), @@ -589,7 +621,9 @@ mod tests { #[tokio::test] async fn maintainers() { assert_eq!( - RepoRef::try_from(create().await).unwrap().maintainers, + RepoRef::try_from((create().await, None)) + .unwrap() + .maintainers, vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], ) } -- cgit v1.2.3