Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5151f5d04d | ||
|
|
463010c9f6 | ||
|
|
d2adff7882 | ||
|
|
ee17c21dbb | ||
|
|
df54a53b7c | ||
|
|
95a46a798f | ||
|
|
03b3d89f48 | ||
|
|
9122557281 | ||
|
|
dfa7467b1e | ||
|
|
8d8ad5f1e5 | ||
|
|
f73245f9c3 | ||
|
|
c3be6248b5 | ||
|
|
4df73316a0 | ||
|
|
be3cdc440d | ||
|
|
4bcf104c80 | ||
|
|
4360aa192c | ||
|
|
cc3aed0e89 | ||
|
|
f996fd351b | ||
|
|
518e573467 | ||
|
|
1e9e00a4f3 | ||
|
|
c308dc1b42 | ||
|
|
089b62a0ef | ||
|
|
5e76e413ab | ||
|
|
ed1bfaf161 | ||
|
|
ec68e972f7 | ||
|
|
629de42215 | ||
|
|
dd05e050cd |
33
CHANGELOG.md
33
CHANGELOG.md
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- New command `repo state` - by Awiteb
|
||||
- Support pull requests - by Awiteb
|
||||
- Add `--personal-fork` flag to `repo announce` command - by Awiteb
|
||||
- Log to stderr and a file - by Awiteb
|
||||
- Accept patches from stdin in `patch send` command - by Awiteb
|
||||
- Write patches to the stdout in `patch fetch` command - by Awiteb
|
||||
|
||||
### Breaking Change
|
||||
|
||||
- Make subject mandatory and optional content - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add `io-std` feature to `tokio` - by Awiteb
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update commands help messages - by Awiteb
|
||||
- Update commands titles and descriptions - by Awiteb
|
||||
|
||||
### Fixed
|
||||
|
||||
- Check the nip07 signer in `CliOptions::ensure_signer` - by Awiteb
|
||||
- Required the `applied_commits` - by Awiteb
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `dbus` C dep - by Awiteb
|
||||
|
||||
## [0.4.0] - 2025-08-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -37,9 +37,7 @@ as the active development branch.
|
||||
|
||||
You can submit issues and patches via any
|
||||
Nostr-compatible client using the address:
|
||||
`naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueq
|
||||
yf8wumn8ghj7mn0wd68yt35wfejumnvqyxhwumn8ghj7mn0wvhxcmmvqy28wumn8ghj7mn0wd68ytn00
|
||||
p68ytnyv4mqwuj6xc`
|
||||
`naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyxhwumn8ghj7mn0wvhxcmmvqy28wumn8ghj7mn0wd68ytn00p68ytnyv4mqwuj6xc`
|
||||
|
||||
|
||||
When using `n34`, there's no need to specify the address, it will automatically
|
||||
@@ -70,8 +68,8 @@ to their read relays, most tools handle this automatically.
|
||||
#### Patch Guidelines
|
||||
|
||||
- Keep patches small: Focused changes are easier to review and merge.
|
||||
- Run `just ci` before submitting your patch.
|
||||
- Update the change log with your patch. Run `just changelog` or `git-cliff > CHANGELOG.md`
|
||||
- Run `./tasks ci` before submitting your patch.
|
||||
- Update the change log with your patch. Run `./tasks changelog` or `git-cliff > CHANGELOG.md`
|
||||
- Add your name to the [AUTHORS](AUTHORS) file if this is your first contribution. (alphabetical order)
|
||||
- Use [Conventional Commits]: Start the patch subject with one of these types:
|
||||
- `feat`: New feature
|
||||
|
||||
1263
Cargo.lock
generated
1263
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -14,6 +14,7 @@ categories = ["command-line-utilities"]
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[dependencies]
|
||||
async-utility = "0.3.1"
|
||||
chrono = "0.4.41"
|
||||
convert_case = "0.8.0"
|
||||
dirs = "6.0.0"
|
||||
@@ -28,6 +29,16 @@ toml = "0.9.4"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
[dependencies.keyring]
|
||||
features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"async-secret-service",
|
||||
"tokio",
|
||||
"crypto-rust",
|
||||
]
|
||||
version = "3.6.3"
|
||||
|
||||
[dependencies.clap]
|
||||
features = ["derive"]
|
||||
version = "4.5.42"
|
||||
@@ -46,10 +57,6 @@ tracing-subscriber = "0.3.19"
|
||||
default-features = false
|
||||
version = "0.43.0"
|
||||
|
||||
[dependencies.nostr-keyring]
|
||||
default-features = false
|
||||
version = "0.43.0"
|
||||
|
||||
[dependencies.nostr-sdk]
|
||||
default-features = false
|
||||
version = "0.43.0"
|
||||
@@ -64,7 +71,7 @@ tracing-subscriber = "0.3.19"
|
||||
version = "1.0.219"
|
||||
|
||||
[dependencies.tokio]
|
||||
features = ["macros", "rt-multi-thread"]
|
||||
features = ["macros", "rt-multi-thread", "io-std"]
|
||||
version = "1.47.1"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -20,12 +20,12 @@ Check the documentation at [n34.dev]
|
||||
## Features
|
||||
|
||||
- [X] Repository announcements
|
||||
- [ ] Repository state announcements
|
||||
- [X] Repository state announcements
|
||||
- [X] Patches (Send, fetch and list)
|
||||
- [X] Issues (Send, view and list)
|
||||
- [X] Replies
|
||||
- [X] Issues and patches status
|
||||
- [ ] Pull requests ([nostr-protocol/nips#1966])
|
||||
- [X] Pull requests ([nostr-protocol/nips#1966])
|
||||
- [X] Gossip Model ([NIP-65])
|
||||
- [X] Proof of Work ([NIP-13])
|
||||
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
||||
|
||||
@@ -55,7 +55,7 @@ filter_unconventional = false
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = '^.*!:.*$', group = "Breaking Change" },
|
||||
{ message = '^.*!:', group = "Breaking Change" },
|
||||
{ message = '^feat', group = "Added" },
|
||||
{ message = '^deprecate', group = "Deprecated" },
|
||||
{ message = '^remove', group = "Removed" },
|
||||
|
||||
@@ -15,20 +15,31 @@
|
||||
- [Manage Repositories](repo/README.md)
|
||||
- [Broadcast and Update a Git Repository](repo/announce.md)
|
||||
- [View Git Repository Details](repo/view.md)
|
||||
- [Repository State Announcements](repo/state.md)
|
||||
- [Reply to Issues and Patches](reply.md)
|
||||
- [Issue Management](issue/README.md)
|
||||
- [Create an Issue](issue/new.md)
|
||||
- [View an Issue By ID](issue/view.md)
|
||||
- [Reopen a Closed Issue](issue/reopen.md)
|
||||
- [Closes an Open Issue](issue/close.md)
|
||||
- [Resolves an Issue](issue/resolve.md)
|
||||
- [List Repositories Issues](issue/list.md)
|
||||
- [View an Issue](issue/view.md)
|
||||
- [Reopen an Issue](issue/reopen.md)
|
||||
- [Close an Issue](issue/close.md)
|
||||
- [Resolve an Issue](issue/resolve.md)
|
||||
- [List Issues](issue/list.md)
|
||||
- [Patch Management](patch/README.md)
|
||||
- [Send Patches to a Repository](patch/send.md)
|
||||
- [Fetch a Patch By ID](patch/fetch.md)
|
||||
- [Reopens a Closed or Drafted Patch](patch/reopen.md)
|
||||
- [Closes an Open or Drafted Patch](patch/close.md)
|
||||
- [Draft an Open Patch](patch/draft.md)
|
||||
- [Apply an Open Patch](patch/apply.md)
|
||||
- [Merge an Open Patch](patch/merge.md)
|
||||
- [List Repositories Patches](patch/list.md)
|
||||
- [Reply to Issues and Patches](reply.md)
|
||||
- [Fetch a Patch](patch/fetch.md)
|
||||
- [Close a Patch](patch/close.md)
|
||||
- [Convert to Draft](patch/draft.md)
|
||||
- [Reopen a Patch](patch/reopen.md)
|
||||
- [Mark as Applied](patch/apply.md)
|
||||
- [Mark as Merged](patch/merge.md)
|
||||
- [List Patches](patch/list.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)
|
||||
- [Mark as Merged](pr/merge.md)
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
The `n34` command-line tool accepts the following options:
|
||||
@@ -19,8 +31,8 @@ The `n34` command-line tool accepts the following options:
|
||||
- `-v`, `--verbose...`: Increases the logging verbosity. Can be used multiple
|
||||
times for more detail (e.g., `-v`, `-vv`).
|
||||
|
||||
**Note:** The `--secret-key` and `--bunker-url` options are mutually exclusive.
|
||||
You must provide exactly one signing method.
|
||||
**Note:** The `--secret-key`, `--nip07` and `--bunker-url` options are mutually
|
||||
exclusive. You must provide exactly one signing method.
|
||||
|
||||
## Multiple Repositories
|
||||
|
||||
|
||||
@@ -355,8 +355,8 @@
|
||||
<span class="feature-text">Repository announcements</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check pending"></span>
|
||||
<span class="feature-text pending">Repository state announcements</span>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Repository state announcements</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
@@ -375,8 +375,8 @@
|
||||
<span class="feature-text">Issues and patches status</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check pending"></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-check completed"></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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -400,7 +400,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text pending">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
||||
<span class="feature-text">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Closes an Open Issue
|
||||
# Close an Issue
|
||||
|
||||
> `n34 issue close` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<ISSUE_ID> The open issue id to close it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1632` (Close status) for the specified issue. The issue have to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List Repositories Issues
|
||||
# List Issues
|
||||
|
||||
> `n34 issue list` command
|
||||
|
||||
@@ -9,7 +9,7 @@ List the repositories issues
|
||||
Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> Maximum number of issues to list [default: 15]
|
||||
|
||||
@@ -9,7 +9,7 @@ Create a new repository issue
|
||||
Usage: n34 issue new [OPTIONS] <--content <CONTENT>|--editor>
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-c, --content <CONTENT> Markdown content for the issue. Cannot be used together with the `--editor` flag
|
||||
-e, --editor Opens the user's default editor to write issue content. The first line will be used as the issue subject
|
||||
--subject <SUBJECT> The issue subject. Cannot be used together with the `--editor` flag
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Reopen a Closed Issue
|
||||
# Reopen an Issue
|
||||
|
||||
> `n34 issue reopen` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<ISSUE_ID> The ID of the closed issue to reopen
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1630` (Open status) for the specified issue. The issue have to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Resolves an Issue
|
||||
# Resolve an Issue
|
||||
|
||||
> `n34 issue resolve` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<ISSUE_ID> The issue id to resolve it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1631` (Resolved status) event for the specified issue.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# View an Issue By ID
|
||||
# View an Issue
|
||||
|
||||
> `n34 issue view` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<ISSUE_ID> The issue id to view it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Simply provide the issue ID in `note` or `nevent` format to retrieve and display
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Apply an Open Patch
|
||||
# Mark as Applied
|
||||
|
||||
> `n34 patch apply` command
|
||||
|
||||
@@ -10,10 +10,10 @@ Usage: n34 patch apply [OPTIONS] <PATCH_ID> [APPLIED_COMMITS]...
|
||||
|
||||
Arguments:
|
||||
<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:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--patches <PATCH-EVENT-ID> Patches that have been applied. Use this when only some patches have been applied, not all
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Closes an Open or Drafted Patch
|
||||
# Close a Patch
|
||||
|
||||
> `n34 patch close` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<PATCH_ID> The open/drafted patch id to close it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1632` (Close status) for the specified patch. The patch have to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Draft an Open Patch
|
||||
# Convert to Draft
|
||||
|
||||
> `n34 patch draft` command
|
||||
|
||||
@@ -12,7 +12,7 @@ Arguments:
|
||||
<PATCH_ID> The open patch id to draft it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1633` (Draft status) for the specified patch. The patch have to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Fetch a Patch By ID
|
||||
# Fetch a Patch
|
||||
|
||||
> `n34 patch fetch` command
|
||||
|
||||
@@ -12,10 +12,20 @@ Arguments:
|
||||
<PATCH_ID> The patch id to fetch it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
-o, --output <PATH> Output directory for the patches. Default to the current directory
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-o, --output <PATH> Directory for saving patches. Defaults to the current directory. Use `-` for stdout output
|
||||
```
|
||||
|
||||
Fetches patches using their original patch ID. All fetched patches will be saved
|
||||
to the specified output directory (current directory by default). You can then
|
||||
apply or merge these patches into your branch as needed.
|
||||
|
||||
You can also write patches to stdout for direct application with tools like
|
||||
`git-am`. For instance, to fetch and apply patches use:
|
||||
|
||||
```bash
|
||||
n34 patch fetch -o '-' <note1...> | git am --empty=drop
|
||||
```
|
||||
|
||||
The `--empty=drop` option ensures empty commits, such as cover letters, are
|
||||
omitted.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# List Repositories Patches
|
||||
# List Patches
|
||||
|
||||
> `n34 patch list` command
|
||||
|
||||
@@ -9,7 +9,7 @@ List the repositories patches
|
||||
Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> Maximum number of patches to list [default: 15]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Merge an Open Patch
|
||||
# Mark as Merged
|
||||
|
||||
> `n34 patch merge` command
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
<MERGE_COMMIT> The merge commit id
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--patches <PATCH-EVENT-ID> Patches that have been merged. Use this when only some patches have been merged, not all
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Reopens a Closed or Drafted Patch
|
||||
# Reopen a Patch
|
||||
|
||||
> `n34 patch reopen` command
|
||||
|
||||
@@ -12,9 +12,9 @@ Arguments:
|
||||
<PATCH_ID> The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository 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.
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ Send one or more patches to a repository
|
||||
Usage: n34 patch send [OPTIONS] <PATCH-PATH>...
|
||||
|
||||
Arguments:
|
||||
<PATCH-PATH>... List of patch files to send (space separated)
|
||||
<PATCH-PATH>... Space-separated list of patch files to send. Use `-` to read from stdin.
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--original-patch <EVENT-ID> Original patch ID if this is a revision of it
|
||||
```
|
||||
|
||||
@@ -20,3 +20,10 @@ Send your generated patches to the repositories specified using the `--repo`
|
||||
option or retrieved from the `nostr-address` file. When submitting a revision
|
||||
of an existing patch, include the original patch ID to ensure it’s correctly
|
||||
referenced in your revision patch event.
|
||||
|
||||
You can also pass patches from stdin, retrieved from a website or directly from
|
||||
`git-format-patch`. Simply use `-` as the patch path. For example:
|
||||
|
||||
```bash
|
||||
git format-patch --stdout --base master master..HEAD | n34 patch send -
|
||||
```
|
||||
|
||||
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 @@
|
||||
# Mark as Merged
|
||||
|
||||
> `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.
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
|
||||
Options:
|
||||
--quote-to Quote the replied-to event in the editor
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-c, --comment <COMMENT> The comment (cannot be used with --editor)
|
||||
-e, --editor Open editor to write comment (cannot be used with --content)
|
||||
```
|
||||
|
||||
@@ -16,6 +16,7 @@ Options:
|
||||
-c, --clone <CLONE> URLs for cloning the repository
|
||||
-m, --maintainers <MAINTAINERS> Additional maintainers of the repository (besides yourself)
|
||||
-l, --label <LABEL> Labels to categorize the repository. Can be specified multiple times
|
||||
--personal-fork Indicates whether the announcement is a personal fork
|
||||
--force-id Skip kebab-case validation for the repository ID
|
||||
--address-file If set, creates a `nostr-address` file to enable automatic address discovery by n34
|
||||
```
|
||||
|
||||
29
docs/repo/state.md
Normal file
29
docs/repo/state.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Repository State Announcements
|
||||
|
||||
> `n34 repo state` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Repository state announcements
|
||||
|
||||
Usage: n34 repo state [OPTIONS] <HEAD>
|
||||
|
||||
Arguments:
|
||||
<HEAD> Name of the repository's primary branch, such as 'master' or 'main'
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--tags <TAGS> Tags to announce a state for, in the format `<tag-name>=<commit-id>`. Separated by comma
|
||||
--branches <BRANCHES> Branches to announce a state for, in the format `<branch-name>=<commit-id>`. Separated by comma
|
||||
```
|
||||
|
||||
This command allows you to announce your repository state, which is useful for
|
||||
pushing to permissionless git repositories like the [GRASP] relay. The relay
|
||||
will verify your repository state and permit pushing commits only if they match
|
||||
the announced state.
|
||||
|
||||
To get the commit ID that a branch or tag points to, use `git rev-parse
|
||||
<tag-or-branch-name>`. You can automate this process by creating a script to
|
||||
generate the required input for this command.
|
||||
|
||||
[GRASP]: https://ngit.dev/grasp
|
||||
@@ -9,7 +9,7 @@ View details of a nostr git repository
|
||||
Usage: n34 repo view [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
```
|
||||
|
||||
This command prints repository details to standard output. If no arguments
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Each set requires a unique name, provided as the final argument to the command.
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Removes an entire set, or specific repositories and relays from it.
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--override Replace existing relays/repositories instead of adding to them
|
||||
```
|
||||
|
||||
|
||||
14
flake.nix
14
flake.nix
@@ -20,15 +20,12 @@
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
# n34 development environment
|
||||
devShells.default = mkShell {
|
||||
packages = [
|
||||
cargo-msrv
|
||||
dbus
|
||||
git-cliff
|
||||
just
|
||||
mdbook
|
||||
nushell
|
||||
pkg-config
|
||||
taplo
|
||||
];
|
||||
|
||||
@@ -39,6 +36,7 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Build n34
|
||||
packages.default =
|
||||
let
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
@@ -50,14 +48,6 @@
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
src = lib.cleanSource ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
dbus
|
||||
];
|
||||
|
||||
meta = {
|
||||
inherit (manifest) description homepage;
|
||||
license = lib.licenses.gpl3Plus;
|
||||
|
||||
104
justfile
104
justfile
@@ -1,104 +0,0 @@
|
||||
# This justfile is for the contrbutors of this project, not for the end user.
|
||||
#
|
||||
# Requirements for this justfile:
|
||||
# - Linux distribution
|
||||
# - just (Of course) <https://github.com/casey/just>
|
||||
# - cargo (For the build and tests) <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
# - mdbook (<https://rust-lang.github.io/mdBook>)
|
||||
# - git-cliff (<https://git-cliff.org>)
|
||||
# - taplo (<https://taplo.tamasfe.dev/>)
|
||||
# - cargo-msrv (<https://github.com/foresterre/cargo-msrv>)
|
||||
# - nushell (<https://nushell.sh>)
|
||||
|
||||
set quiet
|
||||
set unstable
|
||||
set shell := ["/usr/bin/env", "bash", "-c"]
|
||||
set script-interpreter := ["/usr/bin/env", "nu"]
|
||||
|
||||
JUST_EXECUTABLE := "just -u -f " + justfile()
|
||||
header := "Available tasks:\n"
|
||||
BOOK_DEST_DIR := "dest"
|
||||
tag_change_body := '''{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
{{ group | upper_first }}
|
||||
|
||||
{% for commit in commits %}
|
||||
- {{ commit.message | split(pat="\n") | first | split(pat=":") | slice(start=1) | join(sep=":") | upper_first | trim }} - by {{ commit.author.name}}{% endfor %}{% endfor %}
|
||||
'''
|
||||
|
||||
export TZ := "UTC"
|
||||
|
||||
_default:
|
||||
@{{JUST_EXECUTABLE}} --list-heading "{{header}}" --list
|
||||
|
||||
# Run the CI
|
||||
ci: && msrv _done_ci
|
||||
echo "🔨 Building n34..."
|
||||
cargo build -q
|
||||
echo "🔍 Checking code formatting..."
|
||||
cargo fmt -q -- --check
|
||||
RUST_LOG=none taplo fmt --check --config "./.taplo.toml" || (echo "❌Toml files is not properly formatted" && exit 1)
|
||||
echo "🧹 Running linter checks..."
|
||||
cargo clippy -q -- -D warnings
|
||||
echo "🧪 Running tests..."
|
||||
cargo test -q
|
||||
|
||||
# Check that the current MSRV is correct
|
||||
msrv:
|
||||
echo "🔧 Verifying MSRV..."
|
||||
cargo-msrv verify
|
||||
echo "✅ MSRV verification passed"
|
||||
|
||||
_done_ci:
|
||||
echo "🎉 CI pipeline completed successfully"
|
||||
|
||||
# Update the changelog
|
||||
[script]
|
||||
changelog:
|
||||
def get_hash [] { open "./CHANGELOG.md" | hash sha256 }
|
||||
|
||||
let old_hash = get_hash
|
||||
git-cliff out> "CHANGELOG.md"
|
||||
|
||||
if old_hash != get_hash {
|
||||
git add "CHANGELOG.md"
|
||||
git commit -m "chore(changelog): Update the changelog"
|
||||
print "The changes have been added to the changelog file and committed"
|
||||
} else {
|
||||
print "No changes have been added to the changelog"
|
||||
}
|
||||
|
||||
# Releases a new version of n34. Requires a clean file tree with no uncommitted changes.
|
||||
[script]
|
||||
release version:
|
||||
let tag_msg = "Version {{ version }}" + (git-cliff --strip all --unreleased --body '{{ tag_change_body }}')
|
||||
mut cargo_file = open "Cargo.toml"
|
||||
|
||||
$cargo_file.package.version = "{{ version }}"
|
||||
$cargo_file | save -f "Cargo.toml"
|
||||
|
||||
RUST_LOG=none taplo fmt --config "./.taplo.toml"
|
||||
{{ JUST_EXECUTABLE }} ci
|
||||
git-cliff -t "v{{ version }}" out> "./CHANGELOG.md"
|
||||
git add .
|
||||
git commit -m "chore: Bump the version to `v{{ version }}`"
|
||||
git tag -s -m $tag_msg "v{{ version }}"
|
||||
git push origin master --tags
|
||||
cargo publish
|
||||
|
||||
# Deploy the book to Github Pages
|
||||
deploy:
|
||||
mdbook build --dest-dir {{ BOOK_DEST_DIR }}
|
||||
cd {{ BOOK_DEST_DIR }}
|
||||
git init .
|
||||
git checkout -B gh-pages
|
||||
touch ".nojekyll"
|
||||
echo "n34.dev" > "CNAME"
|
||||
|
||||
git add .
|
||||
git commit -m "Deploy the book to github pages"
|
||||
git remote add origin "git@github.com:TheAwiteb/n34-book"
|
||||
git push origin gh-pages -f
|
||||
cd ..
|
||||
rm -fr {{ BOOK_DEST_DIR }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -17,8 +17,9 @@
|
||||
use clap::{ArgGroup, Args};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, CliOptions, traits::CommandRunner},
|
||||
cli::{CliOptions, traits::CommandRunner},
|
||||
error::N34Result,
|
||||
nostr_keyring,
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -46,8 +47,6 @@ impl CommandRunner for KeyringArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);
|
||||
|
||||
if self.enable {
|
||||
options.config.keyring_secret_key = true;
|
||||
} else if self.disable {
|
||||
@@ -55,7 +54,7 @@ impl CommandRunner for KeyringArgs {
|
||||
}
|
||||
|
||||
if self.reset || self.disable {
|
||||
let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);
|
||||
let _ = nostr_keyring::user::delete().await;
|
||||
}
|
||||
|
||||
options.config.dump()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -28,11 +28,17 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CloseArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open issue id to close it
|
||||
issue_id: NostrEvent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,17 +19,24 @@ use std::num::NonZeroUsize;
|
||||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
||||
cli::{
|
||||
CliOptions,
|
||||
common_commands,
|
||||
traits::CommandRunner,
|
||||
types::{EntityType, NaddrOrSet},
|
||||
},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ListArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", value_delimiter = ',')]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// Maximum number of issues to list
|
||||
#[arg(long, default_value = "15")]
|
||||
@@ -40,7 +47,11 @@ impl CommandRunner for ListArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
common_commands::list_patches_and_issues(options, self.naddrs, false, self.limit.into())
|
||||
.await
|
||||
common_commands::list_pr_patches_and_issues::<{ EntityType::Issue as u8 }>(
|
||||
options,
|
||||
self.naddrs,
|
||||
self.limit.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -46,17 +46,17 @@ pub const ISSUE_ALT_PREFIX: &str = "git issue: ";
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum IssueSubcommands {
|
||||
/// Create a new repository issue
|
||||
/// Create an issue
|
||||
New(NewArgs),
|
||||
/// View an issue by its ID
|
||||
/// View an issue
|
||||
View(ViewArgs),
|
||||
/// Reopens a closed issue.
|
||||
/// Reopen an issue.
|
||||
Reopen(ReopenArgs),
|
||||
/// Closes an open issue.
|
||||
/// Closes an issue.
|
||||
Close(CloseArgs),
|
||||
/// Resolves an issue.
|
||||
/// Resolve an issue.
|
||||
Resolve(ResolveArgs),
|
||||
/// List the repositories issues.
|
||||
/// List issues.
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
@@ -72,9 +72,19 @@ pub enum IssueStatus {
|
||||
}
|
||||
|
||||
impl IssueStatus {
|
||||
/// Returns all issue statuses as kinds
|
||||
#[inline]
|
||||
pub const fn all_kinds() -> [Kind; 3] {
|
||||
[
|
||||
Self::Open.kind(),
|
||||
Self::Resolved.kind(),
|
||||
Self::Closed.kind(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Maps the issue status to its corresponding Nostr kind.
|
||||
#[inline]
|
||||
pub fn kind(&self) -> Kind {
|
||||
pub const fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Open => Kind::GitStatusOpen,
|
||||
Self::Resolved => Kind::GitStatusApplied,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -37,33 +37,34 @@ use crate::{
|
||||
/// Arguments for the `issue new` command
|
||||
#[derive(Args, Debug)]
|
||||
#[clap(
|
||||
group(
|
||||
ArgGroup::new("issue-content")
|
||||
.args(["content", "editor"])
|
||||
.required(true)
|
||||
),
|
||||
group(
|
||||
ArgGroup::new("issue-subject")
|
||||
.args(["editor", "subject"])
|
||||
.required(true)
|
||||
)
|
||||
)]
|
||||
pub struct NewArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// Markdown content for the issue. Cannot be used together with the
|
||||
/// `--editor` flag.
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, group = "issue-content")]
|
||||
content: Option<String>,
|
||||
/// Opens the user's default editor to write issue content. The first line
|
||||
/// will be used as the issue subject.
|
||||
#[arg(short, long)]
|
||||
#[arg(short, long, group = "issue-subject", group = "issue-content")]
|
||||
editor: bool,
|
||||
/// The issue subject. Cannot be used together with the `--editor` flag.
|
||||
#[arg(long)]
|
||||
#[arg(long, group = "issue-subject")]
|
||||
subject: Option<String>,
|
||||
/// Labels for the issue. Can be specified as arguments (-l bug) or hashtags
|
||||
/// in content (#bug).
|
||||
@@ -71,29 +72,6 @@ pub struct NewArgs {
|
||||
label: Vec<String>,
|
||||
}
|
||||
|
||||
impl NewArgs {
|
||||
/// Returns the subject and the content of the issue. (subject, content)
|
||||
pub fn issue_content(&self) -> N34Result<(Option<String>, String)> {
|
||||
if let Some(content) = self.content.as_ref() {
|
||||
if let Some(subject) = self.subject.as_ref() {
|
||||
return Ok((Some(subject.trim().to_owned()), content.trim().to_owned()));
|
||||
}
|
||||
return Ok((None, content.trim().to_owned()));
|
||||
}
|
||||
// If the `self.content` is `None` then the `self.editor` is `true`
|
||||
let file_content = utils::read_editor(None, ".md")?;
|
||||
if file_content.contains('\n') {
|
||||
Ok(file_content
|
||||
.split_once('\n')
|
||||
.map(|(s, c)| (Some(s.trim().to_owned()), c.trim().to_owned()))
|
||||
.expect("There is a new line"))
|
||||
} else {
|
||||
tracing::info!("File content contains only issue body without a subject line");
|
||||
Ok((None, file_content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunner for NewArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||
@@ -113,29 +91,42 @@ impl CommandRunner for NewArgs {
|
||||
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||
.await;
|
||||
|
||||
let (subject, content) = self.issue_content()?;
|
||||
let content_details = client.parse_content(&content).await;
|
||||
let (subject, content) = utils::subject_and_body(self.subject, self.content, ".md")?;
|
||||
|
||||
let event =
|
||||
EventBuilder::new_git_issue(coordinates.as_slice(), content, subject, self.label)?
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(content_details.clone().into_tags())
|
||||
.build(user_pubk);
|
||||
let content_details = if let Some(content) = &content {
|
||||
Some(client.parse_content(content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let event = EventBuilder::new_git_issue(
|
||||
coordinates.as_slice(),
|
||||
content.unwrap_or_default(),
|
||||
Some(subject),
|
||||
self.label,
|
||||
)?
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(
|
||||
content_details
|
||||
.clone()
|
||||
.map(|c| c.into_tags())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build(user_pubk);
|
||||
let event_id = event.id.expect("There is an id");
|
||||
|
||||
let write_relays = [
|
||||
relays,
|
||||
naddrs.extract_relays(),
|
||||
utils::add_write_relays(relays_list.as_ref()),
|
||||
client
|
||||
.fetch_repos(&naddrs.into_coordinates())
|
||||
.await?
|
||||
.extract_relays(),
|
||||
repos.extract_relays(),
|
||||
// Include read relays for each maintainer (if found)
|
||||
client.read_relays_from_users(&maintainers).await,
|
||||
content_details.write_relays.clone().into_iter().collect(),
|
||||
content_details
|
||||
.map(|c| c.write_relays.into_iter().collect())
|
||||
.unwrap_or_default(),
|
||||
]
|
||||
.concat();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -28,11 +28,17 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ReopenArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The closed issue id to reopen it
|
||||
issue_id: NostrEvent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -28,11 +28,17 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ResolveArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The issue id to resolve it
|
||||
issue_id: NostrEvent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,29 +15,29 @@
|
||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||
|
||||
use clap::Args;
|
||||
use nostr::{event::Kind, filter::Filter};
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::{
|
||||
NostrClient,
|
||||
traits::{GitIssueUtils, NaddrsUtils, ReposUtils},
|
||||
utils,
|
||||
},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ViewArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The issue id to view it
|
||||
issue_id: NostrEvent,
|
||||
@@ -47,47 +47,7 @@ impl CommandRunner for ViewArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
let 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;
|
||||
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
client.add_relays(&self.issue_id.relays).await;
|
||||
client
|
||||
.add_relays(
|
||||
&client
|
||||
.fetch_repos(&naddrs.into_coordinates())
|
||||
.await?
|
||||
.extract_relays(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let issue = client
|
||||
.fetch_event(
|
||||
Filter::new()
|
||||
.id(self.issue_id.event_id)
|
||||
.kind(Kind::GitIssue),
|
||||
)
|
||||
.await?
|
||||
.ok_or(N34Error::CanNotFoundIssue)?;
|
||||
|
||||
let issue_subject = utils::smart_wrap(issue.extract_issue_subject(), 70);
|
||||
let issue_author = client.get_username(issue.pubkey).await;
|
||||
let mut issue_labels = utils::smart_wrap(&issue.extract_issue_labels(), 70);
|
||||
|
||||
if issue_labels.is_empty() {
|
||||
issue_labels = "\n".to_owned();
|
||||
} else {
|
||||
issue_labels = format!("{issue_labels}\n\n")
|
||||
}
|
||||
|
||||
println!(
|
||||
"{issue_subject} - [by {issue_author}]\n{issue_labels}{}",
|
||||
utils::smart_wrap(&issue.content, 80)
|
||||
);
|
||||
Ok(())
|
||||
crate::cli::common_commands::view_pr_issue::<false>(options, self.naddrs, self.issue_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,6 +20,8 @@ pub mod config;
|
||||
pub mod issue;
|
||||
/// `patch` subcommands
|
||||
pub mod patch;
|
||||
/// `pr` subcommands
|
||||
pub mod pr;
|
||||
/// `reply` command
|
||||
pub mod reply;
|
||||
/// `repo` subcommands
|
||||
@@ -40,6 +42,7 @@ use nostr_connect::client::NostrConnect;
|
||||
use self::config::ConfigSubcommands;
|
||||
use self::issue::IssueSubcommands;
|
||||
use self::patch::PatchSubcommands;
|
||||
use self::pr::PrSubcommands;
|
||||
use self::reply::ReplyArgs;
|
||||
use self::repo::RepoSubcommands;
|
||||
use self::sets::SetsSubcommands;
|
||||
@@ -73,8 +76,8 @@ pub struct CliOptions {
|
||||
#[arg(short = '7', long, group = "signer")]
|
||||
pub nip07: bool,
|
||||
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||
/// passed.
|
||||
#[arg(short, long)]
|
||||
/// passed, separated by commas.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub relays: Vec<RelayOrSet>,
|
||||
/// Proof of Work difficulty when creatring events
|
||||
#[arg(long, value_name = "DIFFICULTY")]
|
||||
@@ -113,6 +116,11 @@ pub enum Commands {
|
||||
#[command(subcommand)]
|
||||
subcommands: PatchSubcommands,
|
||||
},
|
||||
/// Manage pull requests
|
||||
Pr {
|
||||
#[command(subcommand)]
|
||||
subcommands: PrSubcommands,
|
||||
},
|
||||
/// Manage configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
@@ -150,7 +158,7 @@ impl CliOptions {
|
||||
if let Some(ref bunker_url) = self.bunker_url {
|
||||
let mut nostrconnect = NostrConnect::new(
|
||||
bunker_url.clone(),
|
||||
Cli::n34_keypair()?,
|
||||
Cli::n34_keypair().await?,
|
||||
BUNKER_TIMEOUT,
|
||||
None,
|
||||
)
|
||||
@@ -172,8 +180,7 @@ impl CliOptions {
|
||||
|
||||
/// Returns an error if there are no signers
|
||||
pub fn ensure_signer(&self) -> N34Result<()> {
|
||||
if !self.config.keyring_secret_key && self.secret_key.is_none() && self.bunker_url.is_none()
|
||||
{
|
||||
if self.secret_key.is_none() && self.bunker_url.is_none() && !self.nip07 {
|
||||
return Err(N34Error::SignerRequired);
|
||||
}
|
||||
Ok(())
|
||||
@@ -185,6 +192,7 @@ impl fmt::Debug for CliOptions {
|
||||
f.debug_struct("CliOptions")
|
||||
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
||||
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
||||
.field("nip07", &self.nip07)
|
||||
.field("relays", &self.relays)
|
||||
.field("pow", &self.pow)
|
||||
.field("config", &self.config)
|
||||
@@ -197,6 +205,6 @@ impl CommandRunner for Commands {
|
||||
tracing::trace!("Options: {options:#?}");
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -17,23 +17,28 @@
|
||||
use clap::Args;
|
||||
use nostr::hashes::sha1::Hash as Sha1Hash;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::{CommandRunner, VecNostrEventExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ApplyArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open patch id to apply it. Must be orignal root patch or
|
||||
/// revision root
|
||||
@@ -42,18 +47,18 @@ pub struct ApplyArgs {
|
||||
/// been applied, not all.
|
||||
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||
applied_patches: Vec<NostrEvent>,
|
||||
/// The applied commits
|
||||
#[arg(num_args = 1..)]
|
||||
/// 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_status_command(
|
||||
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::MergedApplied,
|
||||
PatchPrStatus::MergedApplied,
|
||||
Some(either::Either::Right(self.applied_commits)),
|
||||
self.applied_patches.into_event_ids(),
|
||||
|patch_status| {
|
||||
@@ -70,9 +75,7 @@ impl CommandRunner for ApplyArgs {
|
||||
}
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply a drafted patch".to_owned(),
|
||||
));
|
||||
return Err(N34Error::InvalidStatus("Cannot apply a draft".to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,23 +16,28 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CloseArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open/drafted patch id to close it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
@@ -40,11 +45,11 @@ pub struct CloseArgs {
|
||||
|
||||
impl CommandRunner for CloseArgs {
|
||||
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,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Closed,
|
||||
PatchPrStatus::Closed,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,23 +16,28 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct DraftArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open patch id to draft it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
@@ -40,17 +45,17 @@ pub struct DraftArgs {
|
||||
|
||||
impl CommandRunner for DraftArgs {
|
||||
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,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Draft,
|
||||
PatchPrStatus::Draft,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft an already drafted patch".to_owned(),
|
||||
"You can't draft an already draft patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -14,11 +14,7 @@
|
||||
// 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::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{fs, ops::Deref, path::PathBuf, str::FromStr};
|
||||
|
||||
use clap::Args;
|
||||
use nostr::{
|
||||
@@ -43,13 +39,20 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct FetchArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// Output directory for the patches. Default to the current directory
|
||||
/// Directory for saving patches. Defaults to the current directory. Use `-`
|
||||
/// for stdout output.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
output: Option<PathBuf>,
|
||||
/// The patch id to fetch it
|
||||
@@ -67,6 +70,10 @@ impl CommandRunner for FetchArgs {
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
let output_path = self.output.unwrap_or_default();
|
||||
let (is_stdout, is_current_dir) = {
|
||||
let str_path = output_path.to_string_lossy();
|
||||
(str_path.deref() == "-", str_path.deref().is_empty())
|
||||
};
|
||||
|
||||
client
|
||||
.add_relays(
|
||||
@@ -111,7 +118,7 @@ impl CommandRunner for FetchArgs {
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let patch = super::GitPatch::from_str(&p.content).map_err(|err| {
|
||||
N34Error::InvalidEvent(format!(
|
||||
N34Error::InvalidPatch(format!(
|
||||
"Failed to parse the patch `{}`: {err}",
|
||||
p.id.to_bech32().expect("Infallible")
|
||||
))
|
||||
@@ -123,13 +130,29 @@ impl CommandRunner for FetchArgs {
|
||||
patches.sort_unstable_by_key(|p| p.0.clone());
|
||||
patches.dedup_by_key(|p| p.0.clone());
|
||||
|
||||
if output_path.as_path() != Path::new("") && !output_path.exists() {
|
||||
if !is_stdout && !is_current_dir && !output_path.exists() {
|
||||
fs::create_dir_all(&output_path)?;
|
||||
}
|
||||
|
||||
for (patch_path, patch) in patches {
|
||||
tracing::info!("Writeing `{}` in `{}`", patch.subject, patch_path.display());
|
||||
fs::write(patch_path, patch.inner)?;
|
||||
if is_stdout {
|
||||
tracing::info!(
|
||||
"Writeing {} patch{} to the stdout",
|
||||
patches.len(),
|
||||
if patches.len() >= 2 { "es" } else { "" }
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
patches
|
||||
.into_iter()
|
||||
.map(|(_, patch)| patch.inner)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
);
|
||||
} else {
|
||||
for (patch_path, patch) in patches {
|
||||
tracing::info!("Writeing `{}` in `{}`", patch.subject, patch_path.display());
|
||||
fs::write(patch_path, patch.inner)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,17 +19,24 @@ use std::num::NonZeroUsize;
|
||||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
||||
cli::{
|
||||
CliOptions,
|
||||
common_commands,
|
||||
traits::CommandRunner,
|
||||
types::{EntityType, NaddrOrSet},
|
||||
},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ListArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", value_delimiter = ',')]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// Maximum number of patches to list
|
||||
#[arg(long, default_value = "15")]
|
||||
@@ -40,7 +47,11 @@ impl CommandRunner for ListArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())
|
||||
.await
|
||||
common_commands::list_pr_patches_and_issues::<{ EntityType::Patch as u8 }>(
|
||||
options,
|
||||
self.naddrs,
|
||||
self.limit.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -17,23 +17,28 @@
|
||||
use clap::Args;
|
||||
use nostr::hashes::sha1::Hash as Sha1Hash;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::{CommandRunner, VecNostrEventExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct MergeArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open patch id to merge it. Must be orignal root patch or
|
||||
/// revision root
|
||||
@@ -48,17 +53,17 @@ pub struct MergeArgs {
|
||||
|
||||
impl CommandRunner for MergeArgs {
|
||||
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,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::MergedApplied,
|
||||
PatchPrStatus::MergedApplied,
|
||||
Some(either::Either::Left(self.merge_commit)),
|
||||
self.merged_patches.into_event_ids(),
|
||||
|patch_status| {
|
||||
if patch_status.is_merged_or_applied() {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -70,7 +75,7 @@ impl CommandRunner for MergeArgs {
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge a drafted patch".to_owned(),
|
||||
"You can't merge a draft patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -34,14 +34,12 @@ mod send;
|
||||
mod tests;
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use clap::Subcommand;
|
||||
use nostr::event::Kind;
|
||||
use regex::Regex;
|
||||
|
||||
use self::apply::ApplyArgs;
|
||||
@@ -55,6 +53,11 @@ use self::send::SendArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
/// Regular expression for checking the first line in the patch.
|
||||
pub static FROM_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^From [a-f0-9]{40} \w+ \w+ \d{1,2} \d{2}:\d{2}:\d{2} \d{4}$").unwrap()
|
||||
});
|
||||
|
||||
/// Regular expression for extracting the patch subject.
|
||||
static SUBJECT_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?m)^Subject: (.*(?:\n .*)*)").unwrap());
|
||||
@@ -79,21 +82,21 @@ pub const LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT: &str = "revision-root";
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum PatchSubcommands {
|
||||
/// Send one or more patches to a repository.
|
||||
/// Send patches to a repository
|
||||
Send(SendArgs),
|
||||
/// Fetches a patch by its id.
|
||||
/// Fetch a patch
|
||||
Fetch(FetchArgs),
|
||||
/// Closes an open or drafted patch.
|
||||
/// Close a patch
|
||||
Close(CloseArgs),
|
||||
/// Converts an open patch to draft state.
|
||||
/// Convert to draft
|
||||
Draft(DraftArgs),
|
||||
/// Reopens a closed or drafted patch.
|
||||
/// Reopen a patch
|
||||
Reopen(ReopenArgs),
|
||||
/// Set an open patch status to applied.
|
||||
/// Mark as applied
|
||||
Apply(ApplyArgs),
|
||||
/// Set an open patch status to merged.
|
||||
/// Mark as merged
|
||||
Merge(MergeArgs),
|
||||
/// List the repositories patches.
|
||||
/// List patches
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
@@ -108,92 +111,6 @@ pub struct GitPatch {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PatchStatus {
|
||||
/// The patch is currently open
|
||||
Open,
|
||||
/// The patch has been merged/applied
|
||||
MergedApplied,
|
||||
/// The patch has been closed
|
||||
Closed,
|
||||
/// A patch that has been drafted but not yet applied.
|
||||
Draft,
|
||||
}
|
||||
|
||||
impl PatchStatus {
|
||||
/// Maps the issue status to its corresponding Nostr kind.
|
||||
#[inline]
|
||||
pub fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Open => Kind::GitStatusOpen,
|
||||
Self::MergedApplied => Kind::GitStatusApplied,
|
||||
Self::Closed => Kind::GitStatusClosed,
|
||||
Self::Draft => Kind::GitStatusDraft,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string representation of the patch status.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "Open",
|
||||
Self::MergedApplied => "Merged/Applied",
|
||||
Self::Closed => "Closed",
|
||||
Self::Draft => "Draft",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the patch is open.
|
||||
#[inline]
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, Self::Open)
|
||||
}
|
||||
|
||||
/// Check if the patch is merged/applied.
|
||||
#[inline]
|
||||
pub fn is_merged_or_applied(&self) -> bool {
|
||||
matches!(self, Self::MergedApplied)
|
||||
}
|
||||
|
||||
/// Check if the patch is closed.
|
||||
#[inline]
|
||||
pub fn is_closed(&self) -> bool {
|
||||
matches!(self, Self::Closed)
|
||||
}
|
||||
|
||||
/// Check if the patch is drafted
|
||||
#[inline]
|
||||
pub fn is_drafted(&self) -> bool {
|
||||
matches!(self, Self::Draft)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PatchStatus> for Kind {
|
||||
fn from(status: &PatchStatus) -> Self {
|
||||
status.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PatchStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TryFrom<Kind> for PatchStatus {
|
||||
type Error = N34Error;
|
||||
|
||||
fn try_from(kind: Kind) -> Result<Self, Self::Error> {
|
||||
match kind {
|
||||
Kind::GitStatusOpen => Ok(Self::Open),
|
||||
Kind::GitStatusApplied => Ok(Self::MergedApplied),
|
||||
Kind::GitStatusClosed => Ok(Self::Closed),
|
||||
Kind::GitStatusDraft => Ok(Self::Draft),
|
||||
_ => Err(N34Error::InvalidPatchStatus(kind)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GitPatch {
|
||||
/// Returns the patch file name from the subject
|
||||
pub fn filename(&self, parent: impl AsRef<Path>) -> N34Result<PathBuf> {
|
||||
@@ -226,6 +143,14 @@ impl FromStr for GitPatch {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(patch_content: &str) -> Result<Self, Self::Err> {
|
||||
if !patch_content
|
||||
.split("\n")
|
||||
.next()
|
||||
.is_some_and(|line| FROM_RE.is_match(line))
|
||||
{
|
||||
return Err("The first line must start with 'From '.".to_owned());
|
||||
}
|
||||
|
||||
// Regex for subject (handles multi-line subjects)
|
||||
let subject = SUBJECT_RE
|
||||
.captures(patch_content)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,23 +16,28 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
use super::PatchStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ReopenArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
@@ -40,11 +45,11 @@ pub struct ReopenArgs {
|
||||
|
||||
impl CommandRunner for ReopenArgs {
|
||||
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,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchStatus::Open,
|
||||
PatchPrStatus::Open,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -31,7 +31,7 @@ use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
|
||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt, VecPatchesExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::N34Result,
|
||||
@@ -47,18 +47,24 @@ const PATCH_ALT_PREFIX: &str = "git patch: ";
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SendArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// List of patch files to send (space separated).
|
||||
/// Space-separated list of patch files to send. Use `-` to read from stdin.
|
||||
///
|
||||
/// For p-tagging users, include them in the cover letter with
|
||||
/// For p-tagging users, include them in the cover letter using
|
||||
/// `nostr:npub1...`.
|
||||
#[arg(value_name = "PATCH-PATH", required = true, value_parser = parse_patch_path)]
|
||||
patches: Vec<GitPatch>,
|
||||
patches: Vec<Option<GitPatch>>,
|
||||
/// Original patch ID if this is a revision of it
|
||||
#[arg(long, value_name = "EVENT-ID")]
|
||||
original_patch: Option<NostrEvent>,
|
||||
@@ -71,6 +77,7 @@ impl CommandRunner for SendArgs {
|
||||
&utils::nostr_address_path()?,
|
||||
)?)?;
|
||||
|
||||
let patches = self.patches.process_patches().await?;
|
||||
let repo_coordinates = naddrs.clone().into_coordinates();
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
@@ -91,7 +98,7 @@ impl CommandRunner for SendArgs {
|
||||
|
||||
let (events, events_write_relays) = make_patch_series(
|
||||
&client,
|
||||
self.patches,
|
||||
patches,
|
||||
self.original_patch.as_ref().map(|e| e.event_id),
|
||||
repos.extract_relays().first().cloned(),
|
||||
repo_coordinates,
|
||||
@@ -139,11 +146,20 @@ impl CommandRunner for SendArgs {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_patch_path(patch_path: &str) -> Result<GitPatch, String> {
|
||||
/// If the patch path is '-', it indicates that patches will be retrieved from
|
||||
/// stdin. Otherwise, it reads and parses the patch file from the specified
|
||||
/// path.
|
||||
fn parse_patch_path(patch_path: &str) -> Result<Option<GitPatch>, String> {
|
||||
if patch_path == "-" {
|
||||
tracing::info!("Reading patches from standard input");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::debug!("Parsing patch file `{patch_path}`");
|
||||
let patch_content = fs::read_to_string(patch_path)
|
||||
.map_err(|err| format!("Failed to read patch file `{patch_path}`: {err}"))?;
|
||||
GitPatch::from_str(&patch_content)
|
||||
|
||||
GitPatch::from_str(&patch_content).map(Option::Some)
|
||||
}
|
||||
|
||||
async fn make_patch_series(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
82
src/cli/commands/pr/apply.rs
Normal file
82
src/cli/commands/pr/apply.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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,
|
||||
traits::CommandRunner,
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
72
src/cli/commands/pr/close.rs
Normal file
72
src/cli/commands/pr/close.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
79
src/cli/commands/pr/draft.rs
Normal file
79
src/cli/commands/pr/draft.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
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 code 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
|
||||
}
|
||||
}
|
||||
82
src/cli/commands/pr/merge.rs
Normal file
82
src/cli/commands/pr/merge.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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,
|
||||
traits::CommandRunner,
|
||||
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
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 code 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 code 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(())
|
||||
}
|
||||
}
|
||||
73
src/cli/commands/pr/reopen.rs
Normal file
73
src/cli/commands/pr/reopen.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||
},
|
||||
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 code 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 code 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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -62,11 +62,17 @@ pub struct ReplyArgs {
|
||||
/// Quote the replied-to event in the editor
|
||||
#[arg(long)]
|
||||
quote_to: bool,
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The comment (cannot be used with --editor)
|
||||
#[arg(short, long)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,7 +18,11 @@ use std::{fs, io::Write};
|
||||
|
||||
use clap::Args;
|
||||
use futures::future;
|
||||
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
||||
use nostr::{
|
||||
event::{EventBuilder, Tag},
|
||||
key::PublicKey,
|
||||
types::Url,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},
|
||||
@@ -43,38 +47,42 @@ const NOSTR_ADDRESS_FILE_HEADER: &str = r##"# This file contains NIP-19 `naddr`
|
||||
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||
|
||||
"##;
|
||||
const PERSONAL_FORK_HASHTAG: &str = "personal-fork";
|
||||
|
||||
/// Arguments for the `repo announce` command
|
||||
#[derive(Args, Debug)]
|
||||
pub struct AnnounceArgs {
|
||||
/// Unique identifier for the repository in kebab-case.
|
||||
#[arg(long = "id")]
|
||||
repo_id: String,
|
||||
repo_id: String,
|
||||
/// A name for the repository.
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
name: Option<String>,
|
||||
/// A description for the repository.
|
||||
#[arg(short, long)]
|
||||
description: Option<String>,
|
||||
description: Option<String>,
|
||||
/// Webpage URLs for the repository (if provided by the git server).
|
||||
#[arg(short, long)]
|
||||
web: Vec<Url>,
|
||||
web: Vec<Url>,
|
||||
/// URLs for cloning the repository.
|
||||
#[arg(short, long)]
|
||||
clone: Vec<Url>,
|
||||
clone: Vec<Url>,
|
||||
/// Additional maintainers of the repository (besides yourself).
|
||||
#[arg(short, long)]
|
||||
maintainers: Vec<PublicKey>,
|
||||
maintainers: Vec<PublicKey>,
|
||||
/// Labels to categorize the repository. Can be specified multiple times.
|
||||
#[arg(short, long)]
|
||||
label: Vec<String>,
|
||||
label: Vec<String>,
|
||||
/// Skip kebab-case validation for the repository ID
|
||||
#[arg(long)]
|
||||
force_id: bool,
|
||||
force_id: bool,
|
||||
/// Indicates whether the announcement is a personal fork.
|
||||
#[arg(long)]
|
||||
personal_fork: bool,
|
||||
/// If set, creates a `nostr-address` file to enable automatic address
|
||||
/// discovery by n34
|
||||
#[arg(long)]
|
||||
address_file: bool,
|
||||
address_file: bool,
|
||||
}
|
||||
|
||||
impl CommandRunner for AnnounceArgs {
|
||||
@@ -94,7 +102,7 @@ impl CommandRunner for AnnounceArgs {
|
||||
}
|
||||
|
||||
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &relays)?;
|
||||
let event = EventBuilder::new_git_repo(
|
||||
let mut event_builder = EventBuilder::new_git_repo(
|
||||
self.repo_id,
|
||||
self.name.map(utils::str_trim),
|
||||
self.description.map(utils::str_trim),
|
||||
@@ -106,9 +114,13 @@ impl CommandRunner for AnnounceArgs {
|
||||
self.force_id,
|
||||
)?
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.build(user_pubk);
|
||||
.pow(options.pow.unwrap_or_default());
|
||||
|
||||
if self.personal_fork {
|
||||
event_builder = event_builder.tag(Tag::hashtag(PERSONAL_FORK_HASHTAG));
|
||||
}
|
||||
|
||||
let event = event_builder.build(user_pubk);
|
||||
|
||||
if self.address_file {
|
||||
let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,12 +16,15 @@
|
||||
|
||||
/// `repo announce` subcommand
|
||||
mod announce;
|
||||
/// `repo state` subcommand
|
||||
mod state;
|
||||
/// `repo view` subcommand
|
||||
mod view;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use self::announce::AnnounceArgs;
|
||||
use self::state::StateArgs;
|
||||
use self::view::ViewArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::N34Result;
|
||||
@@ -32,10 +35,12 @@ pub enum RepoSubcommands {
|
||||
View(ViewArgs),
|
||||
/// Broadcast and update a git repository
|
||||
Announce(AnnounceArgs),
|
||||
/// Repository state announcements
|
||||
State(StateArgs),
|
||||
}
|
||||
|
||||
impl CommandRunner for RepoSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & View Announce)
|
||||
crate::run_command!(self, options, & View Announce State)
|
||||
}
|
||||
}
|
||||
|
||||
155
src/cli/commands/repo/state.rs
Normal file
155
src/cli/commands/repo/state.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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::borrow::Cow;
|
||||
|
||||
use clap::Args;
|
||||
use nostr::event::{Kind, Tag, TagKind};
|
||||
use nostr::{event::EventBuilder, hashes::sha1::Hash as Sha1Hash};
|
||||
|
||||
use crate::nostr_utils::traits::ReposUtils;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
CommandRunner,
|
||||
parsers,
|
||||
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::NaddrOrSet,
|
||||
},
|
||||
error::N34Result,
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
};
|
||||
|
||||
/// Prefix for branch references in Git.
|
||||
const HEADS_REFS: &str = "refs/heads/";
|
||||
|
||||
/// Prefix for tag references in Git.
|
||||
const TAGS_REFS: &str = "refs/tags/";
|
||||
|
||||
/// Repository state announcements kind
|
||||
const REPO_STATE_KIND: Kind = Kind::Custom(30618);
|
||||
|
||||
/// `HEAD` tag kind
|
||||
const HEAD_TAG_KIND: TagKind = TagKind::Custom(Cow::Borrowed("HEAD"));
|
||||
|
||||
/// Arguments for the `repo state` command
|
||||
#[derive(Args, Debug)]
|
||||
pub struct StateArgs {
|
||||
/// 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>>,
|
||||
/// Tags to announce a state for, in the format `<tag-name>=<commit-id>`.
|
||||
/// Separated by comma.
|
||||
///
|
||||
/// Example: `v0.4.0=9aa3b62de02a63aa6a0d49efa7c484aa550cef56`.
|
||||
#[arg(long, value_delimiter = ',', value_parser = parsers::name_and_sha1)]
|
||||
tags: Vec<(String, Sha1Hash)>,
|
||||
/// Branches to announce a state for, in the format
|
||||
/// `<branch-name>=<commit-id>`. Separated by comma.
|
||||
///
|
||||
/// Example: `master=9aa3b62de02a63aa6a0d49efa7c484aa550cef56`.
|
||||
#[arg(long, value_delimiter = ',', value_parser = parsers::name_and_sha1)]
|
||||
branches: Vec<(String, Sha1Hash)>,
|
||||
/// Name of the repository's primary branch, such as 'master' or 'main'.
|
||||
head: String,
|
||||
}
|
||||
|
||||
impl CommandRunner for StateArgs {
|
||||
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 user_pubk = client.pubkey().await?;
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
|
||||
let repos = client
|
||||
.fetch_repos(&naddrs.clone().into_coordinates())
|
||||
.await?;
|
||||
let repos_id = repos
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("It's not empty, checked above")
|
||||
.id;
|
||||
|
||||
let mut event_builder = EventBuilder::new(REPO_STATE_KIND, "")
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tag(Tag::identifier(&repos_id))
|
||||
.tag(Tag::custom(
|
||||
HEAD_TAG_KIND,
|
||||
&[format!("ref: {HEADS_REFS}{}", self.head)],
|
||||
));
|
||||
|
||||
if !self.branches.is_empty() {
|
||||
event_builder = event_builder.tags(refs_tags::<true>(self.branches));
|
||||
}
|
||||
|
||||
if !self.tags.is_empty() {
|
||||
event_builder = event_builder.tags(refs_tags::<false>(self.tags));
|
||||
}
|
||||
|
||||
let event = event_builder.build(user_pubk);
|
||||
let event_id = event.id.expect("There is an id");
|
||||
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||
let write_relays = [
|
||||
relays,
|
||||
naddrs.extract_relays(),
|
||||
repos.extract_relays(),
|
||||
utils::add_write_relays(user_relays_list.as_ref()),
|
||||
// Include read relays for each maintainer (if found)
|
||||
client
|
||||
.read_relays_from_users(&repos.extract_maintainers())
|
||||
.await,
|
||||
]
|
||||
.concat();
|
||||
|
||||
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||
let success = client
|
||||
.send_event_to(event, user_relays_list.as_ref(), &write_relays)
|
||||
.await?;
|
||||
|
||||
let nevent = utils::new_nevent(event_id, &success)?;
|
||||
let naddr = utils::repo_naddr(repos_id, user_pubk, &success)?;
|
||||
println!("Event created: {nevent}");
|
||||
println!("State address: {naddr}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the refs tags
|
||||
#[inline]
|
||||
fn refs_tags<const IS_HEADS: bool>(refs: Vec<(String, Sha1Hash)>) -> impl IntoIterator<Item = Tag> {
|
||||
refs.into_iter().map(|(tag, commit)| {
|
||||
Tag::parse(&[
|
||||
format!("{}{tag}", if IS_HEADS { HEADS_REFS } else { TAGS_REFS }),
|
||||
commit.to_string(),
|
||||
])
|
||||
.expect("Not an empty tag")
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -14,8 +14,6 @@
|
||||
// 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::fmt;
|
||||
|
||||
use clap::Args;
|
||||
use nostr::nips::nip19::ToBech32;
|
||||
|
||||
@@ -33,11 +31,13 @@ use crate::{
|
||||
/// Arguments for the `repo view` command
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ViewArgs {
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
/// 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")]
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", value_delimiter = ',')]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
}
|
||||
|
||||
@@ -66,13 +66,16 @@ impl CommandRunner for ViewArgs {
|
||||
repo_details.push_str(&format!("\nDescription: {desc}"));
|
||||
}
|
||||
if !repo.web.is_empty() {
|
||||
repo_details.push_str(&format!("\nWebpages:\n{}", format_list(repo.web)));
|
||||
repo_details.push_str(&format!("\nWebpages:\n{}", utils::format_iter(repo.web)));
|
||||
}
|
||||
if !repo.clone.is_empty() {
|
||||
repo_details.push_str(&format!("\nClone urls:\n{}", format_list(repo.clone)));
|
||||
repo_details.push_str(&format!(
|
||||
"\nClone urls:\n{}",
|
||||
utils::format_iter(repo.clone)
|
||||
));
|
||||
}
|
||||
if !repo.relays.is_empty() {
|
||||
repo_details.push_str(&format!("\nRelays:\n{}", format_list(repo.relays)));
|
||||
repo_details.push_str(&format!("\nRelays:\n{}", utils::format_iter(repo.relays)));
|
||||
}
|
||||
if let Some(euc) = repo.euc {
|
||||
repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
|
||||
@@ -80,7 +83,7 @@ impl CommandRunner for ViewArgs {
|
||||
if !repo.maintainers.is_empty() {
|
||||
repo_details.push_str(&format!(
|
||||
"\nMaintainers:\n{}",
|
||||
format_list(
|
||||
utils::format_iter(
|
||||
repo.maintainers
|
||||
.iter()
|
||||
.map(|p| p.to_bech32().expect("Infallible"))
|
||||
@@ -94,16 +97,3 @@ impl CommandRunner for ViewArgs {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a vector to print it
|
||||
fn format_list<I, T>(iterator: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: fmt::Display,
|
||||
{
|
||||
iterator
|
||||
.into_iter()
|
||||
.map(|t| format!(" - {t}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -31,12 +31,18 @@ pub struct NewArgs {
|
||||
/// Unique name for the set
|
||||
name: String,
|
||||
/// Optional relay to add it to the set, either as URL or set name to
|
||||
/// extract its relays. [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"))]
|
||||
/// extract its relays, separated by commas. [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||
relays: Vec<RelayOrSet>,
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||
/// 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.
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Vec<NaddrOrSet>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -30,13 +30,19 @@ pub struct RemoveArgs {
|
||||
/// Set name to delete
|
||||
name: String,
|
||||
/// Specific relay to remove it from the set, either as URL or set name to
|
||||
/// extract its relays. [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"))]
|
||||
/// extract its relays, separated by commas. [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||
relays: Vec<RelayOrSet>,
|
||||
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||
/// 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.
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Vec<NaddrOrSet>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -31,13 +31,19 @@ use crate::{
|
||||
pub struct UpdateArgs {
|
||||
/// Name of the set to update
|
||||
name: String,
|
||||
/// Add relay to the set, either as URL or set name to extract its relays.
|
||||
/// [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"))]
|
||||
/// Add relay to the set, either as URL or set name to extract its relays,
|
||||
/// separated by commas. [aliases: `--sr`]
|
||||
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||
relays: Vec<RelayOrSet>,
|
||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||
/// 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.
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Vec<NaddrOrSet>,
|
||||
/// Replace existing relays/repositories instead of adding to them
|
||||
#[arg(long = "override")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,8 +19,8 @@ use std::{iter, str::FromStr, sync::Arc};
|
||||
use either::Either;
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},
|
||||
filter::Filter,
|
||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard},
|
||||
filter::{Alphabet, Filter, SingleLetterTag},
|
||||
hashes::sha1::Hash as Sha1Hash,
|
||||
nips::{nip10::Marker, nip19::ToBech32},
|
||||
types::RelayUrl,
|
||||
@@ -28,17 +28,19 @@ use nostr::{
|
||||
|
||||
use super::{
|
||||
issue::IssueStatus,
|
||||
patch::PatchStatus,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
};
|
||||
use crate::{
|
||||
cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
cli::{CliOptions, patch::GitPatch, types::PatchPrStatus},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::{GitIssuePrMetadata, GitPatchUtils, ReposUtils},
|
||||
};
|
||||
use crate::{
|
||||
cli::{CliOptions, patch::GitPatch},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::{GitIssueUtils, GitPatchUtils, ReposUtils},
|
||||
cli::{
|
||||
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::EntityType,
|
||||
},
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
};
|
||||
|
||||
/// Updates the issue's status to `new_status` after validating it with
|
||||
@@ -123,17 +125,20 @@ pub async fn issue_status_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the patch's status to `new_status` after validating it with
|
||||
/// `check_fn`.
|
||||
pub async fn patch_status_command(
|
||||
/// Updates the patch/pr's status to `new_status` after validating it with
|
||||
/// `check_fn`. The `ENTITY_TYPE` can only be a pull request or a patch
|
||||
pub async fn patch_pr_status_command<const ENTITY_TYPE: u8>(
|
||||
options: CliOptions,
|
||||
patch_id: NostrEvent,
|
||||
patch_pr_id: NostrEvent,
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
new_status: PatchStatus,
|
||||
new_status: PatchPrStatus,
|
||||
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
||||
merge_or_applied_patches: Vec<EventId>,
|
||||
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
|
||||
check_fn: impl FnOnce(&PatchPrStatus) -> N34Result<()>,
|
||||
) -> N34Result<()> {
|
||||
EntityType::is_pr_or_patch::<ENTITY_TYPE>();
|
||||
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||
|
||||
let naddrs = utils::naddrs_or_file(
|
||||
naddrs.flat_naddrs(&options.config.sets)?,
|
||||
&utils::nostr_address_path()?,
|
||||
@@ -142,7 +147,7 @@ pub async fn patch_status_command(
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
let user_pubk = client.pubkey().await?;
|
||||
client
|
||||
.add_relays(&[naddrs.extract_relays(), patch_id.relays].concat())
|
||||
.add_relays(&[naddrs.extract_relays(), patch_pr_id.relays].concat())
|
||||
.await;
|
||||
|
||||
let owners = naddrs.extract_owners();
|
||||
@@ -152,9 +157,14 @@ pub async fn patch_status_command(
|
||||
let relay_hint = repos.extract_relays().first().cloned();
|
||||
client.add_relays(&repos.extract_relays()).await;
|
||||
|
||||
let patch_event = client.fetch_patch(patch_id.event_id).await?;
|
||||
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(
|
||||
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
||||
'close', and 'draft' are not supported."
|
||||
@@ -162,27 +172,31 @@ pub async fn patch_status_command(
|
||||
));
|
||||
}
|
||||
|
||||
let (root_patch, root_revision) = get_patch_root_revision(&patch_event)?;
|
||||
let patch_status = client
|
||||
.fetch_patch_status(
|
||||
root_patch,
|
||||
root_revision,
|
||||
[maintainers.as_slice(), &[patch_event.pubkey], &owners].concat(),
|
||||
)
|
||||
.await?;
|
||||
let (root_patch_or_pr, root_revision) = if entity_type.is_patch() {
|
||||
get_patch_root_revision(&event)?
|
||||
} else {
|
||||
(event.id, None)
|
||||
};
|
||||
let current_status = if entity_type.is_patch() {
|
||||
client
|
||||
.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(), "")
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tag(utils::event_reply_tag(
|
||||
&root_patch,
|
||||
&root_patch_or_pr,
|
||||
relay_hint.as_ref(),
|
||||
Marker::Root,
|
||||
))
|
||||
.tag(Tag::public_key(patch_event.pubkey))
|
||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(owners.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(authorized_pubkeys.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(
|
||||
coordinates
|
||||
.into_iter()
|
||||
@@ -211,6 +225,7 @@ pub async fn patch_status_command(
|
||||
.tags(commits.into_iter().map(Tag::reference));
|
||||
};
|
||||
|
||||
// Patch only
|
||||
if let Some(root_revision) = root_revision {
|
||||
status_builder = status_builder.tag(utils::event_reply_tag(
|
||||
&root_revision,
|
||||
@@ -219,6 +234,7 @@ pub async fn patch_status_command(
|
||||
));
|
||||
}
|
||||
|
||||
// Patch only
|
||||
if !merge_or_applied_patches.is_empty() {
|
||||
status_builder = status_builder.tags(
|
||||
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
||||
@@ -228,7 +244,6 @@ pub async fn patch_status_command(
|
||||
}
|
||||
|
||||
let status_event = status_builder.dedup_tags().build(user_pubk);
|
||||
|
||||
let event_id = status_event.id.expect("There is an id");
|
||||
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||
let write_relays = [
|
||||
@@ -236,10 +251,7 @@ pub async fn patch_status_command(
|
||||
naddrs.extract_relays(),
|
||||
repos.extract_relays(),
|
||||
utils::add_write_relays(user_relays_list.as_ref()),
|
||||
client.read_relays_from_user(patch_event.pubkey).await,
|
||||
client
|
||||
.read_relays_from_users(&[maintainers, owners].concat())
|
||||
.await,
|
||||
client.read_relays_from_users(&authorized_pubkeys).await,
|
||||
]
|
||||
.concat();
|
||||
|
||||
@@ -252,13 +264,14 @@ pub async fn patch_status_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch and display patches and issues for given repositories.
|
||||
/// If `list_patches` is true, lists patches instead of issues.
|
||||
/// `limit` controls the maximum number of items to fetch.
|
||||
pub async fn list_patches_and_issues(
|
||||
/// Fetches and displays pull requests, patches, and issues for specified
|
||||
/// repositories. The `limit` parameter sets the maximum number of items to
|
||||
/// retrieve.
|
||||
///
|
||||
/// The `ENTITY_TYPE` const is `[EntityType]` enum as u8.
|
||||
pub async fn list_pr_patches_and_issues<const ENTITY_TYPE: u8>(
|
||||
options: CliOptions,
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
list_patches: bool,
|
||||
limit: usize,
|
||||
) -> N34Result<()> {
|
||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||
@@ -266,6 +279,7 @@ pub async fn list_patches_and_issues(
|
||||
&utils::nostr_address_path()?,
|
||||
)?)?;
|
||||
|
||||
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
@@ -279,18 +293,12 @@ pub async fn list_patches_and_issues(
|
||||
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
||||
.await;
|
||||
|
||||
let kind = if list_patches {
|
||||
Kind::GitPatch
|
||||
} else {
|
||||
Kind::GitIssue
|
||||
};
|
||||
|
||||
let mut filter = Filter::new()
|
||||
.coordinates(coordinates.iter())
|
||||
.kind(kind)
|
||||
.kind(entity_type.kind())
|
||||
.limit(limit);
|
||||
|
||||
if list_patches {
|
||||
if entity_type.is_patch() {
|
||||
filter = filter.hashtag("root");
|
||||
}
|
||||
|
||||
@@ -302,29 +310,40 @@ pub async fn list_patches_and_issues(
|
||||
arc_client
|
||||
.fetch_events(filter)
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|event| {
|
||||
let c = arc_client.clone();
|
||||
let keys = authorized_pubkeys.clone();
|
||||
async move {
|
||||
let status = if list_patches {
|
||||
let (root, root_revision) = get_patch_root_revision(&event)?;
|
||||
c.fetch_patch_status(
|
||||
root,
|
||||
root_revision,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.await
|
||||
.map(Either::Left)?
|
||||
} else {
|
||||
c.fetch_issue_status(
|
||||
event.id,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.await
|
||||
.map(Either::Right)?
|
||||
let status = match entity_type {
|
||||
EntityType::PullRequest => {
|
||||
c.fetch_pr_status(
|
||||
event.id,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.await
|
||||
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||
}
|
||||
EntityType::Patch => {
|
||||
let (root, root_revision) = get_patch_root_revision(&event)?;
|
||||
c.fetch_patch_status(
|
||||
root,
|
||||
root_revision,
|
||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||
)
|
||||
.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))
|
||||
}
|
||||
}),
|
||||
@@ -332,11 +351,11 @@ pub async fn list_patches_and_issues(
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|r| r.ok()),
|
||||
|(_, status)| status.as_ref().either_into::<Kind>(),
|
||||
|(_, (_, k))| *k,
|
||||
);
|
||||
|
||||
let lines = events
|
||||
.map(|(event, status)| format_patch_and_issue(&event, status))
|
||||
.map(|(event, (status, _))| format_entity::<ENTITY_TYPE>(&event, status))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let max_width = lines
|
||||
@@ -366,33 +385,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
|
||||
/// subject line from the Git patch format. For issues, combines the subject
|
||||
/// with labels. The output includes status and formatted ID.
|
||||
fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus>) -> String {
|
||||
let subject = if status.is_left() {
|
||||
GitPatch::from_str(&event.content)
|
||||
.map(|p| p.subject)
|
||||
.unwrap_or_else(|_| {
|
||||
event
|
||||
.content
|
||||
.lines()
|
||||
.find(|line| line.trim().starts_with("Subject: "))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_start_matches("Subject: ")
|
||||
.to_owned()
|
||||
})
|
||||
} else {
|
||||
let labels = event.extract_issue_labels();
|
||||
let subject = event.extract_issue_subject();
|
||||
/// Formats patch, issue or PR. For patches, extracts the
|
||||
/// subject line from the Git patch format. For issues and PRs, combines the
|
||||
/// subject with labels. The output includes status and formatted ID.
|
||||
fn format_entity<const ENTITY_TYPE: u8>(event: &Event, status: &str) -> String {
|
||||
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||
|
||||
if labels.is_empty() {
|
||||
subject.to_owned()
|
||||
} else {
|
||||
format!(r#""{subject}" {labels}"#)
|
||||
let subject = match entity_type {
|
||||
EntityType::Patch => {
|
||||
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()
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
// 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!(
|
||||
"({status}) {}\nID: {}\n",
|
||||
utils::smart_wrap(&subject, 85),
|
||||
@@ -436,3 +462,115 @@ async fn build_patches_quote(
|
||||
}))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn view_pr_issue<const IS_PR: bool>(
|
||||
options: CliOptions,
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
event_id: NostrEvent,
|
||||
) -> N34Result<()> {
|
||||
let naddrs = utils::naddrs_or_file(
|
||||
naddrs.flat_naddrs(&options.config.sets)?,
|
||||
&utils::nostr_address_path()?,
|
||||
)?;
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
client.add_relays(&event_id.relays).await;
|
||||
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
||||
client.add_relays(&repos.extract_relays()).await;
|
||||
|
||||
let event = client
|
||||
.fetch_event(Filter::new().id(event_id.event_id).kind(
|
||||
const {
|
||||
if IS_PR {
|
||||
crate::cli::pr::PR_KIND
|
||||
} else {
|
||||
Kind::GitIssue
|
||||
}
|
||||
},
|
||||
))
|
||||
.await?
|
||||
.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_author = client.get_username(event.pubkey).await;
|
||||
let mut event_labels = utils::smart_wrap(&event.extract_event_labels(), 70);
|
||||
|
||||
if event_labels.is_empty() {
|
||||
event_labels = "\n".to_owned();
|
||||
} else {
|
||||
event_labels = format!("{event_labels}\n\n")
|
||||
}
|
||||
|
||||
let pr_data = if IS_PR {
|
||||
// Check if there is an update
|
||||
let pr_update = client
|
||||
.fetch_event(
|
||||
Filter::new()
|
||||
.kind(crate::cli::pr::PR_UPDATE_KIND)
|
||||
.custom_tag(SingleLetterTag::uppercase(Alphabet::E), event.id)
|
||||
.author(event.pubkey),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let commit = pr_update
|
||||
.as_ref()
|
||||
.unwrap_or(&event)
|
||||
.tags
|
||||
.find(TagKind::single_letter(Alphabet::C, false))
|
||||
.and_then(|t| t.content())
|
||||
.unwrap_or("N/A");
|
||||
let branch = event
|
||||
.tags
|
||||
.find(TagKind::custom("branch-name"))
|
||||
.and_then(|t| t.content())
|
||||
.unwrap_or("N/A");
|
||||
let clones = pr_update
|
||||
.as_ref()
|
||||
.unwrap_or(&event)
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| t.as_standardized())
|
||||
.find_map(|t| {
|
||||
match t {
|
||||
TagStandard::GitClone(urls) if !urls.is_empty() => Some(urls.clone()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
"\nCommit: {commit}\nBranch: {branch}\nClone:\n{}",
|
||||
utils::format_iter(clones)
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
println!(
|
||||
"({status}) {event_subject} - [by {event_author}]\n{event_labels}{}{pr_data}",
|
||||
utils::smart_wrap(&event.content, 80)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -77,7 +77,7 @@ pub struct CliConfig {
|
||||
#[serde(default)]
|
||||
pub keyring_secret_key: bool,
|
||||
/// Signs events using the browser's NIP-07 extension.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nip07: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -32,6 +32,8 @@ pub mod parsers;
|
||||
pub mod traits;
|
||||
/// Common helper types used throughout the CLI.
|
||||
pub mod types;
|
||||
/// CLI utils
|
||||
pub mod utils;
|
||||
|
||||
|
||||
use clap::Parser;
|
||||
@@ -40,8 +42,6 @@ use nostr::key::Keys;
|
||||
use nostr::key::SecretKey;
|
||||
use nostr_browser_signer_proxy::BrowserSignerProxy;
|
||||
use nostr_browser_signer_proxy::BrowserSignerProxyOptions;
|
||||
use nostr_keyring::KeyringError;
|
||||
use nostr_keyring::NostrKeyring;
|
||||
use types::RelayOrSet;
|
||||
|
||||
pub use self::commands::*;
|
||||
@@ -50,7 +50,7 @@ use self::traits::CommandRunner;
|
||||
use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;
|
||||
use crate::error::N34Error;
|
||||
use crate::error::N34Result;
|
||||
use crate::nostr_utils::traits::NostrKeyringErrorUtils;
|
||||
use crate::nostr_keyring;
|
||||
|
||||
/// Header message, used in the help message
|
||||
const HEADER: &str = r#"Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
@@ -84,13 +84,6 @@ pub struct Cli {
|
||||
|
||||
|
||||
impl Cli {
|
||||
/// Keyring service name of n34
|
||||
pub const N34_KEYRING_SERVICE_NAME: &str = "n34";
|
||||
/// Keyring entry name of the n34 keypair
|
||||
pub const N34_KEY_PAIR_ENTRY: &str = "n34_keypair";
|
||||
/// Keyring entry name of the user secret key
|
||||
pub const USER_KEY_PAIR_ENTRY: &str = "user_keypair";
|
||||
|
||||
/// Executes the command
|
||||
pub async fn run(self) -> N34Result<()> {
|
||||
self.command.run(self.options).await
|
||||
@@ -98,33 +91,30 @@ impl Cli {
|
||||
|
||||
/// Gets the n34 keypair from the keyring or generates and stores a new one
|
||||
/// if none exists.
|
||||
pub fn n34_keypair() -> N34Result<Keys> {
|
||||
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
||||
|
||||
match keyring.get(Self::N34_KEY_PAIR_ENTRY) {
|
||||
pub async fn n34_keypair() -> N34Result<Keys> {
|
||||
match nostr_keyring::n34::get().await {
|
||||
Ok(keys) => Ok(keys),
|
||||
Err(nostr_keyring::Error::Keyring(KeyringError::NoEntry)) => {
|
||||
Err(N34Error::Keyring(keyring::Error::NoEntry)) => {
|
||||
let new_keys = Keys::generate();
|
||||
keyring.set(Self::N34_KEY_PAIR_ENTRY, &new_keys)?;
|
||||
nostr_keyring::n34::set(&new_keys).await?;
|
||||
Ok(new_keys)
|
||||
}
|
||||
Err(err) => Err(N34Error::Keyring(err)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the user's keypair from the keyring. If no key exists and one
|
||||
/// is provided, stores and returns it. If no key exists and none is
|
||||
/// provided, returns an error.
|
||||
pub fn user_keypair(secret_key: Option<SecretKey>) -> N34Result<Keys> {
|
||||
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
||||
let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY);
|
||||
pub async fn user_keypair(secret_key: Option<SecretKey>) -> N34Result<Keys> {
|
||||
let keyring_key = nostr_keyring::user::get().await;
|
||||
|
||||
if let Err(ref err) = keyring_key
|
||||
&& err.is_keyring_no_entry()
|
||||
&& let Some(secret_key) = secret_key
|
||||
{
|
||||
let keypair = Keys::new(secret_key);
|
||||
keyring.set(Self::USER_KEY_PAIR_ENTRY, &keypair)?;
|
||||
nostr_keyring::user::set(&keypair).await?;
|
||||
return Ok(keypair);
|
||||
}
|
||||
|
||||
@@ -132,7 +122,7 @@ impl Cli {
|
||||
if err.is_keyring_no_entry() {
|
||||
N34Error::SecretKeyKeyringWithoutEntry
|
||||
} else {
|
||||
N34Error::Keyring(err)
|
||||
err
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -140,7 +130,7 @@ impl Cli {
|
||||
|
||||
/// Processes the CLI configuration by applying fallback values from config if
|
||||
/// needed. Returns the processed Cli configuration if successful.
|
||||
pub fn post_cli(mut cli: Cli) -> N34Result<Cli> {
|
||||
pub async fn post_cli(mut cli: Cli) -> N34Result<Cli> {
|
||||
cli.options.pow = cli.options.pow.or(cli.options.config.pow);
|
||||
|
||||
if cli.options.relays.is_empty()
|
||||
@@ -167,7 +157,8 @@ pub fn post_cli(mut cli: Cli) -> N34Result<Cli> {
|
||||
cli.options.bunker_url = Some(bunker_url.clone());
|
||||
} else if cli.options.config.keyring_secret_key {
|
||||
cli.options.secret_key = Some(
|
||||
Cli::user_keypair(cli.options.secret_key)?
|
||||
Cli::user_keypair(cli.options.secret_key)
|
||||
.await?
|
||||
.secret_key()
|
||||
.clone(),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -22,6 +22,7 @@ use std::{
|
||||
|
||||
use nostr::{
|
||||
Kind,
|
||||
hashes::sha1::Hash as Sha1Hash,
|
||||
nips::{
|
||||
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||
nip46::NostrConnectURI,
|
||||
@@ -84,6 +85,25 @@ pub fn parse_bunker_url(bunker_url: &str) -> N34Result<NostrConnectURI> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a string in the format `string=sha1` into a name and SHA1 hash.
|
||||
/// The input must contain exactly one `=` character. If the format is invalid
|
||||
/// or the SHA1 hash is incorrect, an error is returned.
|
||||
pub fn name_and_sha1(value: &str) -> Result<(String, Sha1Hash), String> {
|
||||
if value.chars().filter(|c| *c == '=').count() != 1 {
|
||||
return Err("the syntax is `string=SHA1-commit-id`".to_owned());
|
||||
}
|
||||
|
||||
let (name, sha1_hash) = value.split_once('=').expect("There is one `=`");
|
||||
|
||||
Ok((
|
||||
name.trim_matches(['"', '\'']).to_owned(),
|
||||
sha1_hash
|
||||
.trim_matches(['"', '\''])
|
||||
.parse()
|
||||
.map_err(|_| "Invalid SHA1 commit id".to_owned())?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.
|
||||
pub fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -14,14 +14,19 @@
|
||||
// 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::time::Duration;
|
||||
|
||||
use nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::CliOptions;
|
||||
use crate::{
|
||||
cli::{
|
||||
ConfigError,
|
||||
RepoRelaySet,
|
||||
patch::GitPatch,
|
||||
types::{NaddrOrSet, NostrEvent, RelayOrSet},
|
||||
utils,
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
@@ -220,6 +225,47 @@ impl &[RepoRelaySet] {
|
||||
}
|
||||
}
|
||||
|
||||
#[easy_ext::ext(VecPatchesExt)]
|
||||
impl Vec<Option<GitPatch>> {
|
||||
/// Processes a collection of `Option<GitPatch>` values.
|
||||
///
|
||||
/// This function performs one of two actions:
|
||||
/// 1. If all elements are `Some`, it extracts the `GitPatch` values and
|
||||
/// returns them as a `Vec<GitPatch>`.
|
||||
/// 2. If there is at least one `None`, it reads patches from standard
|
||||
/// input, parses them as `GitPatch` values from the stdin, and returns
|
||||
/// them as a `Vec<GitPatch>`.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub async fn process_patches(self) -> N34Result<Vec<GitPatch>> {
|
||||
if self.iter().all(Option::is_some) {
|
||||
return Ok(self.into_iter().map(Option::unwrap).collect());
|
||||
}
|
||||
|
||||
let mut patches = String::new();
|
||||
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(1),
|
||||
tokio::io::stdin().read_to_string(&mut patches),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(bytes)) => {
|
||||
tracing::debug!("Received {bytes} bytes from stdin for patch processing");
|
||||
}
|
||||
Ok(Err(err)) => return Err(N34Error::from(err)),
|
||||
Err(_) => return Err(N34Error::EmptyStdin("patches")),
|
||||
}
|
||||
|
||||
let patches = utils::split_patches(patches)?;
|
||||
|
||||
if patches.is_empty() {
|
||||
return Err(N34Error::InvalidPatch("No valid patches".to_owned()));
|
||||
}
|
||||
|
||||
Ok(patches)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function that checks for duplicates in a sorted slice
|
||||
fn duplicate_in_sorted<T: PartialEq + Clone>(items: &[T]) -> Option<&T> {
|
||||
items.windows(2).find(|w| w[0] == w[1]).map(|w| &w[0])
|
||||
|
||||
169
src/cli/types.rs
169
src/cli/types.rs
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -14,7 +14,7 @@
|
||||
// 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::str::FromStr;
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use nostr::{
|
||||
event::{EventId, Kind},
|
||||
@@ -53,6 +53,18 @@ pub enum RelayOrSet {
|
||||
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`.
|
||||
#[derive(Debug, Clone)]
|
||||
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 {
|
||||
/// 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.
|
||||
@@ -191,6 +259,103 @@ impl FromStr for NostrEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PatchPrStatus {
|
||||
/// The patch/pr is currently open
|
||||
Open,
|
||||
/// The patch/pr has been merged/applied
|
||||
MergedApplied,
|
||||
/// The patch/pr has been closed
|
||||
Closed,
|
||||
/// A patch/pr that has been drafted but not yet applied.
|
||||
Draft,
|
||||
}
|
||||
|
||||
impl PatchPrStatus {
|
||||
/// Returns all status kinds
|
||||
#[inline]
|
||||
pub const fn all_kinds() -> [Kind; 4] {
|
||||
[
|
||||
Self::Open.kind(),
|
||||
Self::MergedApplied.kind(),
|
||||
Self::Closed.kind(),
|
||||
Self::Draft.kind(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Maps the patch/pr status to its corresponding Nostr kind.
|
||||
#[inline]
|
||||
pub const fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Open => Kind::GitStatusOpen,
|
||||
Self::MergedApplied => Kind::GitStatusApplied,
|
||||
Self::Closed => Kind::GitStatusClosed,
|
||||
Self::Draft => Kind::GitStatusDraft,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string representation of the patch/pr status.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "Open",
|
||||
Self::MergedApplied => "Merged/Applied",
|
||||
Self::Closed => "Closed",
|
||||
Self::Draft => "Draft",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the status is open.
|
||||
#[inline]
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, Self::Open)
|
||||
}
|
||||
|
||||
/// Check if the status is merged/applied.
|
||||
#[inline]
|
||||
pub fn is_merged_or_applied(&self) -> bool {
|
||||
matches!(self, Self::MergedApplied)
|
||||
}
|
||||
|
||||
/// Check if the status is closed.
|
||||
#[inline]
|
||||
pub fn is_closed(&self) -> bool {
|
||||
matches!(self, Self::Closed)
|
||||
}
|
||||
|
||||
/// Check if the status is draft
|
||||
#[inline]
|
||||
pub fn is_drafted(&self) -> bool {
|
||||
matches!(self, Self::Draft)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PatchPrStatus> for Kind {
|
||||
fn from(status: &PatchPrStatus) -> Self {
|
||||
status.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PatchPrStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl TryFrom<Kind> for PatchPrStatus {
|
||||
type Error = N34Error;
|
||||
|
||||
fn try_from(kind: Kind) -> Result<Self, Self::Error> {
|
||||
match kind {
|
||||
Kind::GitStatusOpen => Ok(Self::Open),
|
||||
Kind::GitStatusApplied => Ok(Self::MergedApplied),
|
||||
Kind::GitStatusClosed => Ok(Self::Closed),
|
||||
Kind::GitStatusDraft => Ok(Self::Draft),
|
||||
_ => Err(N34Error::InvalidPatchStatus(kind)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
||||
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
||||
|
||||
|
||||
134
src/cli/utils.rs
Normal file
134
src/cli/utils.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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::{
|
||||
fmt::Write as _,
|
||||
fs,
|
||||
io::{self, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::patch::{FROM_RE, GitPatch},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the logs file for writing. If the file size exceeds 5MB, it is opened
|
||||
/// in write mode, otherwise in append mode.
|
||||
pub fn logs_file() -> N34Result<fs::File> {
|
||||
const FIVE_MB: u64 = 1024 * 1024 * 5;
|
||||
|
||||
let logs_path = dirs::data_local_dir()
|
||||
.ok_or(N34Error::CanNotFindDataPath)?
|
||||
.join("n34")
|
||||
.join("logs.log");
|
||||
|
||||
tracing::info!(path = %logs_path.display(), "Logs file");
|
||||
|
||||
if let Some(parent) = logs_path.parent()
|
||||
&& !parent.exists()
|
||||
{
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
_ = fs::File::create_new(&logs_path);
|
||||
|
||||
let is_large = if let Ok(file) = fs::File::open(&logs_path)
|
||||
&& let Ok(metadata) = file.metadata()
|
||||
{
|
||||
metadata.len() >= FIVE_MB
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(!is_large)
|
||||
.truncate(is_large)
|
||||
.open(&logs_path)
|
||||
.map_err(N34Error::from)
|
||||
}
|
||||
|
||||
/// Splits a string with multiple git patches into separate patches. Each patch
|
||||
/// begins with a line formatted as "From \<hash\> ...". Ensures the result is
|
||||
/// never an empty vector.
|
||||
pub fn split_patches(patches: String) -> N34Result<Vec<GitPatch>> {
|
||||
let mut patches = patches.split("\n").peekable();
|
||||
let mut result = Vec::new();
|
||||
let mut current_patch = String::new();
|
||||
|
||||
let push_patch = |patch_str: String| -> N34Result<GitPatch> {
|
||||
tracing::trace!("patch content: {patch_str}");
|
||||
let patch = GitPatch::from_str(&patch_str).map_err(N34Error::InvalidPatch)?;
|
||||
tracing::info!(
|
||||
"Processing patch from stdin: {}",
|
||||
patch
|
||||
.filename("")
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| "Unnamed patch".to_owned())
|
||||
);
|
||||
Ok(patch)
|
||||
};
|
||||
|
||||
while let Some(line) = patches.next() {
|
||||
// If we have a non-empty patch and encounter a "From " line followed by a line
|
||||
// with ":", it indicates a new patch header. Push the current patch and
|
||||
// clear it for the new one. The "From " line will be added as the first
|
||||
// line of the new patch after this check.
|
||||
if !current_patch.is_empty()
|
||||
&& FROM_RE.is_match(line)
|
||||
&& patches.peek().is_some_and(|line| line.contains(":"))
|
||||
{
|
||||
result.push(push_patch(current_patch.clone())?);
|
||||
current_patch.clear()
|
||||
}
|
||||
_ = writeln!(&mut current_patch, "{line}");
|
||||
}
|
||||
|
||||
if !current_patch.is_empty() {
|
||||
result.push(push_patch(current_patch)?);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
39
src/error.rs
39
src/error.rs
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -51,10 +51,21 @@ pub enum N34Error {
|
||||
Addr(#[from] AddrParseError),
|
||||
#[error("Browser Signer Proxy Error: {0}")]
|
||||
BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),
|
||||
#[error("Keys error: {0}")]
|
||||
Keys(#[from] nostr::key::Error),
|
||||
#[error("Keyring error: {0}")]
|
||||
Keyring(#[from] nostr_keyring::Error),
|
||||
Keyring(#[from] keyring::Error),
|
||||
#[error("Join error: {0}")]
|
||||
Join(#[from] tokio::task::JoinError),
|
||||
#[error("{0}")]
|
||||
Config(#[from] ConfigError),
|
||||
#[error(
|
||||
"Could not determine the default data path: both `$XDG_CONFIG_HOME` and `$HOME` \
|
||||
environment variables are missing or unset."
|
||||
)]
|
||||
CanNotFindDataPath,
|
||||
#[error("Empty STDIN: expected '{0}' to be present in the input")]
|
||||
EmptyStdin(&'static str),
|
||||
#[error("No editor specified in the `EDITOR` environment variable")]
|
||||
EditorNotFound,
|
||||
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
||||
@@ -71,6 +82,8 @@ pub enum N34Error {
|
||||
InvalidRepoId,
|
||||
#[error("Invalid event: {0}")]
|
||||
InvalidEvent(String),
|
||||
#[error("Invalid patch: {0}")]
|
||||
InvalidPatch(String),
|
||||
#[error("Bech32 error: {0}")]
|
||||
Bech32(#[from] nostr::nips::nip19::Error),
|
||||
#[error("Event error: {0}")]
|
||||
@@ -123,9 +136,14 @@ pub enum N34Error {
|
||||
)]
|
||||
CanNotFoundIssue,
|
||||
#[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,
|
||||
#[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"#)]
|
||||
NotRootPatch,
|
||||
#[error("This status kind can't be set for an issue: {0}")]
|
||||
@@ -134,7 +152,7 @@ pub enum N34Error {
|
||||
InvalidPatchStatus(Kind),
|
||||
#[error("Can't find the root patch of the given patch-revision")]
|
||||
RevisionRootNotFound,
|
||||
#[error("Invalid status for the issue/patch: {0}")]
|
||||
#[error("Invalid status: {0}")]
|
||||
InvalidStatus(String),
|
||||
#[error("Not valid bunker URL")]
|
||||
NotBunkerUrl,
|
||||
@@ -143,6 +161,13 @@ pub enum N34Error {
|
||||
keyring is enabled to store it"
|
||||
)]
|
||||
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 {
|
||||
@@ -166,4 +191,10 @@ impl N34Error {
|
||||
_ => ExitCode::FAILURE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the error indicates a missing keyring entry.
|
||||
#[inline]
|
||||
pub fn is_keyring_no_entry(&self) -> bool {
|
||||
matches!(self, Self::Keyring(keyring::Error::NoEntry))
|
||||
}
|
||||
}
|
||||
|
||||
75
src/main.rs
75
src/main.rs
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,17 +18,20 @@
|
||||
pub mod cli;
|
||||
/// N34 errors
|
||||
pub mod error;
|
||||
/// Nostr keyring
|
||||
pub mod nostr_keyring;
|
||||
/// Nostr utils module
|
||||
pub mod nostr_utils;
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
process::ExitCode,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
use tracing::Level;
|
||||
use tracing::{Level, level_filters::LevelFilter};
|
||||
use tracing_subscriber::{Layer, filter, layer::SubscriberExt};
|
||||
|
||||
use self::cli::Cli;
|
||||
@@ -37,42 +40,64 @@ use self::cli::Cli;
|
||||
/// open.
|
||||
static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Returns the stderr log layer
|
||||
fn stderr_log_layer<S>() -> impl Layer<S>
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
{
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_ansi(true)
|
||||
.with_writer(std::io::stderr)
|
||||
.without_time()
|
||||
}
|
||||
|
||||
/// Configures the logging level based on the provided verbosity.
|
||||
///
|
||||
/// When verbosity is set to TRACE, includes file and line numbers in logs.
|
||||
fn set_log_level(verbosity: Verbosity) {
|
||||
let is_trace = verbosity
|
||||
.tracing_level()
|
||||
.is_some_and(|l| l == tracing::Level::TRACE);
|
||||
|
||||
let logs_filter = filter::dynamic_filter_fn(move |m, _| {
|
||||
fn set_log_level(verbosity: Verbosity, logs_file: File) {
|
||||
let editor_filter = filter::dynamic_filter_fn(move |m, _| {
|
||||
// Disable all logs while editor is open
|
||||
verbosity.tracing_level().unwrap_or(Level::ERROR) >= *m.level()
|
||||
&& (m.name().starts_with("event src") || m.name().contains("nostr"))
|
||||
&& !EDITOR_OPEN.load(Ordering::Relaxed)
|
||||
});
|
||||
|
||||
let logs_layer = tracing_subscriber::fmt::layer()
|
||||
.with_file(is_trace)
|
||||
.with_line_number(is_trace)
|
||||
.without_time();
|
||||
let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(logs_file)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_filter(LevelFilter::TRACE);
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(stderr_log_layer().with_filter(editor_filter))
|
||||
.with(file_layer);
|
||||
tracing::subscriber::set_global_default(subscriber).ok();
|
||||
}
|
||||
|
||||
async fn try_main() -> error::N34Result<()> {
|
||||
// Initialize a thread-local subscriber for logging during CLI parsing and
|
||||
// post-processing.
|
||||
let guard =
|
||||
tracing::subscriber::set_default(tracing_subscriber::registry().with(stderr_log_layer()));
|
||||
|
||||
let cli = cli::post_cli(Cli::parse()).await?;
|
||||
let logs_file = cli::utils::logs_file()?;
|
||||
|
||||
// Replace the thread-local subscriber with a global default subscriber based on
|
||||
// the CLI verbosity level.
|
||||
drop(guard);
|
||||
set_log_level(cli.verbosity, logs_file);
|
||||
|
||||
cli.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let cli = match cli::post_cli(Cli::parse()) {
|
||||
Ok(cli) => cli,
|
||||
Err(err) => {
|
||||
eprintln!("{err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
set_log_level(cli.verbosity);
|
||||
|
||||
if let Err(err) = cli.run().await {
|
||||
tracing::error!("{err}");
|
||||
if let Err(err) = try_main().await {
|
||||
eprintln!("{err}");
|
||||
return err.exit_code();
|
||||
}
|
||||
|
||||
|
||||
98
src/nostr_keyring.rs
Normal file
98
src/nostr_keyring.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code 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 async_utility::task;
|
||||
use keyring::Entry;
|
||||
use nostr::key::{Keys, SecretKey};
|
||||
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
/// Keyring service name of n34
|
||||
const N34_KEYRING_SERVICE_NAME: &str = "n34";
|
||||
/// Keyring entry name of the n34 keypair
|
||||
const N34_KEY_PAIR_ENTRY: &str = "n34_keypair";
|
||||
/// Keyring entry name of the user secret key
|
||||
const USER_KEY_PAIR_ENTRY: &str = "user_keypair";
|
||||
|
||||
/// Retrieves the keypair entry for `n34`.
|
||||
#[inline]
|
||||
fn n34_entry() -> keyring::Result<Entry> {
|
||||
Entry::new(N34_KEYRING_SERVICE_NAME, N34_KEY_PAIR_ENTRY)
|
||||
}
|
||||
|
||||
/// Retrieves the keypair entry for the user.
|
||||
#[inline]
|
||||
fn user_entry() -> keyring::Result<Entry> {
|
||||
Entry::new(N34_KEYRING_SERVICE_NAME, USER_KEY_PAIR_ENTRY)
|
||||
}
|
||||
|
||||
pub mod user {
|
||||
use super::*;
|
||||
|
||||
/// Stores the user's keypair in the system.
|
||||
pub async fn set(key: &Keys) -> N34Result<()> {
|
||||
let private_key = key.secret_key().to_secret_bytes();
|
||||
|
||||
task::spawn_blocking(move || {
|
||||
user_entry()?
|
||||
.set_secret(&private_key)
|
||||
.map_err(N34Error::from)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Retrieves the user's keypair from the system.
|
||||
pub async fn get() -> N34Result<Keys> {
|
||||
task::spawn_blocking(move || {
|
||||
Ok(Keys::new(SecretKey::from_slice(
|
||||
&user_entry()?.get_secret()?,
|
||||
)?))
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Delete the user's keypair from the system.
|
||||
pub async fn delete() -> N34Result<()> {
|
||||
task::spawn_blocking(move || user_entry()?.delete_credential().map_err(N34Error::from))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
pub mod n34 {
|
||||
use super::*;
|
||||
|
||||
/// Stores the `n34` client's keypair in the system.
|
||||
pub async fn set(key: &Keys) -> N34Result<()> {
|
||||
let private_key = key.secret_key().to_secret_bytes();
|
||||
|
||||
task::spawn_blocking(move || {
|
||||
n34_entry()?
|
||||
.set_secret(&private_key)
|
||||
.map_err(N34Error::from)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Retrieves the `n34` client's keypair from the system.
|
||||
pub async fn get() -> N34Result<Keys> {
|
||||
task::spawn_blocking(move || {
|
||||
Ok(Keys::new(SecretKey::from_slice(
|
||||
&n34_entry()?.get_secret()?,
|
||||
)?))
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
// n34 - A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -39,8 +39,9 @@ use nostr_sdk::{Client, ClientOptions};
|
||||
use traits::TokenUtils;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, issue::IssueStatus, patch::PatchStatus},
|
||||
cli::{CliOptions, issue::IssueStatus, types::PatchPrStatus},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::traits::KindExt,
|
||||
};
|
||||
|
||||
/// Timeout duration for the client.
|
||||
@@ -211,21 +212,17 @@ impl NostrClient {
|
||||
/// Fetches the first event matching the given filter, or None if no event
|
||||
/// is found.
|
||||
pub async fn fetch_event(&self, filter: Filter) -> N34Result<Option<Event>> {
|
||||
Ok(self
|
||||
.client
|
||||
.fetch_events(filter.limit(1), CLIENT_TIMEOUT)
|
||||
.await?
|
||||
.first_owned())
|
||||
Ok(self.fetch_events(filter).await?.next())
|
||||
}
|
||||
|
||||
/// Fetches the events matching the given filter
|
||||
pub async fn fetch_events(&self, filter: Filter) -> N34Result<Vec<Event>> {
|
||||
pub async fn fetch_events(&self, filter: Filter) -> N34Result<impl Iterator<Item = Event>> {
|
||||
// Multiply timeout by 5 to account for multiple events being fetched
|
||||
Ok(self
|
||||
.client
|
||||
.fetch_events(filter, CLIENT_TIMEOUT * 5)
|
||||
.await?
|
||||
.to_vec())
|
||||
.into_iter())
|
||||
}
|
||||
|
||||
/// Try to fetch the repositories and returns them
|
||||
@@ -258,6 +255,13 @@ impl NostrClient {
|
||||
.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,
|
||||
/// falls back to a shortened version of the public key.
|
||||
pub async fn get_username(&self, user: PublicKey) -> String {
|
||||
@@ -281,23 +285,36 @@ impl NostrClient {
|
||||
issue_id: EventId,
|
||||
authorized_pubkeys: Vec<PublicKey>,
|
||||
) -> N34Result<IssueStatus> {
|
||||
self.fetch_events(
|
||||
self.fetch_event(
|
||||
Filter::new()
|
||||
.event(issue_id)
|
||||
.kinds([
|
||||
Kind::GitStatusOpen,
|
||||
Kind::GitStatusApplied,
|
||||
Kind::GitStatusClosed,
|
||||
])
|
||||
.kinds(IssueStatus::all_kinds())
|
||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.max_by_key(|e| e.created_at)
|
||||
.map(|status| IssueStatus::try_from(status.kind))
|
||||
.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_event(
|
||||
Filter::new()
|
||||
.event(pr_id)
|
||||
.kinds(PatchPrStatus::all_kinds())
|
||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||
)
|
||||
.await?
|
||||
.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
|
||||
/// closed when the root patch is already merged/applied but doesn't
|
||||
/// reference this revision. Defaults to Open status if no status event
|
||||
@@ -307,24 +324,17 @@ impl NostrClient {
|
||||
root_patch: EventId,
|
||||
root_revision: Option<EventId>,
|
||||
authorized_pubkeys: Vec<PublicKey>,
|
||||
) -> N34Result<PatchStatus> {
|
||||
) -> N34Result<PatchPrStatus> {
|
||||
let (root_status, event_tags) = self
|
||||
.fetch_events(
|
||||
.fetch_event(
|
||||
Filter::new()
|
||||
.event(root_patch)
|
||||
.kinds([
|
||||
Kind::GitStatusOpen,
|
||||
Kind::GitStatusApplied,
|
||||
Kind::GitStatusClosed,
|
||||
Kind::GitStatusDraft,
|
||||
])
|
||||
.kinds(PatchPrStatus::all_kinds())
|
||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.max_by_key(|e| e.created_at)
|
||||
.map(|status| N34Result::Ok((PatchStatus::try_from(status.kind)?, status.tags)))
|
||||
.unwrap_or_else(|| Ok((PatchStatus::Open, Tags::new())))?;
|
||||
.map(|status| N34Result::Ok((PatchPrStatus::try_from(status.kind)?, status.tags)))
|
||||
.unwrap_or_else(|| Ok((PatchPrStatus::Open, Tags::new())))?;
|
||||
|
||||
if let Some(revision_id) = root_revision
|
||||
&& root_status.is_merged_or_applied()
|
||||
@@ -332,7 +342,7 @@ impl NostrClient {
|
||||
.filter(TagKind::e())
|
||||
.any(|t| t.is_reply() && t.content().is_some_and(|c| c == revision_id.to_hex()))
|
||||
{
|
||||
return Ok(PatchStatus::Closed);
|
||||
return Ok(PatchPrStatus::Closed);
|
||||
}
|
||||
|
||||
Ok(root_status)
|
||||
@@ -351,7 +361,6 @@ impl NostrClient {
|
||||
.event(root_patch_id),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
e.tags.iter().any(|t| {
|
||||
t.is_root() && t.content().is_some_and(|c| c == root_patch_id.to_hex())
|
||||
@@ -360,17 +369,17 @@ impl NostrClient {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Finds the root issue or patch for a given event. If the event is already
|
||||
/// a root (issue/patch), returns it directly. For comments, follows
|
||||
/// parent/root references until finding the root or failing. Returns
|
||||
/// None if no root can be found.
|
||||
/// Finds the root issue, PR or patch for a given event. If the event is
|
||||
/// already a root (issue/patch/PR), returns it directly. For comments,
|
||||
/// follows parent/root references until finding the root or failing.
|
||||
/// Returns None if no root can be found.
|
||||
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);
|
||||
}
|
||||
|
||||
loop {
|
||||
if matches!(event.kind, Kind::GitIssue | Kind::GitPatch) {
|
||||
if event.kind.is_root_kind() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
@@ -380,7 +389,7 @@ impl NostrClient {
|
||||
self.add_relay_hint(relay_hint.cloned()).await;
|
||||
let root_event = self.fetch_event(Filter::new().id(*id)).await?;
|
||||
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);
|
||||
}
|
||||
@@ -395,7 +404,7 @@ impl NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Break if: no root/parent tags found, parent/root event fetch failed
|
||||
// Break if: no root/parent tags found
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -426,16 +435,20 @@ impl NostrClient {
|
||||
/// Returns the read relays of the given users if found, otherwise empty
|
||||
/// vector
|
||||
pub async fn read_relays_from_users(&self, users: &[PublicKey]) -> Vec<RelayUrl> {
|
||||
self.fetch_events(
|
||||
Filter::new()
|
||||
.kind(nostr::event::Kind::RelayList)
|
||||
.authors(utils::dedup(users.iter().copied())),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flat_map(|e| utils::add_read_relays(Some(&e)))
|
||||
.collect()
|
||||
if let Ok(iterator) = self
|
||||
.fetch_events(
|
||||
Filter::new()
|
||||
.kind(nostr::event::Kind::RelayList)
|
||||
.authors(utils::dedup(users.iter().copied())),
|
||||
)
|
||||
.await
|
||||
{
|
||||
iterator
|
||||
.flat_map(|e| utils::add_read_relays(Some(&e)))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the given content and returns the details that inside it
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user