From ee17c21dbbef1577da1834b851e5cd56f77df5f3 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Wed, 24 Sep 2025 11:36:57 +0000
Subject: [PATCH] feat: accept patches from stdin in `patch send` command
Signed-off-by: Awiteb
---
CHANGELOG.md | 1 +
docs/patch/send.md | 9 +++++-
src/cli/commands/patch/fetch.rs | 2 +-
src/cli/commands/patch/mod.rs | 13 +++++++++
src/cli/commands/patch/send.rs | 24 +++++++++++-----
src/cli/traits.rs | 46 ++++++++++++++++++++++++++++++
src/cli/utils.rs | 50 ++++++++++++++++++++++++++++++++-
src/error.rs | 4 +++
8 files changed, 139 insertions(+), 10 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d48c821..76b4e6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
### Breaking Change
diff --git a/docs/patch/send.md b/docs/patch/send.md
index 0526f37..15f77c0 100644
--- a/docs/patch/send.md
+++ b/docs/patch/send.md
@@ -9,7 +9,7 @@ Send one or more patches to a repository
Usage: n34 patch send [OPTIONS] ...
Arguments:
- ... List of patch files to send (space separated)
+ ... Space-separated list of patch files to send. Use `-` to read from stdin.
Options:
--repo Repository addresses
@@ -20,3 +20,10 @@ Send your generated patches to the repositories specified using the `--repo`
option or retrieved from the `nostr-address` file. When submitting a revision
of an existing patch, include the original patch ID to ensure it’s correctly
referenced in your revision patch event.
+
+You can also pass patches from stdin, retrieved from a website or directly from
+`git-format-patch`. Simply use `-` as the patch path. For example:
+
+```bash
+git format-patch --stdout --base master master..HEAD | n34 patch send -
+```
diff --git a/src/cli/commands/patch/fetch.rs b/src/cli/commands/patch/fetch.rs
index 552a8ea..e141ea7 100644
--- a/src/cli/commands/patch/fetch.rs
+++ b/src/cli/commands/patch/fetch.rs
@@ -117,7 +117,7 @@ impl CommandRunner for FetchArgs {
.into_iter()
.map(|p| {
let patch = super::GitPatch::from_str(&p.content).map_err(|err| {
- N34Error::InvalidEvent(format!(
+ N34Error::InvalidPatch(format!(
"Failed to parse the patch `{}`: {err}",
p.id.to_bech32().expect("Infallible")
))
diff --git a/src/cli/commands/patch/mod.rs b/src/cli/commands/patch/mod.rs
index cff1b00..91e96b8 100644
--- a/src/cli/commands/patch/mod.rs
+++ b/src/cli/commands/patch/mod.rs
@@ -53,6 +53,11 @@ use self::send::SendArgs;
use super::{CliOptions, CommandRunner};
use crate::error::{N34Error, N34Result};
+/// Regular expression for checking the first line in the patch.
+pub static FROM_RE: LazyLock = 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 =
LazyLock::new(|| Regex::new(r"(?m)^Subject: (.*(?:\n .*)*)").unwrap());
@@ -138,6 +143,14 @@ impl FromStr for GitPatch {
type Err = String;
fn from_str(patch_content: &str) -> Result {
+ 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)
diff --git a/src/cli/commands/patch/send.rs b/src/cli/commands/patch/send.rs
index 88cc00a..c8df6bc 100644
--- a/src/cli/commands/patch/send.rs
+++ b/src/cli/commands/patch/send.rs
@@ -31,7 +31,7 @@ use crate::{
cli::{
CliOptions,
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
- traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
+ traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt, VecPatchesExt},
types::{NaddrOrSet, NostrEvent},
},
error::N34Result,
@@ -59,12 +59,12 @@ pub struct SendArgs {
value_delimiter = ','
)]
naddrs: Option>,
- /// List of patch files to send (space separated).
+ /// Space-separated list of patch files to send. Use `-` to read from stdin.
///
- /// For p-tagging users, include them in the cover letter with
+ /// For p-tagging users, include them in the cover letter using
/// `nostr:npub1...`.
#[arg(value_name = "PATCH-PATH", required = true, value_parser = parse_patch_path)]
- patches: Vec,
+ patches: Vec