feat: New patch subcommands apply,close,draft,merge and reopen to manage the patch status
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
77
src/cli/commands/patch/apply.rs
Normal file
77
src/cli/commands/patch/apply.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 bitcoin_hashes::Sha1;
|
||||
use clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ApplyArgs {
|
||||
/// 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>>,
|
||||
/// The open patch id to apply it. Must be orignal root patch or
|
||||
/// revision root
|
||||
patch_id: NostrEvent,
|
||||
/// The applied commits
|
||||
#[arg(num_args = 1..)]
|
||||
applied_commits: Vec<Sha1>,
|
||||
}
|
||||
|
||||
impl CommandRunner for ApplyArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_status_command(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::MergedApplied,
|
||||
Some(either::Either::Right(self.applied_commits)),
|
||||
|patch_status| {
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply an already applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply a drafted patch".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
66
src/cli/commands/patch/close.rs
Normal file
66
src/cli/commands/patch/close.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CloseArgs {
|
||||
/// 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>>,
|
||||
/// The open/drafted patch id to close it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for CloseArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_status_command(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Closed,
|
||||
None,
|
||||
|patch_status| {
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close an already closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
73
src/cli/commands/patch/draft.rs
Normal file
73
src/cli/commands/patch/draft.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DraftArgs {
|
||||
/// 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>>,
|
||||
/// The open patch id to draft it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for DraftArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_status_command(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Draft,
|
||||
None,
|
||||
|patch_status| {
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft an already drafted patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
77
src/cli/commands/patch/merge.rs
Normal file
77
src/cli/commands/patch/merge.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 bitcoin_hashes::Sha1;
|
||||
use clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct MergeArgs {
|
||||
/// 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>>,
|
||||
/// The open patch id to merge it. Must be orignal root patch or
|
||||
/// revision root
|
||||
patch_id: NostrEvent,
|
||||
/// The merge commit id
|
||||
merge_commit: Sha1,
|
||||
}
|
||||
|
||||
impl CommandRunner for MergeArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_status_command(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::MergedApplied,
|
||||
Some(either::Either::Left(self.merge_commit)),
|
||||
|patch_status| {
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge an already merged patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge a drafted patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,18 @@
|
||||
// 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>.
|
||||
|
||||
/// `patch apply` suubcommand
|
||||
mod apply;
|
||||
/// `patch close` subcommand
|
||||
mod close;
|
||||
/// `patch draft` subcommand
|
||||
mod draft;
|
||||
/// `patch fetch` subcommand
|
||||
mod fetch;
|
||||
/// `patch merge` subcommand
|
||||
mod merge;
|
||||
/// `patch reopen` subcommand
|
||||
mod reopen;
|
||||
/// `patch send` subcommand
|
||||
mod send;
|
||||
|
||||
@@ -26,14 +36,19 @@ use std::{
|
||||
};
|
||||
|
||||
use clap::Subcommand;
|
||||
use nostr::event::Kind;
|
||||
use regex::Regex;
|
||||
|
||||
use self::apply::ApplyArgs;
|
||||
use self::close::CloseArgs;
|
||||
use self::draft::DraftArgs;
|
||||
use self::fetch::FetchArgs;
|
||||
use self::merge::MergeArgs;
|
||||
use self::reopen::ReopenArgs;
|
||||
use self::send::SendArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
|
||||
/// Regular expression for extracting the patch subject.
|
||||
static SUBJECT_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?m)^Subject: (.*(?:\n .*)*)").unwrap());
|
||||
@@ -49,10 +64,20 @@ static PATCH_VERSION_NUMBER_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum PatchSubcommands {
|
||||
/// Send one or more patches to a repository
|
||||
/// Send one or more patches to a repository.
|
||||
Send(SendArgs),
|
||||
/// Fetches a patch by its id
|
||||
/// Fetches a patch by its id.
|
||||
Fetch(FetchArgs),
|
||||
/// Closes an open or drafted patch.
|
||||
Close(CloseArgs),
|
||||
/// Converts the closed or open patch to draft state.
|
||||
Draft(DraftArgs),
|
||||
/// Reopens a closed or drafted patch.
|
||||
Reopen(ReopenArgs),
|
||||
/// Set an open patch status to applied.
|
||||
Apply(ApplyArgs),
|
||||
/// Set an open patch status to merged.
|
||||
Merge(MergeArgs),
|
||||
}
|
||||
|
||||
/// Represents a git patch
|
||||
@@ -66,6 +91,64 @@ pub struct GitPatch {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PatchStatus {
|
||||
/// The patch is currently open
|
||||
Open,
|
||||
/// The patch has been merged/applied
|
||||
MergedApplied,
|
||||
/// The patch has been closed
|
||||
Closed,
|
||||
/// A patch that has been drafted but not yet applied.
|
||||
Draft,
|
||||
}
|
||||
|
||||
impl PatchStatus {
|
||||
/// Maps the issue status to its corresponding Nostr kind.
|
||||
pub fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Open => Kind::GitStatusOpen,
|
||||
Self::MergedApplied => Kind::GitStatusApplied,
|
||||
Self::Closed => Kind::GitStatusClosed,
|
||||
Self::Draft => Kind::GitStatusDraft,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the patch is open.
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, Self::Open)
|
||||
}
|
||||
|
||||
/// Check if the patch is merged/applied.
|
||||
pub fn is_merged_or_applied(&self) -> bool {
|
||||
matches!(self, Self::MergedApplied)
|
||||
}
|
||||
|
||||
/// Check if the patch is closed.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
matches!(self, Self::Closed)
|
||||
}
|
||||
|
||||
/// Check if the patch is drafted
|
||||
pub fn is_drafted(&self) -> bool {
|
||||
matches!(self, Self::Draft)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Kind> for PatchStatus {
|
||||
type Error = N34Error;
|
||||
|
||||
fn try_from(kind: Kind) -> Result<Self, Self::Error> {
|
||||
match kind {
|
||||
Kind::GitStatusOpen => Ok(Self::Open),
|
||||
Kind::GitStatusApplied => Ok(Self::MergedApplied),
|
||||
Kind::GitStatusClosed => Ok(Self::Closed),
|
||||
Kind::GitStatusDraft => Ok(Self::Draft),
|
||||
_ => Err(N34Error::InvalidPatchStatus(kind)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPatch {
|
||||
/// Returns the patch file name from the subject
|
||||
pub fn filename(&self, parent: impl AsRef<Path>) -> N34Result<PathBuf> {
|
||||
@@ -90,7 +173,7 @@ impl GitPatch {
|
||||
|
||||
impl CommandRunner for PatchSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & Send Fetch)
|
||||
crate::run_command!(self, options, & Send Fetch Close Reopen Draft Apply Merge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +253,6 @@ fn patch_file_name(subject: &str) -> N34Result<String> {
|
||||
.replace("--", "-"))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -202,7 +284,6 @@ index 4120f5a..e68783c 100644
|
||||
assert_eq!(patch.body, "Abc patch");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn patch_normal_with_patch_in_content() {
|
||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||
|
||||
67
src/cli/commands/patch/reopen.rs
Normal file
67
src/cli/commands/patch/reopen.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ReopenArgs {
|
||||
/// 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>>,
|
||||
/// The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for ReopenArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_status_command(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Open,
|
||||
None,
|
||||
|patch_status| {
|
||||
if patch_status.is_open() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't open an already open patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't open a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -14,27 +14,32 @@
|
||||
// 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 bitcoin_hashes::Sha1;
|
||||
use either::Either;
|
||||
use nostr::{
|
||||
event::{EventBuilder, Tag},
|
||||
event::{EventBuilder, Tag, TagKind},
|
||||
filter::Filter,
|
||||
nips::nip10::Marker,
|
||||
};
|
||||
|
||||
use super::{
|
||||
issue::IssueStatus,
|
||||
patch::PatchStatus,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
};
|
||||
use crate::{
|
||||
cli::CliOptions,
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::ReposUtils,
|
||||
nostr_utils::traits::{GitPatchUtils, ReposUtils},
|
||||
};
|
||||
use crate::{
|
||||
cli::types::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
};
|
||||
|
||||
/// Updates an issue's status to `new_status` after validating it with
|
||||
/// Updates the issue's status to `new_status` after validating it with
|
||||
/// `check_fn`.
|
||||
pub async fn issue_status_command(
|
||||
options: CliOptions,
|
||||
@@ -94,14 +99,16 @@ pub async fn issue_status_command(
|
||||
.build(user_pkey);
|
||||
|
||||
let event_id = status_event.id.expect("There is an id");
|
||||
let user_relays_list = client.user_relays_list(issue_event.pubkey).await?;
|
||||
let user_relays_list = client.user_relays_list(user_pkey).await?;
|
||||
let write_relays = [
|
||||
relays,
|
||||
naddrs.extract_relays(),
|
||||
repos.extract_relays(),
|
||||
utils::add_write_relays(user_relays_list.as_ref()),
|
||||
client.read_relays_from_user(issue_event.pubkey).await,
|
||||
client.read_relays_from_users(&maintainers).await,
|
||||
client
|
||||
.read_relays_from_users(&[maintainers, owners].concat())
|
||||
.await,
|
||||
]
|
||||
.concat();
|
||||
|
||||
@@ -113,3 +120,143 @@ pub async fn issue_status_command(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the patch's status to `new_status` after validating it with
|
||||
/// `check_fn`.
|
||||
pub async fn patch_status_command(
|
||||
options: CliOptions,
|
||||
patch_id: NostrEvent,
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
new_status: PatchStatus,
|
||||
merge_or_applied_commits: Option<Either<Sha1, Vec<Sha1>>>,
|
||||
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
|
||||
) -> N34Result<()> {
|
||||
let user_pkey = options.pubkey().await?;
|
||||
let 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(), patch_id.relays].concat())
|
||||
.await;
|
||||
|
||||
let owners = naddrs.extract_owners();
|
||||
let coordinates = naddrs.clone().into_coordinates();
|
||||
let repos = client.fetch_repos(&coordinates).await?;
|
||||
let maintainers = repos.extract_maintainers();
|
||||
let relay_hint = repos.extract_relays().first().cloned();
|
||||
client.add_relays(&repos.extract_relays()).await;
|
||||
|
||||
let patch_event = client.fetch_patch(patch_id.event_id).await?;
|
||||
|
||||
if patch_event.is_revision_patch() && !new_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
||||
'close', and 'draft' are not supported."
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
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 patch_status = client
|
||||
.fetch_patch_status(
|
||||
root_patch,
|
||||
root_revision,
|
||||
[maintainers.as_slice(), &[patch_event.pubkey], &owners].concat(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_fn(&patch_status)?;
|
||||
|
||||
let mut status_builder = EventBuilder::new(new_status.kind(), "")
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tag(utils::event_reply_tag(
|
||||
&root_patch,
|
||||
relay_hint.as_ref(),
|
||||
Marker::Root,
|
||||
))
|
||||
.tag(Tag::public_key(patch_event.pubkey))
|
||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(owners.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(
|
||||
coordinates
|
||||
.into_iter()
|
||||
.map(|c| Tag::coordinate(c, relay_hint.clone())),
|
||||
);
|
||||
|
||||
if new_status.is_merged_or_applied() {
|
||||
if let Some(merge_commit) = merge_or_applied_commits
|
||||
.as_ref()
|
||||
.and_then(|e| e.as_ref().left())
|
||||
{
|
||||
let commit = merge_commit.to_string();
|
||||
status_builder = status_builder
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("merge-commit"),
|
||||
iter::once(&commit),
|
||||
))
|
||||
.tag(Tag::reference(commit));
|
||||
} else if let Some(applied_commits) = merge_or_applied_commits.and_then(|e| e.right()) {
|
||||
let commits = applied_commits
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
status_builder = status_builder
|
||||
.tag(Tag::custom(TagKind::custom("applied-as-commits"), &commits))
|
||||
.tags(commits.into_iter().map(Tag::reference));
|
||||
};
|
||||
|
||||
let root = if let Some(root_revision) = root_revision {
|
||||
status_builder = status_builder.tag(utils::event_reply_tag(
|
||||
&root_revision,
|
||||
relay_hint.as_ref(),
|
||||
Marker::Reply,
|
||||
));
|
||||
root_revision
|
||||
} else {
|
||||
root_patch
|
||||
};
|
||||
let patches = client.fetch_patch_series(root, patch_event.pubkey).await?;
|
||||
status_builder = status_builder.tags(
|
||||
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 event_id = status_event.id.expect("There is an id");
|
||||
let user_relays_list = client.user_relays_list(user_pkey).await?;
|
||||
let write_relays = [
|
||||
relays,
|
||||
naddrs.extract_relays(),
|
||||
repos.extract_relays(),
|
||||
utils::add_write_relays(user_relays_list.as_ref()),
|
||||
client.read_relays_from_user(patch_event.pubkey).await,
|
||||
client
|
||||
.read_relays_from_users(&[maintainers, owners].concat())
|
||||
.await,
|
||||
]
|
||||
.concat();
|
||||
|
||||
let success = client
|
||||
.send_event_to(status_event, user_relays_list.as_ref(), &write_relays)
|
||||
.await?;
|
||||
let nevent = utils::new_nevent(event_id, &success)?;
|
||||
println!("Patch status created: {nevent}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{Event, EventId, Kind, Tag, TagStandard, Tags, UnsignedEvent},
|
||||
event::{Event, EventId, Kind, Tag, TagKind, TagStandard, Tags, UnsignedEvent},
|
||||
filter::Filter,
|
||||
key::PublicKey,
|
||||
nips::{
|
||||
@@ -39,7 +39,7 @@ use nostr_sdk::Client;
|
||||
use traits::TokenUtils;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, issue::IssueStatus},
|
||||
cli::{CliOptions, issue::IssueStatus, patch::PatchStatus},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
@@ -239,6 +239,13 @@ impl NostrClient {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch the patch by the given id. None if not found
|
||||
pub async fn fetch_patch(&self, patch_id: EventId) -> N34Result<Event> {
|
||||
self.fetch_event(Filter::new().id(patch_id).kind(Kind::GitPatch))
|
||||
.await?
|
||||
.ok_or(N34Error::CanNotFoundPatch)
|
||||
}
|
||||
|
||||
/// Returns the username for a given public key. If no username is found,
|
||||
/// falls back to a shortened version of the public key.
|
||||
pub async fn get_username(&self, user: PublicKey) -> String {
|
||||
@@ -279,6 +286,69 @@ impl NostrClient {
|
||||
.unwrap_or_else(|| Ok(IssueStatus::Open))
|
||||
}
|
||||
|
||||
/// Gets the status of a patch. If it's a revision patch, checks if it's
|
||||
/// closed when the root patch is already merged/applied but doesn't
|
||||
/// reference this revision. Defaults to Open status if no status event
|
||||
/// is found.
|
||||
pub async fn fetch_patch_status(
|
||||
&self,
|
||||
root_patch: EventId,
|
||||
root_revision: Option<EventId>,
|
||||
authorized_pubkeys: Vec<PublicKey>,
|
||||
) -> N34Result<PatchStatus> {
|
||||
let (root_status, event_tags) = self
|
||||
.fetch_events(
|
||||
Filter::new()
|
||||
.event(root_patch)
|
||||
.kinds([
|
||||
Kind::GitStatusOpen,
|
||||
Kind::GitStatusApplied,
|
||||
Kind::GitStatusClosed,
|
||||
Kind::GitStatusDraft,
|
||||
])
|
||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.max_by_key(|e| e.created_at)
|
||||
.map(|status| N34Result::Ok((PatchStatus::try_from(status.kind)?, status.tags)))
|
||||
.unwrap_or_else(|| Ok((PatchStatus::Open, Tags::new())))?;
|
||||
|
||||
if let Some(revision_id) = root_revision {
|
||||
if root_status.is_merged_or_applied()
|
||||
&& !event_tags
|
||||
.filter(TagKind::e())
|
||||
.any(|t| t.is_reply() && t.content().is_some_and(|c| c == revision_id.to_hex()))
|
||||
{
|
||||
return Ok(PatchStatus::Closed);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(root_status)
|
||||
}
|
||||
|
||||
pub async fn fetch_patch_series(
|
||||
&self,
|
||||
root_patch_id: EventId,
|
||||
root_patch_author: PublicKey,
|
||||
) -> N34Result<Vec<Event>> {
|
||||
Ok(self
|
||||
.fetch_events(
|
||||
Filter::new()
|
||||
.kind(Kind::GitPatch)
|
||||
.author(root_patch_author)
|
||||
.event(root_patch_id),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
e.tags.iter().any(|t| {
|
||||
t.is_root() && t.content().is_some_and(|c| c == root_patch_id.to_hex())
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Finds the root issue or patch for a given event. If the event is already
|
||||
/// a root (issue/patch), returns it directly. For comments, follows
|
||||
/// parent/root references until finding the root or failing. Returns
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use nostr::{
|
||||
event::{EventBuilder, EventId, Tag, TagKind, TagStandard, Tags},
|
||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, Tags},
|
||||
key::PublicKey,
|
||||
nips::{
|
||||
nip01::Coordinate,
|
||||
@@ -216,4 +216,43 @@ impl Vec<GitRepositoryAnnouncement> {
|
||||
self.iter().flat_map(|r| r.maintainers.clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility functions for working with patch events
|
||||
#[easy_ext::ext(GitPatchUtils)]
|
||||
impl Event {
|
||||
/// Returns whether the patch is a root or not
|
||||
pub fn is_root_patch(&self) -> bool {
|
||||
self.kind == Kind::GitPatch
|
||||
&& self
|
||||
.tags
|
||||
.filter(TagKind::t())
|
||||
.any(|t| t.content() == Some("root"))
|
||||
}
|
||||
|
||||
/// Returns whether the patch is patch-revision or not
|
||||
pub fn is_revision_patch(&self) -> bool {
|
||||
self.kind == Kind::GitPatch
|
||||
&& self
|
||||
.tags
|
||||
.filter(TagKind::t())
|
||||
.any(|t| t.content() == Some("root-revision"))
|
||||
}
|
||||
|
||||
/// Gets the root patch ID from a patch-revision event by finding the `e`
|
||||
/// tag that replies to it. Fails if no such tag is found or if the tag
|
||||
/// contains an invalid event ID.
|
||||
pub fn root_patch_from_revision(&self) -> N34Result<EventId> {
|
||||
self.tags
|
||||
.iter()
|
||||
.find(|tag| tag.is_reply())
|
||||
.ok_or_else(|| {
|
||||
N34Error::InvalidEvent(
|
||||
"A patch revision without `e`-reply to the root patch".to_owned(),
|
||||
)
|
||||
})?
|
||||
.content()
|
||||
.ok_or_else(|| N34Error::InvalidEvent("`e` tag without an event".to_owned()))?
|
||||
.parse()
|
||||
.map_err(|err| N34Error::InvalidEvent(format!("Invalid event ID in `e` tag: {err}")))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user