From 9cd4971c15fa990dd58ef0989b218a50c2d9c275 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 11 Feb 2026 16:33:51 +0000 Subject: add --hashtag flag to ngit init multi-input flag that inherits from the latest pushed announcement event, defaulting to empty when no existing hashtags are found --- src/bin/ngit/sub_commands/init.rs | 59 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) (limited to 'src/bin/ngit/sub_commands/init.rs') diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 95d7aae..238ae29 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs @@ -224,6 +224,41 @@ fn resolve_web( vec![gitworkshop_url.to_string()] } +/// Normalize and validate a hashtag: lowercase, strip leading `#`, allow only +/// `a-z`, `0-9`, and `-` (no leading/trailing/consecutive hyphens). +fn validate_hashtag(s: &str) -> Result { + let trimmed = s.trim().trim_start_matches('#').to_lowercase(); + if trimmed.is_empty() { + bail!("hashtag cannot be empty"); + } + if !trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') { + bail!("hashtag can only contain lowercase letters (a-z), digits (0-9), and hyphens (-)"); + } + if trimmed.starts_with('-') || trimmed.ends_with('-') { + bail!("hashtag cannot start or end with a hyphen"); + } + if trimmed.contains("--") { + bail!("hashtag cannot contain consecutive hyphens"); + } + Ok(trimmed) +} + +/// Resolve the `hashtags` field from args or existing announcement. +fn resolve_hashtags(args_hashtag: &[String], state: &InitState) -> Result> { + if !args_hashtag.is_empty() { + return args_hashtag + .iter() + .map(|h| validate_hashtag(h)) + .collect(); + } + if let Some(rr) = state.repo_ref() { + return Ok( + latest_event_repo_ref(rr).map_or_else(|| rr.hashtags.clone(), |lr| lr.hashtags), + ); + } + Ok(vec![]) +} + /// Derive clone-urls and relays from selected grasp servers. /// /// For each grasp server, adds/replaces the corresponding clone URL in @@ -464,6 +499,9 @@ pub struct SubCommandArgs { #[clap(long, value_parser, num_args = 1..)] /// npubs of other maintainers other_maintainers: Vec, + #[clap(long, value_parser, num_args = 1..)] + /// hashtags for repository discovery + hashtag: Vec, #[clap(long)] /// usually root commit but will be more recent commit for forks earliest_unique_commit: Option, @@ -479,6 +517,7 @@ impl SubCommandArgs { || !self.grasp_server.is_empty() || !self.web.is_empty() || !self.other_maintainers.is_empty() + || !self.hashtag.is_empty() || self.earliest_unique_commit.is_some() } } @@ -1057,9 +1096,23 @@ fn resolve_fields( // --- Hashtags (shared metadata — from latest event, like name/description/web) // --- - let hashtags = latest - .as_ref() - .map_or_else(Vec::new, |lr| lr.hashtags.clone()); + let hashtags_default = resolve_hashtags(&args.hashtag, state)?; + + let hashtags = if !args.hashtag.is_empty() || !interactive || simple_mode { + hashtags_default + } else { + // advanced interactive + let selections: Vec = vec![true; hashtags_default.len()]; + let selected = multi_select_with_custom_value( + "hashtags for repository discovery", + "hashtag", + hashtags_default, + selections, + validate_hashtag, + )?; + show_multi_input_prompt_success("hashtags", &selected); + selected + }; Ok(ResolvedFields { identifier, -- cgit v1.2.3