Skip to content

Commit 2a8b765

Browse files
feat(notes): add --latest to notes get for ambiguous slugs (#730)
Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
1 parent 8390971 commit 2a8b765

2 files changed

Lines changed: 87 additions & 17 deletions

File tree

crates/sprout-cli/src/commands/notes.rs

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
//! - `notes set --name <s> --title T [--summary S] [--tag t]... --content -`
88
//! Idempotent upsert. Read-before-write preserves `published_at` and carries
99
//! forward the existing title when `--title` is omitted on an update.
10-
//! - `notes get (--naddr <n> | --name <slug> [--author <ref>]) [--content-only]`
10+
//! - `notes get (--naddr <n> | --name <slug> [--author <ref>] [--latest]) [--content-only]`
1111
//! `--naddr` / coordinate form is exact. `--name` does a cross-author `#d`
12-
//! query; >1 hit prints candidates and exits 1.
12+
//! query; >1 hit prints candidates and exits 1, unless `--latest` picks the
13+
//! most recently updated one. `--latest` conflicts with `--author`/`--naddr`.
1314
//! - `notes ls [--author <ref>] [--tag t] [--limit N]` — own notes by default.
1415
//! - `notes rm --name <s>` — NIP-09 deletion (kind:5) targeting the addressable
1516
//! coordinate via an `a` tag only (no `e` tag — see [`build_rm_event`]).
@@ -619,11 +620,40 @@ pub async fn cmd_set(
619620
Ok(())
620621
}
621622

623+
/// Validate the flag combination for `notes get`. Pure (booleans only) so the
624+
/// full matrix is unit-testable without a relay. `--naddr` and `--name` are
625+
/// exclusive-or; `--author` and `--latest` only refine `--name`, and they
626+
/// disambiguate the same multi-author case in opposite ways, so they conflict.
627+
fn validate_get_args(naddr: bool, name: bool, author: bool, latest: bool) -> Result<(), CliError> {
628+
if naddr == name {
629+
return Err(CliError::Usage(
630+
"exactly one of --naddr or --name is required".into(),
631+
));
632+
}
633+
if naddr && author {
634+
return Err(CliError::Usage(
635+
"--author only applies with --name; --naddr already identifies the author".into(),
636+
));
637+
}
638+
if naddr && latest {
639+
return Err(CliError::Usage(
640+
"--latest only applies with --name; --naddr already identifies one note".into(),
641+
));
642+
}
643+
if author && latest {
644+
return Err(CliError::Usage(
645+
"--latest and --author are mutually exclusive".into(),
646+
));
647+
}
648+
Ok(())
649+
}
650+
622651
pub async fn cmd_get(
623652
client: &SproutClient,
624653
naddr: Option<&str>,
625654
name: Option<&str>,
626655
author: Option<&str>,
656+
latest: bool,
627657
content_only: bool,
628658
) -> Result<(), CliError> {
629659
let snapshot = if let Some(raw) = naddr {
@@ -653,10 +683,14 @@ pub async fn cmd_get(
653683
1 => snapshots.remove(0),
654684
_ => {
655685
sort_snapshots_newest_first(&mut snapshots);
656-
return Err(CliError::Usage(format!(
657-
"note name {slug:?} is ambiguous; pass --author <pubkey>\n{}",
658-
format_note_candidates(&snapshots)
659-
)));
686+
if latest {
687+
snapshots.remove(0)
688+
} else {
689+
return Err(CliError::Usage(format!(
690+
"note name {slug:?} is ambiguous; pass --author <pubkey> or --latest\n{}",
691+
format_note_candidates(&snapshots)
692+
)));
693+
}
660694
}
661695
}
662696
}
@@ -799,24 +833,16 @@ pub async fn dispatch(cmd: crate::NotesCmd, client: &SproutClient) -> Result<(),
799833
naddr,
800834
name,
801835
author,
836+
latest,
802837
content_only,
803838
} => {
804-
if naddr.is_some() == name.is_some() {
805-
return Err(CliError::Usage(
806-
"exactly one of --naddr or --name is required".into(),
807-
));
808-
}
809-
if naddr.is_some() && author.is_some() {
810-
return Err(CliError::Usage(
811-
"--author only applies with --name; --naddr already identifies the author"
812-
.into(),
813-
));
814-
}
839+
validate_get_args(naddr.is_some(), name.is_some(), author.is_some(), latest)?;
815840
cmd_get(
816841
client,
817842
naddr.as_deref(),
818843
name.as_deref(),
819844
author.as_deref(),
845+
latest,
820846
content_only,
821847
)
822848
.await
@@ -1300,4 +1326,44 @@ mod tests {
13001326
let event = build_and_sign(Some(&prior), "x", None, None, None, "body", 2_000).unwrap();
13011327
assert_eq!(tag_value(&event, "published_at"), Some("2000"));
13021328
}
1329+
1330+
// -- validate_get_args --
1331+
1332+
#[test]
1333+
fn validate_get_args_accepts_minimal_forms() {
1334+
// (naddr, name, author, latest)
1335+
assert!(validate_get_args(true, false, false, false).is_ok()); // --naddr
1336+
assert!(validate_get_args(false, true, false, false).is_ok()); // --name
1337+
assert!(validate_get_args(false, true, true, false).is_ok()); // --name --author
1338+
assert!(validate_get_args(false, true, false, true).is_ok()); // --name --latest
1339+
}
1340+
1341+
#[test]
1342+
fn validate_get_args_requires_exactly_one_selector() {
1343+
let neither = validate_get_args(false, false, false, false);
1344+
let both = validate_get_args(true, true, false, false);
1345+
for err in [neither, both] {
1346+
assert!(matches!(err, Err(CliError::Usage(m)) if m.contains("exactly one")));
1347+
}
1348+
}
1349+
1350+
#[test]
1351+
fn validate_get_args_rejects_naddr_with_refiners() {
1352+
assert!(matches!(
1353+
validate_get_args(true, false, true, false),
1354+
Err(CliError::Usage(m)) if m.contains("--author only applies with --name")
1355+
));
1356+
assert!(matches!(
1357+
validate_get_args(true, false, false, true),
1358+
Err(CliError::Usage(m)) if m.contains("--latest only applies with --name")
1359+
));
1360+
}
1361+
1362+
#[test]
1363+
fn validate_get_args_rejects_author_and_latest_together() {
1364+
assert!(matches!(
1365+
validate_get_args(false, true, true, true),
1366+
Err(CliError::Usage(m)) if m.contains("mutually exclusive")
1367+
));
1368+
}
13031369
}

crates/sprout-cli/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,10 @@ pub enum NotesCmd {
840840
/// Disambiguate `--name` to a specific author (hex pubkey, display name, or `me`).
841841
#[arg(long)]
842842
author: Option<String>,
843+
/// On an ambiguous `--name` (multiple authors), pick the most recently updated note
844+
/// instead of erroring. Mutually exclusive with `--author` and `--naddr`.
845+
#[arg(long, default_value_t = false)]
846+
latest: bool,
843847
/// Print only the markdown body, not the full event JSON.
844848
#[arg(long, default_value_t = false)]
845849
content_only: bool,

0 commit comments

Comments
 (0)