feat: New patch list commands to list the repo patches

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-07-02 21:53:52 +00:00
parent 5c5987e07b
commit 387dd32a37
5 changed files with 252 additions and 20 deletions

View File

@@ -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")
)
}