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/),
|
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).
|
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
|
## [0.4.0] - 2025-08-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ as the active development branch.
|
|||||||
|
|
||||||
You can submit issues and patches via any
|
You can submit issues and patches via any
|
||||||
Nostr-compatible client using the address:
|
Nostr-compatible client using the address:
|
||||||
`naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueq
|
`naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyxhwumn8ghj7mn0wvhxcmmvqy28wumn8ghj7mn0wd68ytn00p68ytnyv4mqwuj6xc`
|
||||||
yf8wumn8ghj7mn0wd68yt35wfejumnvqyxhwumn8ghj7mn0wvhxcmmvqy28wumn8ghj7mn0wd68ytn00
|
|
||||||
p68ytnyv4mqwuj6xc`
|
|
||||||
|
|
||||||
|
|
||||||
When using `n34`, there's no need to specify the address, it will automatically
|
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
|
#### Patch Guidelines
|
||||||
|
|
||||||
- Keep patches small: Focused changes are easier to review and merge.
|
- Keep patches small: Focused changes are easier to review and merge.
|
||||||
- Run `just ci` before submitting your patch.
|
- Run `./tasks ci` before submitting your patch.
|
||||||
- Update the change log with your patch. Run `just changelog` or `git-cliff > CHANGELOG.md`
|
- 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)
|
- 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:
|
- Use [Conventional Commits]: Start the patch subject with one of these types:
|
||||||
- `feat`: New feature
|
- `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"
|
rust-version = "1.88.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-utility = "0.3.1"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.8.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
@@ -28,6 +29,16 @@ toml = "0.9.4"
|
|||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
||||||
|
[dependencies.keyring]
|
||||||
|
features = [
|
||||||
|
"apple-native",
|
||||||
|
"windows-native",
|
||||||
|
"async-secret-service",
|
||||||
|
"tokio",
|
||||||
|
"crypto-rust",
|
||||||
|
]
|
||||||
|
version = "3.6.3"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
version = "4.5.42"
|
version = "4.5.42"
|
||||||
@@ -46,10 +57,6 @@ tracing-subscriber = "0.3.19"
|
|||||||
default-features = false
|
default-features = false
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
|
|
||||||
[dependencies.nostr-keyring]
|
|
||||||
default-features = false
|
|
||||||
version = "0.43.0"
|
|
||||||
|
|
||||||
[dependencies.nostr-sdk]
|
[dependencies.nostr-sdk]
|
||||||
default-features = false
|
default-features = false
|
||||||
version = "0.43.0"
|
version = "0.43.0"
|
||||||
@@ -64,7 +71,7 @@ tracing-subscriber = "0.3.19"
|
|||||||
version = "1.0.219"
|
version = "1.0.219"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
features = ["macros", "rt-multi-thread"]
|
features = ["macros", "rt-multi-thread", "io-std"]
|
||||||
version = "1.47.1"
|
version = "1.47.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ Check the documentation at [n34.dev]
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [X] Repository announcements
|
- [X] Repository announcements
|
||||||
- [ ] Repository state announcements
|
- [X] Repository state announcements
|
||||||
- [X] Patches (Send, fetch and list)
|
- [X] Patches (Send, fetch and list)
|
||||||
- [X] Issues (Send, view and list)
|
- [X] Issues (Send, view and list)
|
||||||
- [X] Replies
|
- [X] Replies
|
||||||
- [X] Issues and patches status
|
- [X] Issues and patches status
|
||||||
- [ ] Pull requests ([nostr-protocol/nips#1966])
|
- [X] Pull requests ([nostr-protocol/nips#1966])
|
||||||
- [X] Gossip Model ([NIP-65])
|
- [X] Gossip Model ([NIP-65])
|
||||||
- [X] Proof of Work ([NIP-13])
|
- [X] Proof of Work ([NIP-13])
|
||||||
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ filter_unconventional = false
|
|||||||
# Assigns commits to groups.
|
# Assigns commits to groups.
|
||||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
{ message = '^.*!:.*$', group = "Breaking Change" },
|
{ message = '^.*!:', group = "Breaking Change" },
|
||||||
{ message = '^feat', group = "Added" },
|
{ message = '^feat', group = "Added" },
|
||||||
{ message = '^deprecate', group = "Deprecated" },
|
{ message = '^deprecate', group = "Deprecated" },
|
||||||
{ message = '^remove', group = "Removed" },
|
{ message = '^remove', group = "Removed" },
|
||||||
|
|||||||
@@ -15,20 +15,31 @@
|
|||||||
- [Manage Repositories](repo/README.md)
|
- [Manage Repositories](repo/README.md)
|
||||||
- [Broadcast and Update a Git Repository](repo/announce.md)
|
- [Broadcast and Update a Git Repository](repo/announce.md)
|
||||||
- [View Git Repository Details](repo/view.md)
|
- [View Git Repository Details](repo/view.md)
|
||||||
|
- [Repository State Announcements](repo/state.md)
|
||||||
|
- [Reply to Issues and Patches](reply.md)
|
||||||
- [Issue Management](issue/README.md)
|
- [Issue Management](issue/README.md)
|
||||||
- [Create an Issue](issue/new.md)
|
- [Create an Issue](issue/new.md)
|
||||||
- [View an Issue By ID](issue/view.md)
|
- [View an Issue](issue/view.md)
|
||||||
- [Reopen a Closed Issue](issue/reopen.md)
|
- [Reopen an Issue](issue/reopen.md)
|
||||||
- [Closes an Open Issue](issue/close.md)
|
- [Close an Issue](issue/close.md)
|
||||||
- [Resolves an Issue](issue/resolve.md)
|
- [Resolve an Issue](issue/resolve.md)
|
||||||
- [List Repositories Issues](issue/list.md)
|
- [List Issues](issue/list.md)
|
||||||
- [Patch Management](patch/README.md)
|
- [Patch Management](patch/README.md)
|
||||||
- [Send Patches to a Repository](patch/send.md)
|
- [Send Patches to a Repository](patch/send.md)
|
||||||
- [Fetch a Patch By ID](patch/fetch.md)
|
- [Fetch a Patch](patch/fetch.md)
|
||||||
- [Reopens a Closed or Drafted Patch](patch/reopen.md)
|
- [Close a Patch](patch/close.md)
|
||||||
- [Closes an Open or Drafted Patch](patch/close.md)
|
- [Convert to Draft](patch/draft.md)
|
||||||
- [Draft an Open Patch](patch/draft.md)
|
- [Reopen a Patch](patch/reopen.md)
|
||||||
- [Apply an Open Patch](patch/apply.md)
|
- [Mark as Applied](patch/apply.md)
|
||||||
- [Merge an Open Patch](patch/merge.md)
|
- [Mark as Merged](patch/merge.md)
|
||||||
- [List Repositories Patches](patch/list.md)
|
- [List Patches](patch/list.md)
|
||||||
- [Reply to Issues and Patches](reply.md)
|
- [Pull Request Management](pr/README.md)
|
||||||
|
- [Create a Pull Request](pr/new.md)
|
||||||
|
- [Update a Pull Request](pr/update.md)
|
||||||
|
- [View a Pull Request](pr/view.md)
|
||||||
|
- [List Pull Requests](pr/list.md)
|
||||||
|
- [Close a Pull Request](pr/close.md)
|
||||||
|
- [Convert to Draft](pr/draft.md)
|
||||||
|
- [Reopen a Pull Request](pr/reopen.md)
|
||||||
|
- [Mark as Applied](pr/apply.md)
|
||||||
|
- [Mark as Merged](pr/merge.md)
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# Command-Line Usage
|
# Command-Line Usage
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
`n34` emphasizes simplicity by exclusively handling Nostr tasks, requiring
|
||||||
|
users to manage Git operations themselves. For instance, it won't merge or
|
||||||
|
rebase patches into a branch but will prompt you to specify where to write them,
|
||||||
|
allowing you to merge them using `git-am`. Similarly, when sending patches, it
|
||||||
|
won't ask for the commits but will request the patch files instead.
|
||||||
|
|
||||||
|
This approach enhances simplicity and freedom, giving you full control over
|
||||||
|
your workflow. Additionally, it simplifies software maintenance by avoiding
|
||||||
|
unnecessary complexity.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
The `n34` command-line tool accepts the following options:
|
The `n34` command-line tool accepts the following options:
|
||||||
@@ -19,8 +31,8 @@ The `n34` command-line tool accepts the following options:
|
|||||||
- `-v`, `--verbose...`: Increases the logging verbosity. Can be used multiple
|
- `-v`, `--verbose...`: Increases the logging verbosity. Can be used multiple
|
||||||
times for more detail (e.g., `-v`, `-vv`).
|
times for more detail (e.g., `-v`, `-vv`).
|
||||||
|
|
||||||
**Note:** The `--secret-key` and `--bunker-url` options are mutually exclusive.
|
**Note:** The `--secret-key`, `--nip07` and `--bunker-url` options are mutually
|
||||||
You must provide exactly one signing method.
|
exclusive. You must provide exactly one signing method.
|
||||||
|
|
||||||
## Multiple Repositories
|
## Multiple Repositories
|
||||||
|
|
||||||
|
|||||||
@@ -355,8 +355,8 @@
|
|||||||
<span class="feature-text">Repository announcements</span>
|
<span class="feature-text">Repository announcements</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check pending"></span>
|
<span class="feature-check completed"></span>
|
||||||
<span class="feature-text pending">Repository state announcements</span>
|
<span class="feature-text">Repository state announcements</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<span class="feature-check completed"></span>
|
||||||
@@ -375,8 +375,8 @@
|
|||||||
<span class="feature-text">Issues and patches status</span>
|
<span class="feature-text">Issues and patches status</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check pending"></span>
|
<span class="feature-check completed"></span>
|
||||||
<span class="feature-text pending">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
<span class="feature-text">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<span class="feature-check completed"></span>
|
<span class="feature-check completed"></span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Closes an Open Issue
|
# Close an Issue
|
||||||
|
|
||||||
> `n34 issue close` command
|
> `n34 issue close` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<ISSUE_ID> The open issue id to close it
|
<ISSUE_ID> The open issue id to close it
|
||||||
|
|
||||||
Options:
|
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
|
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
|
> `n34 issue list` command
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ List the repositories issues
|
|||||||
Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
Arguments:
|
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:
|
Options:
|
||||||
--limit <LIMIT> Maximum number of issues to list [default: 15]
|
--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>
|
Usage: n34 issue new [OPTIONS] <--content <CONTENT>|--editor>
|
||||||
|
|
||||||
Options:
|
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
|
-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
|
-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
|
--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
|
> `n34 issue reopen` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<ISSUE_ID> The ID of the closed issue to reopen
|
<ISSUE_ID> The ID of the closed issue to reopen
|
||||||
|
|
||||||
Options:
|
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
|
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
|
> `n34 issue resolve` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<ISSUE_ID> The issue id to resolve it
|
<ISSUE_ID> The issue id to resolve it
|
||||||
|
|
||||||
Options:
|
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.
|
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
|
> `n34 issue view` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<ISSUE_ID> The issue id to view it
|
<ISSUE_ID> The issue id to view it
|
||||||
|
|
||||||
Options:
|
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
|
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
|
> `n34 patch apply` command
|
||||||
|
|
||||||
@@ -10,10 +10,10 @@ Usage: n34 patch apply [OPTIONS] <PATCH_ID> [APPLIED_COMMITS]...
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
||||||
[APPLIED_COMMITS]... The applied commits
|
<APPLIED_COMMITS>... The applied commits
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--repo <NADDR-NIP05-OR-SET> Repository 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
|
--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
|
> `n34 patch close` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<PATCH_ID> The open/drafted patch id to close it. Must be orignal root patch
|
<PATCH_ID> The open/drafted patch id to close it. Must be orignal root patch
|
||||||
|
|
||||||
Options:
|
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 `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
|
> `n34 patch draft` command
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ Arguments:
|
|||||||
<PATCH_ID> The open patch id to draft it. Must be orignal root patch
|
<PATCH_ID> The open patch id to draft it. Must be orignal root patch
|
||||||
|
|
||||||
Options:
|
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
|
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
|
> `n34 patch fetch` command
|
||||||
|
|
||||||
@@ -12,10 +12,20 @@ Arguments:
|
|||||||
<PATCH_ID> The patch id to fetch it
|
<PATCH_ID> The patch id to fetch it
|
||||||
|
|
||||||
Options:
|
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
|
||||||
-o, --output <PATH> Output directory for the patches. Default to the current directory
|
-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
|
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
|
to the specified output directory (current directory by default). You can then
|
||||||
apply or merge these patches into your branch as needed.
|
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
|
> `n34 patch list` command
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ List the repositories patches
|
|||||||
Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
Arguments:
|
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:
|
Options:
|
||||||
--limit <LIMIT> Maximum number of patches to list [default: 15]
|
--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
|
> `n34 patch merge` command
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
<MERGE_COMMIT> The merge commit id
|
<MERGE_COMMIT> The merge commit id
|
||||||
|
|
||||||
Options:
|
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
|
--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
|
> `n34 patch reopen` command
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ Arguments:
|
|||||||
<PATCH_ID> The closed/drafted patch id to reopen it. Must be orignal root patch
|
<PATCH_ID> The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||||
|
|
||||||
Options:
|
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.
|
be closed or drafted.
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ Send one or more patches to a repository
|
|||||||
Usage: n34 patch send [OPTIONS] <PATCH-PATH>...
|
Usage: n34 patch send [OPTIONS] <PATCH-PATH>...
|
||||||
|
|
||||||
Arguments:
|
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:
|
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
|
--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
|
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
|
of an existing patch, include the original patch ID to ensure it’s correctly
|
||||||
referenced in your revision patch event.
|
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:
|
Options:
|
||||||
--quote-to Quote the replied-to event in the editor
|
--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)
|
-c, --comment <COMMENT> The comment (cannot be used with --editor)
|
||||||
-e, --editor Open editor to write comment (cannot be used with --content)
|
-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
|
-c, --clone <CLONE> URLs for cloning the repository
|
||||||
-m, --maintainers <MAINTAINERS> Additional maintainers of the repository (besides yourself)
|
-m, --maintainers <MAINTAINERS> Additional maintainers of the repository (besides yourself)
|
||||||
-l, --label <LABEL> Labels to categorize the repository. Can be specified multiple times
|
-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
|
--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
|
--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]...
|
Usage: n34 repo view [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
Arguments:
|
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
|
This command prints repository details to standard output. If no arguments
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--set-relay <RELAYS> Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
--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.
|
Each set requires a unique name, provided as the final argument to the command.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--set-relay <RELAYS> Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
--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.
|
Removes an entire set, or specific repositories and relays from it.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--set-relay <RELAYS> Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
--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
|
--override Replace existing relays/repositories instead of adding to them
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
14
flake.nix
14
flake.nix
@@ -20,15 +20,12 @@
|
|||||||
in
|
in
|
||||||
with pkgs;
|
with pkgs;
|
||||||
{
|
{
|
||||||
|
# n34 development environment
|
||||||
devShells.default = mkShell {
|
devShells.default = mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
cargo-msrv
|
|
||||||
dbus
|
|
||||||
git-cliff
|
git-cliff
|
||||||
just
|
|
||||||
mdbook
|
mdbook
|
||||||
nushell
|
nushell
|
||||||
pkg-config
|
|
||||||
taplo
|
taplo
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -39,6 +36,7 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Build n34
|
||||||
packages.default =
|
packages.default =
|
||||||
let
|
let
|
||||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||||
@@ -50,14 +48,6 @@
|
|||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
src = lib.cleanSource ./.;
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkg-config
|
|
||||||
];
|
|
||||||
|
|
||||||
buildInputs = [
|
|
||||||
dbus
|
|
||||||
];
|
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
inherit (manifest) description homepage;
|
inherit (manifest) description homepage;
|
||||||
license = lib.licenses.gpl3Plus;
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,8 +17,9 @@
|
|||||||
use clap::{ArgGroup, Args};
|
use clap::{ArgGroup, Args};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{Cli, CliOptions, traits::CommandRunner},
|
cli::{CliOptions, traits::CommandRunner},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
|
nostr_keyring,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
@@ -46,8 +47,6 @@ impl CommandRunner for KeyringArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||||
let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);
|
|
||||||
|
|
||||||
if self.enable {
|
if self.enable {
|
||||||
options.config.keyring_secret_key = true;
|
options.config.keyring_secret_key = true;
|
||||||
} else if self.disable {
|
} else if self.disable {
|
||||||
@@ -55,7 +54,7 @@ impl CommandRunner for KeyringArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.reset || self.disable {
|
if self.reset || self.disable {
|
||||||
let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);
|
let _ = nostr_keyring::user::delete().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.config.dump()
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -28,11 +28,17 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct CloseArgs {
|
pub struct CloseArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open issue id to close it
|
/// The open issue id to close it
|
||||||
issue_id: NostrEvent,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -19,17 +19,24 @@ use std::num::NonZeroUsize;
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
common_commands,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ListArgs {
|
pub struct ListArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// Maximum number of issues to list
|
/// Maximum number of issues to list
|
||||||
#[arg(long, default_value = "15")]
|
#[arg(long, default_value = "15")]
|
||||||
@@ -40,7 +47,11 @@ impl CommandRunner for ListArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
common_commands::list_patches_and_issues(options, self.naddrs, false, self.limit.into())
|
common_commands::list_pr_patches_and_issues::<{ EntityType::Issue as u8 }>(
|
||||||
|
options,
|
||||||
|
self.naddrs,
|
||||||
|
self.limit.into(),
|
||||||
|
)
|
||||||
.await
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum IssueSubcommands {
|
pub enum IssueSubcommands {
|
||||||
/// Create a new repository issue
|
/// Create an issue
|
||||||
New(NewArgs),
|
New(NewArgs),
|
||||||
/// View an issue by its ID
|
/// View an issue
|
||||||
View(ViewArgs),
|
View(ViewArgs),
|
||||||
/// Reopens a closed issue.
|
/// Reopen an issue.
|
||||||
Reopen(ReopenArgs),
|
Reopen(ReopenArgs),
|
||||||
/// Closes an open issue.
|
/// Closes an issue.
|
||||||
Close(CloseArgs),
|
Close(CloseArgs),
|
||||||
/// Resolves an issue.
|
/// Resolve an issue.
|
||||||
Resolve(ResolveArgs),
|
Resolve(ResolveArgs),
|
||||||
/// List the repositories issues.
|
/// List issues.
|
||||||
List(ListArgs),
|
List(ListArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,9 +72,19 @@ pub enum IssueStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Maps the issue status to its corresponding Nostr kind.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn kind(&self) -> Kind {
|
pub const fn kind(&self) -> Kind {
|
||||||
match self {
|
match self {
|
||||||
Self::Open => Kind::GitStatusOpen,
|
Self::Open => Kind::GitStatusOpen,
|
||||||
Self::Resolved => Kind::GitStatusApplied,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -37,33 +37,34 @@ use crate::{
|
|||||||
/// Arguments for the `issue new` command
|
/// Arguments for the `issue new` command
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
#[clap(
|
#[clap(
|
||||||
group(
|
|
||||||
ArgGroup::new("issue-content")
|
|
||||||
.args(["content", "editor"])
|
|
||||||
.required(true)
|
|
||||||
),
|
|
||||||
group(
|
group(
|
||||||
ArgGroup::new("issue-subject")
|
ArgGroup::new("issue-subject")
|
||||||
.args(["editor", "subject"])
|
.required(true)
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub struct NewArgs {
|
pub struct NewArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// Markdown content for the issue. Cannot be used together with the
|
/// Markdown content for the issue. Cannot be used together with the
|
||||||
/// `--editor` flag.
|
/// `--editor` flag.
|
||||||
#[arg(short, long)]
|
#[arg(short, long, group = "issue-content")]
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
/// Opens the user's default editor to write issue content. The first line
|
/// Opens the user's default editor to write issue content. The first line
|
||||||
/// will be used as the issue subject.
|
/// will be used as the issue subject.
|
||||||
#[arg(short, long)]
|
#[arg(short, long, group = "issue-subject", group = "issue-content")]
|
||||||
editor: bool,
|
editor: bool,
|
||||||
/// The issue subject. Cannot be used together with the `--editor` flag.
|
/// The issue subject. Cannot be used together with the `--editor` flag.
|
||||||
#[arg(long)]
|
#[arg(long, group = "issue-subject")]
|
||||||
subject: Option<String>,
|
subject: Option<String>,
|
||||||
/// Labels for the issue. Can be specified as arguments (-l bug) or hashtags
|
/// Labels for the issue. Can be specified as arguments (-l bug) or hashtags
|
||||||
/// in content (#bug).
|
/// in content (#bug).
|
||||||
@@ -71,29 +72,6 @@ pub struct NewArgs {
|
|||||||
label: Vec<String>,
|
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 {
|
impl CommandRunner for NewArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||||
@@ -113,15 +91,29 @@ impl CommandRunner for NewArgs {
|
|||||||
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (subject, content) = self.issue_content()?;
|
let (subject, content) = utils::subject_and_body(self.subject, self.content, ".md")?;
|
||||||
let content_details = client.parse_content(&content).await;
|
|
||||||
|
|
||||||
let event =
|
let content_details = if let Some(content) = &content {
|
||||||
EventBuilder::new_git_issue(coordinates.as_slice(), content, subject, self.label)?
|
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()
|
.dedup_tags()
|
||||||
.pow(options.pow.unwrap_or_default())
|
.pow(options.pow.unwrap_or_default())
|
||||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||||
.tags(content_details.clone().into_tags())
|
.tags(
|
||||||
|
content_details
|
||||||
|
.clone()
|
||||||
|
.map(|c| c.into_tags())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
.build(user_pubk);
|
.build(user_pubk);
|
||||||
let event_id = event.id.expect("There is an id");
|
let event_id = event.id.expect("There is an id");
|
||||||
|
|
||||||
@@ -129,13 +121,12 @@ impl CommandRunner for NewArgs {
|
|||||||
relays,
|
relays,
|
||||||
naddrs.extract_relays(),
|
naddrs.extract_relays(),
|
||||||
utils::add_write_relays(relays_list.as_ref()),
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
client
|
repos.extract_relays(),
|
||||||
.fetch_repos(&naddrs.into_coordinates())
|
|
||||||
.await?
|
|
||||||
.extract_relays(),
|
|
||||||
// Include read relays for each maintainer (if found)
|
// Include read relays for each maintainer (if found)
|
||||||
client.read_relays_from_users(&maintainers).await,
|
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();
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -28,11 +28,17 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ReopenArgs {
|
pub struct ReopenArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The closed issue id to reopen it
|
/// The closed issue id to reopen it
|
||||||
issue_id: NostrEvent,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -28,11 +28,17 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ResolveArgs {
|
pub struct ResolveArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The issue id to resolve it
|
/// The issue id to resolve it
|
||||||
issue_id: NostrEvent,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::{event::Kind, filter::Filter};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::N34Result,
|
||||||
nostr_utils::{
|
|
||||||
NostrClient,
|
|
||||||
traits::{GitIssueUtils, NaddrsUtils, ReposUtils},
|
|
||||||
utils,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ViewArgs {
|
pub struct ViewArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The issue id to view it
|
/// The issue id to view it
|
||||||
issue_id: NostrEvent,
|
issue_id: NostrEvent,
|
||||||
@@ -47,47 +47,7 @@ impl CommandRunner for ViewArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
let naddrs = utils::naddrs_or_file(
|
crate::cli::common_commands::view_pr_issue::<false>(options, self.naddrs, self.issue_id)
|
||||||
self.naddrs.flat_naddrs(&options.config.sets)?,
|
.await
|
||||||
&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(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -20,6 +20,8 @@ pub mod config;
|
|||||||
pub mod issue;
|
pub mod issue;
|
||||||
/// `patch` subcommands
|
/// `patch` subcommands
|
||||||
pub mod patch;
|
pub mod patch;
|
||||||
|
/// `pr` subcommands
|
||||||
|
pub mod pr;
|
||||||
/// `reply` command
|
/// `reply` command
|
||||||
pub mod reply;
|
pub mod reply;
|
||||||
/// `repo` subcommands
|
/// `repo` subcommands
|
||||||
@@ -40,6 +42,7 @@ use nostr_connect::client::NostrConnect;
|
|||||||
use self::config::ConfigSubcommands;
|
use self::config::ConfigSubcommands;
|
||||||
use self::issue::IssueSubcommands;
|
use self::issue::IssueSubcommands;
|
||||||
use self::patch::PatchSubcommands;
|
use self::patch::PatchSubcommands;
|
||||||
|
use self::pr::PrSubcommands;
|
||||||
use self::reply::ReplyArgs;
|
use self::reply::ReplyArgs;
|
||||||
use self::repo::RepoSubcommands;
|
use self::repo::RepoSubcommands;
|
||||||
use self::sets::SetsSubcommands;
|
use self::sets::SetsSubcommands;
|
||||||
@@ -73,8 +76,8 @@ pub struct CliOptions {
|
|||||||
#[arg(short = '7', long, group = "signer")]
|
#[arg(short = '7', long, group = "signer")]
|
||||||
pub nip07: bool,
|
pub nip07: bool,
|
||||||
/// Fallbacks relay to write and read from it. Multiple relays can be
|
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||||
/// passed.
|
/// passed, separated by commas.
|
||||||
#[arg(short, long)]
|
#[arg(short, long, value_delimiter = ',')]
|
||||||
pub relays: Vec<RelayOrSet>,
|
pub relays: Vec<RelayOrSet>,
|
||||||
/// Proof of Work difficulty when creatring events
|
/// Proof of Work difficulty when creatring events
|
||||||
#[arg(long, value_name = "DIFFICULTY")]
|
#[arg(long, value_name = "DIFFICULTY")]
|
||||||
@@ -113,6 +116,11 @@ pub enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
subcommands: PatchSubcommands,
|
subcommands: PatchSubcommands,
|
||||||
},
|
},
|
||||||
|
/// Manage pull requests
|
||||||
|
Pr {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: PrSubcommands,
|
||||||
|
},
|
||||||
/// Manage configuration
|
/// Manage configuration
|
||||||
Config {
|
Config {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -150,7 +158,7 @@ impl CliOptions {
|
|||||||
if let Some(ref bunker_url) = self.bunker_url {
|
if let Some(ref bunker_url) = self.bunker_url {
|
||||||
let mut nostrconnect = NostrConnect::new(
|
let mut nostrconnect = NostrConnect::new(
|
||||||
bunker_url.clone(),
|
bunker_url.clone(),
|
||||||
Cli::n34_keypair()?,
|
Cli::n34_keypair().await?,
|
||||||
BUNKER_TIMEOUT,
|
BUNKER_TIMEOUT,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -172,8 +180,7 @@ impl CliOptions {
|
|||||||
|
|
||||||
/// Returns an error if there are no signers
|
/// Returns an error if there are no signers
|
||||||
pub fn ensure_signer(&self) -> N34Result<()> {
|
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);
|
return Err(N34Error::SignerRequired);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -185,6 +192,7 @@ impl fmt::Debug for CliOptions {
|
|||||||
f.debug_struct("CliOptions")
|
f.debug_struct("CliOptions")
|
||||||
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
||||||
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
||||||
|
.field("nip07", &self.nip07)
|
||||||
.field("relays", &self.relays)
|
.field("relays", &self.relays)
|
||||||
.field("pow", &self.pow)
|
.field("pow", &self.pow)
|
||||||
.field("config", &self.config)
|
.field("config", &self.config)
|
||||||
@@ -197,6 +205,6 @@ impl CommandRunner for Commands {
|
|||||||
tracing::trace!("Options: {options:#?}");
|
tracing::trace!("Options: {options:#?}");
|
||||||
tracing::trace!("Handling: {self:#?}");
|
tracing::trace!("Handling: {self:#?}");
|
||||||
|
|
||||||
crate::run_command!(self, options, Repo Issue Sets Patch Config & Reply)
|
crate::run_command!(self, options, Repo Issue Sets Patch Pr Config & Reply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,23 +17,28 @@
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::hashes::sha1::Hash as Sha1Hash;
|
use nostr::hashes::sha1::Hash as Sha1Hash;
|
||||||
|
|
||||||
use super::PatchStatus;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::{CommandRunner, VecNostrEventExt},
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ApplyArgs {
|
pub struct ApplyArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open patch id to apply it. Must be orignal root patch or
|
/// The open patch id to apply it. Must be orignal root patch or
|
||||||
/// revision root
|
/// revision root
|
||||||
@@ -42,18 +47,18 @@ pub struct ApplyArgs {
|
|||||||
/// been applied, not all.
|
/// been applied, not all.
|
||||||
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||||
applied_patches: Vec<NostrEvent>,
|
applied_patches: Vec<NostrEvent>,
|
||||||
/// The applied commits
|
/// The applied commits.
|
||||||
#[arg(num_args = 1..)]
|
#[arg(required = true)]
|
||||||
applied_commits: Vec<Sha1Hash>,
|
applied_commits: Vec<Sha1Hash>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for ApplyArgs {
|
impl CommandRunner for ApplyArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::MergedApplied,
|
PatchPrStatus::MergedApplied,
|
||||||
Some(either::Either::Right(self.applied_commits)),
|
Some(either::Either::Right(self.applied_commits)),
|
||||||
self.applied_patches.into_event_ids(),
|
self.applied_patches.into_event_ids(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
@@ -70,9 +75,7 @@ impl CommandRunner for ApplyArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus("Cannot apply a draft".to_owned()));
|
||||||
"You can't apply a drafted patch".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,23 +16,28 @@
|
|||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use super::PatchStatus;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct CloseArgs {
|
pub struct CloseArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open/drafted patch id to close it. Must be orignal root patch
|
/// The open/drafted patch id to close it. Must be orignal root patch
|
||||||
patch_id: NostrEvent,
|
patch_id: NostrEvent,
|
||||||
@@ -40,11 +45,11 @@ pub struct CloseArgs {
|
|||||||
|
|
||||||
impl CommandRunner for CloseArgs {
|
impl CommandRunner for CloseArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Closed,
|
PatchPrStatus::Closed,
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|patch_status| {
|
|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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,23 +16,28 @@
|
|||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use super::PatchStatus;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct DraftArgs {
|
pub struct DraftArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open patch id to draft it. Must be orignal root patch
|
/// The open patch id to draft it. Must be orignal root patch
|
||||||
patch_id: NostrEvent,
|
patch_id: NostrEvent,
|
||||||
@@ -40,17 +45,17 @@ pub struct DraftArgs {
|
|||||||
|
|
||||||
impl CommandRunner for DraftArgs {
|
impl CommandRunner for DraftArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Draft,
|
PatchPrStatus::Draft,
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"You can't draft an already drafted patch".to_owned(),
|
"You can't draft an already draft patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
use std::{
|
use std::{fs, ops::Deref, path::PathBuf, str::FromStr};
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::{
|
use nostr::{
|
||||||
@@ -43,13 +39,20 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct FetchArgs {
|
pub struct FetchArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
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")]
|
#[arg(short, long, value_name = "PATH")]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
/// The patch id to fetch it
|
/// 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 relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let output_path = self.output.unwrap_or_default();
|
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
|
client
|
||||||
.add_relays(
|
.add_relays(
|
||||||
@@ -111,7 +118,7 @@ impl CommandRunner for FetchArgs {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
let patch = super::GitPatch::from_str(&p.content).map_err(|err| {
|
let patch = super::GitPatch::from_str(&p.content).map_err(|err| {
|
||||||
N34Error::InvalidEvent(format!(
|
N34Error::InvalidPatch(format!(
|
||||||
"Failed to parse the patch `{}`: {err}",
|
"Failed to parse the patch `{}`: {err}",
|
||||||
p.id.to_bech32().expect("Infallible")
|
p.id.to_bech32().expect("Infallible")
|
||||||
))
|
))
|
||||||
@@ -123,14 +130,30 @@ impl CommandRunner for FetchArgs {
|
|||||||
patches.sort_unstable_by_key(|p| p.0.clone());
|
patches.sort_unstable_by_key(|p| p.0.clone());
|
||||||
patches.dedup_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)?;
|
fs::create_dir_all(&output_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
for (patch_path, patch) in patches {
|
||||||
tracing::info!("Writeing `{}` in `{}`", patch.subject, patch_path.display());
|
tracing::info!("Writeing `{}` in `{}`", patch.subject, patch_path.display());
|
||||||
fs::write(patch_path, patch.inner)?;
|
fs::write(patch_path, patch.inner)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -19,17 +19,24 @@ use std::num::NonZeroUsize;
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, common_commands, traits::CommandRunner, types::NaddrOrSet},
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
common_commands,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{EntityType, NaddrOrSet},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ListArgs {
|
pub struct ListArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// Maximum number of patches to list
|
/// Maximum number of patches to list
|
||||||
#[arg(long, default_value = "15")]
|
#[arg(long, default_value = "15")]
|
||||||
@@ -40,7 +47,11 @@ impl CommandRunner for ListArgs {
|
|||||||
const NEED_SIGNER: bool = false;
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
common_commands::list_patches_and_issues(options, self.naddrs, true, self.limit.into())
|
common_commands::list_pr_patches_and_issues::<{ EntityType::Patch as u8 }>(
|
||||||
|
options,
|
||||||
|
self.naddrs,
|
||||||
|
self.limit.into(),
|
||||||
|
)
|
||||||
.await
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,23 +17,28 @@
|
|||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::hashes::sha1::Hash as Sha1Hash;
|
use nostr::hashes::sha1::Hash as Sha1Hash;
|
||||||
|
|
||||||
use super::PatchStatus;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::{CommandRunner, VecNostrEventExt},
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct MergeArgs {
|
pub struct MergeArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open patch id to merge it. Must be orignal root patch or
|
/// The open patch id to merge it. Must be orignal root patch or
|
||||||
/// revision root
|
/// revision root
|
||||||
@@ -48,17 +53,17 @@ pub struct MergeArgs {
|
|||||||
|
|
||||||
impl CommandRunner for MergeArgs {
|
impl CommandRunner for MergeArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::MergedApplied,
|
PatchPrStatus::MergedApplied,
|
||||||
Some(either::Either::Left(self.merge_commit)),
|
Some(either::Either::Left(self.merge_commit)),
|
||||||
self.merged_patches.into_event_ids(),
|
self.merged_patches.into_event_ids(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_merged_or_applied() {
|
if patch_status.is_merged_or_applied() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"You can't merge an already merged patch".to_owned(),
|
"You can't merge an already merged/applied patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +75,7 @@ impl CommandRunner for MergeArgs {
|
|||||||
|
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"You can't merge a drafted patch".to_owned(),
|
"You can't merge a draft patch".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -34,14 +34,12 @@ mod send;
|
|||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::LazyLock,
|
sync::LazyLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use nostr::event::Kind;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use self::apply::ApplyArgs;
|
use self::apply::ApplyArgs;
|
||||||
@@ -55,6 +53,11 @@ use self::send::SendArgs;
|
|||||||
use super::{CliOptions, CommandRunner};
|
use super::{CliOptions, CommandRunner};
|
||||||
use crate::error::{N34Error, N34Result};
|
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.
|
/// Regular expression for extracting the patch subject.
|
||||||
static SUBJECT_RE: LazyLock<Regex> =
|
static SUBJECT_RE: LazyLock<Regex> =
|
||||||
LazyLock::new(|| Regex::new(r"(?m)^Subject: (.*(?:\n .*)*)").unwrap());
|
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)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum PatchSubcommands {
|
pub enum PatchSubcommands {
|
||||||
/// Send one or more patches to a repository.
|
/// Send patches to a repository
|
||||||
Send(SendArgs),
|
Send(SendArgs),
|
||||||
/// Fetches a patch by its id.
|
/// Fetch a patch
|
||||||
Fetch(FetchArgs),
|
Fetch(FetchArgs),
|
||||||
/// Closes an open or drafted patch.
|
/// Close a patch
|
||||||
Close(CloseArgs),
|
Close(CloseArgs),
|
||||||
/// Converts an open patch to draft state.
|
/// Convert to draft
|
||||||
Draft(DraftArgs),
|
Draft(DraftArgs),
|
||||||
/// Reopens a closed or drafted patch.
|
/// Reopen a patch
|
||||||
Reopen(ReopenArgs),
|
Reopen(ReopenArgs),
|
||||||
/// Set an open patch status to applied.
|
/// Mark as applied
|
||||||
Apply(ApplyArgs),
|
Apply(ApplyArgs),
|
||||||
/// Set an open patch status to merged.
|
/// Mark as merged
|
||||||
Merge(MergeArgs),
|
Merge(MergeArgs),
|
||||||
/// List the repositories patches.
|
/// List patches
|
||||||
List(ListArgs),
|
List(ListArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,92 +111,6 @@ pub struct GitPatch {
|
|||||||
pub body: String,
|
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 {
|
impl GitPatch {
|
||||||
/// Returns the patch file name from the subject
|
/// Returns the patch file name from the subject
|
||||||
pub fn filename(&self, parent: impl AsRef<Path>) -> N34Result<PathBuf> {
|
pub fn filename(&self, parent: impl AsRef<Path>) -> N34Result<PathBuf> {
|
||||||
@@ -226,6 +143,14 @@ impl FromStr for GitPatch {
|
|||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(patch_content: &str) -> Result<Self, Self::Err> {
|
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)
|
// Regex for subject (handles multi-line subjects)
|
||||||
let subject = SUBJECT_RE
|
let subject = SUBJECT_RE
|
||||||
.captures(patch_content)
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,23 +16,28 @@
|
|||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
use super::PatchStatus;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::CommandRunner,
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{EntityType, NaddrOrSet, NostrEvent, PatchPrStatus},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ReopenArgs {
|
pub struct ReopenArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The closed/drafted patch id to reopen it. Must be orignal root patch
|
/// The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||||
patch_id: NostrEvent,
|
patch_id: NostrEvent,
|
||||||
@@ -40,11 +45,11 @@ pub struct ReopenArgs {
|
|||||||
|
|
||||||
impl CommandRunner for ReopenArgs {
|
impl CommandRunner for ReopenArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::cli::common_commands::patch_status_command(
|
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||||
options,
|
options,
|
||||||
self.patch_id,
|
self.patch_id,
|
||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Open,
|
PatchPrStatus::Open,
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|patch_status| {
|
|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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -31,7 +31,7 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
|
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
|
||||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt, VecPatchesExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
@@ -47,18 +47,24 @@ const PATCH_ALT_PREFIX: &str = "git patch: ";
|
|||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct SendArgs {
|
pub struct SendArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
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...`.
|
/// `nostr:npub1...`.
|
||||||
#[arg(value_name = "PATCH-PATH", required = true, value_parser = parse_patch_path)]
|
#[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
|
/// Original patch ID if this is a revision of it
|
||||||
#[arg(long, value_name = "EVENT-ID")]
|
#[arg(long, value_name = "EVENT-ID")]
|
||||||
original_patch: Option<NostrEvent>,
|
original_patch: Option<NostrEvent>,
|
||||||
@@ -71,6 +77,7 @@ impl CommandRunner for SendArgs {
|
|||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
|
let patches = self.patches.process_patches().await?;
|
||||||
let repo_coordinates = naddrs.clone().into_coordinates();
|
let repo_coordinates = naddrs.clone().into_coordinates();
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
@@ -91,7 +98,7 @@ impl CommandRunner for SendArgs {
|
|||||||
|
|
||||||
let (events, events_write_relays) = make_patch_series(
|
let (events, events_write_relays) = make_patch_series(
|
||||||
&client,
|
&client,
|
||||||
self.patches,
|
patches,
|
||||||
self.original_patch.as_ref().map(|e| e.event_id),
|
self.original_patch.as_ref().map(|e| e.event_id),
|
||||||
repos.extract_relays().first().cloned(),
|
repos.extract_relays().first().cloned(),
|
||||||
repo_coordinates,
|
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}`");
|
tracing::debug!("Parsing patch file `{patch_path}`");
|
||||||
let patch_content = fs::read_to_string(patch_path)
|
let patch_content = fs::read_to_string(patch_path)
|
||||||
.map_err(|err| format!("Failed to read patch file `{patch_path}`: {err}"))?;
|
.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(
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
/// Quote the replied-to event in the editor
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
quote_to: bool,
|
quote_to: bool,
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The comment (cannot be used with --editor)
|
/// The comment (cannot be used with --editor)
|
||||||
#[arg(short, long)]
|
#[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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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 clap::Args;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
use nostr::{
|
||||||
|
event::{EventBuilder, Tag},
|
||||||
|
key::PublicKey,
|
||||||
|
types::Url,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},
|
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},
|
||||||
@@ -43,6 +47,7 @@ const NOSTR_ADDRESS_FILE_HEADER: &str = r##"# This file contains NIP-19 `naddr`
|
|||||||
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||||
|
|
||||||
"##;
|
"##;
|
||||||
|
const PERSONAL_FORK_HASHTAG: &str = "personal-fork";
|
||||||
|
|
||||||
/// Arguments for the `repo announce` command
|
/// Arguments for the `repo announce` command
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
@@ -71,6 +76,9 @@ pub struct AnnounceArgs {
|
|||||||
/// Skip kebab-case validation for the repository ID
|
/// Skip kebab-case validation for the repository ID
|
||||||
#[arg(long)]
|
#[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
|
/// If set, creates a `nostr-address` file to enable automatic address
|
||||||
/// discovery by n34
|
/// discovery by n34
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -94,7 +102,7 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &relays)?;
|
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.repo_id,
|
||||||
self.name.map(utils::str_trim),
|
self.name.map(utils::str_trim),
|
||||||
self.description.map(utils::str_trim),
|
self.description.map(utils::str_trim),
|
||||||
@@ -106,9 +114,13 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
self.force_id,
|
self.force_id,
|
||||||
)?
|
)?
|
||||||
.dedup_tags()
|
.dedup_tags()
|
||||||
.pow(options.pow.unwrap_or_default())
|
.pow(options.pow.unwrap_or_default());
|
||||||
.build(user_pubk);
|
|
||||||
|
|
||||||
|
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 {
|
if self.address_file {
|
||||||
let address_path = std::env::current_dir()?.join(NOSTR_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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,12 +16,15 @@
|
|||||||
|
|
||||||
/// `repo announce` subcommand
|
/// `repo announce` subcommand
|
||||||
mod announce;
|
mod announce;
|
||||||
|
/// `repo state` subcommand
|
||||||
|
mod state;
|
||||||
/// `repo view` subcommand
|
/// `repo view` subcommand
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
use self::announce::AnnounceArgs;
|
use self::announce::AnnounceArgs;
|
||||||
|
use self::state::StateArgs;
|
||||||
use self::view::ViewArgs;
|
use self::view::ViewArgs;
|
||||||
use super::{CliOptions, CommandRunner};
|
use super::{CliOptions, CommandRunner};
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
@@ -32,10 +35,12 @@ pub enum RepoSubcommands {
|
|||||||
View(ViewArgs),
|
View(ViewArgs),
|
||||||
/// Broadcast and update a git repository
|
/// Broadcast and update a git repository
|
||||||
Announce(AnnounceArgs),
|
Announce(AnnounceArgs),
|
||||||
|
/// Repository state announcements
|
||||||
|
State(StateArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for RepoSubcommands {
|
impl CommandRunner for RepoSubcommands {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::nips::nip19::ToBech32;
|
use nostr::nips::nip19::ToBech32;
|
||||||
|
|
||||||
@@ -33,11 +31,13 @@ use crate::{
|
|||||||
/// Arguments for the `repo view` command
|
/// Arguments for the `repo view` command
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct ViewArgs {
|
pub struct ViewArgs {
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
|
/// 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.
|
/// 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>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +66,16 @@ impl CommandRunner for ViewArgs {
|
|||||||
repo_details.push_str(&format!("\nDescription: {desc}"));
|
repo_details.push_str(&format!("\nDescription: {desc}"));
|
||||||
}
|
}
|
||||||
if !repo.web.is_empty() {
|
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() {
|
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() {
|
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 {
|
if let Some(euc) = repo.euc {
|
||||||
repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
|
repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
|
||||||
@@ -80,7 +83,7 @@ impl CommandRunner for ViewArgs {
|
|||||||
if !repo.maintainers.is_empty() {
|
if !repo.maintainers.is_empty() {
|
||||||
repo_details.push_str(&format!(
|
repo_details.push_str(&format!(
|
||||||
"\nMaintainers:\n{}",
|
"\nMaintainers:\n{}",
|
||||||
format_list(
|
utils::format_iter(
|
||||||
repo.maintainers
|
repo.maintainers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.to_bech32().expect("Infallible"))
|
.map(|p| p.to_bech32().expect("Infallible"))
|
||||||
@@ -94,16 +97,3 @@ impl CommandRunner for ViewArgs {
|
|||||||
Ok(())
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -31,12 +31,18 @@ pub struct NewArgs {
|
|||||||
/// Unique name for the set
|
/// Unique name for the set
|
||||||
name: String,
|
name: String,
|
||||||
/// Optional relay to add it to the set, either as URL or set name to
|
/// Optional relay to add it to the set, either as URL or set name to
|
||||||
/// extract its relays. [aliases: `--sr`]
|
/// extract its relays, separated by commas. [aliases: `--sr`]
|
||||||
#[arg(long = "set-relay", alias("sr"))]
|
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||||
relays: Vec<RelayOrSet>,
|
relays: Vec<RelayOrSet>,
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
/// 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>,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -30,13 +30,19 @@ pub struct RemoveArgs {
|
|||||||
/// Set name to delete
|
/// Set name to delete
|
||||||
name: String,
|
name: String,
|
||||||
/// Specific relay to remove it from the set, either as URL or set name to
|
/// Specific relay to remove it from the set, either as URL or set name to
|
||||||
/// extract its relays. [aliases: `--sr`]
|
/// extract its relays, separated by commas. [aliases: `--sr`]
|
||||||
#[arg(long = "set-relay", alias("sr"))]
|
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||||
relays: Vec<RelayOrSet>,
|
relays: Vec<RelayOrSet>,
|
||||||
|
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
/// 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>,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -31,13 +31,19 @@ use crate::{
|
|||||||
pub struct UpdateArgs {
|
pub struct UpdateArgs {
|
||||||
/// Name of the set to update
|
/// Name of the set to update
|
||||||
name: String,
|
name: String,
|
||||||
/// Add relay to the set, either as URL or set name to extract its relays.
|
/// Add relay to the set, either as URL or set name to extract its relays,
|
||||||
/// [aliases: `--sr`]
|
/// separated by commas. [aliases: `--sr`]
|
||||||
#[arg(long = "set-relay", alias("sr"))]
|
#[arg(long = "set-relay", alias("sr"), value_delimiter = ',')]
|
||||||
relays: Vec<RelayOrSet>,
|
relays: Vec<RelayOrSet>,
|
||||||
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
/// Repository addresses
|
||||||
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
///
|
||||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
/// 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>,
|
naddrs: Vec<NaddrOrSet>,
|
||||||
/// Replace existing relays/repositories instead of adding to them
|
/// Replace existing relays/repositories instead of adding to them
|
||||||
#[arg(long = "override")]
|
#[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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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 either::Either;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind},
|
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard},
|
||||||
filter::Filter,
|
filter::{Alphabet, Filter, SingleLetterTag},
|
||||||
hashes::sha1::Hash as Sha1Hash,
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
nips::{nip10::Marker, nip19::ToBech32},
|
nips::{nip10::Marker, nip19::ToBech32},
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
@@ -28,17 +28,19 @@ use nostr::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
issue::IssueStatus,
|
issue::IssueStatus,
|
||||||
patch::PatchStatus,
|
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
cli::{CliOptions, patch::GitPatch, types::PatchPrStatus},
|
||||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::traits::{GitIssuePrMetadata, GitPatchUtils, ReposUtils},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, patch::GitPatch},
|
cli::{
|
||||||
error::{N34Error, N34Result},
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
nostr_utils::traits::{GitIssueUtils, GitPatchUtils, ReposUtils},
|
types::EntityType,
|
||||||
|
},
|
||||||
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Updates the issue's status to `new_status` after validating it with
|
/// Updates the issue's status to `new_status` after validating it with
|
||||||
@@ -123,17 +125,20 @@ pub async fn issue_status_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the patch's status to `new_status` after validating it with
|
/// Updates the patch/pr's status to `new_status` after validating it with
|
||||||
/// `check_fn`.
|
/// `check_fn`. The `ENTITY_TYPE` can only be a pull request or a patch
|
||||||
pub async fn patch_status_command(
|
pub async fn patch_pr_status_command<const ENTITY_TYPE: u8>(
|
||||||
options: CliOptions,
|
options: CliOptions,
|
||||||
patch_id: NostrEvent,
|
patch_pr_id: NostrEvent,
|
||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
new_status: PatchStatus,
|
new_status: PatchPrStatus,
|
||||||
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
||||||
merge_or_applied_patches: Vec<EventId>,
|
merge_or_applied_patches: Vec<EventId>,
|
||||||
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
|
check_fn: impl FnOnce(&PatchPrStatus) -> N34Result<()>,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
|
EntityType::is_pr_or_patch::<ENTITY_TYPE>();
|
||||||
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
|
|
||||||
let naddrs = utils::naddrs_or_file(
|
let naddrs = utils::naddrs_or_file(
|
||||||
naddrs.flat_naddrs(&options.config.sets)?,
|
naddrs.flat_naddrs(&options.config.sets)?,
|
||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
@@ -142,7 +147,7 @@ pub async fn patch_status_command(
|
|||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = client.pubkey().await?;
|
let user_pubk = client.pubkey().await?;
|
||||||
client
|
client
|
||||||
.add_relays(&[naddrs.extract_relays(), patch_id.relays].concat())
|
.add_relays(&[naddrs.extract_relays(), patch_pr_id.relays].concat())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let owners = naddrs.extract_owners();
|
let owners = naddrs.extract_owners();
|
||||||
@@ -152,9 +157,14 @@ pub async fn patch_status_command(
|
|||||||
let relay_hint = repos.extract_relays().first().cloned();
|
let relay_hint = repos.extract_relays().first().cloned();
|
||||||
client.add_relays(&repos.extract_relays()).await;
|
client.add_relays(&repos.extract_relays()).await;
|
||||||
|
|
||||||
let patch_event = client.fetch_patch(patch_id.event_id).await?;
|
let event = if entity_type.is_patch() {
|
||||||
|
client.fetch_patch(patch_pr_id.event_id).await?
|
||||||
|
} else {
|
||||||
|
client.fetch_pr(patch_pr_id.event_id).await?
|
||||||
|
};
|
||||||
|
let authorized_pubkeys = [maintainers.as_slice(), &[event.pubkey], &owners].concat();
|
||||||
|
|
||||||
if patch_event.is_revision_patch() && !new_status.is_merged_or_applied() {
|
if entity_type.is_patch() && event.is_revision_patch() && !new_status.is_merged_or_applied() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
"Invalid action for patch revision. Only 'apply' or 'merge' are allowed, 'open', \
|
||||||
'close', and 'draft' are not supported."
|
'close', and 'draft' are not supported."
|
||||||
@@ -162,27 +172,31 @@ pub async fn patch_status_command(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (root_patch, root_revision) = get_patch_root_revision(&patch_event)?;
|
let (root_patch_or_pr, root_revision) = if entity_type.is_patch() {
|
||||||
let patch_status = client
|
get_patch_root_revision(&event)?
|
||||||
.fetch_patch_status(
|
} else {
|
||||||
root_patch,
|
(event.id, None)
|
||||||
root_revision,
|
};
|
||||||
[maintainers.as_slice(), &[patch_event.pubkey], &owners].concat(),
|
let current_status = if entity_type.is_patch() {
|
||||||
)
|
client
|
||||||
.await?;
|
.fetch_patch_status(root_patch_or_pr, root_revision, authorized_pubkeys.clone())
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.fetch_pr_status(event.id, authorized_pubkeys.clone())
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
check_fn(&patch_status)?;
|
check_fn(¤t_status)?;
|
||||||
|
|
||||||
let mut status_builder = EventBuilder::new(new_status.kind(), "")
|
let mut status_builder = EventBuilder::new(new_status.kind(), "")
|
||||||
.pow(options.pow.unwrap_or_default())
|
.pow(options.pow.unwrap_or_default())
|
||||||
.tag(utils::event_reply_tag(
|
.tag(utils::event_reply_tag(
|
||||||
&root_patch,
|
&root_patch_or_pr,
|
||||||
relay_hint.as_ref(),
|
relay_hint.as_ref(),
|
||||||
Marker::Root,
|
Marker::Root,
|
||||||
))
|
))
|
||||||
.tag(Tag::public_key(patch_event.pubkey))
|
.tags(authorized_pubkeys.iter().map(|p| Tag::public_key(*p)))
|
||||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
|
||||||
.tags(owners.iter().map(|p| Tag::public_key(*p)))
|
|
||||||
.tags(
|
.tags(
|
||||||
coordinates
|
coordinates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -211,6 +225,7 @@ pub async fn patch_status_command(
|
|||||||
.tags(commits.into_iter().map(Tag::reference));
|
.tags(commits.into_iter().map(Tag::reference));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Patch only
|
||||||
if let Some(root_revision) = root_revision {
|
if let Some(root_revision) = root_revision {
|
||||||
status_builder = status_builder.tag(utils::event_reply_tag(
|
status_builder = status_builder.tag(utils::event_reply_tag(
|
||||||
&root_revision,
|
&root_revision,
|
||||||
@@ -219,6 +234,7 @@ pub async fn patch_status_command(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch only
|
||||||
if !merge_or_applied_patches.is_empty() {
|
if !merge_or_applied_patches.is_empty() {
|
||||||
status_builder = status_builder.tags(
|
status_builder = status_builder.tags(
|
||||||
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
||||||
@@ -228,7 +244,6 @@ pub async fn patch_status_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let status_event = status_builder.dedup_tags().build(user_pubk);
|
let status_event = status_builder.dedup_tags().build(user_pubk);
|
||||||
|
|
||||||
let event_id = status_event.id.expect("There is an id");
|
let event_id = status_event.id.expect("There is an id");
|
||||||
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
@@ -236,10 +251,7 @@ pub async fn patch_status_command(
|
|||||||
naddrs.extract_relays(),
|
naddrs.extract_relays(),
|
||||||
repos.extract_relays(),
|
repos.extract_relays(),
|
||||||
utils::add_write_relays(user_relays_list.as_ref()),
|
utils::add_write_relays(user_relays_list.as_ref()),
|
||||||
client.read_relays_from_user(patch_event.pubkey).await,
|
client.read_relays_from_users(&authorized_pubkeys).await,
|
||||||
client
|
|
||||||
.read_relays_from_users(&[maintainers, owners].concat())
|
|
||||||
.await,
|
|
||||||
]
|
]
|
||||||
.concat();
|
.concat();
|
||||||
|
|
||||||
@@ -252,13 +264,14 @@ pub async fn patch_status_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch and display patches and issues for given repositories.
|
/// Fetches and displays pull requests, patches, and issues for specified
|
||||||
/// If `list_patches` is true, lists patches instead of issues.
|
/// repositories. The `limit` parameter sets the maximum number of items to
|
||||||
/// `limit` controls the maximum number of items to fetch.
|
/// retrieve.
|
||||||
pub async fn list_patches_and_issues(
|
///
|
||||||
|
/// The `ENTITY_TYPE` const is `[EntityType]` enum as u8.
|
||||||
|
pub async fn list_pr_patches_and_issues<const ENTITY_TYPE: u8>(
|
||||||
options: CliOptions,
|
options: CliOptions,
|
||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
list_patches: bool,
|
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
let naddrs = utils::check_empty_naddrs(utils::naddrs_or_file(
|
||||||
@@ -266,6 +279,7 @@ pub async fn list_patches_and_issues(
|
|||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
@@ -279,18 +293,12 @@ pub async fn list_patches_and_issues(
|
|||||||
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let kind = if list_patches {
|
|
||||||
Kind::GitPatch
|
|
||||||
} else {
|
|
||||||
Kind::GitIssue
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut filter = Filter::new()
|
let mut filter = Filter::new()
|
||||||
.coordinates(coordinates.iter())
|
.coordinates(coordinates.iter())
|
||||||
.kind(kind)
|
.kind(entity_type.kind())
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
if list_patches {
|
if entity_type.is_patch() {
|
||||||
filter = filter.hashtag("root");
|
filter = filter.hashtag("root");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,13 +310,21 @@ pub async fn list_patches_and_issues(
|
|||||||
arc_client
|
arc_client
|
||||||
.fetch_events(filter)
|
.fetch_events(filter)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map(|event| {
|
.map(|event| {
|
||||||
let c = arc_client.clone();
|
let c = arc_client.clone();
|
||||||
let keys = authorized_pubkeys.clone();
|
let keys = authorized_pubkeys.clone();
|
||||||
async move {
|
async move {
|
||||||
let status = if list_patches {
|
let status = match entity_type {
|
||||||
|
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)?;
|
let (root, root_revision) = get_patch_root_revision(&event)?;
|
||||||
c.fetch_patch_status(
|
c.fetch_patch_status(
|
||||||
root,
|
root,
|
||||||
@@ -316,15 +332,18 @@ pub async fn list_patches_and_issues(
|
|||||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(Either::Left)?
|
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||||
} else {
|
}
|
||||||
|
EntityType::Issue => {
|
||||||
c.fetch_issue_status(
|
c.fetch_issue_status(
|
||||||
event.id,
|
event.id,
|
||||||
[keys.as_slice(), &[event.pubkey]].concat(),
|
[keys.as_slice(), &[event.pubkey]].concat(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(Either::Right)?
|
.map(|s| (s.as_str(), s.kind().as_u16()))?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
N34Result::Ok((event, status))
|
N34Result::Ok((event, status))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -332,11 +351,11 @@ pub async fn list_patches_and_issues(
|
|||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|r| r.ok()),
|
.filter_map(|r| r.ok()),
|
||||||
|(_, status)| status.as_ref().either_into::<Kind>(),
|
|(_, (_, k))| *k,
|
||||||
);
|
);
|
||||||
|
|
||||||
let lines = events
|
let lines = events
|
||||||
.map(|(event, status)| format_patch_and_issue(&event, status))
|
.map(|(event, (status, _))| format_entity::<ENTITY_TYPE>(&event, status))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let max_width = lines
|
let max_width = lines
|
||||||
@@ -366,11 +385,14 @@ fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<Ev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats an event as either a patch or an issue. For patches, extracts the
|
/// Formats patch, issue or PR. For patches, extracts the
|
||||||
/// subject line from the Git patch format. For issues, combines the subject
|
/// subject line from the Git patch format. For issues and PRs, combines the
|
||||||
/// with labels. The output includes status and formatted ID.
|
/// subject with labels. The output includes status and formatted ID.
|
||||||
fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus>) -> String {
|
fn format_entity<const ENTITY_TYPE: u8>(event: &Event, status: &str) -> String {
|
||||||
let subject = if status.is_left() {
|
let entity_type = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
|
|
||||||
|
let subject = match entity_type {
|
||||||
|
EntityType::Patch => {
|
||||||
GitPatch::from_str(&event.content)
|
GitPatch::from_str(&event.content)
|
||||||
.map(|p| p.subject)
|
.map(|p| p.subject)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
@@ -383,16 +405,20 @@ fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus
|
|||||||
.trim_start_matches("Subject: ")
|
.trim_start_matches("Subject: ")
|
||||||
.to_owned()
|
.to_owned()
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
let labels = event.extract_issue_labels();
|
_ => {
|
||||||
let subject = event.extract_issue_subject();
|
// Issues and PRs
|
||||||
|
let labels = event.extract_event_labels();
|
||||||
|
let subject = event.extract_event_subject();
|
||||||
|
|
||||||
if labels.is_empty() {
|
if labels.is_empty() {
|
||||||
subject.to_owned()
|
subject.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!(r#""{subject}" {labels}"#)
|
format!(r#""{subject}" {labels}"#)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"({status}) {}\nID: {}\n",
|
"({status}) {}\nID: {}\n",
|
||||||
utils::smart_wrap(&subject, 85),
|
utils::smart_wrap(&subject, 85),
|
||||||
@@ -436,3 +462,115 @@ async fn build_patches_quote(
|
|||||||
}))
|
}))
|
||||||
.await
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -77,7 +77,7 @@ pub struct CliConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keyring_secret_key: bool,
|
pub keyring_secret_key: bool,
|
||||||
/// Signs events using the browser's NIP-07 extension.
|
/// Signs events using the browser's NIP-07 extension.
|
||||||
#[serde(default)]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub nip07: Option<SocketAddr>,
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -32,6 +32,8 @@ pub mod parsers;
|
|||||||
pub mod traits;
|
pub mod traits;
|
||||||
/// Common helper types used throughout the CLI.
|
/// Common helper types used throughout the CLI.
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
/// CLI utils
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -40,8 +42,6 @@ use nostr::key::Keys;
|
|||||||
use nostr::key::SecretKey;
|
use nostr::key::SecretKey;
|
||||||
use nostr_browser_signer_proxy::BrowserSignerProxy;
|
use nostr_browser_signer_proxy::BrowserSignerProxy;
|
||||||
use nostr_browser_signer_proxy::BrowserSignerProxyOptions;
|
use nostr_browser_signer_proxy::BrowserSignerProxyOptions;
|
||||||
use nostr_keyring::KeyringError;
|
|
||||||
use nostr_keyring::NostrKeyring;
|
|
||||||
use types::RelayOrSet;
|
use types::RelayOrSet;
|
||||||
|
|
||||||
pub use self::commands::*;
|
pub use self::commands::*;
|
||||||
@@ -50,7 +50,7 @@ use self::traits::CommandRunner;
|
|||||||
use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;
|
use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;
|
||||||
use crate::error::N34Error;
|
use crate::error::N34Error;
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
use crate::nostr_utils::traits::NostrKeyringErrorUtils;
|
use crate::nostr_keyring;
|
||||||
|
|
||||||
/// Header message, used in the help message
|
/// Header message, used in the help message
|
||||||
const HEADER: &str = r#"Copyright (C) 2025 Awiteb <a@4rs.nl>
|
const HEADER: &str = r#"Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
@@ -84,13 +84,6 @@ pub struct Cli {
|
|||||||
|
|
||||||
|
|
||||||
impl 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
|
/// Executes the command
|
||||||
pub async fn run(self) -> N34Result<()> {
|
pub async fn run(self) -> N34Result<()> {
|
||||||
self.command.run(self.options).await
|
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
|
/// Gets the n34 keypair from the keyring or generates and stores a new one
|
||||||
/// if none exists.
|
/// if none exists.
|
||||||
pub fn n34_keypair() -> N34Result<Keys> {
|
pub async fn n34_keypair() -> N34Result<Keys> {
|
||||||
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
match nostr_keyring::n34::get().await {
|
||||||
|
|
||||||
match keyring.get(Self::N34_KEY_PAIR_ENTRY) {
|
|
||||||
Ok(keys) => Ok(keys),
|
Ok(keys) => Ok(keys),
|
||||||
Err(nostr_keyring::Error::Keyring(KeyringError::NoEntry)) => {
|
Err(N34Error::Keyring(keyring::Error::NoEntry)) => {
|
||||||
let new_keys = Keys::generate();
|
let new_keys = Keys::generate();
|
||||||
keyring.set(Self::N34_KEY_PAIR_ENTRY, &new_keys)?;
|
nostr_keyring::n34::set(&new_keys).await?;
|
||||||
Ok(new_keys)
|
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
|
/// 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
|
/// is provided, stores and returns it. If no key exists and none is
|
||||||
/// provided, returns an error.
|
/// provided, returns an error.
|
||||||
pub fn user_keypair(secret_key: Option<SecretKey>) -> N34Result<Keys> {
|
pub async fn user_keypair(secret_key: Option<SecretKey>) -> N34Result<Keys> {
|
||||||
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
let keyring_key = nostr_keyring::user::get().await;
|
||||||
let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY);
|
|
||||||
|
|
||||||
if let Err(ref err) = keyring_key
|
if let Err(ref err) = keyring_key
|
||||||
&& err.is_keyring_no_entry()
|
&& err.is_keyring_no_entry()
|
||||||
&& let Some(secret_key) = secret_key
|
&& let Some(secret_key) = secret_key
|
||||||
{
|
{
|
||||||
let keypair = Keys::new(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);
|
return Ok(keypair);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +122,7 @@ impl Cli {
|
|||||||
if err.is_keyring_no_entry() {
|
if err.is_keyring_no_entry() {
|
||||||
N34Error::SecretKeyKeyringWithoutEntry
|
N34Error::SecretKeyKeyringWithoutEntry
|
||||||
} else {
|
} else {
|
||||||
N34Error::Keyring(err)
|
err
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -140,7 +130,7 @@ impl Cli {
|
|||||||
|
|
||||||
/// Processes the CLI configuration by applying fallback values from config if
|
/// Processes the CLI configuration by applying fallback values from config if
|
||||||
/// needed. Returns the processed Cli configuration if successful.
|
/// 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);
|
cli.options.pow = cli.options.pow.or(cli.options.config.pow);
|
||||||
|
|
||||||
if cli.options.relays.is_empty()
|
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());
|
cli.options.bunker_url = Some(bunker_url.clone());
|
||||||
} else if cli.options.config.keyring_secret_key {
|
} else if cli.options.config.keyring_secret_key {
|
||||||
cli.options.secret_key = Some(
|
cli.options.secret_key = Some(
|
||||||
Cli::user_keypair(cli.options.secret_key)?
|
Cli::user_keypair(cli.options.secret_key)
|
||||||
|
.await?
|
||||||
.secret_key()
|
.secret_key()
|
||||||
.clone(),
|
.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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -22,6 +22,7 @@ use std::{
|
|||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
Kind,
|
Kind,
|
||||||
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
nips::{
|
nips::{
|
||||||
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||||
nip46::NostrConnectURI,
|
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.
|
/// 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>
|
pub fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};
|
use nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use super::CliOptions;
|
use super::CliOptions;
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
ConfigError,
|
ConfigError,
|
||||||
RepoRelaySet,
|
RepoRelaySet,
|
||||||
|
patch::GitPatch,
|
||||||
types::{NaddrOrSet, NostrEvent, RelayOrSet},
|
types::{NaddrOrSet, NostrEvent, RelayOrSet},
|
||||||
|
utils,
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
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
|
/// Helper function that checks for duplicates in a sorted slice
|
||||||
fn duplicate_in_sorted<T: PartialEq + Clone>(items: &[T]) -> Option<&T> {
|
fn duplicate_in_sorted<T: PartialEq + Clone>(items: &[T]) -> Option<&T> {
|
||||||
items.windows(2).find(|w| w[0] == w[1]).map(|w| &w[0])
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{EventId, Kind},
|
event::{EventId, Kind},
|
||||||
@@ -53,6 +53,18 @@ pub enum RelayOrSet {
|
|||||||
Set(String),
|
Set(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enum representing the type of entity to handle in common commands.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum EntityType {
|
||||||
|
/// Pull Request
|
||||||
|
PullRequest,
|
||||||
|
/// Patch
|
||||||
|
Patch,
|
||||||
|
/// Issue
|
||||||
|
Issue,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses and represents a Nostr `nevent1` or `note1`.
|
/// Parses and represents a Nostr `nevent1` or `note1`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct NostrEvent {
|
pub struct NostrEvent {
|
||||||
@@ -78,6 +90,62 @@ impl AuthUrlHandler for EchoAuthUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EntityType {
|
||||||
|
/// Returns true if the entity is a pull request.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_pr(&self) -> bool {
|
||||||
|
matches!(self, Self::PullRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the entity is a patch.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_patch(&self) -> bool {
|
||||||
|
matches!(self, Self::Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the entity is an issue.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_issue(&self) -> bool {
|
||||||
|
matches!(self, Self::Issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the kind of the entity
|
||||||
|
#[inline]
|
||||||
|
pub const fn kind(&self) -> Kind {
|
||||||
|
match self {
|
||||||
|
Self::PullRequest => crate::cli::pr::PR_KIND,
|
||||||
|
Self::Patch => Kind::GitPatch,
|
||||||
|
Self::Issue => Kind::GitIssue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a [`u8`] value to the corresponding enum variant.
|
||||||
|
#[inline]
|
||||||
|
pub const fn from_u8<const NUM: u8>() -> Self {
|
||||||
|
const {
|
||||||
|
match NUM {
|
||||||
|
val if val == Self::PullRequest as u8 => Self::PullRequest,
|
||||||
|
val if val == Self::Patch as u8 => Self::Patch,
|
||||||
|
val if val == Self::Issue as u8 => Self::Issue,
|
||||||
|
_ => {
|
||||||
|
panic!("No enum with the given numeric value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures the entity is either a pull request or a patch. Compilation
|
||||||
|
/// will fail if the entity is neither.
|
||||||
|
pub const fn is_pr_or_patch<const ENTITY_TYPE: u8>() {
|
||||||
|
const {
|
||||||
|
let entity = EntityType::from_u8::<ENTITY_TYPE>();
|
||||||
|
if !entity.is_pr() && !entity.is_patch() {
|
||||||
|
panic!("The entity should be a pull request or a patch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NaddrOrSet {
|
impl NaddrOrSet {
|
||||||
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
||||||
/// Returns error if the set naddrs are empty or the set not found.
|
/// Returns error if the set naddrs are empty or the set not found.
|
||||||
@@ -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> {
|
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
||||||
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -51,10 +51,21 @@ pub enum N34Error {
|
|||||||
Addr(#[from] AddrParseError),
|
Addr(#[from] AddrParseError),
|
||||||
#[error("Browser Signer Proxy Error: {0}")]
|
#[error("Browser Signer Proxy Error: {0}")]
|
||||||
BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),
|
BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),
|
||||||
|
#[error("Keys error: {0}")]
|
||||||
|
Keys(#[from] nostr::key::Error),
|
||||||
#[error("Keyring error: {0}")]
|
#[error("Keyring error: {0}")]
|
||||||
Keyring(#[from] nostr_keyring::Error),
|
Keyring(#[from] keyring::Error),
|
||||||
|
#[error("Join error: {0}")]
|
||||||
|
Join(#[from] tokio::task::JoinError),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Config(#[from] ConfigError),
|
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")]
|
#[error("No editor specified in the `EDITOR` environment variable")]
|
||||||
EditorNotFound,
|
EditorNotFound,
|
||||||
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
||||||
@@ -71,6 +82,8 @@ pub enum N34Error {
|
|||||||
InvalidRepoId,
|
InvalidRepoId,
|
||||||
#[error("Invalid event: {0}")]
|
#[error("Invalid event: {0}")]
|
||||||
InvalidEvent(String),
|
InvalidEvent(String),
|
||||||
|
#[error("Invalid patch: {0}")]
|
||||||
|
InvalidPatch(String),
|
||||||
#[error("Bech32 error: {0}")]
|
#[error("Bech32 error: {0}")]
|
||||||
Bech32(#[from] nostr::nips::nip19::Error),
|
Bech32(#[from] nostr::nips::nip19::Error),
|
||||||
#[error("Event error: {0}")]
|
#[error("Event error: {0}")]
|
||||||
@@ -123,9 +136,14 @@ pub enum N34Error {
|
|||||||
)]
|
)]
|
||||||
CanNotFoundIssue,
|
CanNotFoundIssue,
|
||||||
#[error(
|
#[error(
|
||||||
"Patch not found, make sure it is in the relays and make sure that the ID is an patch ID"
|
"Patch not found, make sure it is in the relays and make sure that the ID is a patch ID"
|
||||||
)]
|
)]
|
||||||
CanNotFoundPatch,
|
CanNotFoundPatch,
|
||||||
|
#[error(
|
||||||
|
"Pull request not found, make sure it is in the relays and make sure that the ID is an \
|
||||||
|
pull request ID"
|
||||||
|
)]
|
||||||
|
CanNotFoundPr,
|
||||||
#[error(r#"The given patch id is not a root patch. It must contains `["t", "root"]` tag"#)]
|
#[error(r#"The given patch id is not a root patch. It must contains `["t", "root"]` tag"#)]
|
||||||
NotRootPatch,
|
NotRootPatch,
|
||||||
#[error("This status kind can't be set for an issue: {0}")]
|
#[error("This status kind can't be set for an issue: {0}")]
|
||||||
@@ -134,7 +152,7 @@ pub enum N34Error {
|
|||||||
InvalidPatchStatus(Kind),
|
InvalidPatchStatus(Kind),
|
||||||
#[error("Can't find the root patch of the given patch-revision")]
|
#[error("Can't find the root patch of the given patch-revision")]
|
||||||
RevisionRootNotFound,
|
RevisionRootNotFound,
|
||||||
#[error("Invalid status for the issue/patch: {0}")]
|
#[error("Invalid status: {0}")]
|
||||||
InvalidStatus(String),
|
InvalidStatus(String),
|
||||||
#[error("Not valid bunker URL")]
|
#[error("Not valid bunker URL")]
|
||||||
NotBunkerUrl,
|
NotBunkerUrl,
|
||||||
@@ -143,6 +161,13 @@ pub enum N34Error {
|
|||||||
keyring is enabled to store it"
|
keyring is enabled to store it"
|
||||||
)]
|
)]
|
||||||
SecretKeyKeyringWithoutEntry,
|
SecretKeyKeyringWithoutEntry,
|
||||||
|
#[error(
|
||||||
|
"Your push was not accepted by any GRASP server. Try creating a personal fork, pushing to \
|
||||||
|
your own GRASP servers, and then make a pull request with your GRASP repos as a clone"
|
||||||
|
)]
|
||||||
|
PushRejectedByGraspServers,
|
||||||
|
#[error("The repository doesn't contains any GRASP servers")]
|
||||||
|
NoGraspServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl N34Error {
|
impl N34Error {
|
||||||
@@ -166,4 +191,10 @@ impl N34Error {
|
|||||||
_ => ExitCode::FAILURE,
|
_ => 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/main.rs
73
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,17 +18,20 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
/// N34 errors
|
/// N34 errors
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
/// Nostr keyring
|
||||||
|
pub mod nostr_keyring;
|
||||||
/// Nostr utils module
|
/// Nostr utils module
|
||||||
pub mod nostr_utils;
|
pub mod nostr_utils;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
fs::File,
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap_verbosity_flag::Verbosity;
|
use clap_verbosity_flag::Verbosity;
|
||||||
use tracing::Level;
|
use tracing::{Level, level_filters::LevelFilter};
|
||||||
use tracing_subscriber::{Layer, filter, layer::SubscriberExt};
|
use tracing_subscriber::{Layer, filter, layer::SubscriberExt};
|
||||||
|
|
||||||
use self::cli::Cli;
|
use self::cli::Cli;
|
||||||
@@ -37,42 +40,64 @@ use self::cli::Cli;
|
|||||||
/// open.
|
/// open.
|
||||||
static EDITOR_OPEN: AtomicBool = AtomicBool::new(false);
|
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.
|
/// Configures the logging level based on the provided verbosity.
|
||||||
///
|
///
|
||||||
/// When verbosity is set to TRACE, includes file and line numbers in logs.
|
/// When verbosity is set to TRACE, includes file and line numbers in logs.
|
||||||
fn set_log_level(verbosity: Verbosity) {
|
fn set_log_level(verbosity: Verbosity, logs_file: File) {
|
||||||
let is_trace = verbosity
|
let editor_filter = filter::dynamic_filter_fn(move |m, _| {
|
||||||
.tracing_level()
|
|
||||||
.is_some_and(|l| l == tracing::Level::TRACE);
|
|
||||||
|
|
||||||
let logs_filter = filter::dynamic_filter_fn(move |m, _| {
|
|
||||||
// Disable all logs while editor is open
|
// Disable all logs while editor is open
|
||||||
verbosity.tracing_level().unwrap_or(Level::ERROR) >= *m.level()
|
verbosity.tracing_level().unwrap_or(Level::ERROR) >= *m.level()
|
||||||
|
&& (m.name().starts_with("event src") || m.name().contains("nostr"))
|
||||||
&& !EDITOR_OPEN.load(Ordering::Relaxed)
|
&& !EDITOR_OPEN.load(Ordering::Relaxed)
|
||||||
});
|
});
|
||||||
|
|
||||||
let logs_layer = tracing_subscriber::fmt::layer()
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_file(is_trace)
|
.with_ansi(false)
|
||||||
.with_line_number(is_trace)
|
.with_writer(logs_file)
|
||||||
.without_time();
|
.with_file(true)
|
||||||
let subscriber = tracing_subscriber::registry().with(logs_layer.with_filter(logs_filter));
|
.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();
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
let cli = match cli::post_cli(Cli::parse()) {
|
if let Err(err) = try_main().await {
|
||||||
Ok(cli) => cli,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{err}");
|
eprintln!("{err}");
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
set_log_level(cli.verbosity);
|
|
||||||
|
|
||||||
if let Err(err) = cli.run().await {
|
|
||||||
tracing::error!("{err}");
|
|
||||||
return err.exit_code();
|
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>
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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 traits::TokenUtils;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, issue::IssueStatus, patch::PatchStatus},
|
cli::{CliOptions, issue::IssueStatus, types::PatchPrStatus},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::traits::KindExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Timeout duration for the client.
|
/// Timeout duration for the client.
|
||||||
@@ -211,21 +212,17 @@ impl NostrClient {
|
|||||||
/// Fetches the first event matching the given filter, or None if no event
|
/// Fetches the first event matching the given filter, or None if no event
|
||||||
/// is found.
|
/// is found.
|
||||||
pub async fn fetch_event(&self, filter: Filter) -> N34Result<Option<Event>> {
|
pub async fn fetch_event(&self, filter: Filter) -> N34Result<Option<Event>> {
|
||||||
Ok(self
|
Ok(self.fetch_events(filter).await?.next())
|
||||||
.client
|
|
||||||
.fetch_events(filter.limit(1), CLIENT_TIMEOUT)
|
|
||||||
.await?
|
|
||||||
.first_owned())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches the events matching the given filter
|
/// 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
|
// Multiply timeout by 5 to account for multiple events being fetched
|
||||||
Ok(self
|
Ok(self
|
||||||
.client
|
.client
|
||||||
.fetch_events(filter, CLIENT_TIMEOUT * 5)
|
.fetch_events(filter, CLIENT_TIMEOUT * 5)
|
||||||
.await?
|
.await?
|
||||||
.to_vec())
|
.into_iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to fetch the repositories and returns them
|
/// Try to fetch the repositories and returns them
|
||||||
@@ -258,6 +255,13 @@ impl NostrClient {
|
|||||||
.ok_or(N34Error::CanNotFoundPatch)
|
.ok_or(N34Error::CanNotFoundPatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the pull request by the given id. None if not found
|
||||||
|
pub async fn fetch_pr(&self, pr_id: EventId) -> N34Result<Event> {
|
||||||
|
self.fetch_event(Filter::new().id(pr_id).kind(crate::cli::pr::PR_KIND))
|
||||||
|
.await?
|
||||||
|
.ok_or(N34Error::CanNotFoundPr)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the username for a given public key. If no username is found,
|
/// Returns the username for a given public key. If no username is found,
|
||||||
/// falls back to a shortened version of the public key.
|
/// falls back to a shortened version of the public key.
|
||||||
pub async fn get_username(&self, user: PublicKey) -> String {
|
pub async fn get_username(&self, user: PublicKey) -> String {
|
||||||
@@ -281,23 +285,36 @@ impl NostrClient {
|
|||||||
issue_id: EventId,
|
issue_id: EventId,
|
||||||
authorized_pubkeys: Vec<PublicKey>,
|
authorized_pubkeys: Vec<PublicKey>,
|
||||||
) -> N34Result<IssueStatus> {
|
) -> N34Result<IssueStatus> {
|
||||||
self.fetch_events(
|
self.fetch_event(
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.event(issue_id)
|
.event(issue_id)
|
||||||
.kinds([
|
.kinds(IssueStatus::all_kinds())
|
||||||
Kind::GitStatusOpen,
|
|
||||||
Kind::GitStatusApplied,
|
|
||||||
Kind::GitStatusClosed,
|
|
||||||
])
|
|
||||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
|
||||||
.max_by_key(|e| e.created_at)
|
|
||||||
.map(|status| IssueStatus::try_from(status.kind))
|
.map(|status| IssueStatus::try_from(status.kind))
|
||||||
.unwrap_or_else(|| Ok(IssueStatus::Open))
|
.unwrap_or_else(|| Ok(IssueStatus::Open))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the latest status of PR by its ID, only considering status
|
||||||
|
/// events from authorized_pubkeys. If no valid status event is found,
|
||||||
|
/// defaults to Open.
|
||||||
|
pub async fn fetch_pr_status(
|
||||||
|
&self,
|
||||||
|
pr_id: EventId,
|
||||||
|
authorized_pubkeys: Vec<PublicKey>,
|
||||||
|
) -> N34Result<PatchPrStatus> {
|
||||||
|
self.fetch_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
|
/// Gets the status of a patch. If it's a revision patch, checks if it's
|
||||||
/// closed when the root patch is already merged/applied but doesn't
|
/// closed when the root patch is already merged/applied but doesn't
|
||||||
/// reference this revision. Defaults to Open status if no status event
|
/// reference this revision. Defaults to Open status if no status event
|
||||||
@@ -307,24 +324,17 @@ impl NostrClient {
|
|||||||
root_patch: EventId,
|
root_patch: EventId,
|
||||||
root_revision: Option<EventId>,
|
root_revision: Option<EventId>,
|
||||||
authorized_pubkeys: Vec<PublicKey>,
|
authorized_pubkeys: Vec<PublicKey>,
|
||||||
) -> N34Result<PatchStatus> {
|
) -> N34Result<PatchPrStatus> {
|
||||||
let (root_status, event_tags) = self
|
let (root_status, event_tags) = self
|
||||||
.fetch_events(
|
.fetch_event(
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.event(root_patch)
|
.event(root_patch)
|
||||||
.kinds([
|
.kinds(PatchPrStatus::all_kinds())
|
||||||
Kind::GitStatusOpen,
|
|
||||||
Kind::GitStatusApplied,
|
|
||||||
Kind::GitStatusClosed,
|
|
||||||
Kind::GitStatusDraft,
|
|
||||||
])
|
|
||||||
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
.authors(utils::dedup(authorized_pubkeys.into_iter())),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.map(|status| N34Result::Ok((PatchPrStatus::try_from(status.kind)?, status.tags)))
|
||||||
.max_by_key(|e| e.created_at)
|
.unwrap_or_else(|| Ok((PatchPrStatus::Open, Tags::new())))?;
|
||||||
.map(|status| N34Result::Ok((PatchStatus::try_from(status.kind)?, status.tags)))
|
|
||||||
.unwrap_or_else(|| Ok((PatchStatus::Open, Tags::new())))?;
|
|
||||||
|
|
||||||
if let Some(revision_id) = root_revision
|
if let Some(revision_id) = root_revision
|
||||||
&& root_status.is_merged_or_applied()
|
&& root_status.is_merged_or_applied()
|
||||||
@@ -332,7 +342,7 @@ impl NostrClient {
|
|||||||
.filter(TagKind::e())
|
.filter(TagKind::e())
|
||||||
.any(|t| t.is_reply() && t.content().is_some_and(|c| c == revision_id.to_hex()))
|
.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)
|
Ok(root_status)
|
||||||
@@ -351,7 +361,6 @@ impl NostrClient {
|
|||||||
.event(root_patch_id),
|
.event(root_patch_id),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
e.tags.iter().any(|t| {
|
e.tags.iter().any(|t| {
|
||||||
t.is_root() && t.content().is_some_and(|c| c == root_patch_id.to_hex())
|
t.is_root() && t.content().is_some_and(|c| c == root_patch_id.to_hex())
|
||||||
@@ -360,17 +369,17 @@ impl NostrClient {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the root issue or patch for a given event. If the event is already
|
/// Finds the root issue, PR or patch for a given event. If the event is
|
||||||
/// a root (issue/patch), returns it directly. For comments, follows
|
/// already a root (issue/patch/PR), returns it directly. For comments,
|
||||||
/// parent/root references until finding the root or failing. Returns
|
/// follows parent/root references until finding the root or failing.
|
||||||
/// None if no root can be found.
|
/// Returns None if no root can be found.
|
||||||
pub async fn find_root(&self, mut event: Event) -> N34Result<Option<Event>> {
|
pub async fn find_root(&self, mut event: Event) -> N34Result<Option<Event>> {
|
||||||
if !matches!(event.kind, Kind::GitIssue | Kind::GitPatch | Kind::Comment) {
|
if !event.kind.can_reply_to() {
|
||||||
return Err(N34Error::CanNotReplyToEvent);
|
return Err(N34Error::CanNotReplyToEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if matches!(event.kind, Kind::GitIssue | Kind::GitPatch) {
|
if event.kind.is_root_kind() {
|
||||||
return Ok(Some(event));
|
return Ok(Some(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +389,7 @@ impl NostrClient {
|
|||||||
self.add_relay_hint(relay_hint.cloned()).await;
|
self.add_relay_hint(relay_hint.cloned()).await;
|
||||||
let root_event = self.fetch_event(Filter::new().id(*id)).await?;
|
let root_event = self.fetch_event(Filter::new().id(*id)).await?;
|
||||||
if let Some(ref root_event) = root_event
|
if let Some(ref root_event) = root_event
|
||||||
&& !matches!(root_event.kind, Kind::GitIssue | Kind::GitPatch)
|
&& !root_event.kind.is_root_kind()
|
||||||
{
|
{
|
||||||
return Err(N34Error::CanNotReplyToEvent);
|
return Err(N34Error::CanNotReplyToEvent);
|
||||||
}
|
}
|
||||||
@@ -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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,16 +435,20 @@ impl NostrClient {
|
|||||||
/// Returns the read relays of the given users if found, otherwise empty
|
/// Returns the read relays of the given users if found, otherwise empty
|
||||||
/// vector
|
/// vector
|
||||||
pub async fn read_relays_from_users(&self, users: &[PublicKey]) -> Vec<RelayUrl> {
|
pub async fn read_relays_from_users(&self, users: &[PublicKey]) -> Vec<RelayUrl> {
|
||||||
self.fetch_events(
|
if let Ok(iterator) = self
|
||||||
|
.fetch_events(
|
||||||
Filter::new()
|
Filter::new()
|
||||||
.kind(nostr::event::Kind::RelayList)
|
.kind(nostr::event::Kind::RelayList)
|
||||||
.authors(utils::dedup(users.iter().copied())),
|
.authors(utils::dedup(users.iter().copied())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
{
|
||||||
.into_iter()
|
iterator
|
||||||
.flat_map(|e| utils::add_read_relays(Some(&e)))
|
.flat_map(|e| utils::add_read_relays(Some(&e)))
|
||||||
.collect()
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the given content and returns the details that inside it
|
/// 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