@@ -20,7 +20,7 @@ Check the documentation at [n34.dev]
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [X] Repository announcements
|
- [X] Repository announcements
|
||||||
- [ ] Repository state announcements
|
- [X] Repository state announcements
|
||||||
- [X] Patches (Send, fetch and list)
|
- [X] Patches (Send, fetch and list)
|
||||||
- [X] Issues (Send, view and list)
|
- [X] Issues (Send, view and list)
|
||||||
- [X] Replies
|
- [X] Replies
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
- [Manage Repositories](repo/README.md)
|
- [Manage Repositories](repo/README.md)
|
||||||
- [Broadcast and Update a Git Repository](repo/announce.md)
|
- [Broadcast and Update a Git Repository](repo/announce.md)
|
||||||
- [View Git Repository Details](repo/view.md)
|
- [View Git Repository Details](repo/view.md)
|
||||||
|
- [Repository State Announcements](repo/state.md)
|
||||||
- [Issue Management](issue/README.md)
|
- [Issue Management](issue/README.md)
|
||||||
- [Create an Issue](issue/new.md)
|
- [Create an Issue](issue/new.md)
|
||||||
- [View an Issue By ID](issue/view.md)
|
- [View an Issue By ID](issue/view.md)
|
||||||
|
|||||||
@@ -355,8 +355,8 @@
|
|||||||
<span class="feature-text">Repository announcements</span>
|
<span class="feature-text">Repository announcements</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check pending"></span>
|
<span class="feature-check completed"></span>
|
||||||
<span class="feature-text pending">Repository state announcements</span>
|
<span class="feature-text">Repository state announcements</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<span class="feature-check completed"></span>
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<span class="feature-check completed"></span>
|
||||||
<span class="feature-text pending">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
<span class="feature-text">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<span class="feature-check completed"></span>
|
||||||
|
|||||||
29
docs/repo/state.md
Normal file
29
docs/repo/state.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Repository State Announcements
|
||||||
|
|
||||||
|
> `n34 repo state` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Repository state announcements
|
||||||
|
|
||||||
|
Usage: n34 repo state [OPTIONS] <HEAD>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<HEAD> Name of the repository's primary branch, such as 'master' or 'main'
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
--tags <TAGS> Tags to announce a state for, in the format `<tag-name>=<commit-id>`. Separated by comma
|
||||||
|
--branches <BRANCHES> Branches to announce a state for, in the format `<branch-name>=<commit-id>`. Separated by comma
|
||||||
|
```
|
||||||
|
|
||||||
|
This command allows you to announce your repository state, which is useful for
|
||||||
|
pushing to permissionless git repositories like the [GRASP] relay. The relay
|
||||||
|
will verify your repository state and permit pushing commits only if they match
|
||||||
|
the announced state.
|
||||||
|
|
||||||
|
To get the commit ID that a branch or tag points to, use `git rev-parse
|
||||||
|
<tag-or-branch-name>`. You can automate this process by creating a script to
|
||||||
|
generate the required input for this command.
|
||||||
|
|
||||||
|
[GRASP]: https://ngit.dev/grasp
|
||||||
@@ -16,12 +16,15 @@
|
|||||||
|
|
||||||
/// `repo announce` subcommand
|
/// `repo announce` subcommand
|
||||||
mod announce;
|
mod announce;
|
||||||
|
/// `repo state` subcommand
|
||||||
|
mod state;
|
||||||
/// `repo view` subcommand
|
/// `repo view` subcommand
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
use self::announce::AnnounceArgs;
|
use self::announce::AnnounceArgs;
|
||||||
|
use self::state::StateArgs;
|
||||||
use self::view::ViewArgs;
|
use self::view::ViewArgs;
|
||||||
use super::{CliOptions, CommandRunner};
|
use super::{CliOptions, CommandRunner};
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
@@ -32,10 +35,12 @@ pub enum RepoSubcommands {
|
|||||||
View(ViewArgs),
|
View(ViewArgs),
|
||||||
/// Broadcast and update a git repository
|
/// Broadcast and update a git repository
|
||||||
Announce(AnnounceArgs),
|
Announce(AnnounceArgs),
|
||||||
|
/// Repository state announcements
|
||||||
|
State(StateArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for RepoSubcommands {
|
impl CommandRunner for RepoSubcommands {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::run_command!(self, options, & View Announce)
|
crate::run_command!(self, options, & View Announce State)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
src/cli/commands/repo/state.rs
Normal file
149
src/cli/commands/repo/state.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
use nostr::event::{Kind, Tag, TagKind};
|
||||||
|
use nostr::{event::EventBuilder, hashes::sha1::Hash as Sha1Hash};
|
||||||
|
|
||||||
|
use crate::nostr_utils::traits::ReposUtils;
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
CommandRunner,
|
||||||
|
parsers,
|
||||||
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::NaddrOrSet,
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Prefix for branch references in Git.
|
||||||
|
const HEADS_REFS: &str = "refs/heads/";
|
||||||
|
|
||||||
|
/// Prefix for tag references in Git.
|
||||||
|
const TAGS_REFS: &str = "refs/tags/";
|
||||||
|
|
||||||
|
/// Repository state announcements kind
|
||||||
|
const REPO_STATE_KIND: Kind = Kind::Custom(30618);
|
||||||
|
|
||||||
|
/// `HEAD` tag kind
|
||||||
|
const HEAD_TAG_KIND: TagKind = TagKind::Custom(Cow::Borrowed("HEAD"));
|
||||||
|
|
||||||
|
/// Arguments for the `repo state` command
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct StateArgs {
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// Tags to announce a state for, in the format `<tag-name>=<commit-id>`.
|
||||||
|
/// Separated by comma.
|
||||||
|
///
|
||||||
|
/// Example: `v0.4.0=9aa3b62de02a63aa6a0d49efa7c484aa550cef56`.
|
||||||
|
#[arg(long, value_delimiter = ',', value_parser = parsers::name_and_sha1)]
|
||||||
|
tags: Vec<(String, Sha1Hash)>,
|
||||||
|
/// Branches to announce a state for, in the format
|
||||||
|
/// `<branch-name>=<commit-id>`. Separated by comma.
|
||||||
|
///
|
||||||
|
/// Example: `master=9aa3b62de02a63aa6a0d49efa7c484aa550cef56`.
|
||||||
|
#[arg(long, value_delimiter = ',', value_parser = parsers::name_and_sha1)]
|
||||||
|
branches: Vec<(String, Sha1Hash)>,
|
||||||
|
/// Name of the repository's primary branch, such as 'master' or 'main'.
|
||||||
|
head: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for StateArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||||
|
self.naddrs.flat_naddrs(&options.config.sets)?,
|
||||||
|
&utils::nostr_address_path()?,
|
||||||
|
)?)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
|
||||||
|
let repos = client
|
||||||
|
.fetch_repos(&naddrs.clone().into_coordinates())
|
||||||
|
.await?;
|
||||||
|
let repos_id = repos
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.expect("It's not empty, checked above")
|
||||||
|
.id;
|
||||||
|
|
||||||
|
let mut event_builder = EventBuilder::new(REPO_STATE_KIND, "")
|
||||||
|
.dedup_tags()
|
||||||
|
.pow(options.pow.unwrap_or_default())
|
||||||
|
.tag(Tag::identifier(&repos_id))
|
||||||
|
.tag(Tag::custom(
|
||||||
|
HEAD_TAG_KIND,
|
||||||
|
&[format!("ref: {HEADS_REFS}{}", self.head)],
|
||||||
|
));
|
||||||
|
|
||||||
|
if !self.branches.is_empty() {
|
||||||
|
event_builder = event_builder.tags(refs_tags::<true>(self.branches));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.tags.is_empty() {
|
||||||
|
event_builder = event_builder.tags(refs_tags::<false>(self.tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = event_builder.build(user_pubk);
|
||||||
|
let event_id = event.id.expect("There is an id");
|
||||||
|
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
naddrs.extract_relays(),
|
||||||
|
repos.extract_relays(),
|
||||||
|
utils::add_write_relays(user_relays_list.as_ref()),
|
||||||
|
// Include read relays for each maintainer (if found)
|
||||||
|
client
|
||||||
|
.read_relays_from_users(&repos.extract_maintainers())
|
||||||
|
.await,
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||||
|
let success = client
|
||||||
|
.send_event_to(event, user_relays_list.as_ref(), &write_relays)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let nevent = utils::new_nevent(event_id, &success)?;
|
||||||
|
let naddr = utils::repo_naddr(repos_id, user_pubk, &success)?;
|
||||||
|
println!("Event created: {nevent}");
|
||||||
|
println!("State address: {naddr}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the refs tags
|
||||||
|
#[inline]
|
||||||
|
fn refs_tags<const IS_HEADS: bool>(refs: Vec<(String, Sha1Hash)>) -> impl IntoIterator<Item = Tag> {
|
||||||
|
refs.into_iter().map(|(tag, commit)| {
|
||||||
|
Tag::parse(&[
|
||||||
|
format!("{}{tag}", if IS_HEADS { HEADS_REFS } else { TAGS_REFS }),
|
||||||
|
commit.to_string(),
|
||||||
|
])
|
||||||
|
.expect("Not an empty tag")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ use std::{
|
|||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
Kind,
|
Kind,
|
||||||
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
nips::{
|
nips::{
|
||||||
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||||
nip46::NostrConnectURI,
|
nip46::NostrConnectURI,
|
||||||
@@ -84,6 +85,25 @@ pub fn parse_bunker_url(bunker_url: &str) -> N34Result<NostrConnectURI> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a string in the format `string=sha1` into a name and SHA1 hash.
|
||||||
|
/// The input must contain exactly one `=` character. If the format is invalid
|
||||||
|
/// or the SHA1 hash is incorrect, an error is returned.
|
||||||
|
pub fn name_and_sha1(value: &str) -> Result<(String, Sha1Hash), String> {
|
||||||
|
if value.chars().filter(|c| *c == '=').count() != 1 {
|
||||||
|
return Err("the syntax is `string=SHA1-commit-id`".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, sha1_hash) = value.split_once('=').expect("There is one `=`");
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
name.trim_matches(['"', '\'']).to_owned(),
|
||||||
|
sha1_hash
|
||||||
|
.trim_matches(['"', '\''])
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "Invalid SHA1 commit id".to_owned())?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.
|
/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.
|
||||||
pub fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
Reference in New Issue
Block a user