What is Nostr?
DanConwayDev
npub15qy…yejr
2024-12-09 09:50:18
in reply to nevent1q…v9kf

DanConwayDev on Nostr: 2. Ah, I hadn't considered that the maintainer may want to express a view on whether ...

2. Ah, I hadn't considered that the maintainer may want to express a view on whether contributors should use the nip05 or npub in the clone url.
I planned on it being a user preference and hadn't landed on a default for gitworkshop.dev.
Now we are on the topic of maintainers expressing a view, it's also conceivable that projects may want to suggest a generic project nip05 which may point to different maintainers over time as the project evolves.
Increasing reliance on DNS just feels a bit dirty and unsatisfactory, but I can see the easier UX.
Why don't we start off by adding basic support first, without adding anything to `30317`, and see what the feedback is like?

3. I'm not sure that we do. I've created this to demonstrate why not:
From b4d81f54ad6e5cbf212593527727e7f7cb036aef Mon Sep 17 00:00:00 2001
From: DanConwayDev
Date: Mon, 9 Dec 2024 09:34:08 +0000
Subject: [PATCH] add nostr_url nip05 whilst retaining structure

Following the discussion:
nevent1q…hvk2
I've pulled this together to demonstrate how this feature could be
added to `NostrUrlDecoded` without dramatically changing its
structure or adding a similar struct which omits the `PublicKey`.

it replaces `NostrUrlDecoded::from_str` with
`NostrUrlDecoded::parse_and_resolve`.

In this WIP I have began to add nip05 to repo_ref so that
`RepoRef::to_nostr_git_url` can render the nip05 when originally
used. Unfortunately, there is more work to as the type of
`trusted_maintainer` needs to change to `NostrUrlDecoded`.

There may still be instances where the variable name
`repo_coordinate` is used to create repo_refs when it is exclusively
refering to trusted maintainers and not other repo maintainers.
these should use the type `NostrUrlDecoded` too, to ensure
`RepoRef::to_nostr_git_url` always includes nip05 when originally
taken from the nostr_url.
---
src/bin/git_remote_nostr/main.rs | 12 ++++++------
src/bin/ngit/sub_commands/init.rs | 11 +++++++----
src/lib/git/nostr_url.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
src/lib/repo_ref.rs | 19 +++++++++++++++----
4 files changed, 100 insertions(+), 23 deletions(-)

diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs
index 8e12d68..84327d7 100644
--- a/src/bin/git_remote_nostr/main.rs
+++ b/src/bin/git_remote_nostr/main.rs
@@ -9,7 +9,6 @@ use std::{
collections::HashSet,
env, io,
path::{Path, PathBuf},
- str::FromStr,
};

use anyhow::{bail, Context, Result};
@@ -28,7 +27,7 @@ mod utils;

#[tokio::main]
async fn main() -> Result<()> {
- let Some((decoded_nostr_url, git_repo)) = process_args()? else {
+ let Some((decoded_nostr_url, git_repo)) = process_args().await? else {
return Ok(());
};

@@ -109,7 +108,7 @@ async fn main() -> Result<()> {
}
}

-fn process_args() -> Result> {
+async fn process_args() -> Result> {
let args = env::args();
let args = args.skip(1).take(2).collect::>();

@@ -135,13 +134,14 @@ fn process_args() -> Result> {
return Ok(None);
};

- let decoded_nostr_url =
- NostrUrlDecoded::from_str(nostr_remote_url).context("invalid nostr url")?;
-
let git_repo = Repo::from_path(&PathBuf::from(
std::env::var("GIT_DIR").context("git should set GIT_DIR when remote helper is called")?,
))?;

+ let decoded_nostr_url = NostrUrlDecoded::parse_and_resolve(nostr_remote_url, &Some(&git_repo))
+ .await
+ .context("invalid nostr url")?;
+
Ok(Some((decoded_nostr_url, git_repo)))
}

diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 6fc1ec4..dbad9b6 100644
--- a/src/bin/ngit/sub_commands/init.rs
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, str::FromStr};
+use std::collections::HashMap;

use anyhow::{Context, Result};
use console::Style;
@@ -408,6 +408,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
trusted_maintainer: user_ref.public_key,
maintainers: maintainers.clone(),
events: HashMap::new(),
+ nip05: None,
};
let repo_event = repo_ref.to_event(&signer).await?;

@@ -442,7 +443,7 @@ 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)?;
+ prompt_to_set_nostr_url_as_origin(&repo_ref, &git_repo).await?;

// TODO: if no state event exists and there is currently a remote called
// "origin", automtically push rather than waiting for the next commit
@@ -483,7 +484,7 @@ 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<()> {
+async 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."
);
@@ -493,7 +494,9 @@ fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Res

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 Ok(nostr_url) =
+ NostrUrlDecoded::parse_and_resolve(origin_url, &Some(git_repo)).await
+ {
if nostr_url.coordinate.identifier == repo_ref.identifier {
if nostr_url.coordinate.public_key == repo_ref.trusted_maintainer {
return Ok(());
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs
index c26bb2e..ac57538 100644
--- a/src/lib/git/nostr_url.rs
+++ b/src/lib/git/nostr_url.rs
@@ -2,9 +2,11 @@ use core::fmt;
use std::str::FromStr;

use anyhow::{anyhow, bail, Context, Error, Result};
-use nostr::nips::nip01::Coordinate;
+use nostr::nips::{nip01::Coordinate, nip05};
use nostr_sdk::{PublicKey, RelayUrl, ToBech32, Url};

+use super::{get_git_config_item, save_git_config_item, Repo};
+
#[derive(Debug, PartialEq, Default, Clone)]
pub enum ServerProtocol {
Ssh,
@@ -59,6 +61,7 @@ pub struct NostrUrlDecoded {
pub coordinate: Coordinate,
pub protocol: Option,
pub user: Option,
+ pub nip05: Option,
}

impl fmt::Display for NostrUrlDecoded {
@@ -89,10 +92,8 @@ impl fmt::Display for NostrUrlDecoded {

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 {
- type Err = anyhow::Error;
-
- fn from_str(url: &str) -> Result {
+impl NostrUrlDecoded {
+ pub async fn parse_and_resolve(url: &str, git_repo: &Option<&Repo>) -> Result {
let mut protocol = None;
let mut user = None;
let mut relays = vec![];
@@ -154,6 +155,7 @@ impl std::str::FromStr for NostrUrlDecoded {
}
// extract naddr npub//identifer
let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
+ let mut nip05 = None;
// naddr used
let coordinate = if let Ok(coordinate) = Coordinate::parse(part) {
if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) {
@@ -161,8 +163,9 @@ impl std::str::FromStr for NostrUrlDecoded {
} else {
bail!("naddr doesnt point to a git repository announcement");
}
- // npub//identifer used
- } else if let Ok(public_key) = PublicKey::parse(part) {
+ // //identifer used
+ } else {
+ let npub_or_nip05 = part.to_owned();
parts.remove(0);
let identifier = parts
.pop()
@@ -179,14 +182,41 @@ impl std::str::FromStr for NostrUrlDecoded {
RelayUrl::parse(&decoded).context("could not parse relays in nostr git url")?;
relays.push(url);
}
+ let public_key = match PublicKey::parse(npub_or_nip05) {
+ Ok(public_key) => public_key,
+ Err(_) => {
+ nip05 = Some(npub_or_nip05.to_string());
+ if let Ok(public_key) =
+ resolve_nip05_from_git_config_cache(npub_or_nip05, git_repo)
+ {
+ public_key
+ } else {
+ // TODO eprint loading message
+ let res = nip05::profile(npub_or_nip05, None)
+ .await
+ .context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
+ // TODO clear loading message
+ nip05 = Some(npub_or_nip05.to_string());
+ let _ = save_nip05_to_git_config_cache(
+ npub_or_nip05,
+ &res.public_key,
+ git_repo,
+ );
+ if relays.is_empty() {
+ for r in res.relays {
+ relays.push(r);
+ }
+ }
+ res.public_key
+ }
+ }
+ };
Coordinate {
identifier,
public_key,
kind: nostr_sdk::Kind::GitRepoAnnouncement,
relays,
}
- } else {
- bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR);
};

Ok(Self {
@@ -194,10 +224,43 @@ impl std::str::FromStr for NostrUrlDecoded {
coordinate,
protocol,
user,
+ nip05,
})
}
}

+fn resolve_nip05_from_git_config_cache(nip05: &str, git_repo: &Option<&Repo>) -> Result {
+ let stored_value = get_git_config_item(
+ git_repo,
+ &format!("nostr.nip05.{}", urlencoding::encode(nip05)),
+ )?
+ .context("not in cache")?;
+ PublicKey::parse(stored_value)
+ .context("stored nip05 resolution value did not parse as public key")
+}
+
+fn save_nip05_to_git_config_cache(
+ nip05: &str,
+ public_key: &PublicKey,
+ git_repo: &Option<&Repo>,
+) -> Result<()> {
+ if save_git_config_item(
+ git_repo,
+ &format!("nostr.nip05.{}", urlencoding::encode(nip05)),
+ &public_key.to_bech32()?,
+ )
+ .is_err()
+ {
+ save_git_config_item(
+ &None,
+ &format!("nostr.nip05.{}", urlencoding::encode(nip05)),
+ &public_key.to_bech32()?,
+ )
+ } else {
+ Ok(())
+ }
+}
+
#[derive(Debug, PartialEq, Default)]
pub struct CloneUrl {
original_string: String,
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index 1b25ccf..bf23d30 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -35,12 +35,14 @@ pub struct RepoRef {
pub maintainers: Vec,
pub trusted_maintainer: PublicKey,
pub events: HashMap,
+ pub nip05: Option,
}

impl TryFrom<(nostr::Event, Option)> for RepoRef {
type Error = anyhow::Error;

fn try_from((event, trusted_maintainer): (nostr::Event, Option)) -> Result {
+ // TODO: turn trusted maintainer into NostrUrlDecoded
if !event.kind.eq(&Kind::GitRepoAnnouncement) {
bail!("incorrect kind");
}
@@ -56,6 +58,7 @@ impl TryFrom<(nostr::Event, Option)> for RepoRef {
maintainers: Vec::new(),
trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey),
events: HashMap::new(),
+ nip05: None,
};

for tag in event.tags.iter() {
@@ -239,6 +242,7 @@ impl RepoRef {
coordinate: self.coordinate_with_hint(),
protocol: None,
user: None,
+ nip05: self.nip05.clone(),
}
)
}
@@ -259,7 +263,7 @@ pub async fn get_repo_coordinates_when_remote_unknown(
pub async fn try_and_get_repo_coordinates_when_remote_unknown(
git_repo: &Repo,
) -> Result {
- let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo)?;
+ let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo).await?;
if remote_coordinates.is_empty() {
if let Ok(c) = get_repo_coordinates_from_git_config(git_repo) {
Ok(c)
@@ -327,11 +331,15 @@ fn get_repo_coordinates_from_git_config(git_repo: &Repo) -> Result {
.context("git config item \"nostr.repo\" is not an naddr")
}

-fn get_repo_coordinates_from_nostr_remotes(git_repo: &Repo) -> Result> {
+async fn get_repo_coordinates_from_nostr_remotes(
+ git_repo: &Repo,
+) -> Result> {
let mut repo_coordinates = HashMap::new();
for remote_name in git_repo.git_repo.remotes()?.iter().flatten() {
if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() {
- if let Ok(nostr_url_decoded) = NostrUrlDecoded::from_str(remote_url) {
+ if let Ok(nostr_url_decoded) =
+ NostrUrlDecoded::parse_and_resolve(remote_url, &Some(git_repo)).await
+ {
repo_coordinates.insert(remote_name.to_string(), nostr_url_decoded.coordinate);
}
}
@@ -383,7 +391,9 @@ async fn get_repo_coordinate_from_user_prompt(
.input(PromptInputParms::default().with_prompt("nostr repository"))?;
let coordinate = if let Ok(c) = Coordinate::parse(&input) {
c
- } else if let Ok(nostr_url) = NostrUrlDecoded::from_str(&input) {
+ } else if let Ok(nostr_url) =
+ NostrUrlDecoded::parse_and_resolve(&input, &Some(git_repo)).await
+ {
nostr_url.coordinate
} else {
eprintln!("not a valid naddr or git nostr remote URL starting nostr://");
@@ -540,6 +550,7 @@ mod tests {
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(),
+ nip05: None,
}
.to_event(&TEST_KEY_1_SIGNER)
.await
--
libgit2 1.8.1

Author Public Key
npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr