feat: support pull requests
Support for pull requests, it's not standard in NIPs yet[1] but there must be two clients that support it to be included in NIPs, n34 is one of them. This support: - Create a new PR `n34 pr new` - Update PR tip `n34 pr update` - View a PR `n34 pr view` - List PRs `n34 pr list` - Add `PR` and `PR-update` kinds as roots in `NostrClient::find_root` - Support issuing statuses for PRs - Support GRASP PR and PR-update - Update the docs [1] https://github.com/nostr-protocol/nips/pull/1966
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New command `repo state` - by Awiteb
|
- New command `repo state` - by Awiteb
|
||||||
|
- Support pull requests - by Awiteb
|
||||||
|
|
||||||
### Breaking Change
|
### Breaking Change
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Check the documentation at [n34.dev]
|
|||||||
- [X] Issues (Send, view and list)
|
- [X] Issues (Send, view and list)
|
||||||
- [X] Replies
|
- [X] Replies
|
||||||
- [X] Issues and patches status
|
- [X] Issues and patches status
|
||||||
- [ ] Pull requests ([nostr-protocol/nips#1966])
|
- [X] Pull requests ([nostr-protocol/nips#1966])
|
||||||
- [X] Gossip Model ([NIP-65])
|
- [X] Gossip Model ([NIP-65])
|
||||||
- [X] Proof of Work ([NIP-13])
|
- [X] Proof of Work ([NIP-13])
|
||||||
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
- [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)
|
- [Repository State Announcements](repo/state.md)
|
||||||
|
- [Reply to Issues and Patches](reply.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)
|
||||||
@@ -32,4 +33,13 @@
|
|||||||
- [Apply an Open Patch](patch/apply.md)
|
- [Apply an Open Patch](patch/apply.md)
|
||||||
- [Merge an Open Patch](patch/merge.md)
|
- [Merge an Open Patch](patch/merge.md)
|
||||||
- [List Repositories Patches](patch/list.md)
|
- [List Repositories Patches](patch/list.md)
|
||||||
- [Reply to Issues and Patches](reply.md)
|
- [Pull Request Management](pr/README.md)
|
||||||
|
- [Create a Pull Request](pr/new.md)
|
||||||
|
- [Update a Pull Request](pr/update.md)
|
||||||
|
- [View a Pull Request](pr/view.md)
|
||||||
|
- [List Pull Requests](pr/list.md)
|
||||||
|
- [Close a Pull Request](pr/close.md)
|
||||||
|
- [Convert to Draft](pr/draft.md)
|
||||||
|
- [Reopen a Pull Request](pr/reopen.md)
|
||||||
|
- [Mark as Applied](pr/apply.md)
|
||||||
|
- [Merge a Pull Request](pr/merge.md)
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# Command-Line Usage
|
# Command-Line Usage
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
`n34` emphasizes simplicity by exclusively handling Nostr tasks, requiring
|
||||||
|
users to manage Git operations themselves. For instance, it won't merge or
|
||||||
|
rebase patches into a branch but will prompt you to specify where to write them,
|
||||||
|
allowing you to merge them using `git-am`. Similarly, when sending patches, it
|
||||||
|
won't ask for the commits but will request the patch files instead.
|
||||||
|
|
||||||
|
This approach enhances simplicity and freedom, giving you full control over
|
||||||
|
your workflow. Additionally, it simplifies software maintenance by avoiding
|
||||||
|
unnecessary complexity.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
The `n34` command-line tool accepts the following options:
|
The `n34` command-line tool accepts the following options:
|
||||||
|
|||||||
@@ -375,8 +375,8 @@
|
|||||||
<span class="feature-text">Issues and patches status</span>
|
<span class="feature-text">Issues and patches status</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check pending"></span>
|
<span class="feature-check completed"></span>
|
||||||
<span class="feature-text pending">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
<span class="feature-text">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Usage: n34 patch apply [OPTIONS] <PATCH_ID> [APPLIED_COMMITS]...
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
||||||
[APPLIED_COMMITS]... The applied commits
|
<APPLIED_COMMITS>... The applied commits
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ Options:
|
|||||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
```
|
```
|
||||||
|
|
||||||
Issue a kind `1632` (Close status) for the specified patch. The patch have to
|
Issue a kind `1630` (Open status) for the specified patch. The patch have to
|
||||||
be closed or drafted.
|
be closed or drafted.
|
||||||
|
|
||||||
|
|||||||
43
docs/pr/README.md
Normal file
43
docs/pr/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Pull Request Management
|
||||||
|
|
||||||
|
Pull requests are a new addition to [NIP-34], proposed by [DanConwayDev] in
|
||||||
|
[nostr-protocol/nips#1966]. The concept is straightforward: it involves an event
|
||||||
|
that notifies you of changes on a Git server, which can be any Git server. This
|
||||||
|
event contains two key pieces of information, the Git repository URL and the
|
||||||
|
commit tip. You can add this remote to your project, check out the specified
|
||||||
|
commit, and then review or merge/rebase the changes as needed. Additionally,
|
||||||
|
there is a `PR-update` event that informs you when the previous commit tip is no
|
||||||
|
longer the head of the changes, such as when new changes are added or a rebase
|
||||||
|
occurs.
|
||||||
|
|
||||||
|
## Why Use the Commit Tip?
|
||||||
|
|
||||||
|
Using the commit tip is preferable to relying on a branch name. A branch name
|
||||||
|
simply points to a commit, so checking out a branch is effectively the same
|
||||||
|
as checking out a specific commit. However, if we use the branch name instead
|
||||||
|
of the commit, the changes can be altered by anyone, not just the pull request
|
||||||
|
author. By checking out a specific commit ID, we ensure that the changes are
|
||||||
|
exactly as intended by the pull request author and have not been tampered with
|
||||||
|
by a malicious party.
|
||||||
|
|
||||||
|
## Does `n34` Handle Git Operations?
|
||||||
|
|
||||||
|
No, `n34` does not perform any Git operations. You can use the `n34 pr view`
|
||||||
|
command to retrieve the pull request clone URL and the commit tip, allowing
|
||||||
|
you to fetch the changes manually. Similarly, `n34 pr new` and `n34 pr update`
|
||||||
|
do not interact with Git directly. They only prompt you for the clone URL and
|
||||||
|
commit tip to construct the event. For more details on the philosophy behind
|
||||||
|
`n34`, refer to the [Philosophy] section.
|
||||||
|
|
||||||
|
## Why use pull requests instead of patches?
|
||||||
|
|
||||||
|
Pull requests are useful for large changes. Most relays limit events to 60KB,
|
||||||
|
so if your changes exceed that limit (per event, not in total), consider using
|
||||||
|
pull requests. For [GRASP] servers, the limit is usually higher. Before using pull
|
||||||
|
requests, check the relays, patches are more decentralized.
|
||||||
|
|
||||||
|
[NIP-34]: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||||
|
[DanConwayDev]: https://danconwaydev.com
|
||||||
|
[nostr-protocol/nips#1966]: https://github.com/nostr-protocol/nips/pull/1966
|
||||||
|
[Philosophy]: https://n34.dev/commands.html#philosophy
|
||||||
|
[GRASP]: https://ngit.dev/grasp
|
||||||
24
docs/pr/apply.md
Normal file
24
docs/pr/apply.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Mark as Applied
|
||||||
|
|
||||||
|
> `n34 pr apply` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr apply [OPTIONS] <PR_ID> <APPLIED_COMMITS>...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PR_ID> The open PR id to apply it
|
||||||
|
<APPLIED_COMMITS>... The applied commits
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a kind `1631` event (Applied/Merged status) for the specified PR. The PR
|
||||||
|
must be in open status.
|
||||||
|
|
||||||
|
The `APPLIED_COMMITS` field serves to inform clients about the status of
|
||||||
|
specific commits, whether they have been applied or not. If you need to retrieve
|
||||||
|
the list of commits from a specific point (such as the tip of the master branch)
|
||||||
|
up to the `HEAD`, you can use the following Git command: `git log --pretty=%H
|
||||||
|
'origin/master..HEAD'`.
|
||||||
18
docs/pr/close.md
Normal file
18
docs/pr/close.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Close a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr close` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr close [OPTIONS] <PR_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PR_ID> The open/draft PR id to close it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1632` (Close status) for the specified PR. The PR have to be open
|
||||||
|
or drafted.
|
||||||
|
|
||||||
17
docs/pr/draft.md
Normal file
17
docs/pr/draft.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Convert to Draft
|
||||||
|
|
||||||
|
> `n34 pr draft` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr draft [OPTIONS] <PR_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PR_ID> The open PR id to draft it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1633` (Draft status) for the specified PR. The PR have to
|
||||||
|
be open.
|
||||||
17
docs/pr/list.md
Normal file
17
docs/pr/list.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# List Pull Requests
|
||||||
|
|
||||||
|
> `n34 pr list` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--limit <LIMIT> Maximum number of patches to list [default: 15]
|
||||||
|
```
|
||||||
|
|
||||||
|
List the repositories pull requests. By default `n34` will look for
|
||||||
|
`nostr-address` file and extract the repositories from it.
|
||||||
22
docs/pr/merge.md
Normal file
22
docs/pr/merge.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Merge a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr merge` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr merge [OPTIONS] <PR_ID> <MERGE_COMMIT>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PR_ID> The open PR id to merge it
|
||||||
|
<MERGE_COMMIT> The merge commit id
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a kind `1631` event (Applied/Merged status) for the specified PR. The
|
||||||
|
PR must be in open status.
|
||||||
|
|
||||||
|
You can get the `MERGE_COMMIT` commit using `git rev-parse HEAD` command if
|
||||||
|
the merge commit in the `HEAD` or use `HEAD~n` where the `n` is the number of
|
||||||
|
commits the merge commit before the HEAD
|
||||||
30
docs/pr/new.md
Normal file
30
docs/pr/new.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Create a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr new` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr new [OPTIONS] <--subject <SUBJECT>|--editor> <COMMIT> <CLONES>...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<COMMIT> The SHA-1 hash of the commit at the tip of the PR branch
|
||||||
|
<CLONES>... Repositories to clone for the pull request, separated by commas
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
--body <BODY> The body content of the pull request. Cannot be used together with the `--editor` flag
|
||||||
|
--subject <SUBJECT> The subject or title of the pull request. Cannot be used together with the `--editor` flag
|
||||||
|
-e, --editor Opens the user's default editor to write PR subject and body
|
||||||
|
--labels <LABELS> Labels to associate with the pull request, separated by commas
|
||||||
|
--branch <BRANCH> The branch name for the pull request
|
||||||
|
--grasp Push the pull request to the repository GRASP server
|
||||||
|
```
|
||||||
|
|
||||||
|
Submit a pull request to the repositories specified using the `--repo` option or
|
||||||
|
obtained from the `nostr-address` file.
|
||||||
|
|
||||||
|
Utilize the `--grasp` option if you intend to send the pull request to the
|
||||||
|
[GRASP] servers. Note that `n34` will not automatically send the pull request
|
||||||
|
for you but will indicate where to push your changes.
|
||||||
|
|
||||||
|
[GRASP]: https://ngit.dev/grasp
|
||||||
17
docs/pr/reopen.md
Normal file
17
docs/pr/reopen.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Reopen a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr reopen` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr reopen [OPTIONS] <PR_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PR_ID> The closed/drafted patch id to reopen it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1630` (Open status) for the specified PR. The PR have to
|
||||||
|
be closed or drafted.
|
||||||
25
docs/pr/update.md
Normal file
25
docs/pr/update.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Update a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr update` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr update [OPTIONS] <EVENT-ID> <COMMIT> <CLONES>...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<EVENT-ID> Original PR ID
|
||||||
|
<COMMIT> The SHA-1 hash of the commit at the tip of the PR branch
|
||||||
|
<CLONES>... Repositories to clone for the pull request, separated by commas
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
--grasp Push the pull request update to the repository GRASP server
|
||||||
|
```
|
||||||
|
|
||||||
|
Update an existing pull request with the new changes.
|
||||||
|
|
||||||
|
Utilize the `--grasp` option if you intend to send the pull request to the
|
||||||
|
[GRASP] servers. Note that `n34` will not automatically send the pull request
|
||||||
|
for you but will indicate where to push your changes.
|
||||||
|
|
||||||
|
[GRASP]: https://ngit.dev/grasp
|
||||||
18
docs/pr/view.md
Normal file
18
docs/pr/view.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# View a Pull Request
|
||||||
|
|
||||||
|
> `n34 pr view` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Usage: n34 pr view [OPTIONS] <EVENT-ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<EVENT-ID> Pull request ID
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
View a specific pull request. This includes the pull request title, labels,
|
||||||
|
description, clone URLs, and the latest commit tip. Use this information to
|
||||||
|
fetch the changes.
|
||||||
@@ -19,7 +19,12 @@ use std::num::NonZeroUsize;
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
common_commands,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +47,11 @@ impl CommandRunner for ListArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
common_commands::list_patches_and_issues(options, self.naddrs, false, self.limit.into())
|
common_commands::list_pr_patches_and_issues::<{ EntityType::Issue as u8 }>(
|
||||||
.await
|
options,
|
||||||
|
self.naddrs,
|
||||||
|
self.limit.into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub mod config;
|
|||||||
pub mod issue;
|
pub mod issue;
|
||||||
/// `patch` subcommands
|
/// `patch` subcommands
|
||||||
pub mod patch;
|
pub mod patch;
|
||||||
|
/// `pr` subcommands
|
||||||
|
pub mod pr;
|
||||||
/// `reply` command
|
/// `reply` command
|
||||||
pub mod reply;
|
pub mod reply;
|
||||||
/// `repo` subcommands
|
/// `repo` subcommands
|
||||||
@@ -40,6 +42,7 @@ use nostr_connect::client::NostrConnect;
|
|||||||
use self::config::ConfigSubcommands;
|
use self::config::ConfigSubcommands;
|
||||||
use self::issue::IssueSubcommands;
|
use self::issue::IssueSubcommands;
|
||||||
use self::patch::PatchSubcommands;
|
use self::patch::PatchSubcommands;
|
||||||
|
use self::pr::PrSubcommands;
|
||||||
use self::reply::ReplyArgs;
|
use self::reply::ReplyArgs;
|
||||||
use self::repo::RepoSubcommands;
|
use self::repo::RepoSubcommands;
|
||||||
use self::sets::SetsSubcommands;
|
use self::sets::SetsSubcommands;
|
||||||
@@ -113,6 +116,11 @@ pub enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
subcommands: PatchSubcommands,
|
subcommands: PatchSubcommands,
|
||||||
},
|
},
|
||||||
|
/// Manage pull requests
|
||||||
|
Pr {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: PrSubcommands,
|
||||||
|
},
|
||||||
/// Manage configuration
|
/// Manage configuration
|
||||||
Config {
|
Config {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -197,6 +205,6 @@ impl CommandRunner for Commands {
|
|||||||
tracing::trace!("Options: {options:#?}");
|
tracing::trace!("Options: {options:#?}");
|
||||||
tracing::trace!("Handling: {self:#?}");
|
tracing::trace!("Handling: {self:#?}");
|
||||||
|
|
||||||
crate::run_command!(self, options, Repo Issue Sets Patch Config & Reply)
|
crate::run_command!(self, options, Repo Issue Sets Patch Pr Config & Reply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::{CommandRunner, VecNostrEventExt},
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
@@ -55,7 +55,7 @@ pub struct ApplyArgs {
|
|||||||
|
|
||||||
impl CommandRunner for ApplyArgs {
|
impl CommandRunner for ApplyArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
@@ -76,9 +76,7 @@ impl CommandRunner for ApplyArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus("Cannot apply a draft".to_owned()));
|
||||||
"You can't apply a drafted patch".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ pub struct CloseArgs {
|
|||||||
|
|
||||||
impl CommandRunner for CloseArgs {
|
impl CommandRunner for CloseArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ pub struct DraftArgs {
|
|||||||
|
|
||||||
impl CommandRunner for DraftArgs {
|
impl CommandRunner for DraftArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
@@ -56,7 +56,7 @@ impl CommandRunner for DraftArgs {
|
|||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"You can't draft an already drafted patch".to_owned(),
|
"You can't draft an already draft patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ use std::num::NonZeroUsize;
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
common_commands,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +47,11 @@ impl CommandRunner for ListArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())
|
common_commands::list_pr_patches_and_issues::<{ EntityType::Patch as u8 }>(
|
||||||
.await
|
options,
|
||||||
|
self.naddrs,
|
||||||
|
self.limit.into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::{CommandRunner, VecNostrEventExt},
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ pub struct MergeArgs {
|
|||||||
|
|
||||||
impl CommandRunner for MergeArgs {
|
impl CommandRunner for MergeArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
@@ -64,7 +64,7 @@ impl CommandRunner for MergeArgs {
|
|||||||
|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(
|
||||||
"You can't merge an already merged patch".to_owned(),
|
"You can't merge an already merged/applied patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ impl CommandRunner for MergeArgs {
|
|||||||
|
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"You can't merge a drafted patch".to_owned(),
|
"You can't merge a draft patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ pub struct ReopenArgs {
|
|||||||
|
|
||||||
impl CommandRunner for ReopenArgs {
|
impl CommandRunner for ReopenArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
|
|||||||
83
src/cli/commands/pr/apply.rs
Normal file
83
src/cli/commands/pr/apply.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// 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 nostr::hashes::sha1::Hash as Sha1Hash;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
patch::PatchPrStatus,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct ApplyArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The open PR id to apply it.
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
/// The applied commits.
|
||||||
|
#[arg(required = true)]
|
||||||
|
applied_commits: Vec<Sha1Hash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ApplyArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.pr_id,
|
||||||
|
self.naddrs,
|
||||||
|
PatchPrStatus::MergedApplied,
|
||||||
|
Some(either::Either::Right(self.applied_commits)),
|
||||||
|
Vec::new(),
|
||||||
|
|pr_status| {
|
||||||
|
if pr_status.is_merged_or_applied() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't apply an already applied pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_closed() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't apply a closed pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_drafted() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"Cannot apply a draft pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/cli/commands/pr/close.rs
Normal file
73
src/cli/commands/pr/close.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 crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
patch::PatchPrStatus,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct CloseArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The open/draft PR id to close it.
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for CloseArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.pr_id,
|
||||||
|
self.naddrs,
|
||||||
|
PatchPrStatus::Closed,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
|pr_status| {
|
||||||
|
if pr_status.is_closed() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't close an already closed pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_merged_or_applied() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't close a merged/applied pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/cli/commands/pr/draft.rs
Normal file
80
src/cli/commands/pr/draft.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// 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 crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
patch::PatchPrStatus,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct DraftArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The open PR id to draft it.
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for DraftArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.pr_id,
|
||||||
|
self.naddrs,
|
||||||
|
PatchPrStatus::Draft,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
|pr_status| {
|
||||||
|
if pr_status.is_drafted() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't draft an already draft pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_closed() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't draft a closed pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_merged_or_applied() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't draft a merged/applied pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/cli/commands/pr/list.rs
Normal file
58
src/cli/commands/pr/list.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// 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::{EntityType, NaddrOrSet},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct ListArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", value_delimiter = ',')]
|
||||||
|
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_pr_patches_and_issues::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.naddrs,
|
||||||
|
self.limit.into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/cli/commands/pr/merge.rs
Normal file
83
src/cli/commands/pr/merge.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// 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 nostr::hashes::sha1::Hash as Sha1Hash;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
patch::PatchPrStatus,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct MergeArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The open PR id to merge it.
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
/// The merge commit id
|
||||||
|
merge_commit: Sha1Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for MergeArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.pr_id,
|
||||||
|
self.naddrs,
|
||||||
|
PatchPrStatus::MergedApplied,
|
||||||
|
Some(either::Either::Left(self.merge_commit)),
|
||||||
|
Vec::new(),
|
||||||
|
|pr_status| {
|
||||||
|
if pr_status.is_merged_or_applied() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't merge an already merged/applied pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_closed() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't merge a closed pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_drafted() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't merge a draft pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/cli/commands/pr/mod.rs
Normal file
84
src/cli/commands/pr/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// 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>.
|
||||||
|
|
||||||
|
/// `pr apply` suubcommand
|
||||||
|
mod apply;
|
||||||
|
/// `pr close` subcommand
|
||||||
|
mod close;
|
||||||
|
/// `pr draft` subcommand
|
||||||
|
mod draft;
|
||||||
|
/// `pr list` subcommand
|
||||||
|
mod list;
|
||||||
|
/// `pr merge` subcommand
|
||||||
|
mod merge;
|
||||||
|
/// `pr new` subcommand
|
||||||
|
mod new;
|
||||||
|
/// `pr reopen` subcommand
|
||||||
|
mod reopen;
|
||||||
|
/// `pr update` subcommand
|
||||||
|
mod update;
|
||||||
|
/// `pr view` subcommand
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use self::apply::ApplyArgs;
|
||||||
|
use self::close::CloseArgs;
|
||||||
|
use self::draft::DraftArgs;
|
||||||
|
use self::list::ListArgs;
|
||||||
|
use self::merge::MergeArgs;
|
||||||
|
use self::new::NewArgs;
|
||||||
|
use self::reopen::ReopenArgs;
|
||||||
|
use self::update::UpdateArgs;
|
||||||
|
use self::view::ViewArgs;
|
||||||
|
use crate::{
|
||||||
|
cli::{CliOptions, traits::CommandRunner},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The kind of the pull request
|
||||||
|
pub const PR_KIND: nostr::event::Kind = nostr::event::Kind::Custom(1618);
|
||||||
|
/// The kind of the pull request update
|
||||||
|
pub const PR_UPDATE_KIND: nostr::event::Kind = nostr::event::Kind::Custom(1619);
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum PrSubcommands {
|
||||||
|
/// Create a pull request.
|
||||||
|
New(NewArgs),
|
||||||
|
/// Update a pull request.
|
||||||
|
Update(UpdateArgs),
|
||||||
|
/// View a pull request.
|
||||||
|
View(ViewArgs),
|
||||||
|
/// List pull requests.
|
||||||
|
List(ListArgs),
|
||||||
|
/// Close a pull request.
|
||||||
|
Close(CloseArgs),
|
||||||
|
/// Convert to draft.
|
||||||
|
Draft(DraftArgs),
|
||||||
|
/// Reopen pull request.
|
||||||
|
Reopen(ReopenArgs),
|
||||||
|
/// Mark as applied.
|
||||||
|
Apply(ApplyArgs),
|
||||||
|
/// Merge a pull request.
|
||||||
|
Merge(MergeArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for PrSubcommands {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::run_command!(self, options, & New Update View List Close Draft Reopen Apply Merge)
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/cli/commands/pr/new.rs
Normal file
194
src/cli/commands/pr/new.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// 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::iter;
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Args};
|
||||||
|
use nostr::{
|
||||||
|
event::{EventBuilder, Tag, TagKind, TagStandard},
|
||||||
|
filter::Alphabet,
|
||||||
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::NaddrOrSet,
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
nostr_utils::{
|
||||||
|
NostrClient,
|
||||||
|
traits::{NaddrsUtils, ReposUtils},
|
||||||
|
utils,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("pr-subject")
|
||||||
|
.required(true)
|
||||||
|
),
|
||||||
|
group(
|
||||||
|
ArgGroup::new("clone-or-grasp")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct NewArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The body content of the pull request. Cannot be used together with the
|
||||||
|
/// `--editor` flag.
|
||||||
|
#[arg(long, group = "pr-body")]
|
||||||
|
body: Option<String>,
|
||||||
|
/// The subject or title of the pull request. Cannot be used together with
|
||||||
|
/// the `--editor` flag.
|
||||||
|
#[arg(long, group = "pr-subject")]
|
||||||
|
subject: Option<String>,
|
||||||
|
/// Opens the user's default editor to write PR subject and body.
|
||||||
|
///
|
||||||
|
/// The first line will be used as the issue subject.
|
||||||
|
#[arg(short, long, group = "pr-subject", group = "pr-body")]
|
||||||
|
editor: bool,
|
||||||
|
/// Labels to associate with the pull request, separated by commas.
|
||||||
|
#[arg(long, value_delimiter = ',')]
|
||||||
|
labels: Vec<String>,
|
||||||
|
/// The branch name for the pull request.
|
||||||
|
#[arg(long)]
|
||||||
|
branch: Option<String>,
|
||||||
|
/// Push the pull request to the repository GRASP server.
|
||||||
|
#[arg(long, group = "clone-or-grasp")]
|
||||||
|
grasp: bool,
|
||||||
|
/// The SHA-1 hash of the commit at the tip of the PR branch.
|
||||||
|
///
|
||||||
|
/// You can get it using `git rev-parse <branch-name>`
|
||||||
|
commit: Sha1Hash,
|
||||||
|
/// Repositories to clone for the pull request, separated by commas.
|
||||||
|
#[arg(value_delimiter = ',', group = "clone-or-grasp")]
|
||||||
|
clones: Vec<nostr::Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for NewArgs {
|
||||||
|
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 naddrs_relays = naddrs.extract_relays();
|
||||||
|
client.add_relays(&naddrs_relays).await;
|
||||||
|
let coordinates = naddrs.into_coordinates();
|
||||||
|
let repos = client.fetch_repos(coordinates.as_slice()).await?;
|
||||||
|
let maintainers = repos.extract_maintainers();
|
||||||
|
let repos_relays = repos.extract_relays();
|
||||||
|
client.add_relays(&repos_relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
|
client
|
||||||
|
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (subject, body) = utils::subject_and_body(self.subject, self.body, ".md")?;
|
||||||
|
let body_details = if let Some(body) = &body {
|
||||||
|
Some(client.parse_content(body).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut event_builder = EventBuilder::new(super::PR_KIND, body.unwrap_or_default())
|
||||||
|
.dedup_tags()
|
||||||
|
.pow(options.pow.unwrap_or_default())
|
||||||
|
.tags(
|
||||||
|
coordinates
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| Tag::coordinate(c, repos_relays.first().cloned())),
|
||||||
|
)
|
||||||
|
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||||
|
.tags(
|
||||||
|
body_details
|
||||||
|
.clone()
|
||||||
|
.map(|c| c.into_tags())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.tag(Tag::from_standardized_without_cell(TagStandard::Subject(
|
||||||
|
subject,
|
||||||
|
)))
|
||||||
|
.tags(self.labels.into_iter().map(Tag::hashtag))
|
||||||
|
.tag(Tag::custom(
|
||||||
|
TagKind::single_letter(Alphabet::C, false),
|
||||||
|
iter::once(self.commit.to_string()),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(euc) = repos.extract_euc() {
|
||||||
|
event_builder = event_builder.tag(Tag::reference(euc.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(branch) = self.branch {
|
||||||
|
event_builder = event_builder.tag(Tag::custom(
|
||||||
|
TagKind::custom("branch-name"),
|
||||||
|
iter::once(branch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = if self.grasp {
|
||||||
|
utils::build_grasp_event(&repos, user_pubk, event_builder.clone())?
|
||||||
|
} else {
|
||||||
|
// Since `grasp` is false, `clones` must be provided
|
||||||
|
event_builder = event_builder.tag(Tag::custom(
|
||||||
|
TagKind::custom("clone"),
|
||||||
|
self.clones.iter().map(ToString::to_string),
|
||||||
|
));
|
||||||
|
event_builder.build(user_pubk)
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_id = event.id.expect("There is an id");
|
||||||
|
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
repos_relays,
|
||||||
|
naddrs_relays,
|
||||||
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
|
// Include read relays for each maintainer (if found)
|
||||||
|
client.read_relays_from_users(&maintainers).await,
|
||||||
|
body_details
|
||||||
|
.map(|c| c.write_relays.into_iter().collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||||
|
let success = client
|
||||||
|
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let nevent = utils::new_nevent(event_id, &success)?;
|
||||||
|
println!("Pull request created: {nevent}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/cli/commands/pr/reopen.rs
Normal file
74
src/cli/commands/pr/reopen.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// 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 crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
patch::PatchPrStatus,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct ReopenArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The closed/drafted patch id to reopen it.
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ReopenArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::PullRequest as u8 }>(
|
||||||
|
options,
|
||||||
|
self.pr_id,
|
||||||
|
self.naddrs,
|
||||||
|
PatchPrStatus::Open,
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
|pr_status| {
|
||||||
|
if pr_status.is_open() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't open an already open pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr_status.is_merged_or_applied() {
|
||||||
|
return Err(N34Error::InvalidStatus(
|
||||||
|
"You can't open a merged/applied pull request".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/cli/commands/pr/update.rs
Normal file
178
src/cli/commands/pr/update.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 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::iter;
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Args};
|
||||||
|
use nostr::{
|
||||||
|
event::{EventBuilder, Tag, TagKind, TagStandard, Tags},
|
||||||
|
filter::{Alphabet, Filter},
|
||||||
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::{NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::{
|
||||||
|
NostrClient,
|
||||||
|
traits::{NaddrsUtils, ReposUtils},
|
||||||
|
utils,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("clone-or-grasp")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct UpdateArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// Original PR ID
|
||||||
|
#[arg(value_name = "EVENT-ID")]
|
||||||
|
original_pr: NostrEvent,
|
||||||
|
/// Push the pull request update to the repository GRASP server.
|
||||||
|
#[arg(long, group = "clone-or-grasp")]
|
||||||
|
grasp: bool,
|
||||||
|
/// The SHA-1 hash of the commit at the tip of the PR branch.
|
||||||
|
///
|
||||||
|
/// You can get it using `git rev-parse <branch-name>`
|
||||||
|
commit: Sha1Hash,
|
||||||
|
/// Repositories to clone for the pull request, separated by commas.
|
||||||
|
#[arg(value_delimiter = ',', group = "clone-or-grasp")]
|
||||||
|
clones: Vec<nostr::Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for UpdateArgs {
|
||||||
|
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 naddrs_relays = naddrs.extract_relays();
|
||||||
|
client.add_relays(&naddrs_relays).await;
|
||||||
|
let coordinates = naddrs.into_coordinates();
|
||||||
|
let repos = client.fetch_repos(coordinates.as_slice()).await?;
|
||||||
|
let maintainers = repos.extract_maintainers();
|
||||||
|
let repos_relays = repos.extract_relays();
|
||||||
|
client.add_relays(&repos_relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
|
client
|
||||||
|
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let Some(orignal_pr) = client
|
||||||
|
.fetch_event(
|
||||||
|
Filter::new()
|
||||||
|
.id(self.original_pr.event_id)
|
||||||
|
.kind(super::PR_KIND),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
tracing::error!("Can't find the original pull request");
|
||||||
|
return Err(N34Error::EventNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Use `CommentTarget` to mention the orignal PR
|
||||||
|
let mut nip22_orignal_pr = Tags::new();
|
||||||
|
nip22_orignal_pr.push(Tag::from_standardized_without_cell(TagStandard::Event {
|
||||||
|
event_id: orignal_pr.id,
|
||||||
|
relay_url: None,
|
||||||
|
marker: None,
|
||||||
|
public_key: Some(orignal_pr.pubkey),
|
||||||
|
uppercase: true,
|
||||||
|
}));
|
||||||
|
nip22_orignal_pr.push(Tag::from_standardized_without_cell(
|
||||||
|
TagStandard::PublicKey {
|
||||||
|
public_key: orignal_pr.pubkey,
|
||||||
|
relay_url: None,
|
||||||
|
alias: None,
|
||||||
|
uppercase: true,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
nip22_orignal_pr.push(Tag::from_standardized_without_cell(TagStandard::Kind {
|
||||||
|
kind: orignal_pr.kind,
|
||||||
|
uppercase: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut event_builder = EventBuilder::new(super::PR_UPDATE_KIND, "")
|
||||||
|
.pow(options.pow.unwrap_or_default())
|
||||||
|
.tags(nip22_orignal_pr)
|
||||||
|
.tags(
|
||||||
|
coordinates
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| Tag::coordinate(c, repos_relays.first().cloned())),
|
||||||
|
)
|
||||||
|
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||||
|
.tag(Tag::custom(
|
||||||
|
TagKind::single_letter(Alphabet::C, false),
|
||||||
|
iter::once(self.commit.to_string()),
|
||||||
|
));
|
||||||
|
|
||||||
|
let event = if self.grasp {
|
||||||
|
utils::build_grasp_event(&repos, user_pubk, event_builder.clone())?
|
||||||
|
} else {
|
||||||
|
// Since `grasp` is false, `clones` must be provided
|
||||||
|
event_builder = event_builder.tag(Tag::custom(
|
||||||
|
TagKind::custom("clone"),
|
||||||
|
self.clones.iter().map(ToString::to_string),
|
||||||
|
));
|
||||||
|
|
||||||
|
event_builder.build(user_pubk)
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_id = event.id.expect("There is an id");
|
||||||
|
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
repos_relays,
|
||||||
|
naddrs_relays,
|
||||||
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
|
// Include read relays for each maintainer (if found)
|
||||||
|
client.read_relays_from_users(&maintainers).await,
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||||
|
let success = client
|
||||||
|
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let nevent = utils::new_nevent(event_id, &success)?;
|
||||||
|
println!("PR update tip created: {nevent}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/cli/commands/pr/view.rs
Normal file
54
src/cli/commands/pr/view.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// 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 crate::{
|
||||||
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct ViewArgs {
|
||||||
|
/// Repository addresses
|
||||||
|
///
|
||||||
|
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||||
|
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(
|
||||||
|
value_name = "NADDR-NIP05-OR-SET",
|
||||||
|
long = "repo",
|
||||||
|
value_delimiter = ','
|
||||||
|
)]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// Pull request ID
|
||||||
|
#[arg(value_name = "EVENT-ID")]
|
||||||
|
pr_id: NostrEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ViewArgs {
|
||||||
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
crate::cli::common_commands::view_pr_issue::<true>(options, self.naddrs, self.pr_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ use std::{iter, str::FromStr, sync::Arc};
|
|||||||
use either::Either;
|
use either::Either;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},
|
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard},
|
||||||
filter::{Alphabet, Filter, SingleLetterTag},
|
filter::{Alphabet, Filter, SingleLetterTag},
|
||||||
hashes::sha1::Hash as Sha1Hash,
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
nips::{nip10::Marker, nip19::ToBech32},
|
nips::{nip10::Marker, nip19::ToBech32},
|
||||||
@@ -31,19 +31,18 @@ use super::{
|
|||||||
patch::PatchPrStatus,
|
patch::PatchPrStatus,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
};
|
};
|
||||||
use crate::{
|
|
||||||
cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
|
||||||
nostr_utils::{
|
|
||||||
NostrClient,
|
|
||||||
traits::{NaddrsUtils, TagsExt},
|
|
||||||
utils,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, patch::GitPatch},
|
cli::{CliOptions, patch::GitPatch},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
nostr_utils::traits::{GitIssuePrMetadata, GitPatchUtils, ReposUtils},
|
nostr_utils::traits::{GitIssuePrMetadata, GitPatchUtils, ReposUtils},
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::EntityType,
|
||||||
|
},
|
||||||
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
|
};
|
||||||
|
|
||||||
/// Updates the issue's status to `new_status` after validating it with
|
/// Updates the issue's status to `new_status` after validating it with
|
||||||
/// `check_fn`.
|
/// `check_fn`.
|
||||||
@@ -127,17 +126,20 @@ pub async fn issue_status_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the patch's status to `new_status` after validating it with
|
/// Updates the patch/pr's status to `new_status` after validating it with
|
||||||
/// `check_fn`.
|
/// `check_fn`. The `ENTITY_TYPE` can only be a pull request or a patch
|
||||||
pub async fn patch_status_command(
|
pub async fn patch_pr_status_command<const ENTITY_TYPE: u8>(
|
||||||
options: CliOptions,
|
options: CliOptions,
|
||||||
patch_id: NostrEvent,
|
patch_pr_id: NostrEvent,
|
||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
new_status: PatchPrStatus,
|
new_status: PatchPrStatus,
|
||||||
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
||||||
merge_or_applied_patches: Vec<EventId>,
|
merge_or_applied_patches: Vec<EventId>,
|
||||||
check_fn: impl FnOnce(&PatchPrStatus) -> N34Result<()>,
|
check_fn: impl FnOnce(&PatchPrStatus) -> N34Result<()>,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
|
EntityType::is_pr_or_patch::<ENTITY_TYPE>();
|
||||||
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
|
|
||||||
let naddrs = utils::naddrs_or_file(
|
let naddrs = utils::naddrs_or_file(
|
||||||
naddrs.flat_naddrs(&options.config.sets)?,
|
naddrs.flat_naddrs(&options.config.sets)?,
|
||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
@@ -146,7 +148,7 @@ pub async fn patch_status_command(
|
|||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = client.pubkey().await?;
|
let user_pubk = client.pubkey().await?;
|
||||||
client
|
client
|
||||||
.add_relays(&[naddrs.extract_relays(), patch_id.relays].concat())
|
.add_relays(&[naddrs.extract_relays(), patch_pr_id.relays].concat())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let owners = naddrs.extract_owners();
|
let owners = naddrs.extract_owners();
|
||||||
@@ -156,9 +158,14 @@ pub async fn patch_status_command(
|
|||||||
let relay_hint = repos.extract_relays().first().cloned();
|
let relay_hint = repos.extract_relays().first().cloned();
|
||||||
client.add_relays(&repos.extract_relays()).await;
|
client.add_relays(&repos.extract_relays()).await;
|
||||||
|
|
||||||
let patch_event = client.fetch_patch(patch_id.event_id).await?;
|
let event = if entity_type.is_patch() {
|
||||||
|
client.fetch_patch(patch_pr_id.event_id).await?
|
||||||
|
} else {
|
||||||
|
client.fetch_pr(patch_pr_id.event_id).await?
|
||||||
|
};
|
||||||
|
let authorized_pubkeys = [maintainers.as_slice(), &[event.pubkey], &owners].concat();
|
||||||
|
|
||||||
if patch_event.is_revision_patch() && !new_status.is_merged_or_applied() {
|
if entity_type.is_patch() && event.is_revision_patch() && !new_status.is_merged_or_applied() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
||||||
'close', and 'draft' are not supported."
|
'close', and 'draft' are not supported."
|
||||||
@@ -166,27 +173,31 @@ pub async fn patch_status_command(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (root_patch, root_revision) = get_patch_root_revision(&patch_event)?;
|
let (root_patch_or_pr, root_revision) = if entity_type.is_patch() {
|
||||||
let patch_status = client
|
get_patch_root_revision(&event)?
|
||||||
.fetch_patch_status(
|
} else {
|
||||||
root_patch,
|
(event.id, None)
|
||||||
root_revision,
|
};
|
||||||
[maintainers.as_slice(), &[patch_event.pubkey], &owners].concat(),
|
let current_status = if entity_type.is_patch() {
|
||||||
)
|
client
|
||||||
.await?;
|
.fetch_patch_status(root_patch_or_pr, root_revision, authorized_pubkeys.clone())
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.fetch_pr_status(event.id, authorized_pubkeys.clone())
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
check_fn(&patch_status)?;
|
check_fn(¤t_status)?;
|
||||||
|
|
||||||
let mut status_builder = EventBuilder::new(new_status.kind(), "")
|
let mut status_builder = EventBuilder::new(new_status.kind(), "")
|
||||||
.pow(options.pow.unwrap_or_default())
|
.pow(options.pow.unwrap_or_default())
|
||||||
.tag(utils::event_reply_tag(
|
.tag(utils::event_reply_tag(
|
||||||
&root_patch,
|
&root_patch_or_pr,
|
||||||
relay_hint.as_ref(),
|
relay_hint.as_ref(),
|
||||||
Marker::Root,
|
Marker::Root,
|
||||||
))
|
))
|
||||||
.tag(Tag::public_key(patch_event.pubkey))
|
.tags(authorized_pubkeys.iter().map(|p| Tag::public_key(*p)))
|
||||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
|
||||||
.tags(owners.iter().map(|p| Tag::public_key(*p)))
|
|
||||||
.tags(
|
.tags(
|
||||||
coordinates
|
coordinates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -215,6 +226,7 @@ pub async fn patch_status_command(
|
|||||||
.tags(commits.into_iter().map(Tag::reference));
|
.tags(commits.into_iter().map(Tag::reference));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Patch only
|
||||||
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,
|
||||||
@@ -223,6 +235,7 @@ pub async fn patch_status_command(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch only
|
||||||
if !merge_or_applied_patches.is_empty() {
|
if !merge_or_applied_patches.is_empty() {
|
||||||
status_builder = status_builder.tags(
|
status_builder = status_builder.tags(
|
||||||
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
||||||
@@ -232,7 +245,6 @@ pub async fn patch_status_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let status_event = status_builder.dedup_tags().build(user_pubk);
|
let status_event = status_builder.dedup_tags().build(user_pubk);
|
||||||
|
|
||||||
let event_id = status_event.id.expect("There is an id");
|
let event_id = status_event.id.expect("There is an id");
|
||||||
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
@@ -240,10 +252,7 @@ pub async fn patch_status_command(
|
|||||||
naddrs.extract_relays(),
|
naddrs.extract_relays(),
|
||||||
repos.extract_relays(),
|
repos.extract_relays(),
|
||||||
utils::add_write_relays(user_relays_list.as_ref()),
|
utils::add_write_relays(user_relays_list.as_ref()),
|
||||||
client.read_relays_from_user(patch_event.pubkey).await,
|
client.read_relays_from_users(&authorized_pubkeys).await,
|
||||||
client
|
|
||||||
.read_relays_from_users(&[maintainers, owners].concat())
|
|
||||||
.await,
|
|
||||||
]
|
]
|
||||||
.concat();
|
.concat();
|
||||||
|
|
||||||
@@ -256,13 +265,14 @@ pub async fn patch_status_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch and display patches and issues for given repositories.
|
/// Fetches and displays pull requests, patches, and issues for specified
|
||||||
/// If `list_patches` is true, lists patches instead of issues.
|
/// repositories. The `limit` parameter sets the maximum number of items to
|
||||||
/// `limit` controls the maximum number of items to fetch.
|
/// retrieve.
|
||||||
pub async fn list_patches_and_issues(
|
///
|
||||||
|
/// The `ENTITY_TYPE` const is `[EntityType]` enum as u8.
|
||||||
|
pub async fn list_pr_patches_and_issues<const ENTITY_TYPE: u8>(
|
||||||
options: CliOptions,
|
options: CliOptions,
|
||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
list_patches: bool,
|
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||||
@@ -270,6 +280,7 @@ pub async fn list_patches_and_issues(
|
|||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
@@ -283,18 +294,12 @@ 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 {
|
|
||||||
Kind::GitPatch
|
|
||||||
} else {
|
|
||||||
Kind::GitIssue
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut filter = Filter::new()
|
let mut filter = Filter::new()
|
||||||
.coordinates(coordinates.iter())
|
.coordinates(coordinates.iter())
|
||||||
.kind(kind)
|
.kind(entity_type.kind())
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
if list_patches {
|
if entity_type.is_patch() {
|
||||||
filter = filter.hashtag("root");
|
filter = filter.hashtag("root");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,23 +317,35 @@ pub async fn list_patches_and_issues(
|
|||||||
let c = arc_client.clone();
|
let c = arc_client.clone();
|
||||||
let keys = authorized_pubkeys.clone();
|
let keys = authorized_pubkeys.clone();
|
||||||
async move {
|
async move {
|
||||||
let status = if list_patches {
|
let status = match entity_type {
|
||||||
let (root, root_revision) = get_patch_root_revision(&event)?;
|
EntityType::PullRequest => {
|
||||||
c.fetch_patch_status(
|
c.fetch_pr_status(
|
||||||
root,
|
event.id,
|
||||||
root_revision,
|
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
)
|
||||||
)
|
.await
|
||||||
.await
|
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||||
.map(Either::Left)?
|
}
|
||||||
} else {
|
EntityType::Patch => {
|
||||||
c.fetch_issue_status(
|
let (root, root_revision) = get_patch_root_revision(&event)?;
|
||||||
event.id,
|
c.fetch_patch_status(
|
||||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
root,
|
||||||
)
|
root_revision,
|
||||||
.await
|
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||||
.map(Either::Right)?
|
)
|
||||||
|
.await
|
||||||
|
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||||
|
}
|
||||||
|
EntityType::Issue => {
|
||||||
|
c.fetch_issue_status(
|
||||||
|
event.id,
|
||||||
|
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
N34Result::Ok((event, status))
|
N34Result::Ok((event, status))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -336,11 +353,11 @@ pub async fn list_patches_and_issues(
|
|||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|r| r.ok()),
|
.filter_map(|r| r.ok()),
|
||||||
|(_, status)| status.as_ref().either_into::<Kind>(),
|
|(_, (_, k))| *k,
|
||||||
);
|
);
|
||||||
|
|
||||||
let lines = events
|
let lines = events
|
||||||
.map(|(event, status)| format_patch_and_issue(&event, status))
|
.map(|(event, (status, _))| format_entity::<ENTITY_TYPE>(&event, status))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let max_width = lines
|
let max_width = lines
|
||||||
@@ -370,33 +387,40 @@ fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<Ev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats an event as either a patch or an issue. For patches, extracts the
|
/// Formats patch, issue or PR. For patches, extracts the
|
||||||
/// subject line from the Git patch format. For issues, combines the subject
|
/// subject line from the Git patch format. For issues and PRs, combines the
|
||||||
/// with labels. The output includes status and formatted ID.
|
/// subject with labels. The output includes status and formatted ID.
|
||||||
fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus>) -> String {
|
fn format_entity<const ENTITY_TYPE: u8>(event: &Event, status: &str) -> String {
|
||||||
let subject = if status.is_left() {
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
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_event_labels();
|
|
||||||
let subject = event.extract_event_subject();
|
|
||||||
|
|
||||||
if labels.is_empty() {
|
let subject = match entity_type {
|
||||||
subject.to_owned()
|
EntityType::Patch => {
|
||||||
} else {
|
GitPatch::from_str(&event.content)
|
||||||
format!(r#""{subject}" {labels}"#)
|
.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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Issues and PRs
|
||||||
|
let labels = event.extract_event_labels();
|
||||||
|
let subject = event.extract_event_subject();
|
||||||
|
|
||||||
|
if labels.is_empty() {
|
||||||
|
subject.to_owned()
|
||||||
|
} else {
|
||||||
|
format!(r#""{subject}" {labels}"#)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"({status}) {}\nID: {}\n",
|
"({status}) {}\nID: {}\n",
|
||||||
utils::smart_wrap(&subject, 85),
|
utils::smart_wrap(&subject, 85),
|
||||||
@@ -455,14 +479,8 @@ pub async fn view_pr_issue<const IS_PR: bool>(
|
|||||||
|
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
client.add_relays(&event_id.relays).await;
|
client.add_relays(&event_id.relays).await;
|
||||||
client
|
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
||||||
.add_relays(
|
client.add_relays(&repos.extract_relays()).await;
|
||||||
&client
|
|
||||||
.fetch_repos(&naddrs.into_coordinates())
|
|
||||||
.await?
|
|
||||||
.extract_relays(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let event = client
|
let event = client
|
||||||
.fetch_event(Filter::new().id(event_id.event_id).kind(
|
.fetch_event(Filter::new().id(event_id.event_id).kind(
|
||||||
@@ -475,7 +493,27 @@ pub async fn view_pr_issue<const IS_PR: bool>(
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await?
|
.await?
|
||||||
.ok_or(N34Error::CanNotFoundIssue)?;
|
.ok_or(
|
||||||
|
const {
|
||||||
|
if IS_PR {
|
||||||
|
N34Error::CanNotFoundPr
|
||||||
|
} else {
|
||||||
|
N34Error::CanNotFoundIssue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let authorized_pubkeys = [repos.extract_maintainers().as_slice(), &[event.pubkey]].concat();
|
||||||
|
let status = if IS_PR {
|
||||||
|
client
|
||||||
|
.fetch_pr_status(event.id, authorized_pubkeys)
|
||||||
|
.await?
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.fetch_issue_status(event.id, authorized_pubkeys)
|
||||||
|
.await?
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let event_subject = utils::smart_wrap(event.extract_event_subject(), 70);
|
let event_subject = utils::smart_wrap(event.extract_event_subject(), 70);
|
||||||
let event_author = client.get_username(event.pubkey).await;
|
let event_author = client.get_username(event.pubkey).await;
|
||||||
@@ -516,10 +554,12 @@ pub async fn view_pr_issue<const IS_PR: bool>(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&event)
|
.unwrap_or(&event)
|
||||||
.tags
|
.tags
|
||||||
.map_tag(TagKind::Clone, |t| {
|
.iter()
|
||||||
|
.filter_map(|t| t.as_standardized())
|
||||||
|
.find_map(|t| {
|
||||||
match t {
|
match t {
|
||||||
nostr::event::TagStandard::GitClone(urls) => urls.clone(),
|
TagStandard::GitClone(urls) if !urls.is_empty() => Some(urls.clone()),
|
||||||
_ => unreachable!(),
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -533,7 +573,7 @@ pub async fn view_pr_issue<const IS_PR: bool>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{event_subject} - [by {event_author}]\n{event_labels}{}{pr_data}",
|
"({status}) {event_subject} - [by {event_author}]\n{event_labels}{}{pr_data}",
|
||||||
utils::smart_wrap(&event.content, 80)
|
utils::smart_wrap(&event.content, 80)
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub mod parsers;
|
|||||||
pub mod traits;
|
pub mod traits;
|
||||||
/// Common helper types used throughout the CLI.
|
/// Common helper types used throughout the CLI.
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
/// CLI utils
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ pub enum RelayOrSet {
|
|||||||
Set(String),
|
Set(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum representing the type of entity to handle in common commands.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum EntityType {
|
||||||
|
/// Pull Request
|
||||||
|
PullRequest,
|
||||||
|
/// Patch
|
||||||
|
Patch,
|
||||||
|
/// Issue
|
||||||
|
Issue,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses and represents a Nostr `nevent1` or `note1`.
|
/// Parses and represents a Nostr `nevent1` or `note1`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NostrEvent {
|
pub struct NostrEvent {
|
||||||
@@ -78,6 +90,62 @@ impl AuthUrlHandler for EchoAuthUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EntityType {
|
||||||
|
/// Returns true if the entity is a pull request.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_pr(&self) -> bool {
|
||||||
|
matches!(self, Self::PullRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the entity is a patch.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_patch(&self) -> bool {
|
||||||
|
matches!(self, Self::Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the entity is an issue.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_issue(&self) -> bool {
|
||||||
|
matches!(self, Self::Issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the kind of the entity
|
||||||
|
#[inline]
|
||||||
|
pub const fn kind(&self) -> Kind {
|
||||||
|
match self {
|
||||||
|
Self::PullRequest => crate::cli::pr::PR_KIND,
|
||||||
|
Self::Patch => Kind::GitPatch,
|
||||||
|
Self::Issue => Kind::GitIssue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a [`u8`] value to the corresponding enum variant.
|
||||||
|
#[inline]
|
||||||
|
pub const fn from_u8<const NUM: u8>() -> Self {
|
||||||
|
const {
|
||||||
|
match NUM {
|
||||||
|
val if val == Self::PullRequest as u8 => Self::PullRequest,
|
||||||
|
val if val == Self::Patch as u8 => Self::Patch,
|
||||||
|
val if val == Self::Issue as u8 => Self::Issue,
|
||||||
|
_ => {
|
||||||
|
panic!("No enum with the given numeric value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures the entity is either a pull request or a patch. Compilation
|
||||||
|
/// will fail if the entity is neither.
|
||||||
|
pub const fn is_pr_or_patch<const ENTITY_TYPE: u8>() {
|
||||||
|
const {
|
||||||
|
let entity = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
|
if !entity.is_pr() && !entity.is_patch() {
|
||||||
|
panic!("The entity should be a pull request or a patch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NaddrOrSet {
|
impl NaddrOrSet {
|
||||||
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
||||||
/// Returns error if the set naddrs are empty or the set not found.
|
/// Returns error if the set naddrs are empty or the set not found.
|
||||||
|
|||||||
45
src/cli/utils.rs
Normal file
45
src/cli/utils.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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::io::{self, Write};
|
||||||
|
|
||||||
|
/// Displays the given prompt and reads a line of input from the user.
|
||||||
|
pub fn read_line(prompt: &str) -> io::Result<String> {
|
||||||
|
{
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
||||||
|
write!(&mut stdout, "{prompt}: ")?;
|
||||||
|
_ = stdout.flush();
|
||||||
|
}
|
||||||
|
let mut user_input = String::new();
|
||||||
|
io::stdin().read_line(&mut user_input)?;
|
||||||
|
Ok(user_input.trim().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompts the user with a message and repeatedly asks until they enter a valid
|
||||||
|
/// boolean response. Recognizes "yes", "y", "true" for `true` and "no", "n",
|
||||||
|
/// "false" for `false`.
|
||||||
|
pub fn prompt_bool(prompt: &str) -> io::Result<bool> {
|
||||||
|
loop {
|
||||||
|
let user_input = read_line(prompt)?.to_ascii_lowercase();
|
||||||
|
|
||||||
|
match user_input.as_str() {
|
||||||
|
"yes" | "y" | "true" => return Ok(true),
|
||||||
|
"no" | "n" | "false" => return Ok(false),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/error.rs
16
src/error.rs
@@ -123,9 +123,14 @@ pub enum N34Error {
|
|||||||
)]
|
)]
|
||||||
CanNotFoundIssue,
|
CanNotFoundIssue,
|
||||||
#[error(
|
#[error(
|
||||||
"Patch not found, make sure it is in the relays and make sure that the ID is an patch ID"
|
"Patch not found, make sure it is in the relays and make sure that the ID is a patch ID"
|
||||||
)]
|
)]
|
||||||
CanNotFoundPatch,
|
CanNotFoundPatch,
|
||||||
|
#[error(
|
||||||
|
"Pull request not found, make sure it is in the relays and make sure that the ID is an \
|
||||||
|
pull request ID"
|
||||||
|
)]
|
||||||
|
CanNotFoundPr,
|
||||||
#[error(r#"The given patch id is not a root patch. It must contains `["t", "root"]` tag"#)]
|
#[error(r#"The given patch id is not a root patch. It must contains `["t", "root"]` tag"#)]
|
||||||
NotRootPatch,
|
NotRootPatch,
|
||||||
#[error("This status kind can't be set for an issue: {0}")]
|
#[error("This status kind can't be set for an issue: {0}")]
|
||||||
@@ -134,7 +139,7 @@ pub enum N34Error {
|
|||||||
InvalidPatchStatus(Kind),
|
InvalidPatchStatus(Kind),
|
||||||
#[error("Can't find the root patch of the given patch-revision")]
|
#[error("Can't find the root patch of the given patch-revision")]
|
||||||
RevisionRootNotFound,
|
RevisionRootNotFound,
|
||||||
#[error("Invalid status for the issue/patch: {0}")]
|
#[error("Invalid status: {0}")]
|
||||||
InvalidStatus(String),
|
InvalidStatus(String),
|
||||||
#[error("Not valid bunker URL")]
|
#[error("Not valid bunker URL")]
|
||||||
NotBunkerUrl,
|
NotBunkerUrl,
|
||||||
@@ -143,6 +148,13 @@ pub enum N34Error {
|
|||||||
keyring is enabled to store it"
|
keyring is enabled to store it"
|
||||||
)]
|
)]
|
||||||
SecretKeyKeyringWithoutEntry,
|
SecretKeyKeyringWithoutEntry,
|
||||||
|
#[error(
|
||||||
|
"Your push was not accepted by any GRASP server. Try creating a personal fork, pushing to \
|
||||||
|
your own GRASP servers, and then make a pull request with your GRASP repos as a clone"
|
||||||
|
)]
|
||||||
|
PushRejectedByGraspServers,
|
||||||
|
#[error("The repository doesn't contains any GRASP servers")]
|
||||||
|
NoGraspServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl N34Error {
|
impl N34Error {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ use traits::TokenUtils;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, issue::IssueStatus, patch::PatchPrStatus},
|
cli::{CliOptions, issue::IssueStatus, patch::PatchPrStatus},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::traits::KindExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Timeout duration for the client.
|
/// Timeout duration for the client.
|
||||||
@@ -258,6 +259,13 @@ impl NostrClient {
|
|||||||
.ok_or(N34Error::CanNotFoundPatch)
|
.ok_or(N34Error::CanNotFoundPatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the pull request by the given id. None if not found
|
||||||
|
pub async fn fetch_pr(&self, pr_id: EventId) -> N34Result<Event> {
|
||||||
|
self.fetch_event(Filter::new().id(pr_id).kind(crate::cli::pr::PR_KIND))
|
||||||
|
.await?
|
||||||
|
.ok_or(N34Error::CanNotFoundPr)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the username for a given public key. If no username is found,
|
/// Returns the username for a given public key. If no username is found,
|
||||||
/// falls back to a shortened version of the public key.
|
/// falls back to a shortened version of the public key.
|
||||||
pub async fn get_username(&self, user: PublicKey) -> String {
|
pub async fn get_username(&self, user: PublicKey) -> String {
|
||||||
@@ -298,6 +306,27 @@ impl NostrClient {
|
|||||||
.unwrap_or_else(|| Ok(IssueStatus::Open))
|
.unwrap_or_else(|| Ok(IssueStatus::Open))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the latest status of PR by its ID, only considering status
|
||||||
|
/// events from authorized_pubkeys. If no valid status event is found,
|
||||||
|
/// defaults to Open.
|
||||||
|
pub async fn fetch_pr_status(
|
||||||
|
&self,
|
||||||
|
pr_id: EventId,
|
||||||
|
authorized_pubkeys: Vec<PublicKey>,
|
||||||
|
) -> N34Result<PatchPrStatus> {
|
||||||
|
self.fetch_events(
|
||||||
|
Filter::new()
|
||||||
|
.event(pr_id)
|
||||||
|
.kinds(PatchPrStatus::all_kinds())
|
||||||
|
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.max_by_key(|e| e.created_at)
|
||||||
|
.map(|status| PatchPrStatus::try_from(status.kind))
|
||||||
|
.unwrap_or_else(|| Ok(PatchPrStatus::Open))
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the status of a patch. If it's a revision patch, checks if it's
|
/// 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
|
/// closed when the root patch is already merged/applied but doesn't
|
||||||
/// reference this revision. Defaults to Open status if no status event
|
/// reference this revision. Defaults to Open status if no status event
|
||||||
@@ -355,17 +384,17 @@ impl NostrClient {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the root issue or patch for a given event. If the event is already
|
/// Finds the root issue, PR or patch for a given event. If the event is
|
||||||
/// a root (issue/patch), returns it directly. For comments, follows
|
/// already a root (issue/patch/PR), returns it directly. For comments,
|
||||||
/// parent/root references until finding the root or failing. Returns
|
/// follows parent/root references until finding the root or failing.
|
||||||
/// None if no root can be found.
|
/// Returns None if no root can be found.
|
||||||
pub async fn find_root(&self, mut event: Event) -> N34Result<Option<Event>> {
|
pub async fn find_root(&self, mut event: Event) -> N34Result<Option<Event>> {
|
||||||
if !matches!(event.kind, Kind::GitIssue | Kind::GitPatch | Kind::Comment) {
|
if !event.kind.can_reply_to() {
|
||||||
return Err(N34Error::CanNotReplyToEvent);
|
return Err(N34Error::CanNotReplyToEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if matches!(event.kind, Kind::GitIssue | Kind::GitPatch) {
|
if event.kind.is_root_kind() {
|
||||||
return Ok(Some(event));
|
return Ok(Some(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +404,7 @@ impl NostrClient {
|
|||||||
self.add_relay_hint(relay_hint.cloned()).await;
|
self.add_relay_hint(relay_hint.cloned()).await;
|
||||||
let root_event = self.fetch_event(Filter::new().id(*id)).await?;
|
let root_event = self.fetch_event(Filter::new().id(*id)).await?;
|
||||||
if let Some(ref root_event) = root_event
|
if let Some(ref root_event) = root_event
|
||||||
&& !matches!(root_event.kind, Kind::GitIssue | Kind::GitPatch)
|
&& !root_event.kind.is_root_kind()
|
||||||
{
|
{
|
||||||
return Err(N34Error::CanNotReplyToEvent);
|
return Err(N34Error::CanNotReplyToEvent);
|
||||||
}
|
}
|
||||||
@@ -390,7 +419,7 @@ impl NostrClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Break if: no root/parent tags found, parent/root event fetch failed
|
// Break if: no root/parent tags found
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,11 @@
|
|||||||
// 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 std::ops::Deref;
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use nostr::hashes::sha1::Hash as Sha1Hash;
|
use nostr::hashes::sha1::Hash as Sha1Hash;
|
||||||
|
use nostr::nips::nip19::FromBech32;
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, Tags},
|
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, Tags},
|
||||||
key::PublicKey,
|
key::PublicKey,
|
||||||
@@ -228,6 +231,20 @@ impl Vec<GitRepositoryAnnouncement> {
|
|||||||
self.iter().flat_map(|n| n.relays.clone()).collect()
|
self.iter().flat_map(|n| n.relays.clone()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the grasp servers.
|
||||||
|
#[inline]
|
||||||
|
pub fn extract_grasp_servers(&self) -> Vec<&Url> {
|
||||||
|
self.iter()
|
||||||
|
.flat_map(RepoUtils::extract_grasp_servers)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract all clones
|
||||||
|
#[inline]
|
||||||
|
pub fn extract_clones(&self) -> Vec<&Url> {
|
||||||
|
self.iter().flat_map(|r| r.clone.deref()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract all the maintainers from these repositories
|
/// Extract all the maintainers from these repositories
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn extract_maintainers(&self) -> Vec<PublicKey> {
|
pub fn extract_maintainers(&self) -> Vec<PublicKey> {
|
||||||
@@ -241,6 +258,57 @@ impl Vec<GitRepositoryAnnouncement> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RepoUtils)]
|
||||||
|
impl GitRepositoryAnnouncement {
|
||||||
|
/// Retrieves the GRASP servers from the list of clones based on the relay's
|
||||||
|
/// GRASP check.
|
||||||
|
pub fn extract_grasp_servers(&self) -> Vec<&Url> {
|
||||||
|
self.clone
|
||||||
|
.iter()
|
||||||
|
.filter(|clone| self.relays.is_grasp(clone))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RelayUrlsUtils)]
|
||||||
|
impl Vec<RelayUrl> {
|
||||||
|
/// Checks if the provided clone URL is a GRASP clone URL and is present in
|
||||||
|
/// the relays.
|
||||||
|
pub fn is_grasp(&self, clone_url: &Url) -> bool {
|
||||||
|
clone_url.is_grasp_clone()
|
||||||
|
&& self.iter().any(|relay| {
|
||||||
|
clone_url
|
||||||
|
.as_str()
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://")
|
||||||
|
.starts_with(
|
||||||
|
relay
|
||||||
|
.as_str_without_trailing_slash()
|
||||||
|
.trim_start_matches("wss://")
|
||||||
|
.trim_start_matches("ws://"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(IsGraspClone)]
|
||||||
|
impl Url {
|
||||||
|
/// Checks if the URL is a GRASP clone URL. A GRASP clone URL is identified
|
||||||
|
/// by having a `<string>.git` suffix in the last part and a valid public
|
||||||
|
/// key in the second last part.
|
||||||
|
pub fn is_grasp_clone(&self) -> bool {
|
||||||
|
let mut parts = self.as_str().split('/').rev().take(2);
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.and_then(|r| (r.len() > 4 && r.ends_with(".git")).then_some(r))
|
||||||
|
.is_some()
|
||||||
|
&& parts
|
||||||
|
.next()
|
||||||
|
.and_then(|p| PublicKey::from_bech32(p).ok())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Utility functions for working with patch events
|
/// Utility functions for working with patch events
|
||||||
#[easy_ext::ext(GitPatchUtils)]
|
#[easy_ext::ext(GitPatchUtils)]
|
||||||
impl Event {
|
impl Event {
|
||||||
@@ -318,3 +386,24 @@ impl nostr_keyring::Error {
|
|||||||
matches!(self, nostr_keyring::Error::Keyring(KeyringError::NoEntry))
|
matches!(self, nostr_keyring::Error::Keyring(KeyringError::NoEntry))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(KindExt)]
|
||||||
|
impl Kind {
|
||||||
|
/// Checks if the kind is a root kind, such as GitIssue, GitPatch, GitPr or
|
||||||
|
/// GitPrUpdate.
|
||||||
|
pub fn is_root_kind(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
val if val == &crate::cli::pr::PR_KIND || val == &crate::cli::pr::PR_UPDATE_KIND => {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Kind::GitIssue | Kind::GitPatch => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this kind supports replies, such as Comment, GitIssue,
|
||||||
|
/// GitPatch, GitPr, or GitPrUpdate.
|
||||||
|
pub fn can_reply_to(&self) -> bool {
|
||||||
|
self == &Kind::Comment || self.is_root_kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,13 +17,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
fs,
|
fs,
|
||||||
|
iter,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::atomic::Ordering,
|
sync::atomic::Ordering,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{Event, EventId, Kind, Tag, TagKind, TagStandard},
|
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard, UnsignedEvent},
|
||||||
filter::Alphabet,
|
filter::Alphabet,
|
||||||
key::PublicKey,
|
key::PublicKey,
|
||||||
nips::{
|
nips::{
|
||||||
@@ -38,8 +39,9 @@ use nostr::{
|
|||||||
|
|
||||||
use super::traits::TagsExt;
|
use super::traits::TagsExt;
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{NOSTR_ADDRESS_FILE, parsers},
|
cli::{NOSTR_ADDRESS_FILE, parsers, utils as cli_utils},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::traits::ReposUtils,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the value of the given tag
|
/// Returns the value of the given tag
|
||||||
@@ -351,3 +353,48 @@ where
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a grasp event by extracting grasp servers from the provided
|
||||||
|
/// repositories. If no grasp servers are found, an error is returned. If one or
|
||||||
|
/// more are found, the user is prompted to push to each server until one is
|
||||||
|
/// successful. The successful event is returned, or an error if no push
|
||||||
|
/// succeeds.
|
||||||
|
pub fn build_grasp_event(
|
||||||
|
repos: &Vec<GitRepositoryAnnouncement>,
|
||||||
|
author_pubkey: PublicKey,
|
||||||
|
event_builder: EventBuilder,
|
||||||
|
) -> N34Result<UnsignedEvent> {
|
||||||
|
let mut event = None;
|
||||||
|
let grasp_repos = repos.extract_grasp_servers();
|
||||||
|
tracing::debug!("GRASP repos: {grasp_repos:?}");
|
||||||
|
|
||||||
|
if grasp_repos.is_empty() {
|
||||||
|
tracing::trace!(
|
||||||
|
"Repository clones: {:?}",
|
||||||
|
dedup(repos.extract_clones().iter())
|
||||||
|
);
|
||||||
|
return Err(N34Error::NoGraspServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for grasp_repo in grasp_repos {
|
||||||
|
let mut grasp_event = event_builder
|
||||||
|
.clone()
|
||||||
|
.tag(Tag::custom(
|
||||||
|
TagKind::custom("clone"),
|
||||||
|
iter::once(grasp_repo.to_string()),
|
||||||
|
))
|
||||||
|
.build(author_pubkey);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Attempt to push your changes to `refs/nostr/{}` in the repository: {}",
|
||||||
|
grasp_event.id(),
|
||||||
|
grasp_repo
|
||||||
|
);
|
||||||
|
|
||||||
|
if cli_utils::prompt_bool("Was the push successful? (yes/no)")? {
|
||||||
|
event = Some(grasp_event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.ok_or(N34Error::PushRejectedByGraspServers)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user