feat: New flag to patch apply and merge to mention patches

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-08-03 00:17:20 +00:00
parent bc8c6f3154
commit 67e25da8c0
11 changed files with 88 additions and 20 deletions

View File

@@ -11,10 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support signing using NIP-46 bunker - by Awiteb - Support signing using NIP-46 bunker - by Awiteb
- Keyring the secret key `n34 config keyring --enable` - by Awiteb - Keyring the secret key `n34 config keyring --enable` - by Awiteb
- New flag to `patch apply and merge` to mention patches - by Awiteb
### Dependencies ### Dependencies
- Add `keyring`, `nostr-connect`, `nostr-keyring` and `url` to the dependencies - by Awiteb - Add `keyring`, `nostr-connect`, `nostr-keyring` and `url` to the dependencies - by Awiteb
- Remove `url` from `n34` dependencies - by Awiteb
### Documentation
- N34 book - by Awiteb
- Fix status command docs - by Awiteb
## [0.3.0] - 2025-07-05 ## [0.3.0] - 2025-07-05

View File

@@ -14,6 +14,7 @@ Arguments:
Options: 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` --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`
--patches <PATCH-EVENT-ID> Patches that have been applied. Use this when only some patches have been applied, not all
``` ```
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The Creates a kind `1631` event (Applied/Merged status) for the specified patch. The

View File

@@ -14,6 +14,7 @@ Arguments:
Options: 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` --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`
--patches <PATCH-EVENT-ID> Patches that have been merged. Use this when only some patches have been merged, not all
``` ```
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The Creates a kind `1631` event (Applied/Merged status) for the specified patch. The

View File

@@ -21,7 +21,7 @@ use super::PatchStatus;
use crate::{ use crate::{
cli::{ cli::{
CliOptions, CliOptions,
traits::CommandRunner, traits::{CommandRunner, VecNostrEventExt},
types::{NaddrOrSet, NostrEvent}, types::{NaddrOrSet, NostrEvent},
}, },
error::{N34Error, N34Result}, error::{N34Error, N34Result},
@@ -38,6 +38,10 @@ pub struct ApplyArgs {
/// The open patch id to apply it. Must be orignal root patch or /// The open patch id to apply it. Must be orignal root patch or
/// revision root /// revision root
patch_id: NostrEvent, patch_id: NostrEvent,
/// Patches that have been applied. Use this when only some patches have
/// been applied, not all.
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
applied_patches: Vec<NostrEvent>,
/// The applied commits /// The applied commits
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
applied_commits: Vec<Sha1Hash>, applied_commits: Vec<Sha1Hash>,
@@ -51,6 +55,7 @@ impl CommandRunner for ApplyArgs {
self.naddrs, self.naddrs,
PatchStatus::MergedApplied, PatchStatus::MergedApplied,
Some(either::Either::Right(self.applied_commits)), Some(either::Either::Right(self.applied_commits)),
self.applied_patches.into_event_ids(),
|patch_status| { |patch_status| {
if patch_status.is_merged_or_applied() { if patch_status.is_merged_or_applied() {
return Err(N34Error::InvalidStatus( return Err(N34Error::InvalidStatus(

View File

@@ -46,6 +46,7 @@ impl CommandRunner for CloseArgs {
self.naddrs, self.naddrs,
PatchStatus::Closed, PatchStatus::Closed,
None, None,
Vec::new(),
|patch_status| { |patch_status| {
if patch_status.is_closed() { if patch_status.is_closed() {
return Err(N34Error::InvalidStatus( return Err(N34Error::InvalidStatus(

View File

@@ -46,6 +46,7 @@ impl CommandRunner for DraftArgs {
self.naddrs, self.naddrs,
PatchStatus::Draft, PatchStatus::Draft,
None, None,
Vec::new(),
|patch_status| { |patch_status| {
if patch_status.is_drafted() { if patch_status.is_drafted() {
return Err(N34Error::InvalidStatus( return Err(N34Error::InvalidStatus(

View File

@@ -21,7 +21,7 @@ use super::PatchStatus;
use crate::{ use crate::{
cli::{ cli::{
CliOptions, CliOptions,
traits::CommandRunner, traits::{CommandRunner, VecNostrEventExt},
types::{NaddrOrSet, NostrEvent}, types::{NaddrOrSet, NostrEvent},
}, },
error::{N34Error, N34Result}, error::{N34Error, N34Result},
@@ -34,12 +34,16 @@ pub struct MergeArgs {
/// ///
/// If omitted, looks for a `nostr-address` file. /// If omitted, looks for a `nostr-address` file.
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")] #[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
naddrs: Option<Vec<NaddrOrSet>>, naddrs: Option<Vec<NaddrOrSet>>,
/// The open patch id to merge it. Must be orignal root patch or /// The open patch id to merge it. Must be orignal root patch or
/// revision root /// revision root
patch_id: NostrEvent, patch_id: NostrEvent,
/// Patches that have been merged. Use this when only some patches have been
/// merged, not all.
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
merged_patches: Vec<NostrEvent>,
/// The merge commit id /// The merge commit id
merge_commit: Sha1Hash, merge_commit: Sha1Hash,
} }
impl CommandRunner for MergeArgs { impl CommandRunner for MergeArgs {
@@ -50,6 +54,7 @@ impl CommandRunner for MergeArgs {
self.naddrs, self.naddrs,
PatchStatus::MergedApplied, PatchStatus::MergedApplied,
Some(either::Either::Left(self.merge_commit)), Some(either::Either::Left(self.merge_commit)),
self.merged_patches.into_event_ids(),
|patch_status| { |patch_status| {
if patch_status.is_merged_or_applied() { if patch_status.is_merged_or_applied() {
return Err(N34Error::InvalidStatus( return Err(N34Error::InvalidStatus(

View File

@@ -46,6 +46,7 @@ impl CommandRunner for ReopenArgs {
self.naddrs, self.naddrs,
PatchStatus::Open, PatchStatus::Open,
None, None,
Vec::new(),
|patch_status| { |patch_status| {
if patch_status.is_open() { if patch_status.is_open() {
return Err(N34Error::InvalidStatus( return Err(N34Error::InvalidStatus(

View File

@@ -23,6 +23,7 @@ use nostr::{
filter::Filter, filter::Filter,
hashes::sha1::Hash as Sha1Hash, hashes::sha1::Hash as Sha1Hash,
nips::{nip10::Marker, nip19::ToBech32}, nips::{nip10::Marker, nip19::ToBech32},
types::RelayUrl,
}; };
use super::{ use super::{
@@ -130,6 +131,7 @@ pub async fn patch_status_command(
naddrs: Option<Vec<NaddrOrSet>>, naddrs: Option<Vec<NaddrOrSet>>,
new_status: PatchStatus, new_status: PatchStatus,
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>, merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
merge_or_applied_patches: Vec<EventId>,
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>, check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
) -> N34Result<()> { ) -> N34Result<()> {
let user_pkey = options.pubkey().await?; let user_pkey = options.pubkey().await?;
@@ -209,22 +211,20 @@ pub async fn patch_status_command(
.tags(commits.into_iter().map(Tag::reference)); .tags(commits.into_iter().map(Tag::reference));
}; };
let root = if let Some(root_revision) = root_revision { if let Some(root_revision) = root_revision {
status_builder = status_builder.tag(utils::event_reply_tag( status_builder = status_builder.tag(utils::event_reply_tag(
&root_revision, &root_revision,
relay_hint.as_ref(), relay_hint.as_ref(),
Marker::Reply, Marker::Reply,
)); ));
root_revision }
} else {
root_patch if !merge_or_applied_patches.is_empty() {
}; status_builder = status_builder.tags(
let patches = client.fetch_patch_series(root, patch_event.pubkey).await?; build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
status_builder = status_builder.tags( .await,
patches );
.into_iter() }
.map(|p| utils::event_reply_tag(&p.id, relay_hint.as_ref(), Marker::Mention)),
);
} }
let status_event = status_builder.dedup_tags().build(user_pkey); let status_event = status_builder.dedup_tags().build(user_pkey);
@@ -279,7 +279,6 @@ pub async fn list_patches_and_issues(
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await) .add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
.await; .await;
let kind = if list_patches { let kind = if list_patches {
Kind::GitPatch Kind::GitPatch
} else { } else {
@@ -352,7 +351,6 @@ pub async fn list_patches_and_issues(
Ok(()) Ok(())
} }
/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision /// Returns a tuple of (root_id, patch_id) if this is a valid root or revision
/// patch. /// patch.
fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<EventId>)> { fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<EventId>)> {
@@ -401,3 +399,40 @@ fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus
event.id.to_bech32().expect("Infallible") event.id.to_bech32().expect("Infallible")
) )
} }
/// Generates a list of tags for quoting patches in merge/applied status events.
async fn build_patches_quote(
client: NostrClient,
relay_hint: Option<RelayUrl>,
patches: Vec<EventId>,
) -> Vec<Tag> {
let client = Arc::new(client);
let relay_hint = Arc::new(relay_hint);
future::join_all(patches.into_iter().map(|eid| {
let task_relay = Arc::clone(&relay_hint);
let task_client = Arc::clone(&client);
async move {
Tag::custom(
TagKind::q(),
[
eid.to_hex(),
task_relay
.as_ref()
.as_ref()
.map(|r| r.to_string())
.unwrap_or_default(),
task_client
.event_author(eid)
.await
.ok()
.flatten()
.map(|p| p.to_hex())
.unwrap_or_default(),
],
)
}
}))
.await
}

View File

@@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License // 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>. // along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use nostr::event::EventId;
use super::CliOptions; use super::CliOptions;
use crate::error::N34Result; use crate::{cli::types::NostrEvent, error::N34Result};
/// A trait defining the interface for command runners in the CLI. /// A trait defining the interface for command runners in the CLI.
pub trait CommandRunner { pub trait CommandRunner {
@@ -29,3 +31,12 @@ pub trait CommandRunner {
/// Executes the command and returns a Result indicating success or failure. /// Executes the command and returns a Result indicating success or failure.
fn run(self, options: CliOptions) -> impl Future<Output = N34Result<()>> + Send; fn run(self, options: CliOptions) -> impl Future<Output = N34Result<()>> + Send;
} }
#[easy_ext::ext(VecNostrEventExt)]
impl Vec<NostrEvent> {
/// Extracts `EventId` from each `NostrEvent` and collects them into a
/// `Vec<EventId>`.
pub fn into_event_ids(self) -> Vec<EventId> {
self.into_iter().map(|e| e.event_id).collect()
}
}

View File

@@ -62,6 +62,7 @@ pub struct ContentDetails {
} }
/// A client for interacting with the Nostr relays /// A client for interacting with the Nostr relays
#[derive(Clone)]
pub struct NostrClient { pub struct NostrClient {
/// The underlying Nostr client implementation /// The underlying Nostr client implementation
pub client: Client, pub client: Client,
@@ -323,7 +324,6 @@ impl NostrClient {
return Ok(PatchStatus::Closed); return Ok(PatchStatus::Closed);
} }
Ok(root_status) Ok(root_status)
} }