feat: New patch list commands to list the repo patches
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
46
src/cli/commands/patch/list.rs
Normal file
46
src/cli/commands/patch/list.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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::num::NonZeroUsize;
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ListArgs {
|
||||
/// 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")]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// Maximum number of patches to list
|
||||
#[arg(long, default_value = "15")]
|
||||
limit: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl CommandRunner for ListArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ mod close;
|
||||
mod draft;
|
||||
/// `patch fetch` subcommand
|
||||
mod fetch;
|
||||
/// `patch list` subcommand
|
||||
mod list;
|
||||
/// `patch merge` subcommand
|
||||
mod merge;
|
||||
/// `patch reopen` subcommand
|
||||
@@ -30,6 +32,7 @@ mod reopen;
|
||||
mod send;
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::LazyLock,
|
||||
@@ -43,6 +46,7 @@ use self::apply::ApplyArgs;
|
||||
use self::close::CloseArgs;
|
||||
use self::draft::DraftArgs;
|
||||
use self::fetch::FetchArgs;
|
||||
use self::list::ListArgs;
|
||||
use self::merge::MergeArgs;
|
||||
use self::reopen::ReopenArgs;
|
||||
use self::send::SendArgs;
|
||||
@@ -78,6 +82,8 @@ pub enum PatchSubcommands {
|
||||
Apply(ApplyArgs),
|
||||
/// Set an open patch status to merged.
|
||||
Merge(MergeArgs),
|
||||
/// List the repositories patches.
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
/// Represents a git patch
|
||||
@@ -115,6 +121,16 @@ impl PatchStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string representation of the patch status.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "Open",
|
||||
Self::MergedApplied => "Merged/Applied",
|
||||
Self::Closed => "Closed",
|
||||
Self::Draft => "Draft",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the patch is open.
|
||||
#[inline]
|
||||
pub fn is_open(&self) -> bool {
|
||||
@@ -140,6 +156,19 @@ impl PatchStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PatchStatus> for Kind {
|
||||
fn from(status: &PatchStatus) -> Self {
|
||||
status.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PatchStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TryFrom<Kind> for PatchStatus {
|
||||
type Error = N34Error;
|
||||
|
||||
@@ -178,7 +207,7 @@ impl GitPatch {
|
||||
|
||||
impl CommandRunner for PatchSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & Send Fetch Close Reopen Draft Apply Merge)
|
||||
crate::run_command!(self, options, & Send Fetch Close Reopen Draft Apply Merge List)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
// 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::iter;
|
||||
use std::{iter, str::FromStr, sync::Arc};
|
||||
|
||||
use either::Either;
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{EventBuilder, Tag, TagKind},
|
||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},
|
||||
filter::Filter,
|
||||
hashes::sha1::Hash as Sha1Hash,
|
||||
nips::nip10::Marker,
|
||||
nips::{nip10::Marker, nip19::ToBech32},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -29,15 +30,15 @@ use super::{
|
||||
patch::PatchStatus,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
};
|
||||
use crate::{
|
||||
cli::CliOptions,
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::{GitPatchUtils, ReposUtils},
|
||||
};
|
||||
use crate::{
|
||||
cli::types::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
};
|
||||
use crate::{
|
||||
cli::{CliOptions, patch::GitPatch},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::{GitIssueUtils, GitPatchUtils, ReposUtils},
|
||||
};
|
||||
|
||||
/// Updates the issue's status to `new_status` after validating it with
|
||||
/// `check_fn`.
|
||||
@@ -159,17 +160,7 @@ pub async fn patch_status_command(
|
||||
));
|
||||
}
|
||||
|
||||
let (root_patch, root_revision) = if patch_event.is_revision_patch() {
|
||||
(
|
||||
patch_event.root_patch_from_revision()?,
|
||||
Some(patch_event.id),
|
||||
)
|
||||
} else if patch_event.is_root_patch() {
|
||||
(patch_event.id, None)
|
||||
} else {
|
||||
return Err(N34Error::NotRootPatch);
|
||||
};
|
||||
|
||||
let (root_patch, root_revision) = get_patch_root_revision(&patch_event)?;
|
||||
let patch_status = client
|
||||
.fetch_patch_status(
|
||||
root_patch,
|
||||
@@ -260,3 +251,153 @@ pub async fn patch_status_command(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch and display patches and issues for given repositories.
|
||||
/// If `list_patches` is true, lists patches instead of issues.
|
||||
/// `limit` controls the maximum number of items to fetch.
|
||||
pub async fn list_patches_and_issues(
|
||||
options: CliOptions,
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
list_patches: bool,
|
||||
limit: usize,
|
||||
) -> N34Result<()> {
|
||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||
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;
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
|
||||
let coordinates = naddrs.clone().into_coordinates();
|
||||
let repos = client.fetch_repos(&coordinates).await?;
|
||||
let authorized_pubkeys = [naddrs.extract_owners(), repos.extract_maintainers()].concat();
|
||||
client.add_relays(&repos.extract_relays()).await;
|
||||
// This helps discover issues and their status.
|
||||
client
|
||||
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
||||
.await;
|
||||
|
||||
|
||||
let kind = if list_patches {
|
||||
Kind::GitPatch
|
||||
} else {
|
||||
Kind::GitIssue
|
||||
};
|
||||
|
||||
let mut filter = Filter::new()
|
||||
.coordinates(coordinates.iter())
|
||||
.kind(kind)
|
||||
.limit(limit);
|
||||
|
||||
if list_patches {
|
||||
filter = filter.hashtag("root");
|
||||
}
|
||||
|
||||
let arc_client = Arc::new(client);
|
||||
// Events are sorted by kind in ascending order:
|
||||
// 1630 (Open), 1631 (Resolved/Applied), 1632 (Closed), 1633 (Draft)
|
||||
let events = utils::sort_by_key(
|
||||
future::join_all(
|
||||
arc_client
|
||||
.fetch_events(filter)
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|event| {
|
||||
let c = arc_client.clone();
|
||||
let keys = authorized_pubkeys.clone();
|
||||
async move {
|
||||
let status = if list_patches {
|
||||
let (root, root_revision) = get_patch_root_revision(&event)?;
|
||||
c.fetch_patch_status(
|
||||
root,
|
||||
root_revision,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.await
|
||||
.map(Either::Left)?
|
||||
} else {
|
||||
c.fetch_issue_status(
|
||||
event.id,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.await
|
||||
.map(Either::Right)?
|
||||
};
|
||||
N34Result::Ok((event, status))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|r| r.ok()),
|
||||
|(_, status)| status.as_ref().either_into::<Kind>(),
|
||||
);
|
||||
|
||||
let lines = events
|
||||
.map(|(event, status)| format_patch_and_issue(&event, status))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let max_width = lines
|
||||
.iter()
|
||||
.map(|s| s.split_once('\n').map_or(85, |(l, _)| l.chars().count()))
|
||||
.max()
|
||||
.unwrap_or(85)
|
||||
.max(67); // length of the event id
|
||||
|
||||
println!("{}", lines.join(&format!("{}\n", "-".repeat(max_width))));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision
|
||||
/// patch.
|
||||
fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<EventId>)> {
|
||||
if patch_event.is_revision_patch() {
|
||||
Ok((
|
||||
patch_event.root_patch_from_revision()?,
|
||||
Some(patch_event.id),
|
||||
))
|
||||
} else if patch_event.is_root_patch() {
|
||||
Ok((patch_event.id, None))
|
||||
} else {
|
||||
Err(N34Error::NotRootPatch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an event as either a patch or an issue. For patches, extracts the
|
||||
/// subject line from the Git patch format. For issues, combines the subject
|
||||
/// with labels. The output includes status and formatted ID.
|
||||
fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus>) -> String {
|
||||
let subject = if status.is_left() {
|
||||
GitPatch::from_str(&event.content)
|
||||
.map(|p| p.subject)
|
||||
.unwrap_or_else(|_| {
|
||||
event
|
||||
.content
|
||||
.lines()
|
||||
.find(|line| line.trim().starts_with("Subject: "))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_start_matches("Subject: ")
|
||||
.to_owned()
|
||||
})
|
||||
} else {
|
||||
let labels = event.extract_issue_labels();
|
||||
let subject = event.extract_issue_subject();
|
||||
|
||||
if labels.is_empty() {
|
||||
subject.to_owned()
|
||||
} else {
|
||||
format!(r#""{subject}" {labels}"#)
|
||||
}
|
||||
};
|
||||
format!(
|
||||
"({status}) {}\nID: {}\n",
|
||||
utils::smart_wrap(&subject, 85),
|
||||
event.id.to_bech32().expect("Infallible")
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user