Compare commits
207 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 | ||
|
|
9aa3b62de0 | ||
|
|
a8015d618b | ||
|
|
2bcdb29a88 | ||
|
|
4a4c0dcd34 | ||
|
|
43ca689190 | ||
|
|
14ab9b38e2 | ||
|
|
802da70056 | ||
|
|
cff0b2f3b2 | ||
|
|
9c55bf8af1 | ||
|
|
904d14064f | ||
|
|
ad215002e2 | ||
|
|
67339a8a47 | ||
|
|
1d83e445a4 | ||
|
|
8bb4cf0da4 | ||
|
|
e17e75e53b | ||
|
|
67e25da8c0 | ||
|
|
bc8c6f3154 | ||
|
|
0ba58dcbd0 | ||
|
|
78113c7402 | ||
|
|
bd3ba1bdbf | ||
|
|
2630104e99 | ||
|
|
03d5c8020e | ||
|
|
c3aef2e0ad | ||
|
|
4e0ecdceaf | ||
|
|
f0c20c3677 | ||
|
|
e6943aefab | ||
|
|
353358f0ed | ||
|
|
c029ddf86c | ||
|
|
fc7d7baeb6 | ||
|
|
8715cd5547 | ||
|
|
70e76f1699 | ||
|
|
9e840be820 | ||
|
|
3fb7abe62a | ||
|
|
3814d1b4ec | ||
|
|
1c11b8f6bb | ||
|
|
74d05033f8 | ||
|
|
9bbeda8de8 | ||
|
|
3510b59fbb | ||
|
|
05b4ae30f7 | ||
|
|
387dd32a37 | ||
|
|
5c5987e07b | ||
|
|
9e1e5d7c42 | ||
|
|
7e479f3c7a | ||
|
|
4cc01668fd | ||
|
|
7d700602ef | ||
|
|
7c589f1c6c | ||
|
|
f3a09d30ea | ||
|
|
2874ba8457 | ||
|
|
55e5d86c03 | ||
|
|
6467bc3bb5 | ||
|
|
da284a084c | ||
|
|
45ea7d25f2 | ||
|
|
8b09cff5e3 | ||
|
|
d6dd63f576 | ||
|
|
ae6cff1a1d | ||
|
|
a38a811cbf | ||
|
|
93136fb767 | ||
|
|
035e22a6ba | ||
|
|
043ef6fc96 | ||
|
|
54cae48123 | ||
|
|
1651fd5a47 | ||
|
|
ec08a1febc | ||
|
|
a9a2cb2b16 | ||
|
|
63f6c1fb11 | ||
|
|
3e6fed8bc6 | ||
|
|
431bfd2512 | ||
|
|
364356a573 | ||
|
|
2a3e2e09c0 | ||
|
|
8870f8ced3 | ||
|
|
da96985a40 | ||
|
|
76b76bc8d6 | ||
|
|
983d2f209d | ||
|
|
7ba98bbbe8 | ||
|
|
8f70778c26 | ||
|
|
9069adbe22 | ||
|
|
b1027b3b04 | ||
|
|
5dc8e31eb3 | ||
|
|
51bd239024 | ||
|
|
53a2eb5bda | ||
|
|
a6a61aedf1 | ||
|
|
30b3056d39 | ||
|
|
8fe6755d3b | ||
|
|
812a06a276 | ||
|
|
494cced9cb | ||
|
|
16e6300c81 | ||
|
|
ef8d6c1c4f | ||
|
|
01f5fa60e6 | ||
|
|
7d9b968f16 | ||
|
|
57fc868b63 | ||
|
|
08505151bd | ||
|
|
864f3018f6 | ||
|
|
37128a8c1a | ||
|
|
ce0068be53 | ||
|
|
4c6578ccb1 | ||
|
|
d6d3f8a52f | ||
|
|
84bfafa2e9 | ||
|
|
24e8522268 | ||
|
|
f670859b92 | ||
|
|
bca86f115f | ||
|
|
a9b9a7a02a | ||
|
|
1abb8e3f36 | ||
|
|
15d834dc0f | ||
|
|
ea862dac5f | ||
|
|
55a4868813 | ||
|
|
60978f43b5 | ||
|
|
37cf601c01 | ||
|
|
87da5035da | ||
|
|
ddea5028ee | ||
|
|
de68d61c3f | ||
|
|
f7e837e3ab | ||
|
|
02070c2868 | ||
|
|
998ef8f4b1 | ||
|
|
6fdf0dbcf5 | ||
|
|
15c0eaa5dc | ||
|
|
226909ef16 | ||
|
|
8ca88804dd | ||
|
|
55f9b9194e | ||
|
|
a26116702d | ||
|
|
35d376ca6c | ||
|
|
57b48c7368 | ||
|
|
b29937a04e | ||
|
|
06374fe1fc | ||
|
|
2d5c0bf549 | ||
|
|
0a5b4656c3 | ||
|
|
cad2c7faa3 | ||
|
|
19cfecb9ea | ||
|
|
89d0710ce5 | ||
|
|
c1afa9a93d | ||
|
|
92165de6fd | ||
|
|
c5085b3af7 | ||
|
|
ecee54c9a3 | ||
|
|
55aa48c1fe | ||
|
|
0cc656b780 | ||
|
|
8bff000b33 | ||
|
|
5c201b0cec | ||
|
|
c608f2120b | ||
|
|
60ad1fe5bb | ||
|
|
b3b7ce7a4b | ||
|
|
a137a3f9de | ||
|
|
724e2707c6 | ||
|
|
9444fc1a8c | ||
|
|
a324ba2ff7 | ||
|
|
56316d36d3 | ||
|
|
074245733e | ||
|
|
661266c67b | ||
|
|
a9b088d8a5 | ||
|
|
1af731ef67 | ||
|
|
4298ca5d2d | ||
|
|
70ee4a59b2 | ||
|
|
bd086538fd | ||
|
|
54f1c7e0e6 | ||
|
|
f1378a2eb7 | ||
|
|
2627fe7980 | ||
|
|
42508fc8b9 | ||
|
|
c69fbca2a0 | ||
|
|
552136400d | ||
|
|
ac7c1e6216 | ||
|
|
5139f8ea11 | ||
|
|
6aac4b7a85 | ||
|
|
ce58f29022 | ||
|
|
d9fe376746 | ||
|
|
d9f8ce2418 | ||
|
|
577de41054 | ||
|
|
3857753b7d | ||
|
|
93c8743dc7 | ||
|
|
c0a5e47793 | ||
|
|
a56acf5752 | ||
|
|
9b4dfb9374 | ||
|
|
6fc59bfe59 | ||
|
|
b444aeba0a | ||
|
|
273058fe3f | ||
|
|
4dea4785b4 | ||
|
|
4314cebfbb | ||
|
|
8a553f4de8 | ||
|
|
57434ac39b | ||
|
|
99e46d7633 | ||
|
|
fb77a8ca14 | ||
|
|
d962732e52 | ||
|
|
654aee620e | ||
|
|
d37c6964f7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
/book
|
||||
|
||||
11
.taplo.toml
Normal file
11
.taplo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
exclude = ["target/**/*.toml", "Cargo.lock"]
|
||||
|
||||
[formatting]
|
||||
align_entries = true
|
||||
indent_tables = true
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies"]
|
||||
formatting.reorder_keys = true
|
||||
1
AUTHORS
Normal file
1
AUTHORS
Normal file
@@ -0,0 +1 @@
|
||||
Awiteb <a@4rs.nl> (nprofile1qqsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgpzfmhxue69uhkummnw3ezudrjwvhxumq3dg0ly)
|
||||
155
CHANGELOG.md
155
CHANGELOG.md
@@ -5,11 +5,164 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- New command `repo state` - by Awiteb
|
||||
- Support pull requests - by Awiteb
|
||||
- Add `--personal-fork` flag to `repo announce` command - by Awiteb
|
||||
- Log to stderr and a file - by Awiteb
|
||||
- Accept patches from stdin in `patch send` command - by Awiteb
|
||||
- Write patches to the stdout in `patch fetch` command - by Awiteb
|
||||
|
||||
### Breaking Change
|
||||
|
||||
- Make subject mandatory and optional content - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add `io-std` feature to `tokio` - by Awiteb
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update commands help messages - by Awiteb
|
||||
- Update commands titles and descriptions - by Awiteb
|
||||
|
||||
### Fixed
|
||||
|
||||
- Check the nip07 signer in `CliOptions::ensure_signer` - by Awiteb
|
||||
- Required the `applied_commits` - by Awiteb
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove `dbus` C dep - by Awiteb
|
||||
|
||||
## [0.4.0] - 2025-08-08
|
||||
|
||||
### Added
|
||||
|
||||
- Support signing using NIP-46 bunker - by Awiteb
|
||||
- Keyring the secret key `n34 config keyring --enable` - by Awiteb
|
||||
- New flag to `patch apply and merge` to mention patches - by Awiteb
|
||||
- Sign using NIP-07 - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add `keyring`, `nostr-connect`, `nostr-keyring` and `url` to the dependencies - by Awiteb
|
||||
- Remove `url` from `n34` dependencies - by Awiteb
|
||||
- Upgrade to `nostr@0.43.0` - by Awiteb
|
||||
|
||||
### Documentation
|
||||
|
||||
- N34 book - by Awiteb
|
||||
- Fix status command docs - by Awiteb
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move the trait extensions to `traits.rs` - by Awiteb
|
||||
- Move `root` and `root-revision` to constants - by Awiteb
|
||||
|
||||
## [0.3.0] - 2025-07-05
|
||||
|
||||
### Added
|
||||
|
||||
- New `patch send` command to send patches - by Awiteb
|
||||
- Add `alt` tag to the git issue - by Awiteb
|
||||
- Add `description` tag to the patch - by Awiteb
|
||||
- New `config pow` command to set the default PoW difficulty - by Awiteb
|
||||
- New `config relays` command to set the default fallbacks relays - by Awiteb
|
||||
- New `issue view` command to view an issue - by Awiteb
|
||||
- New `patch fetch` command to fetch patches - by Awiteb
|
||||
- New `issue {reopen,close,resolve}` commands to manage issue status - by Awiteb
|
||||
- New `patch` subcommands apply,close,draft,merge and reopen to manage the patch status - by Awiteb
|
||||
- View the repo maintainers as `npub` - by Awiteb
|
||||
- New `patch list` commands to list the repo patches - by Awiteb
|
||||
- New `issue list` commands to list the repo issues - by Awiteb
|
||||
- Improve exit codes and make them more specific - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add `either@1.15.0` to the dependencies - by Awiteb
|
||||
- Update `nostr` to `0.42.2` - by Awiteb
|
||||
- Remove `bitcoin_hashes` and use `nostr::hashes` re-export - by Awiteb
|
||||
|
||||
### Fixed
|
||||
|
||||
- Not to return an error if `nostr-address` file does not exist - by Awiteb
|
||||
- Spelling in help content - by DanConwayDev
|
||||
- Fix a typo in `EmptySetRelays` error message - by Awiteb
|
||||
- Require a repo in `repo view` command - by Awiteb
|
||||
|
||||
### Refactor
|
||||
|
||||
- Store the config in `CliOptions` instead of its path - by Awiteb
|
||||
- Improve commands running and make the signer optional for some commands - by Awiteb
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the `--repo` option and make the repo an argument - by Awiteb
|
||||
- Remove the `--to` flag from `reply` command and make it an argument - by Awiteb
|
||||
- Remove `--euc` flag from `patch send` command and use the repo euc - by Awiteb
|
||||
|
||||
## [0.2.0] - 2025-06-01
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--force-id` flag to bypass case validation in `repo announce` - by Awiteb
|
||||
- Add `--address-file` flag to `repo announce` command - by Awiteb
|
||||
- Read the `nostr-address` file in `repo view` command - by Awiteb
|
||||
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
||||
- Read the `nostr-address` file in `reply` command - by Awiteb
|
||||
- A `--quote-to` flag to quote the replied to content in the editor - by Awiteb
|
||||
- Enter repository as nip5 - by Awiteb
|
||||
- Make the relays list optional - by Awiteb
|
||||
- Events and naddrs can starts with `nostr:` - by Awiteb
|
||||
- Support relays and naddrs sets - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add `chrono@0.4.41` to the dependencies - by Awiteb
|
||||
- Enable `nip05` feature of `nostr` crate - by Awiteb
|
||||
- Add `serde@1.0.219`, `dirs@6.0.0` and `toml@0.8.22` - by Awiteb
|
||||
|
||||
### Fixed
|
||||
|
||||
- Create a valid naddr string - by Awiteb
|
||||
|
||||
### Refactor
|
||||
|
||||
- Support more than one naddr instead of one - by Awiteb
|
||||
|
||||
## [0.1.0] - 2025-05-21
|
||||
|
||||
### Added
|
||||
|
||||
- Setup the CLI and create `repo view` command - by Awiteb
|
||||
- Add `repo announce` command - by Awiteb
|
||||
- Support PoW - by Awiteb
|
||||
- Add `issue new` command - by Awiteb
|
||||
- New `reply` command - by Awiteb
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Add nedded dependencies - by Awiteb
|
||||
- Add `easy_ext@1.0.2` to the dependencies - by Awiteb
|
||||
- Add `convert_case@0.8.0` to the dependencies - by Awiteb
|
||||
- Add `tempfile@3.19.1` to the dependencies - by Awiteb
|
||||
- Add `futures@0.3.31` to the dependencies - by Awiteb
|
||||
- Bump `nostr` and `nostr_sdk` to `0.42.0` - by Awiteb
|
||||
|
||||
## [0.0.0] - 2025-05-01
|
||||
|
||||
### Added
|
||||
|
||||
- Feat: Initialize the project
|
||||
- Initialize the project - by Awiteb
|
||||
|
||||
[0.4.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.4.0
|
||||
[0.3.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.3.0
|
||||
[0.2.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.2.0
|
||||
[0.1.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.1.0
|
||||
[0.0.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.0.0
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
|
||||
113
CONTRIBUTING.md
Normal file
113
CONTRIBUTING.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Contributing to `n34`
|
||||
|
||||
For basic information about the `n34` project, please read the
|
||||
[README.md](README.md). The project is licensed under **GPL-3.0**, and by
|
||||
contributing, your work will also be licensed under the same terms.
|
||||
|
||||
Before submitting changes, please read the [Developer Certificate of Origin](DCO).
|
||||
All patches must include a `Signed-off-by: NAME <EMAIL>` line to acknowledge
|
||||
your agreement with the DCO.
|
||||
|
||||
Ensure your Git name and email are correctly configured. While you don’t need to
|
||||
use your real details, avoid leaving them as the default values. To verify your
|
||||
current settings, run:
|
||||
|
||||
```bash
|
||||
git config user.name
|
||||
git config user.email
|
||||
```
|
||||
|
||||
If they’re incorrect or unset, update them using:
|
||||
|
||||
```bash
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.email "your.email@example.com"
|
||||
```
|
||||
|
||||
We welcome all contributions, whether it be bug reports, fixes, feature
|
||||
submissions, feature requests, or improving documentation or testing. Enjoy
|
||||
collaborating!
|
||||
|
||||
## Git Repository
|
||||
|
||||
The repository is hosted at <https://git.4rs.nl/awiteb/n34.git>, with `master`
|
||||
as the active development branch.
|
||||
|
||||
## Nostr Repository Address
|
||||
|
||||
You can submit issues and patches via any
|
||||
Nostr-compatible client using the address:
|
||||
`naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyxhwumn8ghj7mn0wvhxcmmvqy28wumn8ghj7mn0wd68ytn00p68ytnyv4mqwuj6xc`
|
||||
|
||||
|
||||
When using `n34`, there's no need to specify the address, it will automatically
|
||||
check the `nostr-address` file. Simply submit your issues and patches without
|
||||
worrying about this detail.
|
||||
|
||||
## Contribution Workflow
|
||||
|
||||
Before submitting changes, open an issue to discuss your proposed contribution.
|
||||
Clearly indicate that you intend to work on it and wait for a maintainer's
|
||||
response. If the issue remains open, you may proceed with submitting your patch.
|
||||
|
||||
### Reporting Issues
|
||||
|
||||
When opening an issue, include:
|
||||
- Detailed steps to reproduce the problem
|
||||
- Relevant error messages or logs (use the `-vvvv` flag for verbose output)
|
||||
- Expected vs. actual behavior
|
||||
|
||||
Please label your issue appropriately (e.g., `bug`, `feature`, `question`) to
|
||||
help categorize it.
|
||||
|
||||
### Your Patch
|
||||
|
||||
Ensure your patch submission tool notifies the maintainers and sends the patch
|
||||
to their read relays, most tools handle this automatically.
|
||||
|
||||
#### Patch Guidelines
|
||||
|
||||
- Keep patches small: Focused changes are easier to review and merge.
|
||||
- Run `./tasks ci` before submitting your patch.
|
||||
- Update the change log with your patch. Run `./tasks changelog` or `git-cliff > CHANGELOG.md`
|
||||
- Add your name to the [AUTHORS](AUTHORS) file if this is your first contribution. (alphabetical order)
|
||||
- Use [Conventional Commits]: Start the patch subject with one of these types:
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation updates
|
||||
- `refactor`: Code restructuring without behavioral changes
|
||||
- `deprecate`: Marking code as deprecated
|
||||
- `remove`: Removing deprecated code
|
||||
- `security`: Security-related changes
|
||||
- `perf`: Performance improvements
|
||||
- `test`: Test additions or corrections
|
||||
- For all other changes, use `chore`.
|
||||
- Add `!` to the subject if your patch contains a breacking change, e.g.
|
||||
`remove!: text` and `fix(reply)!: text`
|
||||
- Use the `--cover-letter` flag to include a cover letter with your patch. Describe the issue you’re addressing, whether it’s a one-line bug fix or a 5000-line new feature.
|
||||
- Specify the base commit for your patch using the `--base` flag.
|
||||
- First-time contributors: Review the [Submitting Patches guide](https://www.kernel.org/doc/html/latest/process/submitting-patches.html) before sending your patch.
|
||||
- If you revise your patch, you should reference all previous revisions (or the
|
||||
root patch if this is the first revision) and explain the changes made (i.e.,
|
||||
the differences between this patch and the prior one).
|
||||
|
||||
#### Code Style
|
||||
|
||||
When writing code, make sure to folow this:
|
||||
- Using Rust's official formatting tool, `rustfmt`, to format your code.
|
||||
- Writing clear and concise code with meaningful variable and function names.
|
||||
- Adding comments to explain complex logic or algorithms.
|
||||
|
||||
#### Cover Letter Description
|
||||
|
||||
Your patch description should provide a clear and concise summary of the changes you
|
||||
have made. It should also include any relevant context or background information
|
||||
that will help the project maintainers understand the purpose of the changes.
|
||||
Make sure to reference the issue[^1] that your patch is addressing, and note any breaking
|
||||
changes that your patch introduces.
|
||||
|
||||
|
||||
|
||||
[^1]: When referencing, avoid URLs or `nevent` formats with relays. Instead, use only the note ID in `note1` bech32 format.
|
||||
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/
|
||||
3559
Cargo.lock
generated
3559
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
Cargo.toml
86
Cargo.toml
@@ -1,13 +1,79 @@
|
||||
[package]
|
||||
name = "n34"
|
||||
description = "A CLI to interact with NIP-34 and other stuff related to codes in nostr"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Awiteb <a@4rs.nl>"]
|
||||
readme = "README.md"
|
||||
repository = "https://git.4rs.nl/awiteb/n34"
|
||||
keywords = ["nostr", "NIP-34", "CLI"]
|
||||
categories = ["command-line-utilities"]
|
||||
name = "n34"
|
||||
description = "A CLI to interact with NIP-34 and other stuff related to code in Nostr"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Awiteb <a@4rs.nl>"]
|
||||
readme = "README.md"
|
||||
documentation = "https://n34.dev/commands.html"
|
||||
homepage = "https://n34.dev"
|
||||
repository = "https://git.4rs.nl/awiteb/n34.git"
|
||||
keywords = ["nostr", "NIP-34", "CLI"]
|
||||
categories = ["command-line-utilities"]
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[dependencies]
|
||||
async-utility = "0.3.1"
|
||||
chrono = "0.4.41"
|
||||
convert_case = "0.8.0"
|
||||
dirs = "6.0.0"
|
||||
easy-ext = "1.0.2"
|
||||
either = "1.15.0"
|
||||
futures = "0.3.31"
|
||||
nostr-browser-signer-proxy = "0.43.0"
|
||||
regex = "1.11.1"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
toml = "0.9.4"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
|
||||
[dependencies.keyring]
|
||||
features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"async-secret-service",
|
||||
"tokio",
|
||||
"crypto-rust",
|
||||
]
|
||||
version = "3.6.3"
|
||||
|
||||
[dependencies.clap]
|
||||
features = ["derive"]
|
||||
version = "4.5.42"
|
||||
|
||||
[dependencies.clap-verbosity-flag]
|
||||
default-features = false
|
||||
features = ["tracing"]
|
||||
version = "3.0.3"
|
||||
|
||||
[dependencies.nostr]
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
version = "0.43.0"
|
||||
|
||||
[dependencies.nostr-connect]
|
||||
default-features = false
|
||||
version = "0.43.0"
|
||||
|
||||
[dependencies.nostr-sdk]
|
||||
default-features = false
|
||||
version = "0.43.0"
|
||||
|
||||
[dependencies.reqwest]
|
||||
default-features = false
|
||||
features = ["http2", "json", "rustls-tls"]
|
||||
version = "0.12.22"
|
||||
|
||||
[dependencies.serde]
|
||||
features = ["derive"]
|
||||
version = "1.0.219"
|
||||
|
||||
[dependencies.tokio]
|
||||
features = ["macros", "rt-multi-thread", "io-std"]
|
||||
version = "1.47.1"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
opt-level = 3
|
||||
|
||||
34
DCO
Normal file
34
DCO
Normal file
@@ -0,0 +1,34 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
135
README.md
135
README.md
@@ -1,9 +1,142 @@
|
||||
# n34
|
||||
|
||||
A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||
A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||
|
||||
## About
|
||||
|
||||
`n34` is a command-line interface (CLI) tool for sending and receiving Git
|
||||
issues, patches, and comments over the Nostr protocol. It supports creating,
|
||||
replying to, and managing issues and patches, making Git collaboration
|
||||
decentralized and censorship-resistant.
|
||||
|
||||
The primary goal of `n34` is to implement [NIP-34] (`git` stuff), but its
|
||||
flexible design allows for additional use cases beyond Git workflows. For more
|
||||
details, see the following section.
|
||||
|
||||
## Documentation
|
||||
|
||||
Check the documentation at [n34.dev]
|
||||
|
||||
## Features
|
||||
|
||||
- [X] Repository announcements
|
||||
- [X] Repository state announcements
|
||||
- [X] Patches (Send, fetch and list)
|
||||
- [X] Issues (Send, view and list)
|
||||
- [X] Replies
|
||||
- [X] Issues and patches status
|
||||
- [X] Pull requests ([nostr-protocol/nips#1966])
|
||||
- [X] Gossip Model ([NIP-65])
|
||||
- [X] Proof of Work ([NIP-13])
|
||||
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
||||
- [X] Signing using bunker ([NIP-46])
|
||||
- [X] Signing using [NIP-07] proxy ([nostr-browser-signer-proxy])
|
||||
- [ ] Code Snippets ([NIP-C0])
|
||||
- [X] In device relays and repos bookmark (`sets` command)
|
||||
|
||||
|
||||
## Why Nostr?
|
||||
|
||||
Nostr is fundamentally different from traditional platforms because it’s not
|
||||
an application or service, it’s a decentralized protocol. This means any tool or
|
||||
app can integrate with it, enabling open, permissionless collaboration without
|
||||
relying on centralized gatekeepers. Unlike proprietary systems, Nostr doesn’t
|
||||
require emails, passwords, or accounts. You interact directly through relays,
|
||||
whether you self-host your own or use public ones, ensuring no single point of
|
||||
failure or control.
|
||||
|
||||
What makes Nostr uniquely resilient is its design, the protocol itself is just
|
||||
a set of rules, not a company or product that can disappear. Your Git issues,
|
||||
patches, and comments persist as long as relays choose to store them, immune to
|
||||
the whims of corporate shutdowns or policy changes. Nostr is infrastructure in
|
||||
its purest form, an idea that outlives any temporary implementation. `n34` taps
|
||||
into a future-proof foundation for decentralized collaboration.
|
||||
|
||||
### More about Nostr
|
||||
|
||||
- <https://nostr.com>
|
||||
- <https://nostr.org>
|
||||
- <https://nostr.how/en/what-is-nostr>
|
||||
|
||||
## Installation
|
||||
|
||||
You can install n34 either by cloning the repository and building it with Cargo,
|
||||
or by using `cargo install` or Nix.
|
||||
|
||||
### Building from source
|
||||
|
||||
- Clone the repository:
|
||||
```sh
|
||||
git clone git://git.4rs.nl/awiteb/n34.git
|
||||
cd n34
|
||||
```
|
||||
|
||||
- Build the release version:
|
||||
```sh
|
||||
cargo build --release
|
||||
```
|
||||
The binary will be available at `target/release/n34`.
|
||||
|
||||
### Using cargo install
|
||||
|
||||
```sh
|
||||
cargo install n34
|
||||
```
|
||||
The binary will be installed to your Cargo binary directory (typically `~/.cargo/bin/n34`).
|
||||
|
||||
Make sure `~/.cargo/bin` is in your `PATH` environment variable to run the binary from anywhere.
|
||||
|
||||
### Using `nix build` (+v0.4)
|
||||
|
||||
- Clone the repository.
|
||||
- Run the `nix build` command.
|
||||
|
||||
The binary will be available at `result/bin/n34`.
|
||||
|
||||
### Adding it to your [home-manager] (+v0.4)
|
||||
|
||||
- Add it as an input to your `flake.nix`:
|
||||
|
||||
```nix
|
||||
inputs = {
|
||||
# Specify the version you want to install, or remove `?ref` for the unreleased
|
||||
# version. You can also use any mirror; it doesn't have to be `git.4rs.nl`.
|
||||
n34.url = "git+https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x";
|
||||
};
|
||||
```
|
||||
|
||||
- Add it to your packages (ensure your home-manager `extraSpecialArgs` includes the `inputs`):
|
||||
|
||||
```nix
|
||||
packages = [ inputs.n34.packages."${pkgs.system}".default ];
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `n34` are welcome! You can help by opening issues (such as bug
|
||||
reports or feature requests) or submitting patches. **All contributions must be
|
||||
submitted through Nostr**. For more details on the process, please refer to the
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) file. Your support is greatly appreciated!
|
||||
|
||||
## Contributions & Changes
|
||||
|
||||
You can find the changelog at [CHANGELOG.md](CHANGELOG.md) and the list of
|
||||
contributors at [AUTHORS](AUTHORS) file.
|
||||
|
||||
## License
|
||||
|
||||
n34 is licensed under the GPL-3.0 License. This means that you are free to use,
|
||||
modify, and distribute the software under the terms of this license. Please
|
||||
refer to the [LICENSE](LICENSE) file for more details.
|
||||
|
||||
[NIP-34]: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
[NIP-65]: https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||
[NIP-13]: https://github.com/nostr-protocol/nips/blob/master/13.md
|
||||
[NIP-21]: https://github.com/nostr-protocol/nips/blob/master/21.md
|
||||
[NIP-C0]: https://github.com/nostr-protocol/nips/blob/master/C0.md
|
||||
[NIP-46]: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
[NIP-07]: https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||
[nostr-protocol/nips#1966]: https://github.com/nostr-protocol/nips/pull/1966
|
||||
[nostr-browser-signer-proxy]: https://crates.io/crates/nostr-browser-signer-proxy
|
||||
[home-manager]: https://github.com/nix-community/home-manager
|
||||
[n34.dev]: https://n34.dev
|
||||
|
||||
10
book.toml
Normal file
10
book.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[book]
|
||||
authors = ["Awiteb"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "docs"
|
||||
title = "n34 documentation"
|
||||
|
||||
[output.html]
|
||||
git-repository-icon = "fa-git"
|
||||
git-repository-url = "https://git.4rs.nl/awiteb/n34.git"
|
||||
33
cliff.toml
33
cliff.toml
@@ -17,12 +17,13 @@ body = """
|
||||
{% if version -%}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else -%}
|
||||
## [Unreleased]
|
||||
## Unreleased
|
||||
{% endif -%}
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
|
||||
- {{ commit.message | split(pat="\n") | first | split(pat=":") | slice(start=1) | join(sep=":") | trim | upper_first }} - \
|
||||
by {{ commit.author.name}}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
@@ -31,12 +32,7 @@ body = """
|
||||
footer = """
|
||||
{% for release in releases -%}
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
https://git.4rs.nl/awiteb/n34/compare/{{ release.previous.version }}..{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: https://git.4rs.nl/awiteb/n34/compare/{{ release.previous.version }}..HEAD
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: https://git.4rs.nl/awiteb/n34.git/tag/?h={{ release.version }}
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
<!-- generated by git-cliff -->
|
||||
@@ -44,6 +40,11 @@ footer = """
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = true
|
||||
|
||||
postprocessors = [
|
||||
# Replace the placeholder `<REPO>` with a URL.
|
||||
{ pattern = '\$REPO', replace = "https://git.4rs.nl/awiteb/n34.git" },
|
||||
]
|
||||
|
||||
[git]
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
@@ -54,14 +55,14 @@ filter_unconventional = false
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^.*!:.*$", group = "Breaking Change" },
|
||||
{ message = "^feat", group = "Added" },
|
||||
{ message = "^deprecate", group = "Deprecated" },
|
||||
{ message = "^remove", group = "Removed" },
|
||||
{ message = "^deps", group = "Dependencies" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^fix", group = "Fixed" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = '^.*!:', group = "Breaking Change" },
|
||||
{ message = '^feat', group = "Added" },
|
||||
{ message = '^deprecate', group = "Deprecated" },
|
||||
{ message = '^remove', group = "Removed" },
|
||||
{ message = '^deps', group = "Dependencies" },
|
||||
{ message = '^refactor', group = "Refactor" },
|
||||
{ message = '^fix', group = "Fixed" },
|
||||
{ message = '^docs', group = "Documentation" },
|
||||
]
|
||||
# Prevent commits that are breaking from being excluded by commit parsers.
|
||||
filter_commits = false
|
||||
|
||||
45
docs/SUMMARY.md
Normal file
45
docs/SUMMARY.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# n34 documentation
|
||||
|
||||
- [Command Line Usage](commands.md)
|
||||
- [Managing Repository and Relay Sets](sets/README.md)
|
||||
- [Create a Set](sets/new.md)
|
||||
- [Modify a Set](sets/update.md)
|
||||
- [Show Sets](sets/show.md)
|
||||
- [Remove a Set](sets/remove.md)
|
||||
- [Manage Configuration](config/README.md)
|
||||
- [Default PoW Difficulty](config/pow.md)
|
||||
- [Fallback Relays](config/relays.md)
|
||||
- [NIP-07 Browser Signer Proxy](config/nip07.md)
|
||||
- [NIP-46 Bunker](config/bunker.md)
|
||||
- [Secret Key Keyring](config/keyring.md)
|
||||
- [Manage Repositories](repo/README.md)
|
||||
- [Broadcast and Update a Git Repository](repo/announce.md)
|
||||
- [View Git Repository Details](repo/view.md)
|
||||
- [Repository State Announcements](repo/state.md)
|
||||
- [Reply to Issues and Patches](reply.md)
|
||||
- [Issue Management](issue/README.md)
|
||||
- [Create an Issue](issue/new.md)
|
||||
- [View an Issue](issue/view.md)
|
||||
- [Reopen an Issue](issue/reopen.md)
|
||||
- [Close an Issue](issue/close.md)
|
||||
- [Resolve an Issue](issue/resolve.md)
|
||||
- [List Issues](issue/list.md)
|
||||
- [Patch Management](patch/README.md)
|
||||
- [Send Patches to a Repository](patch/send.md)
|
||||
- [Fetch a Patch](patch/fetch.md)
|
||||
- [Close a Patch](patch/close.md)
|
||||
- [Convert to Draft](patch/draft.md)
|
||||
- [Reopen a Patch](patch/reopen.md)
|
||||
- [Mark as Applied](patch/apply.md)
|
||||
- [Mark as Merged](patch/merge.md)
|
||||
- [List Patches](patch/list.md)
|
||||
- [Pull Request Management](pr/README.md)
|
||||
- [Create a Pull Request](pr/new.md)
|
||||
- [Update a Pull Request](pr/update.md)
|
||||
- [View a Pull Request](pr/view.md)
|
||||
- [List Pull Requests](pr/list.md)
|
||||
- [Close a Pull Request](pr/close.md)
|
||||
- [Convert to Draft](pr/draft.md)
|
||||
- [Reopen a Pull Request](pr/reopen.md)
|
||||
- [Mark as Applied](pr/apply.md)
|
||||
- [Mark as Merged](pr/merge.md)
|
||||
78
docs/commands.md
Normal file
78
docs/commands.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Command-Line Usage
|
||||
|
||||
## Philosophy
|
||||
|
||||
`n34` emphasizes simplicity by exclusively handling Nostr tasks, requiring
|
||||
users to manage Git operations themselves. For instance, it won't merge or
|
||||
rebase patches into a branch but will prompt you to specify where to write them,
|
||||
allowing you to merge them using `git-am`. Similarly, when sending patches, it
|
||||
won't ask for the commits but will request the patch files instead.
|
||||
|
||||
This approach enhances simplicity and freedom, giving you full control over
|
||||
your workflow. Additionally, it simplifies software maintenance by avoiding
|
||||
unnecessary complexity.
|
||||
|
||||
## Options
|
||||
|
||||
The `n34` command-line tool accepts the following options:
|
||||
|
||||
- `-s`, `--secret-key`: Your Nostr secret key (in `nsec` format), used for
|
||||
signing events.
|
||||
- `-b`, `--bunker-url`: The URL of a NIP-46 bunker service used for remote
|
||||
signing of events.
|
||||
- `-7`, `--nip07`: Enables signing events using the browser's NIP-07
|
||||
extension. Listens on `127.0.0.1:51034`. You can configure the address with `n34
|
||||
config nip07`
|
||||
- `-r`, `--relays`: A relay to read from and write to. This option can be
|
||||
specified multiple times to connect to several relays.
|
||||
- `--pow`: Sets the Proof of Work difficulty required when creating events.
|
||||
- `--config`: Specifies a custom path to the configuration file (Default:
|
||||
`$HOME/.config/n34/config.toml`).
|
||||
- `-v`, `--verbose...`: Increases the logging verbosity. Can be used multiple
|
||||
times for more detail (e.g., `-v`, `-vv`).
|
||||
|
||||
**Note:** The `--secret-key`, `--nip07` and `--bunker-url` options are mutually
|
||||
exclusive. You must provide exactly one signing method.
|
||||
|
||||
## Multiple Repositories
|
||||
|
||||
Commands that interact with a repository, such as submitting an issue or a
|
||||
patch, can accept multiple repository addresses (`naddr`). This feature is
|
||||
useful for projects with multiple maintainers who each have their own repository
|
||||
fork.
|
||||
|
||||
> **Important:** When you provide multiple repositories, `n34` does not
|
||||
create a separate issue or patch for each one. Instead, it creates a single
|
||||
event that references all of the specified repositories.
|
||||
|
||||
## The `nostr-address` File
|
||||
|
||||
The `nostr-address` file is a plain text file that stores a list of project
|
||||
repository addresses. This allows the `n34` to find and use them
|
||||
without requiring you to enter the addresses manually.
|
||||
|
||||
### Format
|
||||
|
||||
- Each line must contain a single addressable event coordinate `naddr` which is
|
||||
the repository address.
|
||||
- Lines beginning with a `#` are treated as comments and are ignored.
|
||||
- Empty lines are also ignored.
|
||||
|
||||
## Passing repositories
|
||||
|
||||
By default, `n34` will look for a `nostr-address` file to extract repositories
|
||||
from it. This is why repositories are not required for commands like `patch
|
||||
send` and `issue new`. You can also pass repositories using the `--repo`
|
||||
option or the `<NADDR-NIP05-OR-SET>` argument for commands that accept them. The
|
||||
supported formats for manual input are:
|
||||
|
||||
- A [NIP-19] addressable event coordinate `naddr`.
|
||||
- A [NIP-05] identifier and repository name, in the format
|
||||
`<nip05>/<repo-name>`.
|
||||
- A set name that contains repository addresses.
|
||||
|
||||
You do not need to specify relays for these commands if your `naddr` or `NIP-05`
|
||||
identifier already includes relays; `n34` will automatically extract them.
|
||||
|
||||
[NIP-19]: https://github.com/nostr-protocol/nips/blob/master/19.md
|
||||
[NIP-05]: https://github.com/nostr-protocol/nips/blob/master/05.md
|
||||
7
docs/config/README.md
Normal file
7
docs/config/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Manage Configuration
|
||||
|
||||
Configuration allows you to set default values for various command parameters,
|
||||
such as fallback relays, Proof of Work (PoW) difficulty, a default bunker URL,
|
||||
and your Nostr private key (keyring). This avoids the need to enter your private key,
|
||||
bunker URL, relays, or PoW difficulty for every command, making `n34` more
|
||||
convenient to use.
|
||||
27
docs/config/bunker.md
Normal file
27
docs/config/bunker.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# NIP-46 Bunker
|
||||
|
||||
> `n34 config bunker` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Sets a URL of NIP-46 bunker server used for signing events
|
||||
|
||||
Usage: n34 config bunker [BUNKER_URL]
|
||||
|
||||
Arguments:
|
||||
[BUNKER_URL] Nostr Connect URL for the bunker. Omit this to remove the current bunker URL
|
||||
```
|
||||
|
||||
This command configures `n34` to use a remote signer ([NIP-46]), known as a
|
||||
bunker, for all cryptographic operations.
|
||||
|
||||
When `n34` communicates with the bunker, it uses a persistent, locally-generated
|
||||
keypair. You should add this keypair's public key to your bunker's list of
|
||||
authorized applications. This allows `n34` to operate securely without needing
|
||||
direct access to your main private key.
|
||||
|
||||
Once configured, actions such as fetching your public key or signing events are
|
||||
delegated to the bunker. To remove the bunker configuration, run the command
|
||||
again without providing a URL.
|
||||
|
||||
[NIP-46]: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
27
docs/config/keyring.md
Normal file
27
docs/config/keyring.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Secret Key Keyring
|
||||
|
||||
> `n34 config keyring` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Manages the secret key keyring, including enabling, disabling, or resetting it.
|
||||
|
||||
Usage: n34 config keyring <--enable|--disable|--reset>
|
||||
|
||||
Options:
|
||||
--enable Enables the secret key keyring. You will be prompted for your key one last time to store it.
|
||||
--disable Disables the secret key keyring. This removes the stored key and prevents new ones from being saved.
|
||||
--reset Resets the keyring. This deletes the current key, allowing a new one to be stored on the next use.
|
||||
```
|
||||
|
||||
To avoid entering your private key for every command, you can enable the keyring
|
||||
to store it securely. First, run `n34 config keyring --enable`. The next time
|
||||
you run an `n34` command that requires your private key, it will be saved
|
||||
to your system's keyring. You will not need to enter it again for subsequent
|
||||
commands.
|
||||
|
||||
To replace the stored key with a new one, use the `--reset` flag. To stop using
|
||||
the keyring and remove the stored key, use the `--disable` flag.
|
||||
|
||||
`n34` uses your operating system's native secret management system. For example,
|
||||
it uses `keyutils` on Linux and `Keychain` on macOS.
|
||||
22
docs/config/nip07.md
Normal file
22
docs/config/nip07.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# NIP-07 Browser Signer Proxy
|
||||
|
||||
> `n34 config nip07`
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Manage the NIP-07 browser signer proxy by enabling or disabling it and configuring the `ip:port` address.
|
||||
|
||||
Usage: n34 config nip07 [OPTIONS] <--enable|--disable>
|
||||
|
||||
Options:
|
||||
--enable Enable NIP-07 as the default signer
|
||||
--disable Disable NIP-07 as the default signer
|
||||
--addr <ADDR> Set the `ip:port` for the browser signer proxy (default: 127.0.0.1:51034)
|
||||
```
|
||||
|
||||
Use [NIP-07] (Browser Extension Signer) as your default signer. This is achieved
|
||||
by running a proxy at the specified `ADDR`, which defaults to `127.0.0.1:51034`.
|
||||
The proxy forwards `n34` requests to the browser signer and relays the responses
|
||||
back.
|
||||
|
||||
[NIP-07]: https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||
20
docs/config/pow.md
Normal file
20
docs/config/pow.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Default PoW Difficulty
|
||||
|
||||
> `n34 config pow`
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Sets the default PoW difficulty (0 if not specified)
|
||||
|
||||
Usage: n34 config pow <DIFFICULTY>
|
||||
|
||||
Arguments:
|
||||
<DIFFICULTY> The new default PoW difficulty
|
||||
```
|
||||
|
||||
This command configures the default Proof of Work (PoW) difficulty for newly
|
||||
created events. This setting is applied to most generated events, but it
|
||||
intentionally skips patch events. Because patches can be numerous, calculating
|
||||
PoW for each one would significantly slow down operations.
|
||||
|
||||
If you want to disable the PoW just make it 0.
|
||||
21
docs/config/relays.md
Normal file
21
docs/config/relays.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Fallback Relays
|
||||
|
||||
> `n34 config relays` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Sets the default fallback relays if none provided. Use this relays for read and write
|
||||
|
||||
Usage: n34 config relays [OPTIONS] [RELAYS]...
|
||||
|
||||
Arguments:
|
||||
[RELAYS]... List of relay URLs to append to fallback relays. If empty, removes all fallback relays
|
||||
|
||||
Options:
|
||||
--override Replace existing fallback relays instead of appending new ones
|
||||
```
|
||||
|
||||
This command configures the default fallback relays, which `n34` uses to read
|
||||
from and write to. To add relays, provide their URLs as arguments to append
|
||||
them to the current list. Use the `--override` flag to replace the existing list
|
||||
entirely. To clear all fallback relays, run the command without any arguments.
|
||||
482
docs/index.html
Normal file
482
docs/index.html
Normal file
@@ -0,0 +1,482 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<meta property="og:title" content="n34 - CLI for NIP-34 and Nostr Code Collaboration">
|
||||
<meta property="og:description" content="An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.">
|
||||
<meta property="og:url" content="https://n34.dev">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="n34 - CLI for NIP-34 and Nostr Code Collaboration">
|
||||
<meta name="twitter:description" content="An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>n34 - CLI for NIP-34 and Nostr Code Collaboration</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: radial-gradient(125% 125% at 50% 90%, #000000 40%, #072607 100%);
|
||||
color: #ffffff;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(135deg, #ffffff, #cccccc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.hero p {
|
||||
font-size: 1.2rem;
|
||||
color: #cccccc;
|
||||
margin-bottom: 40px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 14px 28px;
|
||||
background: transparent;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #ffffff;
|
||||
color: #0a0a0a;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 30px;
|
||||
color: #ffffff;
|
||||
font-weight: 400;
|
||||
}
|
||||
.section p {
|
||||
font-size: 1.1rem;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
text-align: justify;
|
||||
}
|
||||
a {
|
||||
color: #cccccc;
|
||||
text-decoration: underline;
|
||||
font-size: 1rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.feature {
|
||||
background: #1a1a1a;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
.feature:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: #555;
|
||||
}
|
||||
.feature h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 15px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.feature-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.feature p {
|
||||
color: #cccccc;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
.feature-list {
|
||||
background: #111;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.feature-column ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.feature-column li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1rem;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.feature-check {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feature-check.completed {
|
||||
background: #22c55e;
|
||||
color: #ffffff;
|
||||
}
|
||||
.feature-check.pending {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
.feature-check.completed::after {
|
||||
content: "✓";
|
||||
}
|
||||
.feature-check.pending::after {
|
||||
content: "○";
|
||||
}
|
||||
.feature-text {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.feature-text.pending {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.install-section {
|
||||
background: #111;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.install-section h2 {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.code-block {
|
||||
background: #000;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #333;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.code-block p {
|
||||
color: #00ff00;
|
||||
}
|
||||
.code-block p::before {
|
||||
content: "$ ";
|
||||
color: #888;
|
||||
}
|
||||
.code-block span::before {
|
||||
content: "$ ";
|
||||
color: #888;
|
||||
}
|
||||
.code-block span {
|
||||
color: #777;
|
||||
}
|
||||
.code-block pre {
|
||||
color: #00ff00;
|
||||
}
|
||||
.links {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.links h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 15px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.links ul {
|
||||
list-style: none;
|
||||
}
|
||||
.links li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.links a {
|
||||
color: #cccccc;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.links a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
.links a:before {
|
||||
content: "→ ";
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: #1a4d1a;
|
||||
color: #4ade80;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
border-top: 1px solid #333;
|
||||
margin-top: 80px;
|
||||
color: #888;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.hero h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.hero p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
width: 200px;
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.install-section,
|
||||
.feature-list {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<div class="status-badge">✨ Looking for feedback</div>
|
||||
<h1>n34</h1>
|
||||
<p>
|
||||
n34 is an open source command-line interface (CLI) tool for
|
||||
sending and receiving Git issues, patches and comments over the
|
||||
Nostr protocol. It supports creating, replying to, and managing
|
||||
issues and patches, making Git collaboration decentralized and
|
||||
censorship-resistant.
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<a href="https://n34.dev/commands.html" class="btn">Documentation</a>
|
||||
<a href="https://git.4rs.nl/awiteb/n34.git" class="btn">Git Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Key Features</h2>
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<h3><span class="feature-icon">🔄</span>Complete Git Workflow</h3>
|
||||
<p>Handle the full development lifecycle with patches, issues, replies, and status tracking, all through the decentralized Nostr protocol.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3><span class="feature-icon">🔗</span><a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank" rel="noreferrer">NIP-34</a> Compliant</h3>
|
||||
<p>Fully implements the <a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank" rel="noreferrer">NIP-34</a> specification for Git repositories on Nostr, ensuring compatibility with the decentralized ecosystem.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3><span class="feature-icon">🛠️</span>Developer Friendly</h3>
|
||||
<p>Intuitive CLI interface designed for developers who want to integrate Git workflows with Nostr seamlessly.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3><span class="feature-icon">🔐</span>Self-Sovereign</h3>
|
||||
<p>No accounts, no passwords, no centralized servers. You control your identity and data through cryptographic keys.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-list">
|
||||
<h2>Feature Roadmap</h2>
|
||||
<p style="text-align: center; margin-bottom: 30px; color: #cccccc;">Current implementation status and upcoming features</p>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-column">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Repository announcements</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Repository state announcements</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Patches (Send, fetch and list)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Issues (Send, view and list)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Replies</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Issues and patches status</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feature-column">
|
||||
<ul>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Gossip Model (<a href="https://github.com/nostr-protocol/nips/blob/master/65.md" target="_blank" rel="noreferrer">NIP-65</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Proof of Work (<a href="https://github.com/nostr-protocol/nips/blob/master/13.md" target="_blank" rel="noreferrer">NIP-13</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">nostr: URI scheme (<a href="https://github.com/nostr-protocol/nips/blob/master/21.md" target="_blank" rel="noreferrer">NIP-21</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Signing using bunker (<a href="https://github.com/nostr-protocol/nips/blob/master/46.md" target="_blank" rel="noreferrer">NIP-46</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">Secret key keyring</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check pending"></span>
|
||||
<span class="feature-text pending">Code Snippets (<a href="https://github.com/nostr-protocol/nips/blob/master/C0.md" target="_blank" rel="noreferrer">NIP-C0</a>)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="feature-check completed"></span>
|
||||
<span class="feature-text">In device relays and repos bookmark</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-section">
|
||||
<h2>Quick Start</h2>
|
||||
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">Get started with n34 in seconds</p>
|
||||
<div class="code-block">
|
||||
<p>cargo install n34</p>
|
||||
<span># The execautable will be in ~/.cargo/bin/n34</span>
|
||||
</div>
|
||||
<p style="text-align: center; margin: 20px 0; color: #888;">or</p>
|
||||
<div class="code-block">
|
||||
<p>git clone https://git.4rs.nl/awiteb/n34.git</p>
|
||||
<p>cd n34 && cargo build --release</p>
|
||||
<span># The execautable will be in target/release/n34</span>
|
||||
</div>
|
||||
<p style="text-align: center; margin: 20px 0; color: #888;">NixOS (Version 0.4.0 or later)</p>
|
||||
<div class="code-block">
|
||||
<p>git clone https://git.4rs.nl/awiteb/n34.git</p>
|
||||
<p>cd n34 && nix build</p>
|
||||
<span># The execautable will be in result/bin/n34</span>
|
||||
</div>
|
||||
<p style="text-align: center; margin: 20px 0; color: #888;">home-manager</p>
|
||||
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">Add this to your <code>flake.nix</code> inputs
|
||||
<br>
|
||||
Specify the version you want to install, or remove <code>?ref</code> for the unreleased version. You can also use any mirror; it doesn't have to be <code>git.4rs.nl</code>
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<pre>
|
||||
inputs = {
|
||||
n34.url = "git+https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x";
|
||||
};</pre>
|
||||
</div>
|
||||
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">And this in your home packages</p>
|
||||
<div class="code-block">
|
||||
<pre>packages = [ inputs.n34.packages."${pkgs.system}".default ];</pre>
|
||||
</div>
|
||||
<p style="text-align: center; margin-top: 20px; color: #cccccc;">Once installed, run <code style="background: #333; padding: 2px 6px; border-radius: 4px;">n34 --help</code> to see available commands.</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Why Nostr?</h2>
|
||||
<p>
|
||||
Nostr is fundamentally different from traditional platforms because it's not an application or service, it's a decentralized protocol. This means any tool or app can integrate with it, enabling open, permissionless collaboration
|
||||
without relying on centralized gatekeepers. Unlike proprietary systems, Nostr doesn't require emails, passwords, or accounts. You interact directly through relays, whether you self-host your own or use public ones, ensuring no
|
||||
single point of failure or control.
|
||||
</p>
|
||||
<p>
|
||||
What makes Nostr uniquely resilient is its design, the protocol itself is just a set of rules, not a company or product that can disappear. Your Git issues, patches, and comments persist as long as relays choose to store them,
|
||||
immune to the whims of corporate shutdowns or policy changes. Nostr is infrastructure in its purest form, an idea that outlives any temporary implementation. n34 taps into a future-proof foundation for decentralized collaboration.
|
||||
</p>
|
||||
<div class="links">
|
||||
<h3>More about Nostr</h3>
|
||||
<ul>
|
||||
<li><a href="https://nostr.com">nostr.com</a></li>
|
||||
<li><a href="https://nostr.org">nostr.org</a></li>
|
||||
<li><a href="https://nostr.how/en/what-is-nostr">nostr.how/en/what-is-nostr</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Built with ❤️ for the decentralized future</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
docs/issue/README.md
Normal file
15
docs/issue/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Issue Management
|
||||
|
||||
Using `n34`, you can manage Git issues stored in Nostr relays, adhering to
|
||||
the [NIP-34] standard. In Nostr, events are immutable, meaning their IDs are
|
||||
derived from the SHA-256 hash of their timestamp, content, author, and tags.
|
||||
As a result, issues cannot be edited directly. However, with `n34`, you can
|
||||
create new issues, view existing ones, or update their status—such as closing,
|
||||
resolving, or reopening them.
|
||||
|
||||
[NIP-34] introduces support for drafting issues, though this feature is not
|
||||
currently implemented in `n34` due to the lack of a clear use case for drafting
|
||||
issues. The inclusion of this functionality may stem from its shared use in both
|
||||
issues and patches, suggesting it was primarily designed for patch management.
|
||||
|
||||
[NIP-34]: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
19
docs/issue/close.md
Normal file
19
docs/issue/close.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Close an Issue
|
||||
|
||||
> `n34 issue close` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Closes an open issue
|
||||
|
||||
Usage: n34 issue close [OPTIONS] <ISSUE_ID>
|
||||
|
||||
Arguments:
|
||||
<ISSUE_ID> The open issue id to close it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1632` (Close status) for the specified issue. The issue have to
|
||||
be open.
|
||||
19
docs/issue/list.md
Normal file
19
docs/issue/list.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# List Issues
|
||||
|
||||
> `n34 issue list` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
List the repositories issues
|
||||
|
||||
Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> Maximum number of issues to list [default: 15]
|
||||
```
|
||||
|
||||
List the repositories issues. By default `n34` will look for `nostr-address`
|
||||
file and extract the repositories from it.
|
||||
29
docs/issue/new.md
Normal file
29
docs/issue/new.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Create an Issue
|
||||
|
||||
> `n34 issue new` Command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Create a new repository issue
|
||||
|
||||
Usage: n34 issue new [OPTIONS] <--content <CONTENT>|--editor>
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-c, --content <CONTENT> Markdown content for the issue. Cannot be used together with the `--editor` flag
|
||||
-e, --editor Opens the user's default editor to write issue content. The first line will be used as the issue subject
|
||||
--subject <SUBJECT> The issue subject. Cannot be used together with the `--editor` flag
|
||||
-l, --label <LABEL> Labels for the issue. Can be specified as arguments (-l bug) or hashtags in content (#bug)
|
||||
```
|
||||
|
||||
Use the `n34 issue new` command to create a new issue in a repository. This
|
||||
command supports the [NIP-21] (`nostr:` URI scheme) and hashtags within the
|
||||
issue content. When you mention public keys in the content, they will be
|
||||
included in the event tags. Additionally, using hashtags like `#bug` in the
|
||||
issue body will automatically apply them as labels.
|
||||
|
||||
You must choose between the `--content` and `--editor` options. With
|
||||
`--content`, you provide the issue content directly in the command. With
|
||||
`--editor`, your default `$EDITOR` will open, allowing you to write the issue
|
||||
content. The first line of the editor's output will be used as the issue
|
||||
subject.
|
||||
19
docs/issue/reopen.md
Normal file
19
docs/issue/reopen.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Reopen an Issue
|
||||
|
||||
> `n34 issue reopen` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Reopens a closed issue
|
||||
|
||||
Usage: n34 issue reopen [OPTIONS] <ISSUE_ID>
|
||||
|
||||
Arguments:
|
||||
<ISSUE_ID> The ID of the closed issue to reopen
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1630` (Open status) for the specified issue. The issue have to
|
||||
be closed.
|
||||
18
docs/issue/resolve.md
Normal file
18
docs/issue/resolve.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Resolve an Issue
|
||||
|
||||
> `n34 issue resolve` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Resolves an issue
|
||||
|
||||
Usage: n34 issue resolve [OPTIONS] <ISSUE_ID>
|
||||
|
||||
Arguments:
|
||||
<ISSUE_ID> The issue id to resolve it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1631` (Resolved status) event for the specified issue.
|
||||
19
docs/issue/view.md
Normal file
19
docs/issue/view.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# View an Issue
|
||||
|
||||
> `n34 issue view` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
View an issue by its ID
|
||||
|
||||
Usage: n34 issue view [OPTIONS] <ISSUE_ID>
|
||||
|
||||
Arguments:
|
||||
<ISSUE_ID> The issue id to view it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Simply provide the issue ID in `note` or `nevent` format to retrieve and display
|
||||
the issue details.
|
||||
21
docs/patch/README.md
Normal file
21
docs/patch/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Patch Management
|
||||
|
||||
In `n34`, patch management is designed to give you complete control. You can
|
||||
manually generate patch files using `git-format-patch` and then broadcast them
|
||||
to Nostr relays. This ensures that you have full authority over the content
|
||||
and structure of your patches, allowing for precise customization as per your
|
||||
requirements.
|
||||
|
||||
Similarly, when fetching patches, `n34` provides them to you without
|
||||
automatically applying, merging, or checking them. This empowers you to review
|
||||
the patches at your own pace and decide whether to merge or apply them as
|
||||
needed. You retain full control over the entire process, ensuring a tailored
|
||||
approach to patch management.
|
||||
|
||||
## Patch Status Management
|
||||
|
||||
You can assign a status to original patches, but revision patches do not have
|
||||
a specific status assigned to them. Instead, they inherit the status of the
|
||||
original patch. However, if the original patch is marked as `Applied/Merged`,
|
||||
the revision patch must be explicitly tagged to claim the same status. If not
|
||||
tagged, the revision patch status will be `Closed`.
|
||||
31
docs/patch/apply.md
Normal file
31
docs/patch/apply.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Mark as Applied
|
||||
|
||||
> `n34 patch apply` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Set an open patch status to applied
|
||||
|
||||
Usage: n34 patch apply [OPTIONS] <PATCH_ID> [APPLIED_COMMITS]...
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
||||
<APPLIED_COMMITS>... The applied commits
|
||||
|
||||
Options:
|
||||
--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
|
||||
```
|
||||
|
||||
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The
|
||||
patch must be in open status.
|
||||
|
||||
You can specify either an original patch or revision patch ID, but the status
|
||||
event will only reference the original patch. Revision patches will be mentioned
|
||||
in the event.
|
||||
|
||||
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'`.
|
||||
20
docs/patch/close.md
Normal file
20
docs/patch/close.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Close a Patch
|
||||
|
||||
> `n34 patch close` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Closes an open or drafted patch
|
||||
|
||||
Usage: n34 patch close [OPTIONS] <PATCH_ID>
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The open/drafted patch id to close it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1632` (Close status) for the specified patch. The patch have to
|
||||
be open or drafted.
|
||||
|
||||
19
docs/patch/draft.md
Normal file
19
docs/patch/draft.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Convert to Draft
|
||||
|
||||
> `n34 patch draft` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Converts an open patch to draft state
|
||||
|
||||
Usage: n34 patch draft [OPTIONS] <PATCH_ID>
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The open patch id to draft it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1633` (Draft status) for the specified patch. The patch have to
|
||||
be open.
|
||||
31
docs/patch/fetch.md
Normal file
31
docs/patch/fetch.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Fetch a Patch
|
||||
|
||||
> `n34 patch fetch` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Fetches a patch by its id
|
||||
|
||||
Usage: n34 patch fetch [OPTIONS] <PATCH_ID>
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The patch id to fetch it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-o, --output <PATH> Directory for saving patches. Defaults to the current directory. Use `-` for stdout output
|
||||
```
|
||||
|
||||
Fetches patches using their original patch ID. All fetched patches will be saved
|
||||
to the specified output directory (current directory by default). You can then
|
||||
apply or merge these patches into your branch as needed.
|
||||
|
||||
You can also write patches to stdout for direct application with tools like
|
||||
`git-am`. For instance, to fetch and apply patches use:
|
||||
|
||||
```bash
|
||||
n34 patch fetch -o '-' <note1...> | git am --empty=drop
|
||||
```
|
||||
|
||||
The `--empty=drop` option ensures empty commits, such as cover letters, are
|
||||
omitted.
|
||||
20
docs/patch/list.md
Normal file
20
docs/patch/list.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# List Patches
|
||||
|
||||
> `n34 patch list` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
List the repositories patches
|
||||
|
||||
Usage: n34 patch 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 patches. By default `n34` will look for `nostr-address`
|
||||
file and extract the repositories from it.
|
||||
|
||||
29
docs/patch/merge.md
Normal file
29
docs/patch/merge.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Mark as Merged
|
||||
|
||||
> `n34 patch merge` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Set an open patch status to merged
|
||||
|
||||
Usage: n34 patch merge [OPTIONS] <PATCH_ID> <MERGE_COMMIT>
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The open patch id to merge it. Must be orignal root patch or revision root
|
||||
<MERGE_COMMIT> The merge commit id
|
||||
|
||||
Options:
|
||||
--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
|
||||
```
|
||||
|
||||
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The
|
||||
patch must be in open status.
|
||||
|
||||
You can specify either an original patch or revision patch ID, but the status
|
||||
event will only reference the original patch. Revision patches will be mentioned
|
||||
in the event.
|
||||
|
||||
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
|
||||
20
docs/patch/reopen.md
Normal file
20
docs/patch/reopen.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Reopen a Patch
|
||||
|
||||
> `n34 patch reopen` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Reopens a closed or drafted patch
|
||||
|
||||
Usage: n34 patch reopen [OPTIONS] <PATCH_ID>
|
||||
|
||||
Arguments:
|
||||
<PATCH_ID> The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1630` (Open status) for the specified patch. The patch have to
|
||||
be closed or drafted.
|
||||
|
||||
29
docs/patch/send.md
Normal file
29
docs/patch/send.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Send Patches to a Repository
|
||||
|
||||
> `n34 patch send` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Send one or more patches to a repository
|
||||
|
||||
Usage: n34 patch send [OPTIONS] <PATCH-PATH>...
|
||||
|
||||
Arguments:
|
||||
<PATCH-PATH>... Space-separated list of patch files to send. Use `-` to read from stdin.
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--original-patch <EVENT-ID> Original patch ID if this is a revision of it
|
||||
```
|
||||
|
||||
Send your generated patches to the repositories specified using the `--repo`
|
||||
option or retrieved from the `nostr-address` file. When submitting a revision
|
||||
of an existing patch, include the original patch ID to ensure it’s correctly
|
||||
referenced in your revision patch event.
|
||||
|
||||
You can also pass patches from stdin, retrieved from a website or directly from
|
||||
`git-format-patch`. Simply use `-` as the patch path. For example:
|
||||
|
||||
```bash
|
||||
git format-patch --stdout --base master master..HEAD | n34 patch send -
|
||||
```
|
||||
43
docs/pr/README.md
Normal file
43
docs/pr/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Pull Request Management
|
||||
|
||||
Pull requests are a new addition to [NIP-34], proposed by [DanConwayDev] in
|
||||
[nostr-protocol/nips#1966]. The concept is straightforward: it involves an event
|
||||
that notifies you of changes on a Git server, which can be any Git server. This
|
||||
event contains two key pieces of information, the Git repository URL and the
|
||||
commit tip. You can add this remote to your project, check out the specified
|
||||
commit, and then review or merge/rebase the changes as needed. Additionally,
|
||||
there is a `PR-update` event that informs you when the previous commit tip is no
|
||||
longer the head of the changes, such as when new changes are added or a rebase
|
||||
occurs.
|
||||
|
||||
## Why Use the Commit Tip?
|
||||
|
||||
Using the commit tip is preferable to relying on a branch name. A branch name
|
||||
simply points to a commit, so checking out a branch is effectively the same
|
||||
as checking out a specific commit. However, if we use the branch name instead
|
||||
of the commit, the changes can be altered by anyone, not just the pull request
|
||||
author. By checking out a specific commit ID, we ensure that the changes are
|
||||
exactly as intended by the pull request author and have not been tampered with
|
||||
by a malicious party.
|
||||
|
||||
## Does `n34` Handle Git Operations?
|
||||
|
||||
No, `n34` does not perform any Git operations. You can use the `n34 pr view`
|
||||
command to retrieve the pull request clone URL and the commit tip, allowing
|
||||
you to fetch the changes manually. Similarly, `n34 pr new` and `n34 pr update`
|
||||
do not interact with Git directly. They only prompt you for the clone URL and
|
||||
commit tip to construct the event. For more details on the philosophy behind
|
||||
`n34`, refer to the [Philosophy] section.
|
||||
|
||||
## Why use pull requests instead of patches?
|
||||
|
||||
Pull requests are useful for large changes. Most relays limit events to 60KB,
|
||||
so if your changes exceed that limit (per event, not in total), consider using
|
||||
pull requests. For [GRASP] servers, the limit is usually higher. Before using pull
|
||||
requests, check the relays, patches are more decentralized.
|
||||
|
||||
[NIP-34]: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
[DanConwayDev]: https://danconwaydev.com
|
||||
[nostr-protocol/nips#1966]: https://github.com/nostr-protocol/nips/pull/1966
|
||||
[Philosophy]: https://n34.dev/commands.html#philosophy
|
||||
[GRASP]: https://ngit.dev/grasp
|
||||
24
docs/pr/apply.md
Normal file
24
docs/pr/apply.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Mark as Applied
|
||||
|
||||
> `n34 pr apply` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr apply [OPTIONS] <PR_ID> <APPLIED_COMMITS>...
|
||||
|
||||
Arguments:
|
||||
<PR_ID> The open PR id to apply it
|
||||
<APPLIED_COMMITS>... The applied commits
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Creates a kind `1631` event (Applied/Merged status) for the specified PR. The PR
|
||||
must be in open status.
|
||||
|
||||
The `APPLIED_COMMITS` field serves to inform clients about the status of
|
||||
specific commits, whether they have been applied or not. If you need to retrieve
|
||||
the list of commits from a specific point (such as the tip of the master branch)
|
||||
up to the `HEAD`, you can use the following Git command: `git log --pretty=%H
|
||||
'origin/master..HEAD'`.
|
||||
18
docs/pr/close.md
Normal file
18
docs/pr/close.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Close a Pull Request
|
||||
|
||||
> `n34 pr close` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr close [OPTIONS] <PR_ID>
|
||||
|
||||
Arguments:
|
||||
<PR_ID> The open/draft PR id to close it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1632` (Close status) for the specified PR. The PR have to be open
|
||||
or drafted.
|
||||
|
||||
17
docs/pr/draft.md
Normal file
17
docs/pr/draft.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Convert to Draft
|
||||
|
||||
> `n34 pr draft` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr draft [OPTIONS] <PR_ID>
|
||||
|
||||
Arguments:
|
||||
<PR_ID> The open PR id to draft it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1633` (Draft status) for the specified PR. The PR have to
|
||||
be open.
|
||||
17
docs/pr/list.md
Normal file
17
docs/pr/list.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# List Pull Requests
|
||||
|
||||
> `n34 pr list` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> Maximum number of patches to list [default: 15]
|
||||
```
|
||||
|
||||
List the repositories pull requests. By default `n34` will look for
|
||||
`nostr-address` file and extract the repositories from it.
|
||||
22
docs/pr/merge.md
Normal file
22
docs/pr/merge.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Mark as Merged
|
||||
|
||||
> `n34 pr merge` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr merge [OPTIONS] <PR_ID> <MERGE_COMMIT>
|
||||
|
||||
Arguments:
|
||||
<PR_ID> The open PR id to merge it
|
||||
<MERGE_COMMIT> The merge commit id
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Creates a kind `1631` event (Applied/Merged status) for the specified PR. The
|
||||
PR must be in open status.
|
||||
|
||||
You can get the `MERGE_COMMIT` commit using `git rev-parse HEAD` command if
|
||||
the merge commit in the `HEAD` or use `HEAD~n` where the `n` is the number of
|
||||
commits the merge commit before the HEAD
|
||||
30
docs/pr/new.md
Normal file
30
docs/pr/new.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Create a Pull Request
|
||||
|
||||
> `n34 pr new` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr new [OPTIONS] <--subject <SUBJECT>|--editor> <COMMIT> <CLONES>...
|
||||
|
||||
Arguments:
|
||||
<COMMIT> The SHA-1 hash of the commit at the tip of the PR branch
|
||||
<CLONES>... Repositories to clone for the pull request, separated by commas
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--body <BODY> The body content of the pull request. Cannot be used together with the `--editor` flag
|
||||
--subject <SUBJECT> The subject or title of the pull request. Cannot be used together with the `--editor` flag
|
||||
-e, --editor Opens the user's default editor to write PR subject and body
|
||||
--labels <LABELS> Labels to associate with the pull request, separated by commas
|
||||
--branch <BRANCH> The branch name for the pull request
|
||||
--grasp Push the pull request to the repository GRASP server
|
||||
```
|
||||
|
||||
Submit a pull request to the repositories specified using the `--repo` option or
|
||||
obtained from the `nostr-address` file.
|
||||
|
||||
Utilize the `--grasp` option if you intend to send the pull request to the
|
||||
[GRASP] servers. Note that `n34` will not automatically send the pull request
|
||||
for you but will indicate where to push your changes.
|
||||
|
||||
[GRASP]: https://ngit.dev/grasp
|
||||
17
docs/pr/reopen.md
Normal file
17
docs/pr/reopen.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Reopen a Pull Request
|
||||
|
||||
> `n34 pr reopen` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr reopen [OPTIONS] <PR_ID>
|
||||
|
||||
Arguments:
|
||||
<PR_ID> The closed/drafted patch id to reopen it
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Issue a kind `1630` (Open status) for the specified PR. The PR have to
|
||||
be closed or drafted.
|
||||
25
docs/pr/update.md
Normal file
25
docs/pr/update.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Update a Pull Request
|
||||
|
||||
> `n34 pr update` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr update [OPTIONS] <EVENT-ID> <COMMIT> <CLONES>...
|
||||
|
||||
Arguments:
|
||||
<EVENT-ID> Original PR ID
|
||||
<COMMIT> The SHA-1 hash of the commit at the tip of the PR branch
|
||||
<CLONES>... Repositories to clone for the pull request, separated by commas
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--grasp Push the pull request update to the repository GRASP server
|
||||
```
|
||||
|
||||
Update an existing pull request with the new changes.
|
||||
|
||||
Utilize the `--grasp` option if you intend to send the pull request to the
|
||||
[GRASP] servers. Note that `n34` will not automatically send the pull request
|
||||
for you but will indicate where to push your changes.
|
||||
|
||||
[GRASP]: https://ngit.dev/grasp
|
||||
18
docs/pr/view.md
Normal file
18
docs/pr/view.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# View a Pull Request
|
||||
|
||||
> `n34 pr view` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Usage: n34 pr view [OPTIONS] <EVENT-ID>
|
||||
|
||||
Arguments:
|
||||
<EVENT-ID> Pull request ID
|
||||
|
||||
Options:
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
View a specific pull request. This includes the pull request title, labels,
|
||||
description, clone URLs, and the latest commit tip. Use this information to
|
||||
fetch the changes.
|
||||
28
docs/reply.md
Normal file
28
docs/reply.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Reply to Issues and Patches
|
||||
|
||||
> `n34 reply` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Reply to issues and patches
|
||||
|
||||
Usage: n34 reply [OPTIONS] <--comment <COMMENT>|--editor> <nevent1-or-note1>
|
||||
|
||||
Arguments:
|
||||
<nevent1-or-note1> The issue, patch, or comment to reply to
|
||||
|
||||
Options:
|
||||
--quote-to Quote the replied-to event in the editor
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
-c, --comment <COMMENT> The comment (cannot be used with --editor)
|
||||
-e, --editor Open editor to write comment (cannot be used with --content)
|
||||
```
|
||||
|
||||
Craft replies ([NIP-22] Comment) to issues, patches, or comments with ease
|
||||
using the `n34 reply` command. You can either input your reply directly with
|
||||
the `--comment` option or open an editor for a more detailed response using
|
||||
`--editor`. Additionally, when using `--editor`, the `--quote-to` option
|
||||
allows you to include the original content in your editor, enabling precise and
|
||||
context-aware replies.
|
||||
|
||||
[NIP-22]: https://github.com/nostr-protocol/nips/blob/master/22.md
|
||||
4
docs/repo/README.md
Normal file
4
docs/repo/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Manage Repositories
|
||||
|
||||
In `n34` you can manage your repositories. This includes announcing new ones,
|
||||
viewing existing ones, and announcing state updates (coming soon).
|
||||
33
docs/repo/announce.md
Normal file
33
docs/repo/announce.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Broadcast and Update a Git Repository
|
||||
|
||||
> `n34 repo announce` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Broadcast and update a git repository
|
||||
|
||||
Usage: n34 repo announce [OPTIONS] --id <REPO_ID>
|
||||
|
||||
Options:
|
||||
--id <REPO_ID> Unique identifier for the repository in kebab-case
|
||||
-n, --name <NAME> A name for the repository
|
||||
-d, --description <DESCRIPTION> A description for the repository
|
||||
-w, --web <WEB> Webpage URLs for the repository (if provided by the git server)
|
||||
-c, --clone <CLONE> URLs for cloning the repository
|
||||
-m, --maintainers <MAINTAINERS> Additional maintainers of the repository (besides yourself)
|
||||
-l, --label <LABEL> Labels to categorize the repository. Can be specified multiple times
|
||||
--personal-fork Indicates whether the announcement is a personal fork
|
||||
--force-id Skip kebab-case validation for the repository ID
|
||||
--address-file If set, creates a `nostr-address` file to enable automatic address discovery by n34
|
||||
```
|
||||
|
||||
This command generates an announcement event to publish your project. It can be
|
||||
used to announce a new repository or update an existing one.
|
||||
|
||||
When updating, you must resubmit all repository fields, not just the fields
|
||||
you wish to change. The command uses this information to build and publish a
|
||||
completely new announcement event that will replace the old one.
|
||||
|
||||
It is recommended to use the `--address-file` flag. This option creates
|
||||
a `nostr-address` file that enables `n34` to automatically discover the
|
||||
repository's address, simplifying the workflow for contributors.
|
||||
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
|
||||
20
docs/repo/view.md
Normal file
20
docs/repo/view.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# View Git Repository Details
|
||||
|
||||
> `n34 repo view` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
View details of a nostr git repository
|
||||
|
||||
Usage: n34 repo view [NADDR-NIP05-OR-SET]...
|
||||
|
||||
Arguments:
|
||||
[NADDR-NIP05-OR-SET]... Repository addresses
|
||||
```
|
||||
|
||||
This command prints repository details to standard output. If no arguments
|
||||
are provided, it looks for a `nostr-address` file in the current directory
|
||||
and displays the details for the address specified within it. See [passing
|
||||
repositories] for details on accepted formats.
|
||||
|
||||
[passing repositories]: /commands.html#passing-repositories
|
||||
11
docs/sets/README.md
Normal file
11
docs/sets/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Managing Repository and Relay Sets
|
||||
|
||||
Sets are a convenience feature for contributing to projects that do not have a
|
||||
`nostr-address` file. Instead of manually specifying the project's repositories
|
||||
and relays for every command, you can define them once as a named "set". You can
|
||||
then reference this set by its name in commands. This allows you to use the set
|
||||
as a shortcut for a list of relays (`--relays <set_name>`) or as the project's
|
||||
address in commands like `issue` and `patch`.
|
||||
|
||||
Sets are defined in your configuration file. To use a specific configuration
|
||||
file, pass its path using the `--config` option.
|
||||
25
docs/sets/new.md
Normal file
25
docs/sets/new.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Create a Set
|
||||
|
||||
> `n34 sets new` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Create a new set
|
||||
|
||||
Usage: n34 sets new [OPTIONS] <NAME>
|
||||
|
||||
Arguments:
|
||||
<NAME> Unique name for the set
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Each set requires a unique name, provided as the final argument to the command.
|
||||
Use the `--set-relays`/`--sr` option to specify the relays for the new set;
|
||||
this can be a relay URL or the name of an existing set whose relays you wish to
|
||||
use. To add repositories, use the `--repo` option. Check [passing repositories]
|
||||
format.
|
||||
|
||||
[passing repositories]: /commands.html#passing-repositories
|
||||
24
docs/sets/remove.md
Normal file
24
docs/sets/remove.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Remove a Set
|
||||
|
||||
> `n34 sets remove` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Remove a set, or specific repos and relays within it
|
||||
|
||||
Usage: n34 sets remove [OPTIONS] <NAME>
|
||||
|
||||
Arguments:
|
||||
<NAME> Set name to delete
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
```
|
||||
|
||||
Removes an entire set, or specific repositories and relays from it.
|
||||
Without options, this command deletes the entire set.
|
||||
|
||||
See the [passing repositories] section for more details on supported formats.
|
||||
|
||||
[passing repositories]: /commands.html#passing-repositories
|
||||
17
docs/sets/show.md
Normal file
17
docs/sets/show.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Show a Set
|
||||
|
||||
> `n34 sets show` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Show a single set or all the stored sets
|
||||
|
||||
Usage: n34 sets show [NAME]
|
||||
|
||||
Arguments:
|
||||
[NAME] Name of the set to display. If not provided, lists all available sets
|
||||
```
|
||||
|
||||
Prints all set names to standard output, each followed by its associated repos
|
||||
and relays. To view the details for a specific set, provide its name as an
|
||||
argument.
|
||||
25
docs/sets/update.md
Normal file
25
docs/sets/update.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Modify a Set
|
||||
|
||||
> `n34 sets update` command
|
||||
|
||||
**Usage:**
|
||||
```
|
||||
Modify an existing set
|
||||
|
||||
Usage: n34 sets update [OPTIONS] <NAME>
|
||||
|
||||
Arguments:
|
||||
<NAME> Name of the set to update
|
||||
|
||||
Options:
|
||||
--set-relay <RELAYS> Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||
--repo <NADDR-NIP05-OR-SET> Repository addresses
|
||||
--override Replace existing relays/repositories instead of adding to them
|
||||
```
|
||||
|
||||
Use this command to update an existing set by its name. By default, providing
|
||||
relays via `--set-relay` or repositories via `--repo` will add them to the set's
|
||||
existing entries. To replace the current relays and repositories with the new
|
||||
values, use the `--override` flag.
|
||||
|
||||
[passing repositories]: /commands.html#passing-repositories
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1745930157,
|
||||
"narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=",
|
||||
"lastModified": 1754498491,
|
||||
"narHash": "sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae",
|
||||
"rev": "c2ae88e026f9525daf89587f3cbee584b92b6134",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -62,11 +62,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1746067100,
|
||||
"narHash": "sha256-6JeEbboDvRjLwB9kzCnmWj+f+ZnMtKOe5c2F1VBpaTs=",
|
||||
"lastModified": 1754575663,
|
||||
"narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "026e8fedefd6b167d92ed04b195c658d95ffc7a5",
|
||||
"rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
29
flake.nix
29
flake.nix
@@ -20,20 +20,39 @@
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
# n34 development environment
|
||||
devShells.default = mkShell {
|
||||
packages = [
|
||||
just
|
||||
cargo-msrv
|
||||
git-cliff
|
||||
mdbook
|
||||
nushell
|
||||
taplo
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
(lib.hiPrio rust-bin.nightly."2025-04-30".rustfmt)
|
||||
(lib.hiPrio rust-bin.nightly."2025-08-07".rustfmt)
|
||||
rust-bin.stable.latest.default
|
||||
rust-analyzer
|
||||
];
|
||||
|
||||
buildInputs = [ ];
|
||||
};
|
||||
|
||||
# Build n34
|
||||
packages.default =
|
||||
let
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
in
|
||||
with pkgs;
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = manifest.name;
|
||||
version = manifest.version;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
src = lib.cleanSource ./.;
|
||||
|
||||
meta = {
|
||||
inherit (manifest) description homepage;
|
||||
license = lib.licenses.gpl3Plus;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
49
justfile
49
justfile
@@ -1,49 +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>
|
||||
|
||||
set quiet
|
||||
set unstable
|
||||
set shell := ["/usr/bin/env", "bash", "-c"]
|
||||
set script-interpreter := ["/usr/bin/env", "bash", "-c"]
|
||||
|
||||
JUST_EXECUTABLE := "just -u -f " + justfile()
|
||||
header := "Available tasks:\n"
|
||||
# Get the MSRV from the Cargo.toml
|
||||
msrv := `cat Cargo.toml | grep "rust-version" | sed 's/.*"\(.*\)".*/\1/'`
|
||||
|
||||
|
||||
_default:
|
||||
@{{JUST_EXECUTABLE}} --list-heading "{{header}}" --list
|
||||
|
||||
# Run the CI
|
||||
ci: && msrv
|
||||
cargo build -q
|
||||
cargo fmt -- --check
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
# Check that the current MSRV is correct
|
||||
msrv:
|
||||
rustup toolchain install {{msrv}}
|
||||
echo "Checking MSRV ({{msrv}})"
|
||||
cargo +{{msrv}} check -q
|
||||
echo "MSRV is correct"
|
||||
|
||||
# Update the changelog
|
||||
[script]
|
||||
change-log:
|
||||
OLD_HASH=$(sha256sum CHANGELOG.md | head -c 64)
|
||||
git-cliff > CHANGELOG.md
|
||||
NEW_HASH=$(sha256sum CHANGELOG.md | head -c 64)
|
||||
if [[ $OLD_HASH != $NEW_HASH ]]; then
|
||||
TZ=UTC git add CHANGELOG.md
|
||||
TZ=UTC git commit -m 'chore(changelog): Update the changelog'
|
||||
echo 'The changes have been added to the changelog file and committed'
|
||||
else
|
||||
echo 'No changes have been added to the changelog'
|
||||
fi
|
||||
|
||||
alias cl := change-log
|
||||
15
nostr-address
Normal file
15
nostr-address
Normal file
@@ -0,0 +1,15 @@
|
||||
# This file contains NIP-19 `naddr` entities for repositories that accept this
|
||||
# project's issues and patches.
|
||||
#
|
||||
# The file acts as a **read-only reference** for retrieving repository relays
|
||||
# when embedded in an `naddr` and mentions those repositories when opening
|
||||
# patches or issues. Modifications here will not affect in the relays, as the
|
||||
# file is **explicitly untracked**. Its goal is to simplify contributions by
|
||||
# removing the need for manual address entry.
|
||||
#
|
||||
# Each entry must start with "naddr". Embedded relays are **strongly recommended**
|
||||
# to assist client-side discovery.
|
||||
#
|
||||
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||
|
||||
naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj
|
||||
38
rustfmt.toml
38
rustfmt.toml
@@ -1,22 +1,22 @@
|
||||
unstable_features = true
|
||||
style_edition = "2024"
|
||||
style_edition = "2024"
|
||||
|
||||
blank_lines_upper_bound = 2
|
||||
combine_control_expr = false
|
||||
wrap_comments = true
|
||||
condense_wildcard_suffixes = true
|
||||
edition = "2024"
|
||||
enum_discrim_align_threshold = 20
|
||||
force_multiline_blocks = true
|
||||
format_code_in_doc_comments = true
|
||||
format_generated_files = false
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
imports_layout = "HorizontalVertical"
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
reorder_impl_items = true
|
||||
group_imports = "StdExternalCrate"
|
||||
blank_lines_upper_bound = 2
|
||||
combine_control_expr = false
|
||||
wrap_comments = true
|
||||
condense_wildcard_suffixes = true
|
||||
edition = "2024"
|
||||
enum_discrim_align_threshold = 20
|
||||
force_multiline_blocks = true
|
||||
format_code_in_doc_comments = true
|
||||
format_generated_files = false
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
imports_layout = "HorizontalVertical"
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
reorder_impl_items = true
|
||||
group_imports = "StdExternalCrate"
|
||||
single_line_let_else_max_width = 0
|
||||
struct_field_align_threshold = 20
|
||||
use_try_shorthand = true
|
||||
struct_field_align_threshold = 20
|
||||
use_try_shorthand = true
|
||||
|
||||
46
src/cli/commands/config/bunker.rs
Normal file
46
src/cli/commands/config/bunker.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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::nips::nip46::NostrConnectURI;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, traits::CommandRunner},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct BunkerArgs {
|
||||
/// Nostr Connect URL for the bunker. Omit this to remove the current bunker
|
||||
/// URL.
|
||||
bunker_url: Option<NostrConnectURI>,
|
||||
}
|
||||
|
||||
|
||||
impl CommandRunner for BunkerArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
if let Some(ref bunker_url) = self.bunker_url
|
||||
&& !bunker_url.is_bunker()
|
||||
{
|
||||
return Err(N34Error::NotBunkerUrl);
|
||||
}
|
||||
options.config.bunker_url = self.bunker_url;
|
||||
options.config.dump()
|
||||
}
|
||||
}
|
||||
62
src/cli/commands/config/keyring.rs
Normal file
62
src/cli/commands/config/keyring.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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::{ArgGroup, Args};
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, traits::CommandRunner},
|
||||
error::N34Result,
|
||||
nostr_keyring,
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[clap(
|
||||
group(
|
||||
ArgGroup::new("options")
|
||||
.required(true)
|
||||
)
|
||||
)]
|
||||
pub struct KeyringArgs {
|
||||
/// Turns on secret key keyring. Requires entering the key once when
|
||||
/// enabled.
|
||||
#[arg(long, group = "options")]
|
||||
enable: bool,
|
||||
/// Turns off secret key keyring. Removes any existing key and prevents
|
||||
/// storing new ones.
|
||||
#[arg(long, group = "options")]
|
||||
disable: bool,
|
||||
/// Deletes current key and stores the next provided key.
|
||||
#[arg(long, group = "options")]
|
||||
reset: bool,
|
||||
}
|
||||
|
||||
impl CommandRunner for KeyringArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
if self.enable {
|
||||
options.config.keyring_secret_key = true;
|
||||
} else if self.disable {
|
||||
options.config.keyring_secret_key = false;
|
||||
}
|
||||
|
||||
if self.reset || self.disable {
|
||||
let _ = nostr_keyring::user::delete().await;
|
||||
}
|
||||
|
||||
options.config.dump()
|
||||
}
|
||||
}
|
||||
60
src/cli/commands/config/mod.rs
Normal file
60
src/cli/commands/config/mod.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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>.
|
||||
|
||||
/// `config bunker` subcommand
|
||||
mod bunker;
|
||||
/// `config keyring` subcommand
|
||||
mod keyring;
|
||||
/// `config nip07` subcommand
|
||||
mod nip07;
|
||||
/// `config pow` subcommand
|
||||
mod pow;
|
||||
/// `config relays` subcommand
|
||||
mod relays;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use self::bunker::BunkerArgs;
|
||||
use self::keyring::KeyringArgs;
|
||||
use self::nip07::Nip07Args;
|
||||
use self::pow::PowArgs;
|
||||
use self::relays::RelaysArgs;
|
||||
use super::CliOptions;
|
||||
use crate::{cli::traits::CommandRunner, error::N34Result};
|
||||
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ConfigSubcommands {
|
||||
/// Sets the default PoW difficulty (0 if not specified)
|
||||
Pow(PowArgs),
|
||||
/// Sets the default fallback relays if none provided. Use this relays for
|
||||
/// read and write.
|
||||
Relays(RelaysArgs),
|
||||
/// Sets a URL of NIP-46 bunker server used for signing events.
|
||||
Bunker(BunkerArgs),
|
||||
/// Managing the secret key keyring, including enabling, disabling, or
|
||||
/// resetting it.
|
||||
Keyring(KeyringArgs),
|
||||
/// Controls the NIP-07 browser signer proxy, turning it on or off, and
|
||||
/// configures the `ip:port` address.
|
||||
Nip07(Nip07Args),
|
||||
}
|
||||
|
||||
impl CommandRunner for ConfigSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & Pow Relays Bunker Keyring Nip07)
|
||||
}
|
||||
}
|
||||
58
src/cli/commands/config/nip07.rs
Normal file
58
src/cli/commands/config/nip07.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::net::SocketAddr;
|
||||
|
||||
use clap::{ArgGroup, Args};
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
#[clap(
|
||||
group(
|
||||
ArgGroup::new("options")
|
||||
.required(true)
|
||||
)
|
||||
)]
|
||||
pub struct Nip07Args {
|
||||
/// Enable NIP-07 as the default signer.
|
||||
#[arg(long, group = "options")]
|
||||
enable: bool,
|
||||
/// Disable NIP-07 as the default signer.
|
||||
#[arg(long, group = "options", group = "disable_options")]
|
||||
disable: bool,
|
||||
/// Set the default `ip:port` for the browser signer proxy.
|
||||
#[arg(long, group = "disable_options")]
|
||||
addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl CommandRunner for Nip07Args {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
if self.enable {
|
||||
let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR);
|
||||
options.config.nip07 = Some(addr)
|
||||
} else {
|
||||
options.config.nip07 = None
|
||||
}
|
||||
|
||||
options.config.dump()
|
||||
}
|
||||
}
|
||||
37
src/cli/commands/config/pow.rs
Normal file
37
src/cli/commands/config/pow.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct PowArgs {
|
||||
/// The new default PoW difficulty
|
||||
difficulty: u8,
|
||||
}
|
||||
|
||||
impl CommandRunner for PowArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
options.config.pow = Some(self.difficulty);
|
||||
options.config.dump()
|
||||
}
|
||||
}
|
||||
53
src/cli/commands/config/relays.rs
Normal file
53
src/cli/commands/config/relays.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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::types::RelayUrl;
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, traits::CommandRunner},
|
||||
error::N34Result,
|
||||
};
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct RelaysArgs {
|
||||
/// List of relay URLs to append to fallback relays. If empty, removes all
|
||||
/// fallback relays.
|
||||
relays: Vec<RelayUrl>,
|
||||
/// Replace existing fallback relays instead of appending new ones.
|
||||
#[arg(long = "override")]
|
||||
override_relays: bool,
|
||||
}
|
||||
|
||||
impl CommandRunner for RelaysArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||
if self.relays.is_empty() {
|
||||
options.config.fallback_relays = None;
|
||||
} else if self.override_relays {
|
||||
options.config.fallback_relays = Some(self.relays);
|
||||
} else {
|
||||
let mut relays = options.config.fallback_relays.clone().unwrap_or_default();
|
||||
relays.extend(self.relays);
|
||||
relays.sort_unstable();
|
||||
relays.dedup();
|
||||
options.config.fallback_relays = Some(relays);
|
||||
}
|
||||
|
||||
options.config.dump()
|
||||
}
|
||||
}
|
||||
71
src/cli/commands/issue/close.rs
Normal file
71
src/cli/commands/issue/close.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 super::IssueStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CloseArgs {
|
||||
/// Repository addresses
|
||||
///
|
||||
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||
///
|
||||
/// If omitted, looks for a `nostr-address` file.
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The open issue id to close it
|
||||
issue_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for CloseArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::issue_status_command(
|
||||
options,
|
||||
self.issue_id,
|
||||
self.naddrs,
|
||||
IssueStatus::Closed,
|
||||
|issue_status| {
|
||||
if issue_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close an already closed issue".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if issue_status.is_resolved() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close a resolved issue".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
57
src/cli/commands/issue/list.rs
Normal file
57
src/cli/commands/issue/list.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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(Debug, Args)]
|
||||
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 issues 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::Issue as u8 }>(
|
||||
options,
|
||||
self.naddrs,
|
||||
self.limit.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
152
src/cli/commands/issue/mod.rs
Normal file
152
src/cli/commands/issue/mod.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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>.
|
||||
|
||||
/// `issue close` subcommand
|
||||
mod close;
|
||||
/// `issue list` subcommand
|
||||
mod list;
|
||||
/// `issue new` subcommand
|
||||
mod new;
|
||||
/// `issue reopen` subcommand
|
||||
mod reopen;
|
||||
/// `issue resolve` subcommand
|
||||
mod resolve;
|
||||
/// `issue view` subcommand
|
||||
mod view;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use clap::Subcommand;
|
||||
use nostr::event::Kind;
|
||||
|
||||
use self::close::CloseArgs;
|
||||
use self::list::ListArgs;
|
||||
use self::new::NewArgs;
|
||||
use self::reopen::ReopenArgs;
|
||||
use self::resolve::ResolveArgs;
|
||||
use self::view::ViewArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
/// Prefix used for git issue alt.
|
||||
pub const ISSUE_ALT_PREFIX: &str = "git issue: ";
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum IssueSubcommands {
|
||||
/// Create an issue
|
||||
New(NewArgs),
|
||||
/// View an issue
|
||||
View(ViewArgs),
|
||||
/// Reopen an issue.
|
||||
Reopen(ReopenArgs),
|
||||
/// Closes an issue.
|
||||
Close(CloseArgs),
|
||||
/// Resolve an issue.
|
||||
Resolve(ResolveArgs),
|
||||
/// List issues.
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
/// Possible states for a Git issue
|
||||
#[derive(Debug)]
|
||||
pub enum IssueStatus {
|
||||
/// The issue is currently open
|
||||
Open,
|
||||
/// The issue has been resolved
|
||||
Resolved,
|
||||
/// The issue has been closed
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl IssueStatus {
|
||||
/// Returns all issue statuses as kinds
|
||||
#[inline]
|
||||
pub const fn all_kinds() -> [Kind; 3] {
|
||||
[
|
||||
Self::Open.kind(),
|
||||
Self::Resolved.kind(),
|
||||
Self::Closed.kind(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Maps the issue status to its corresponding Nostr kind.
|
||||
#[inline]
|
||||
pub const fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Self::Open => Kind::GitStatusOpen,
|
||||
Self::Resolved => Kind::GitStatusApplied,
|
||||
Self::Closed => Kind::GitStatusClosed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string representation of the issue status.
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Open => "Open",
|
||||
Self::Resolved => "Resolved",
|
||||
Self::Closed => "Closed",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the issue is open.
|
||||
#[inline]
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, Self::Open)
|
||||
}
|
||||
|
||||
/// Check if the issue is resolved.
|
||||
#[inline]
|
||||
pub fn is_resolved(&self) -> bool {
|
||||
matches!(self, Self::Resolved)
|
||||
}
|
||||
|
||||
/// Check if the issue is closed.
|
||||
#[inline]
|
||||
pub fn is_closed(&self) -> bool {
|
||||
matches!(self, Self::Closed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&IssueStatus> for Kind {
|
||||
fn from(status: &IssueStatus) -> Self {
|
||||
status.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IssueStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Kind> for IssueStatus {
|
||||
type Error = N34Error;
|
||||
|
||||
fn try_from(kind: Kind) -> Result<Self, Self::Error> {
|
||||
match kind {
|
||||
Kind::GitStatusOpen => Ok(Self::Open),
|
||||
Kind::GitStatusApplied => Ok(Self::Resolved),
|
||||
Kind::GitStatusClosed => Ok(Self::Closed),
|
||||
_ => Err(N34Error::InvalidIssueStatus(kind)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunner for IssueSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & New View Reopen Close Resolve List)
|
||||
}
|
||||
}
|
||||
143
src/cli/commands/issue/new.rs
Normal file
143
src/cli/commands/issue/new.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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::{ArgGroup, Args};
|
||||
use nostr::event::{EventBuilder, Tag};
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
CommandRunner,
|
||||
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::NaddrOrSet,
|
||||
},
|
||||
error::N34Result,
|
||||
nostr_utils::{
|
||||
NostrClient,
|
||||
traits::{NaddrsUtils, NewGitRepositoryAnnouncement, ReposUtils},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/// Arguments for the `issue new` command
|
||||
#[derive(Args, Debug)]
|
||||
#[clap(
|
||||
group(
|
||||
ArgGroup::new("issue-subject")
|
||||
.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>>,
|
||||
/// Markdown content for the issue. Cannot be used together with the
|
||||
/// `--editor` flag.
|
||||
#[arg(short, long, group = "issue-content")]
|
||||
content: Option<String>,
|
||||
/// Opens the user's default editor to write issue content. The first line
|
||||
/// will be used as the issue subject.
|
||||
#[arg(short, long, group = "issue-subject", group = "issue-content")]
|
||||
editor: bool,
|
||||
/// The issue subject. Cannot be used together with the `--editor` flag.
|
||||
#[arg(long, group = "issue-subject")]
|
||||
subject: Option<String>,
|
||||
/// Labels for the issue. Can be specified as arguments (-l bug) or hashtags
|
||||
/// in content (#bug).
|
||||
#[arg(short, long)]
|
||||
label: Vec<String>,
|
||||
}
|
||||
|
||||
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 user_pubk = client.pubkey().await?;
|
||||
let coordinates = naddrs.clone().into_coordinates();
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
let repos = client.fetch_repos(coordinates.as_slice()).await?;
|
||||
let maintainers = repos.extract_maintainers();
|
||||
client.add_relays(&repos.extract_relays()).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, content) = utils::subject_and_body(self.subject, self.content, ".md")?;
|
||||
|
||||
let content_details = if let Some(content) = &content {
|
||||
Some(client.parse_content(content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let event = EventBuilder::new_git_issue(
|
||||
coordinates.as_slice(),
|
||||
content.unwrap_or_default(),
|
||||
Some(subject),
|
||||
self.label,
|
||||
)?
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tags(maintainers.iter().map(|p| Tag::public_key(*p)))
|
||||
.tags(
|
||||
content_details
|
||||
.clone()
|
||||
.map(|c| c.into_tags())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build(user_pubk);
|
||||
let event_id = event.id.expect("There is an id");
|
||||
|
||||
let write_relays = [
|
||||
relays,
|
||||
naddrs.extract_relays(),
|
||||
utils::add_write_relays(relays_list.as_ref()),
|
||||
repos.extract_relays(),
|
||||
// Include read relays for each maintainer (if found)
|
||||
client.read_relays_from_users(&maintainers).await,
|
||||
content_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!("Issue created: {nevent}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
71
src/cli/commands/issue/reopen.rs
Normal file
71
src/cli/commands/issue/reopen.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 super::IssueStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ReopenArgs {
|
||||
/// Repository addresses
|
||||
///
|
||||
/// In `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or
|
||||
/// `_@4rs.nl/n34`), or a set name like `kernel`, separated by commas.
|
||||
///
|
||||
/// If omitted, looks for a `nostr-address` file.
|
||||
#[arg(
|
||||
value_name = "NADDR-NIP05-OR-SET",
|
||||
long = "repo",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
/// The closed issue id to reopen it
|
||||
issue_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for ReopenArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::issue_status_command(
|
||||
options,
|
||||
self.issue_id,
|
||||
self.naddrs,
|
||||
IssueStatus::Open,
|
||||
|issue_status| {
|
||||
if issue_status.is_open() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't reopen an open issue".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if issue_status.is_resolved() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't open a resolved issue".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
65
src/cli/commands/issue/resolve.rs
Normal file
65
src/cli/commands/issue/resolve.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 super::IssueStatus;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::CommandRunner,
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ResolveArgs {
|
||||
/// 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 issue id to resolve it
|
||||
issue_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for ResolveArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::issue_status_command(
|
||||
options,
|
||||
self.issue_id,
|
||||
self.naddrs,
|
||||
IssueStatus::Resolved,
|
||||
|issue_status| {
|
||||
if issue_status.is_resolved() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't resolve an resolved issue".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
53
src/cli/commands/issue/view.rs
Normal file
53
src/cli/commands/issue/view.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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(Debug, Args)]
|
||||
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>>,
|
||||
/// The issue id to view it
|
||||
issue_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::<false>(options, self.naddrs, self.issue_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
210
src/cli/commands/mod.rs
Normal file
210
src/cli/commands/mod.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
// 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>.
|
||||
|
||||
/// `config` subcommands
|
||||
pub mod config;
|
||||
/// `issue` subcommands
|
||||
pub mod issue;
|
||||
/// `patch` subcommands
|
||||
pub mod patch;
|
||||
/// `pr` subcommands
|
||||
pub mod pr;
|
||||
/// `reply` command
|
||||
pub mod reply;
|
||||
/// `repo` subcommands
|
||||
pub mod repo;
|
||||
/// `sets` subcommands
|
||||
pub mod sets;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{Args, Parser};
|
||||
use nostr::key::{Keys, SecretKey};
|
||||
use nostr::nips::nip46::NostrConnectURI;
|
||||
use nostr::signer::{IntoNostrSigner, NostrSigner};
|
||||
use nostr_connect::client::NostrConnect;
|
||||
|
||||
use self::config::ConfigSubcommands;
|
||||
use self::issue::IssueSubcommands;
|
||||
use self::patch::PatchSubcommands;
|
||||
use self::pr::PrSubcommands;
|
||||
use self::reply::ReplyArgs;
|
||||
use self::repo::RepoSubcommands;
|
||||
use self::sets::SetsSubcommands;
|
||||
use super::CliConfig;
|
||||
use super::options_state::OptionsState;
|
||||
use super::types::RelayOrSet;
|
||||
use super::{parsers, traits::CommandRunner};
|
||||
use crate::cli::Cli;
|
||||
use crate::cli::types::EchoAuthUrl;
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
/// Default path used when no path is provided via command line arguments.
|
||||
///
|
||||
/// This is a workaround since Clap doesn't support lazy evaluation of defaults.
|
||||
pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT";
|
||||
|
||||
/// How long to wait for bunker response (3 minutes).
|
||||
const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3);
|
||||
|
||||
/// The command-line interface options
|
||||
#[derive(Args)]
|
||||
pub struct CliOptions {
|
||||
/// Your Nostr secret key
|
||||
#[arg(short, long, group = "signer")]
|
||||
pub secret_key: Option<SecretKey>,
|
||||
/// NIP-46 bunker url used for signing events
|
||||
#[arg(short, long, group = "signer", value_parser = parsers::parse_bunker_url)]
|
||||
pub bunker_url: Option<NostrConnectURI>,
|
||||
/// Enables signing events using the browser's NIP-07 extension. Listens on
|
||||
/// `127.0.0.1:51034`.
|
||||
#[arg(short = '7', long, group = "signer")]
|
||||
pub nip07: bool,
|
||||
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||
/// passed, separated by commas.
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub relays: Vec<RelayOrSet>,
|
||||
/// Proof of Work difficulty when creatring events
|
||||
#[arg(long, value_name = "DIFFICULTY")]
|
||||
pub pow: Option<u8>,
|
||||
/// Config path [default: `$XDG_CONFIG_HOME` or `$HOME/.config`]
|
||||
#[arg(long, value_name = "PATH", default_value = DEFAULT_FALLBACK_PATH,
|
||||
hide_default_value = true, value_parser = parsers::parse_config_path
|
||||
)]
|
||||
pub config: CliConfig,
|
||||
/// The state of options. Some values that are used by them but should not
|
||||
/// be entered via the CLI
|
||||
#[arg(skip)]
|
||||
pub state: OptionsState,
|
||||
}
|
||||
|
||||
/// N34 commands
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum Commands {
|
||||
/// Manage repositories and relays sets
|
||||
Sets {
|
||||
#[command(subcommand)]
|
||||
subcommands: SetsSubcommands,
|
||||
},
|
||||
/// Manage repositories
|
||||
Repo {
|
||||
#[command(subcommand)]
|
||||
subcommands: RepoSubcommands,
|
||||
},
|
||||
/// Manage issues
|
||||
Issue {
|
||||
#[command(subcommand)]
|
||||
subcommands: IssueSubcommands,
|
||||
},
|
||||
/// Manage patches
|
||||
Patch {
|
||||
#[command(subcommand)]
|
||||
subcommands: PatchSubcommands,
|
||||
},
|
||||
/// Manage pull requests
|
||||
Pr {
|
||||
#[command(subcommand)]
|
||||
subcommands: PrSubcommands,
|
||||
},
|
||||
/// Manage configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
subcommands: ConfigSubcommands,
|
||||
},
|
||||
/// Reply to issues and patches.
|
||||
Reply(ReplyArgs),
|
||||
}
|
||||
|
||||
|
||||
impl CliOptions {
|
||||
/// Returns the signer
|
||||
pub async fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
|
||||
if self.nip07 {
|
||||
self.state.browser_signer_proxy.start().await?;
|
||||
|
||||
println!(
|
||||
"Browser signer proxy started at: {}",
|
||||
self.state.browser_signer_proxy.url()
|
||||
);
|
||||
|
||||
// FIXME: Use `BrowserSignerProxy::is_session_active` after it release
|
||||
// nostr@0.44.0
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
|
||||
return Ok(Some(
|
||||
self.state.browser_signer_proxy.clone().into_nostr_signer(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(sk) = &self.secret_key {
|
||||
return Ok(Some(Keys::new(sk.clone()).into_nostr_signer()));
|
||||
}
|
||||
|
||||
if let Some(ref bunker_url) = self.bunker_url {
|
||||
let mut nostrconnect = NostrConnect::new(
|
||||
bunker_url.clone(),
|
||||
Cli::n34_keypair().await?,
|
||||
BUNKER_TIMEOUT,
|
||||
None,
|
||||
)
|
||||
.expect("It's a bunker url and not a client");
|
||||
|
||||
nostrconnect.auth_url_handler(EchoAuthUrl);
|
||||
return Ok(Some(nostrconnect.into_nostr_signer()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns an error if there are no relays.
|
||||
pub fn ensure_relays(&self) -> N34Result<()> {
|
||||
if self.relays.is_empty() {
|
||||
return Err(N34Error::EmptyRelays);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns an error if there are no signers
|
||||
pub fn ensure_signer(&self) -> N34Result<()> {
|
||||
if self.secret_key.is_none() && self.bunker_url.is_none() && !self.nip07 {
|
||||
return Err(N34Error::SignerRequired);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CliOptions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CliOptions")
|
||||
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
||||
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
||||
.field("nip07", &self.nip07)
|
||||
.field("relays", &self.relays)
|
||||
.field("pow", &self.pow)
|
||||
.field("config", &self.config)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunner for Commands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
tracing::trace!("Options: {options:#?}");
|
||||
tracing::trace!("Handling: {self:#?}");
|
||||
|
||||
crate::run_command!(self, options, Repo Issue Sets Patch Pr Config & Reply)
|
||||
}
|
||||
}
|
||||
85
src/cli/commands/patch/apply.rs
Normal file
85
src/cli/commands/patch/apply.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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, VecNostrEventExt},
|
||||
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 patch id to apply it. Must be orignal root patch or
|
||||
/// revision root
|
||||
patch_id: NostrEvent,
|
||||
/// Patches that have been applied. Use this when only some patches have
|
||||
/// been applied, not all.
|
||||
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||
applied_patches: Vec<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::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchPrStatus::MergedApplied,
|
||||
Some(either::Either::Right(self.applied_commits)),
|
||||
self.applied_patches.into_event_ids(),
|
||||
|patch_status| {
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply an already applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't apply a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus("Cannot apply a draft".to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
72
src/cli/commands/patch/close.rs
Normal file
72
src/cli/commands/patch/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/drafted patch id to close it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for CloseArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchPrStatus::Closed,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close an already closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't close a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
79
src/cli/commands/patch/draft.rs
Normal file
79
src/cli/commands/patch/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 patch id to draft it. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for DraftArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchPrStatus::Draft,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft an already draft patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't draft a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
160
src/cli/commands/patch/fetch.rs
Normal file
160
src/cli/commands/patch/fetch.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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::{fs, ops::Deref, path::PathBuf, str::FromStr};
|
||||
|
||||
use clap::Args;
|
||||
use nostr::{
|
||||
event::{Kind, TagKind},
|
||||
filter::Filter,
|
||||
nips::nip19::ToBech32,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::{
|
||||
NostrClient,
|
||||
traits::{NaddrsUtils, ReposUtils},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct FetchArgs {
|
||||
/// 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>>,
|
||||
/// Directory for saving patches. Defaults to the current directory. Use `-`
|
||||
/// for stdout output.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
output: Option<PathBuf>,
|
||||
/// The patch id to fetch it
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for FetchArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
let naddrs = utils::naddrs_or_file(
|
||||
self.naddrs.flat_naddrs(&options.config.sets)?,
|
||||
&utils::nostr_address_path()?,
|
||||
)?;
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
let output_path = self.output.unwrap_or_default();
|
||||
let (is_stdout, is_current_dir) = {
|
||||
let str_path = output_path.to_string_lossy();
|
||||
(str_path.deref() == "-", str_path.deref().is_empty())
|
||||
};
|
||||
|
||||
client
|
||||
.add_relays(
|
||||
&[
|
||||
naddrs.extract_relays(),
|
||||
self.patch_id.relays,
|
||||
client
|
||||
.fetch_repos(&naddrs.into_coordinates())
|
||||
.await?
|
||||
.extract_relays(),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let root_patch = client
|
||||
.fetch_event(
|
||||
Filter::new()
|
||||
.id(self.patch_id.event_id)
|
||||
.kind(Kind::GitPatch),
|
||||
)
|
||||
.await?
|
||||
.ok_or(N34Error::CanNotFoundPatch)?;
|
||||
|
||||
if !root_patch
|
||||
.tags
|
||||
.iter()
|
||||
.any(|t| t.kind() == TagKind::t() && t.content().is_some_and(|c| c == "root"))
|
||||
{
|
||||
return Err(N34Error::NotRootPatch);
|
||||
}
|
||||
|
||||
let root_author = root_patch.pubkey;
|
||||
let root_patch = super::GitPatch::from_str(&root_patch.content)
|
||||
.map_err(|err| N34Error::InvalidEvent(format!("Failed to parse the patch: {err}")))?;
|
||||
|
||||
tracing::info!("Found the root patch: `{}`", root_patch.subject);
|
||||
|
||||
let mut patches = client
|
||||
.fetch_patch_series(self.patch_id.event_id, root_author)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let patch = super::GitPatch::from_str(&p.content).map_err(|err| {
|
||||
N34Error::InvalidPatch(format!(
|
||||
"Failed to parse the patch `{}`: {err}",
|
||||
p.id.to_bech32().expect("Infallible")
|
||||
))
|
||||
})?;
|
||||
N34Result::Ok((patch.filename(&output_path)?, patch))
|
||||
})
|
||||
.collect::<N34Result<Vec<_>>>()?;
|
||||
patches.push((root_patch.filename(&output_path)?, root_patch));
|
||||
patches.sort_unstable_by_key(|p| p.0.clone());
|
||||
patches.dedup_by_key(|p| p.0.clone());
|
||||
|
||||
if !is_stdout && !is_current_dir && !output_path.exists() {
|
||||
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 {
|
||||
tracing::info!("Writeing `{}` in `{}`", patch.subject, patch_path.display());
|
||||
fs::write(patch_path, patch.inner)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
57
src/cli/commands/patch/list.rs
Normal file
57
src/cli/commands/patch/list.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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(Debug, Args)]
|
||||
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::Patch as u8 }>(
|
||||
options,
|
||||
self.naddrs,
|
||||
self.limit.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
87
src/cli/commands/patch/merge.rs
Normal file
87
src/cli/commands/patch/merge.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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, VecNostrEventExt},
|
||||
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 patch id to merge it. Must be orignal root patch or
|
||||
/// revision root
|
||||
patch_id: NostrEvent,
|
||||
/// Patches that have been merged. Use this when only some patches have been
|
||||
/// merged, not all.
|
||||
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||
merged_patches: Vec<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::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchPrStatus::MergedApplied,
|
||||
Some(either::Either::Left(self.merge_commit)),
|
||||
self.merged_patches.into_event_ids(),
|
||||
|patch_status| {
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge an already merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_closed() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge a closed patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_drafted() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't merge a draft patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
224
src/cli/commands/patch/mod.rs
Normal file
224
src/cli/commands/patch/mod.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
// 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>.
|
||||
|
||||
/// `patch apply` suubcommand
|
||||
mod apply;
|
||||
/// `patch close` subcommand
|
||||
mod close;
|
||||
/// `patch draft` subcommand
|
||||
mod draft;
|
||||
/// `patch fetch` subcommand
|
||||
mod fetch;
|
||||
/// `patch list` subcommand
|
||||
mod list;
|
||||
/// `patch merge` subcommand
|
||||
mod merge;
|
||||
/// `patch reopen` subcommand
|
||||
mod reopen;
|
||||
/// `patch send` subcommand
|
||||
mod send;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use clap::Subcommand;
|
||||
use regex::Regex;
|
||||
|
||||
use self::apply::ApplyArgs;
|
||||
use self::close::CloseArgs;
|
||||
use self::draft::DraftArgs;
|
||||
use self::fetch::FetchArgs;
|
||||
use self::list::ListArgs;
|
||||
use self::merge::MergeArgs;
|
||||
use self::reopen::ReopenArgs;
|
||||
use self::send::SendArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::{N34Error, N34Result};
|
||||
|
||||
/// Regular expression for checking the first line in the patch.
|
||||
pub static FROM_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^From [a-f0-9]{40} \w+ \w+ \d{1,2} \d{2}:\d{2}:\d{2} \d{4}$").unwrap()
|
||||
});
|
||||
|
||||
/// Regular expression for extracting the patch subject.
|
||||
static SUBJECT_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?m)^Subject: (.*(?:\n .*)*)").unwrap());
|
||||
|
||||
/// Regular expression for extracting the patch body.
|
||||
static BODY_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\n\n((?:.|\n)*?)(?:\n--[ -]|\z)").unwrap());
|
||||
|
||||
/// Regular expiration for extracting the patch version and number
|
||||
static PATCH_VERSION_NUMBER_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"\[PATCH\s+(?:v(?<version>\d+)\s*)?(?<number>\d+)/(?:\d+)").unwrap()
|
||||
});
|
||||
|
||||
/// Content of the hashtag representing the root patch.
|
||||
pub const ROOT_HASHTAG_CONTENT: &str = "root";
|
||||
/// Content of the hashtag representing the root revision patch.
|
||||
pub const REVISION_ROOT_HASHTAG_CONTENT: &str = "root-revision";
|
||||
/// The content of the hashtag used by `ngit-cli` to represent a root revision
|
||||
/// patch before the commit 6ae42e67d9da36f6c2e1356acba30a3a62112bc7. This was a
|
||||
/// typo.
|
||||
pub const LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT: &str = "revision-root";
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum PatchSubcommands {
|
||||
/// Send patches to a repository
|
||||
Send(SendArgs),
|
||||
/// Fetch a patch
|
||||
Fetch(FetchArgs),
|
||||
/// Close a patch
|
||||
Close(CloseArgs),
|
||||
/// Convert to draft
|
||||
Draft(DraftArgs),
|
||||
/// Reopen a patch
|
||||
Reopen(ReopenArgs),
|
||||
/// Mark as applied
|
||||
Apply(ApplyArgs),
|
||||
/// Mark as merged
|
||||
Merge(MergeArgs),
|
||||
/// List patches
|
||||
List(ListArgs),
|
||||
}
|
||||
|
||||
/// Represents a git patch
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitPatch {
|
||||
/// Full content of the patch file
|
||||
pub inner: String,
|
||||
/// Short description of the patch changes
|
||||
pub subject: String,
|
||||
/// Detailed explanation of the patch changes
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
impl GitPatch {
|
||||
/// Returns the patch file name from the subject
|
||||
pub fn filename(&self, parent: impl AsRef<Path>) -> N34Result<PathBuf> {
|
||||
let (patch_version, patch_number) = if self.subject.contains("[PATCH]") {
|
||||
(String::new(), "1")
|
||||
} else {
|
||||
patch_version_and_subject(&self.subject)?
|
||||
};
|
||||
|
||||
let patch_name = if patch_number == "0" {
|
||||
"cover-letter".to_owned()
|
||||
} else {
|
||||
patch_file_name(&self.subject)?
|
||||
};
|
||||
|
||||
Ok(parent
|
||||
.as_ref()
|
||||
.join(format!("{patch_version}{patch_number:0>4}-{patch_name}").replace("--", "-"))
|
||||
.with_extension("patch"))
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunner for PatchSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & Send Fetch Close Reopen Draft Apply Merge List)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GitPatch {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(patch_content: &str) -> Result<Self, Self::Err> {
|
||||
if !patch_content
|
||||
.split("\n")
|
||||
.next()
|
||||
.is_some_and(|line| FROM_RE.is_match(line))
|
||||
{
|
||||
return Err("The first line must start with 'From '.".to_owned());
|
||||
}
|
||||
|
||||
// Regex for subject (handles multi-line subjects)
|
||||
let subject = SUBJECT_RE
|
||||
.captures(patch_content)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.ok_or("No subject found")?
|
||||
.as_str()
|
||||
.trim()
|
||||
.replace('\n', "")
|
||||
.to_string();
|
||||
|
||||
// Regex for body
|
||||
let body = BODY_RE
|
||||
.captures(patch_content)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.ok_or("No body found")?
|
||||
.as_str()
|
||||
.trim()
|
||||
.to_string();
|
||||
Ok(Self {
|
||||
inner: patch_content.to_owned(),
|
||||
subject,
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the version prefix and patch number from a patch subject string.
|
||||
///
|
||||
/// The version prefix is formatted as "v{version}-" if present, or an empty
|
||||
/// string. The patch number is mandatory and will cause an error if not found.
|
||||
fn patch_version_and_subject(subject: &str) -> N34Result<(String, &str)> {
|
||||
let captures = PATCH_VERSION_NUMBER_RE.captures(subject).ok_or_else(|| {
|
||||
N34Error::InvalidEvent(format!("Can not parse the patch subject `{subject}`"))
|
||||
})?;
|
||||
Ok((
|
||||
captures
|
||||
.name("version")
|
||||
.map(|m| format!("v{}-", m.as_str()))
|
||||
.unwrap_or_default(),
|
||||
captures
|
||||
.name("number")
|
||||
.map(|m| m.as_str())
|
||||
.expect("It's not optional, regex will fail if it's not found"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Extracts a clean file name from the patch subject by removing version info
|
||||
/// and special characters. Converts to lowercase and ensures the name only
|
||||
/// contains alphanumeric, '.', '-', or '_' characters.
|
||||
fn patch_file_name(subject: &str) -> N34Result<String> {
|
||||
Ok(subject
|
||||
.split_once("]")
|
||||
.ok_or_else(|| {
|
||||
N34Error::InvalidEvent(format!(
|
||||
"Invalid patch subject. No `[PATCH ...]`: `{subject}`",
|
||||
))
|
||||
})?
|
||||
.1
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.replace(
|
||||
|c: char| !c.is_ascii_alphanumeric() && !['.', '-', '_'].contains(&c),
|
||||
"-",
|
||||
)
|
||||
.chars()
|
||||
.take(60)
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.trim()
|
||||
.replace("--", "-"))
|
||||
}
|
||||
73
src/cli/commands/patch/reopen.rs
Normal file
73
src/cli/commands/patch/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. Must be orignal root patch
|
||||
patch_id: NostrEvent,
|
||||
}
|
||||
|
||||
impl CommandRunner for ReopenArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::cli::common_commands::patch_pr_status_command::<{ EntityType::Patch as u8 }>(
|
||||
options,
|
||||
self.patch_id,
|
||||
self.naddrs,
|
||||
PatchPrStatus::Open,
|
||||
None,
|
||||
Vec::new(),
|
||||
|patch_status| {
|
||||
if patch_status.is_open() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't open an already open patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
if patch_status.is_merged_or_applied() {
|
||||
return Err(N34Error::InvalidStatus(
|
||||
"You can't open a merged/applied patch".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
285
src/cli/commands/patch/send.rs
Normal file
285
src/cli/commands/patch/send.rs
Normal file
@@ -0,0 +1,285 @@
|
||||
// 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::{fs, str::FromStr};
|
||||
|
||||
use clap::Args;
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{EventBuilder, EventId, Kind, Tag, TagKind, Tags, UnsignedEvent},
|
||||
hashes::sha1::Hash as Sha1Hash,
|
||||
key::PublicKey,
|
||||
nips::{nip01::Coordinate, nip10::Marker},
|
||||
types::RelayUrl,
|
||||
};
|
||||
|
||||
use super::GitPatch;
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
|
||||
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt, VecPatchesExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::N34Result,
|
||||
nostr_utils::{
|
||||
NostrClient,
|
||||
traits::{NaddrsUtils, ReposUtils},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
/// Prefix used for git patch alt.
|
||||
const PATCH_ALT_PREFIX: &str = "git patch: ";
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SendArgs {
|
||||
/// 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>>,
|
||||
/// Space-separated list of patch files to send. Use `-` to read from stdin.
|
||||
///
|
||||
/// For p-tagging users, include them in the cover letter using
|
||||
/// `nostr:npub1...`.
|
||||
#[arg(value_name = "PATCH-PATH", required = true, value_parser = parse_patch_path)]
|
||||
patches: Vec<Option<GitPatch>>,
|
||||
/// Original patch ID if this is a revision of it
|
||||
#[arg(long, value_name = "EVENT-ID")]
|
||||
original_patch: Option<NostrEvent>,
|
||||
}
|
||||
|
||||
impl CommandRunner for SendArgs {
|
||||
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 patches = self.patches.process_patches().await?;
|
||||
let repo_coordinates = naddrs.clone().into_coordinates();
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &relays).await;
|
||||
let user_pubk = client.pubkey().await?;
|
||||
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
if let Some(original_patch) = &self.original_patch {
|
||||
client.add_relays(&original_patch.relays).await;
|
||||
}
|
||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||
client
|
||||
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||
.await;
|
||||
let repos = client.fetch_repos(&repo_coordinates).await?;
|
||||
let euc = repos.extract_euc();
|
||||
let maintainers = repos.extract_maintainers();
|
||||
client.add_relays(&repos.extract_relays()).await;
|
||||
|
||||
let (events, events_write_relays) = make_patch_series(
|
||||
&client,
|
||||
patches,
|
||||
self.original_patch.as_ref().map(|e| e.event_id),
|
||||
repos.extract_relays().first().cloned(),
|
||||
repo_coordinates,
|
||||
euc,
|
||||
user_pubk,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let write_relays = [
|
||||
relays,
|
||||
repos.extract_relays(),
|
||||
events_write_relays,
|
||||
naddrs.extract_relays(),
|
||||
self.original_patch.map(|e| e.relays).unwrap_or_default(),
|
||||
utils::add_write_relays(relays_list.as_ref()),
|
||||
client.read_relays_from_users(&maintainers).await,
|
||||
]
|
||||
.concat();
|
||||
|
||||
tracing::trace!(write_relays = ?write_relays, "Write relays of the patches");
|
||||
let nevents = future::join_all(events.into_iter().map(|mut event| {
|
||||
async {
|
||||
let event_id = event.id();
|
||||
let subject = event
|
||||
.tags
|
||||
.find(TagKind::Alt)
|
||||
.and_then(Tag::content)
|
||||
.expect("There is an alt")
|
||||
.replace(PATCH_ALT_PREFIX, "");
|
||||
client
|
||||
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||
.await
|
||||
.map(|r| Ok((subject, utils::new_nevent(event_id, &r)?)))?
|
||||
}
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<N34Result<Vec<_>>>()?;
|
||||
|
||||
for (subject, nevent) in nevents {
|
||||
println!("Created '{subject}': {nevent}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// If the patch path is '-', it indicates that patches will be retrieved from
|
||||
/// stdin. Otherwise, it reads and parses the patch file from the specified
|
||||
/// path.
|
||||
fn parse_patch_path(patch_path: &str) -> Result<Option<GitPatch>, String> {
|
||||
if patch_path == "-" {
|
||||
tracing::info!("Reading patches from standard input");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::debug!("Parsing patch file `{patch_path}`");
|
||||
let patch_content = fs::read_to_string(patch_path)
|
||||
.map_err(|err| format!("Failed to read patch file `{patch_path}`: {err}"))?;
|
||||
|
||||
GitPatch::from_str(&patch_content).map(Option::Some)
|
||||
}
|
||||
|
||||
async fn make_patch_series(
|
||||
client: &NostrClient,
|
||||
patches: Vec<GitPatch>,
|
||||
original_patch: Option<EventId>,
|
||||
relay_hint: Option<RelayUrl>,
|
||||
repo_coordinates: Vec<Coordinate>,
|
||||
euc: Option<&Sha1Hash>,
|
||||
author_pkey: PublicKey,
|
||||
) -> N34Result<(Vec<UnsignedEvent>, Vec<RelayUrl>)> {
|
||||
let mut write_relays = Vec::new();
|
||||
let mut patch_series = Vec::new();
|
||||
let mut patches = patches.into_iter();
|
||||
let root_patch = patches.next().expect("Patches can't be empty");
|
||||
let (root_event, root_relays) = make_patch(
|
||||
client,
|
||||
root_patch,
|
||||
None,
|
||||
original_patch,
|
||||
relay_hint.as_ref(),
|
||||
&repo_coordinates,
|
||||
euc,
|
||||
author_pkey,
|
||||
)
|
||||
.await;
|
||||
write_relays.extend(root_relays);
|
||||
let root_id = *root_event.id.as_ref().expect("There is an id");
|
||||
let mut previous_patch = root_id;
|
||||
patch_series.push(root_event);
|
||||
|
||||
for patch in patches {
|
||||
let (patch_event, patch_relays) = make_patch(
|
||||
client,
|
||||
patch,
|
||||
Some(root_id),
|
||||
Some(previous_patch),
|
||||
relay_hint.as_ref(),
|
||||
&repo_coordinates,
|
||||
euc,
|
||||
author_pkey,
|
||||
)
|
||||
.await;
|
||||
previous_patch = patch_event.id.expect("there is an id");
|
||||
write_relays.extend(patch_relays);
|
||||
patch_series.push(patch_event);
|
||||
}
|
||||
|
||||
Ok((patch_series, write_relays))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn make_patch(
|
||||
client: &NostrClient,
|
||||
patch: GitPatch,
|
||||
root: Option<EventId>,
|
||||
reply_to: Option<EventId>,
|
||||
write_relay: Option<&RelayUrl>,
|
||||
repo_coordinates: &[Coordinate],
|
||||
euc: Option<&Sha1Hash>,
|
||||
author_pkey: PublicKey,
|
||||
) -> (UnsignedEvent, Vec<RelayUrl>) {
|
||||
let content_details = client.parse_content(&patch.body).await;
|
||||
let content_relays = content_details.write_relays.clone();
|
||||
// NIP-34 compliance requires referencing the previous patch using `NIP-10 e
|
||||
// reply`. However, this fails for the second patch when
|
||||
// `EventBuilder::dedup_tags` is enabled because:
|
||||
// 1. The tag is treated as a duplicate based on its content (the root ID).
|
||||
// 2. The second patch would reply to the root twice:
|
||||
// - First with the 'root' marker
|
||||
// - Then with the 'reply' marker
|
||||
// The `EventBuilder::dedup_tags` function then removes the 'reply' marker as a
|
||||
// duplicate.
|
||||
let mut safe_dedup_tags = Tags::new();
|
||||
safe_dedup_tags.push(Tag::alt(format!("{PATCH_ALT_PREFIX}{}", patch.subject)));
|
||||
safe_dedup_tags.push(Tag::description(patch.subject));
|
||||
safe_dedup_tags.extend(content_details.into_tags());
|
||||
safe_dedup_tags.extend(
|
||||
repo_coordinates
|
||||
.iter()
|
||||
.map(|c| Tag::coordinate(c.clone(), None)),
|
||||
);
|
||||
safe_dedup_tags.extend(
|
||||
repo_coordinates
|
||||
.iter()
|
||||
.map(|c| Tag::public_key(c.public_key)),
|
||||
);
|
||||
if let Some(euc) = euc {
|
||||
safe_dedup_tags.push(Tag::reference(euc.to_string()));
|
||||
}
|
||||
safe_dedup_tags.dedup();
|
||||
let mut event_builder = EventBuilder::new(Kind::GitPatch, patch.inner).tags(safe_dedup_tags);
|
||||
|
||||
// If the root is None, this indicates we're handling the root event
|
||||
if let Some(root_id) = root {
|
||||
event_builder =
|
||||
event_builder.tag(utils::event_reply_tag(&root_id, write_relay, Marker::Root));
|
||||
} else {
|
||||
event_builder = event_builder.tag(Tag::hashtag(ROOT_HASHTAG_CONTENT));
|
||||
}
|
||||
|
||||
// Handles the case where there is a patch to reply to but no root. This
|
||||
// indicates we are processing a revision, as the root revision should reply
|
||||
// directly to the original patch.
|
||||
if let Some(reply_to_id) = reply_to {
|
||||
if root.is_none() {
|
||||
event_builder = event_builder.tags([
|
||||
utils::event_reply_tag(&reply_to_id, write_relay, Marker::Reply),
|
||||
Tag::hashtag(REVISION_ROOT_HASHTAG_CONTENT),
|
||||
]);
|
||||
} else {
|
||||
event_builder = event_builder.tag(utils::event_reply_tag(
|
||||
&reply_to_id,
|
||||
write_relay,
|
||||
Marker::Reply,
|
||||
));
|
||||
}
|
||||
}
|
||||
(
|
||||
event_builder.build(author_pkey),
|
||||
content_relays.into_iter().collect(),
|
||||
)
|
||||
}
|
||||
351
src/cli/commands/patch/tests.rs
Normal file
351
src/cli/commands/patch/tests.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
// 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 super::*;
|
||||
|
||||
#[test]
|
||||
fn patch_normal() {
|
||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||
Subject: [PATCH] chore: a to abc
|
||||
|
||||
Abc patch
|
||||
---
|
||||
src/nostr_utils/mod.rs | 1 +
|
||||
1files changed, 3 insertions(+), 1 deletions(-)
|
||||
|
||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||
index 4120f5a..e68783c 100644
|
||||
--- a/src/nostr_utils/mod.rs
|
||||
+++ b/src/nostr_utils/mod.rs
|
||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||
|
||||
- a
|
||||
+ abc
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
||||
assert_eq!(patch.body, "Abc patch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_normal_with_patch_in_content() {
|
||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||
Subject: [PATCH] chore: Subject in subject
|
||||
|
||||
A good test patch
|
||||
---
|
||||
src/nostr_utils/mod.rs | 1 +
|
||||
1files changed, 3 insertions(+), 1 deletions(-)
|
||||
|
||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||
index 4120f5a..e68783c 100644
|
||||
--- a/src/nostr_utils/mod.rs
|
||||
+++ b/src/nostr_utils/mod.rs
|
||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||
Subject: [PATCH] chore: What a subject
|
||||
|
||||
hi
|
||||
---
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(patch.subject, "[PATCH] chore: Subject in subject");
|
||||
assert_eq!(patch.body, "A good test patch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_multiline_subject() {
|
||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||
Subject: [PATCH] chore: Some long subject yes so long one Some long subject yes
|
||||
so long one
|
||||
|
||||
Abc patch
|
||||
---
|
||||
src/nostr_utils/mod.rs | 1 +
|
||||
1files changed, 3 insertions(+), 1 deletions(-)
|
||||
|
||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||
index 4120f5a..e68783c 100644
|
||||
--- a/src/nostr_utils/mod.rs
|
||||
+++ b/src/nostr_utils/mod.rs
|
||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||
|
||||
- a
|
||||
+ abc
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(
|
||||
patch.subject,
|
||||
"[PATCH] chore: Some long subject yes so long one Some long subject yes so long one"
|
||||
);
|
||||
assert_eq!(patch.body, "Abc patch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_multiline_body() {
|
||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||
Subject: [PATCH] chore: a to abc
|
||||
|
||||
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||
aspernatur.
|
||||
---
|
||||
src/nostr_utils/mod.rs | 1 +
|
||||
1files changed, 3 insertions(+), 1 deletions(-)
|
||||
|
||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||
index 4120f5a..e68783c 100644
|
||||
--- a/src/nostr_utils/mod.rs
|
||||
+++ b/src/nostr_utils/mod.rs
|
||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||
|
||||
- a
|
||||
+ abc
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
||||
assert_eq!(
|
||||
patch.body,
|
||||
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||
aspernatur."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_cover_letter() {
|
||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||
Subject: [PATCH v2 0/7] feat: Some test just a test
|
||||
|
||||
Cover body
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(patch.subject, "[PATCH v2 0/7] feat: Some test just a test");
|
||||
assert_eq!(
|
||||
patch.body,
|
||||
"Cover body
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_multiline_cover_subject() {
|
||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
||||
test just a test
|
||||
|
||||
Cover body
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(
|
||||
patch.subject,
|
||||
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test"
|
||||
);
|
||||
assert_eq!(
|
||||
patch.body,
|
||||
"Cover body
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_multiline_cover_body() {
|
||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||
From: Awiteb <a@4rs.nl>
|
||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
||||
test just a test
|
||||
|
||||
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||
aspernatur.
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||
--
|
||||
2.49.0"#;
|
||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||
assert_eq!(
|
||||
patch.subject,
|
||||
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test"
|
||||
);
|
||||
assert_eq!(
|
||||
patch.body,
|
||||
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||
aspernatur.
|
||||
|
||||
Awiteb (1):
|
||||
chore: Update `README.md`
|
||||
|
||||
README.md | 2 +-
|
||||
|
||||
|
||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_patch_filename() {
|
||||
let mut patch = GitPatch {
|
||||
inner: String::new(),
|
||||
subject: String::new(),
|
||||
body: String::new(),
|
||||
};
|
||||
|
||||
patch.subject = "[PATCH v2 0/3] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("v2-0000-cover-letter.patch")
|
||||
);
|
||||
patch.subject = "[PATCH 0/3] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("0000-cover-letter.patch")
|
||||
);
|
||||
patch.subject = "[PATCH v2 1/3] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("v2-0001-feat-some-test-just-a-test.patch")
|
||||
);
|
||||
patch.subject = "[PATCH v42 1/3] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("v42-0001-feat-some-test-just-a-test.patch")
|
||||
);
|
||||
patch.subject = "[PATCH v42 23/30] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("v42-0023-feat-some-test-just-a-test.patch")
|
||||
);
|
||||
patch.subject = "[PATCH 1/3] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("0001-feat-some-test-just-a-test.patch")
|
||||
);
|
||||
patch.subject = "[PATCH 32/50] feat: Some test just a test".to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("0032-feat-some-test-just-a-test.patch")
|
||||
);
|
||||
patch.subject = "[PATCH v100 32/50] feat: some long subject some long subject some long \
|
||||
subject some long subject"
|
||||
.to_owned();
|
||||
assert_eq!(
|
||||
patch.filename("").unwrap(),
|
||||
PathBuf::from("v100-0032-feat-some-long-subject-some-long-subject-some-long-subject.patch")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_filename_without_patch() {
|
||||
let mut patch = GitPatch {
|
||||
inner: String::new(),
|
||||
subject: "[RFC v5 1/2] Something".to_owned(),
|
||||
body: String::new(),
|
||||
};
|
||||
|
||||
assert!(patch.filename("").is_err());
|
||||
patch.subject = "Something".to_owned();
|
||||
assert!(patch.filename("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_filename_without_number() {
|
||||
let mut patch = GitPatch {
|
||||
inner: String::new(),
|
||||
subject: "[PATCH v5 /2] Something".to_owned(),
|
||||
body: String::new(),
|
||||
};
|
||||
|
||||
assert!(patch.filename("").is_err());
|
||||
patch.subject = "[PATCH v5 2/] Something".to_owned();
|
||||
assert!(patch.filename("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_filename_without_version() {
|
||||
let patch = GitPatch {
|
||||
inner: String::new(),
|
||||
subject: "[PATCH 1/2] Something".to_owned(),
|
||||
body: String::new(),
|
||||
};
|
||||
|
||||
assert!(patch.filename("").is_ok());
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
236
src/cli/commands/reply.rs
Normal file
236
src/cli/commands/reply.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
// 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::fs;
|
||||
|
||||
use clap::{ArgGroup, Args};
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{Event, EventBuilder, Kind},
|
||||
filter::Filter,
|
||||
nips::nip01::Coordinate,
|
||||
types::RelayUrl,
|
||||
};
|
||||
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::{
|
||||
cli::{
|
||||
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::{NaddrOrSet, NostrEvent},
|
||||
},
|
||||
error::{N34Error, N34Result},
|
||||
nostr_utils::{
|
||||
NostrClient,
|
||||
traits::{NaddrsUtils, ReposUtils},
|
||||
utils,
|
||||
},
|
||||
};
|
||||
|
||||
/// The max date "9999-01-01 at 00:00 UTC"
|
||||
const MAX_DATE: i64 = 253370764800;
|
||||
|
||||
/// Arguments for the `reply` command
|
||||
#[derive(Args, Debug)]
|
||||
#[clap(
|
||||
group(
|
||||
ArgGroup::new("comment-content")
|
||||
.args(["comment", "editor"])
|
||||
.required(true)
|
||||
),
|
||||
group(
|
||||
ArgGroup::new("quote-reply-to")
|
||||
.args(["comment", "quote_to"])
|
||||
)
|
||||
)]
|
||||
pub struct ReplyArgs {
|
||||
/// The issue, patch, or comment to reply to
|
||||
#[arg(value_name = "nevent1-or-note1")]
|
||||
to: NostrEvent,
|
||||
/// Quote the replied-to event in the editor
|
||||
#[arg(long)]
|
||||
quote_to: bool,
|
||||
/// 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 comment (cannot be used with --editor)
|
||||
#[arg(short, long)]
|
||||
comment: Option<String>,
|
||||
/// Open editor to write comment (cannot be used with --content)
|
||||
#[arg(short, long)]
|
||||
editor: bool,
|
||||
}
|
||||
|
||||
impl CommandRunner for ReplyArgs {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
let nostr_address_path = 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?;
|
||||
let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&options.config.sets)? {
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
Some(naddrs)
|
||||
} else if fs::exists(&nostr_address_path).is_ok() {
|
||||
let naddrs = utils::naddrs_or_file(None, &nostr_address_path)?;
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
Some(naddrs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
client.add_relays(&self.to.relays).await;
|
||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||
let author_read_relays =
|
||||
utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());
|
||||
client.add_relays(&author_read_relays).await;
|
||||
|
||||
|
||||
let reply_to = client
|
||||
.fetch_event(Filter::new().id(self.to.event_id))
|
||||
.await?
|
||||
.ok_or(N34Error::EventNotFound)?;
|
||||
let root = client.find_root(reply_to.clone()).await?;
|
||||
|
||||
let repos_coordinate = if let Some(naddrs) = repo_naddrs {
|
||||
naddrs.into_coordinates()
|
||||
} else if let Some(ref root_event) = root {
|
||||
coordinates_from_root(root_event)?
|
||||
} else {
|
||||
return Err(N34Error::NotFoundRepo);
|
||||
};
|
||||
|
||||
let repos = client.fetch_repos(&repos_coordinate).await?;
|
||||
let maintainers = repos.extract_maintainers();
|
||||
|
||||
let quoted_content = if self.quote_to {
|
||||
Some(quote_reply_to_content(&client, &reply_to).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let content = utils::get_content(self.comment.as_ref(), quoted_content.as_ref(), ".txt")?;
|
||||
let content_details = client.parse_content(&content).await;
|
||||
|
||||
let event = EventBuilder::comment(
|
||||
content,
|
||||
&reply_to,
|
||||
root.as_ref(),
|
||||
repos.first().and_then(|r| r.relays.first()).cloned(),
|
||||
)
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default())
|
||||
.tags(content_details.clone().into_tags())
|
||||
.build(user_pubk);
|
||||
|
||||
let event_id = event.id.expect("There is an id");
|
||||
let write_relays = [
|
||||
relays,
|
||||
utils::add_write_relays(relays_list.as_ref()),
|
||||
// Merge repository announcement relays into write relays
|
||||
repos.extract_relays(),
|
||||
// Include read relays for each repository maintainer (if found)
|
||||
client.read_relays_from_users(&maintainers).await,
|
||||
// read relays of the root event and the reply to event
|
||||
{
|
||||
let (r1, r2) = future::join(
|
||||
client.read_relays_from_user(reply_to.pubkey),
|
||||
event_author_read_relays(&client, root.as_ref()),
|
||||
)
|
||||
.await;
|
||||
[r1, r2].concat()
|
||||
},
|
||||
content_details.write_relays.into_iter().collect(),
|
||||
]
|
||||
.concat();
|
||||
|
||||
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||
let (success, ..) = futures::join!(
|
||||
client.send_event_to(event, relays_list.as_ref(), &write_relays),
|
||||
client.broadcast(&reply_to, &author_read_relays),
|
||||
async {
|
||||
if let Some(root_event) = root {
|
||||
let _ = client.broadcast(&root_event, &author_read_relays).await;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
let nevent = utils::new_nevent(event_id, &success?)?;
|
||||
println!("Comment created: {nevent}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a quoted reply string in the format "On yyyy-mm-dd at hh:mm UTC,
|
||||
/// {author} wrote:" followed by the event content. Uses display name if
|
||||
/// available, otherwise falls back to a shortened npub string. Dates are
|
||||
/// formatted in UTC.
|
||||
async fn quote_reply_to_content(client: &NostrClient, quoted_event: &Event) -> String {
|
||||
let author_name = client.get_username(quoted_event.pubkey).await;
|
||||
|
||||
let fdate = chrono::DateTime::from_timestamp(
|
||||
quoted_event
|
||||
.created_at
|
||||
.as_u64()
|
||||
.try_into()
|
||||
.unwrap_or(MAX_DATE),
|
||||
0,
|
||||
)
|
||||
.map(|datetime| datetime.format("On %F at %R UTC, ").to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
"{fdate}{author_name} wrote:\n> {}",
|
||||
quoted_event.content.trim().replace("\n", "\n> ")
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the repository coordinate from a root Nostr event's tags.
|
||||
/// The event must contain a coordinate tag with GitRepoAnnouncement kind.
|
||||
fn coordinates_from_root(root: &Event) -> N34Result<Vec<Coordinate>> {
|
||||
let coordinates: Vec<Coordinate> = root
|
||||
.tags
|
||||
.coordinates()
|
||||
.filter(|c| c.kind == Kind::GitRepoAnnouncement)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if coordinates.is_empty() {
|
||||
return Err(N34Error::InvalidEvent(
|
||||
"The Git issue/patch does not specify a target repository".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(coordinates)
|
||||
}
|
||||
|
||||
/// Returns the event author read relays if found, otherwise an empty vector
|
||||
async fn event_author_read_relays(client: &NostrClient, event: Option<&Event>) -> Vec<RelayUrl> {
|
||||
if let Some(root_event) = event {
|
||||
client.read_relays_from_user(root_event.pubkey).await
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
169
src/cli/commands/repo/announce.rs
Normal file
169
src/cli/commands/repo/announce.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
// 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::{fs, io::Write};
|
||||
|
||||
use clap::Args;
|
||||
use futures::future;
|
||||
use nostr::{
|
||||
event::{EventBuilder, Tag},
|
||||
key::PublicKey,
|
||||
types::Url,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},
|
||||
error::N34Result,
|
||||
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
||||
};
|
||||
|
||||
/// Header written to new `nostr-address` files. Contains two trailing newline
|
||||
/// for formatting.
|
||||
const NOSTR_ADDRESS_FILE_HEADER: &str = r##"# This file contains NIP-19 `naddr` entities for repositories that accept this
|
||||
# project's issues and patches.
|
||||
#
|
||||
# The file acts as a **read-only reference** for retrieving repository relays
|
||||
# when embedded in an `naddr` and mentions those repositories when opening
|
||||
# patches or issues. Modifications here will not affect in the relays, as the
|
||||
# file is **explicitly untracked**. Its goal is to simplify contributions by
|
||||
# removing the need for manual address entry.
|
||||
#
|
||||
# Each entry must start with "naddr". Embedded relays are **strongly recommended**
|
||||
# to assist client-side discovery.
|
||||
#
|
||||
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||
|
||||
"##;
|
||||
const PERSONAL_FORK_HASHTAG: &str = "personal-fork";
|
||||
|
||||
/// Arguments for the `repo announce` command
|
||||
#[derive(Args, Debug)]
|
||||
pub struct AnnounceArgs {
|
||||
/// Unique identifier for the repository in kebab-case.
|
||||
#[arg(long = "id")]
|
||||
repo_id: String,
|
||||
/// A name for the repository.
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
/// A description for the repository.
|
||||
#[arg(short, long)]
|
||||
description: Option<String>,
|
||||
/// Webpage URLs for the repository (if provided by the git server).
|
||||
#[arg(short, long)]
|
||||
web: Vec<Url>,
|
||||
/// URLs for cloning the repository.
|
||||
#[arg(short, long)]
|
||||
clone: Vec<Url>,
|
||||
/// Additional maintainers of the repository (besides yourself).
|
||||
#[arg(short, long)]
|
||||
maintainers: Vec<PublicKey>,
|
||||
/// Labels to categorize the repository. Can be specified multiple times.
|
||||
#[arg(short, long)]
|
||||
label: Vec<String>,
|
||||
/// Skip kebab-case validation for the repository ID
|
||||
#[arg(long)]
|
||||
force_id: bool,
|
||||
/// Indicates whether the announcement is a personal fork.
|
||||
#[arg(long)]
|
||||
personal_fork: bool,
|
||||
/// If set, creates a `nostr-address` file to enable automatic address
|
||||
/// discovery by n34
|
||||
#[arg(long)]
|
||||
address_file: bool,
|
||||
}
|
||||
|
||||
impl CommandRunner for AnnounceArgs {
|
||||
const NEED_RELAYS: bool = true;
|
||||
|
||||
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||
let client = NostrClient::init(&options, &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;
|
||||
|
||||
if !self.maintainers.contains(&user_pubk) {
|
||||
self.maintainers.insert(0, user_pubk);
|
||||
}
|
||||
|
||||
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &relays)?;
|
||||
let mut event_builder = EventBuilder::new_git_repo(
|
||||
self.repo_id,
|
||||
self.name.map(utils::str_trim),
|
||||
self.description.map(utils::str_trim),
|
||||
self.web,
|
||||
self.clone,
|
||||
relays.clone(),
|
||||
self.maintainers.clone(),
|
||||
self.label.into_iter().map(utils::str_trim).collect(),
|
||||
self.force_id,
|
||||
)?
|
||||
.dedup_tags()
|
||||
.pow(options.pow.unwrap_or_default());
|
||||
|
||||
if self.personal_fork {
|
||||
event_builder = event_builder.tag(Tag::hashtag(PERSONAL_FORK_HASHTAG));
|
||||
}
|
||||
|
||||
let event = event_builder.build(user_pubk);
|
||||
|
||||
if self.address_file {
|
||||
let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);
|
||||
if !address_path.exists() {
|
||||
tracing::info!(
|
||||
"Creating new address file: '{NOSTR_ADDRESS_FILE}' at path '{}' with default \
|
||||
header",
|
||||
address_path.display()
|
||||
);
|
||||
fs::write(&address_path, NOSTR_ADDRESS_FILE_HEADER)?;
|
||||
}
|
||||
|
||||
let mut file = fs::OpenOptions::new().append(true).open(&address_path)?;
|
||||
|
||||
tracing::info!("Appending naddr '{naddr}' to address file: '{NOSTR_ADDRESS_FILE}'");
|
||||
file.write_all(format!("{naddr}\n").as_bytes())?;
|
||||
tracing::info!("Successfully wrote naddr to address file");
|
||||
}
|
||||
|
||||
let write_relays = [
|
||||
relays,
|
||||
utils::add_write_relays(relays_list.as_ref()),
|
||||
// Include read relays for each maintainer (if found)
|
||||
future::join_all(
|
||||
self.maintainers
|
||||
.iter()
|
||||
.map(|pkey| client.read_relays_from_user(*pkey)),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
]
|
||||
.concat();
|
||||
let nevent = utils::new_nevent(event.id.expect("There is an id"), &write_relays)?;
|
||||
|
||||
client
|
||||
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||
.await?;
|
||||
|
||||
println!("Event: {nevent}",);
|
||||
println!("Repo Address: {naddr}",);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
46
src/cli/commands/repo/mod.rs
Normal file
46
src/cli/commands/repo/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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>.
|
||||
|
||||
/// `repo announce` subcommand
|
||||
mod announce;
|
||||
/// `repo state` subcommand
|
||||
mod state;
|
||||
/// `repo view` subcommand
|
||||
mod view;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
use self::announce::AnnounceArgs;
|
||||
use self::state::StateArgs;
|
||||
use self::view::ViewArgs;
|
||||
use super::{CliOptions, CommandRunner};
|
||||
use crate::error::N34Result;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum RepoSubcommands {
|
||||
/// View details of a nostr git repository
|
||||
View(ViewArgs),
|
||||
/// Broadcast and update a git repository
|
||||
Announce(AnnounceArgs),
|
||||
/// Repository state announcements
|
||||
State(StateArgs),
|
||||
}
|
||||
|
||||
impl CommandRunner for RepoSubcommands {
|
||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||
crate::run_command!(self, options, & View Announce 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")
|
||||
})
|
||||
}
|
||||
99
src/cli/commands/repo/view.rs
Normal file
99
src/cli/commands/repo/view.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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::nips::nip19::ToBech32;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
CliOptions,
|
||||
CommandRunner,
|
||||
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||
types::NaddrOrSet,
|
||||
},
|
||||
error::N34Result,
|
||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||
};
|
||||
|
||||
/// Arguments for the `repo view` command
|
||||
#[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", value_delimiter = ',')]
|
||||
naddrs: Option<Vec<NaddrOrSet>>,
|
||||
}
|
||||
|
||||
impl CommandRunner for ViewArgs {
|
||||
const NEED_SIGNER: bool = false;
|
||||
|
||||
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;
|
||||
client.add_relays(&naddrs.extract_relays()).await;
|
||||
|
||||
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
||||
let mut repos_details: Vec<String> = Vec::new();
|
||||
|
||||
for repo in repos {
|
||||
let mut repo_details = format!("ID: {}", repo.id);
|
||||
|
||||
if let Some(name) = repo.name {
|
||||
repo_details.push_str(&format!("\nName: {name}"));
|
||||
}
|
||||
if let Some(desc) = repo.description {
|
||||
repo_details.push_str(&format!("\nDescription: {desc}"));
|
||||
}
|
||||
if !repo.web.is_empty() {
|
||||
repo_details.push_str(&format!("\nWebpages:\n{}", utils::format_iter(repo.web)));
|
||||
}
|
||||
if !repo.clone.is_empty() {
|
||||
repo_details.push_str(&format!(
|
||||
"\nClone urls:\n{}",
|
||||
utils::format_iter(repo.clone)
|
||||
));
|
||||
}
|
||||
if !repo.relays.is_empty() {
|
||||
repo_details.push_str(&format!("\nRelays:\n{}", utils::format_iter(repo.relays)));
|
||||
}
|
||||
if let Some(euc) = repo.euc {
|
||||
repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
|
||||
}
|
||||
if !repo.maintainers.is_empty() {
|
||||
repo_details.push_str(&format!(
|
||||
"\nMaintainers:\n{}",
|
||||
utils::format_iter(
|
||||
repo.maintainers
|
||||
.iter()
|
||||
.map(|p| p.to_bech32().expect("Infallible"))
|
||||
)
|
||||
));
|
||||
}
|
||||
repos_details.push(repo_details);
|
||||
}
|
||||
|
||||
println!("{}", repos_details.join("\n----------\n"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user