upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin/ngit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-11 16:33:51 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-11 17:07:00 +0000
commit9cd4971c15fa990dd58ef0989b218a50c2d9c275 (patch)
tree0a49573b74f30f5b007d87714e5ce23534095ca6 /src/bin/ngit
parentaf43092f473089745d9faa24602d37afd9512707 (diff)
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
Diffstat (limited to 'src/bin/ngit')
-rw-r--r--src/bin/ngit/sub_commands/init.rs59
1 files changed, 56 insertions, 3 deletions
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(
224 vec![gitworkshop_url.to_string()] 224 vec![gitworkshop_url.to_string()]
225} 225}
226 226
227/// Normalize and validate a hashtag: lowercase, strip leading `#`, allow only
228/// `a-z`, `0-9`, and `-` (no leading/trailing/consecutive hyphens).
229fn validate_hashtag(s: &str) -> Result<String> {
230 let trimmed = s.trim().trim_start_matches('#').to_lowercase();
231 if trimmed.is_empty() {
232 bail!("hashtag cannot be empty");
233 }
234 if !trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
235 bail!("hashtag can only contain lowercase letters (a-z), digits (0-9), and hyphens (-)");
236 }
237 if trimmed.starts_with('-') || trimmed.ends_with('-') {
238 bail!("hashtag cannot start or end with a hyphen");
239 }
240 if trimmed.contains("--") {
241 bail!("hashtag cannot contain consecutive hyphens");
242 }
243 Ok(trimmed)
244}
245
246/// Resolve the `hashtags` field from args or existing announcement.
247fn resolve_hashtags(args_hashtag: &[String], state: &InitState) -> Result<Vec<String>> {
248 if !args_hashtag.is_empty() {
249 return args_hashtag
250 .iter()
251 .map(|h| validate_hashtag(h))
252 .collect();
253 }
254 if let Some(rr) = state.repo_ref() {
255 return Ok(
256 latest_event_repo_ref(rr).map_or_else(|| rr.hashtags.clone(), |lr| lr.hashtags),
257 );
258 }
259 Ok(vec![])
260}
261
227/// Derive clone-urls and relays from selected grasp servers. 262/// Derive clone-urls and relays from selected grasp servers.
228/// 263///
229/// For each grasp server, adds/replaces the corresponding clone URL in 264/// For each grasp server, adds/replaces the corresponding clone URL in
@@ -464,6 +499,9 @@ pub struct SubCommandArgs {
464 #[clap(long, value_parser, num_args = 1..)] 499 #[clap(long, value_parser, num_args = 1..)]
465 /// npubs of other maintainers 500 /// npubs of other maintainers
466 other_maintainers: Vec<String>, 501 other_maintainers: Vec<String>,
502 #[clap(long, value_parser, num_args = 1..)]
503 /// hashtags for repository discovery
504 hashtag: Vec<String>,
467 #[clap(long)] 505 #[clap(long)]
468 /// usually root commit but will be more recent commit for forks 506 /// usually root commit but will be more recent commit for forks
469 earliest_unique_commit: Option<String>, 507 earliest_unique_commit: Option<String>,
@@ -479,6 +517,7 @@ impl SubCommandArgs {
479 || !self.grasp_server.is_empty() 517 || !self.grasp_server.is_empty()
480 || !self.web.is_empty() 518 || !self.web.is_empty()
481 || !self.other_maintainers.is_empty() 519 || !self.other_maintainers.is_empty()
520 || !self.hashtag.is_empty()
482 || self.earliest_unique_commit.is_some() 521 || self.earliest_unique_commit.is_some()
483 } 522 }
484} 523}
@@ -1057,9 +1096,23 @@ fn resolve_fields(
1057 1096
1058 // --- Hashtags (shared metadata — from latest event, like name/description/web) 1097 // --- Hashtags (shared metadata — from latest event, like name/description/web)
1059 // --- 1098 // ---
1060 let hashtags = latest 1099 let hashtags_default = resolve_hashtags(&args.hashtag, state)?;
1061 .as_ref() 1100
1062 .map_or_else(Vec::new, |lr| lr.hashtags.clone()); 1101 let hashtags = if !args.hashtag.is_empty() || !interactive || simple_mode {
1102 hashtags_default
1103 } else {
1104 // advanced interactive
1105 let selections: Vec<bool> = vec![true; hashtags_default.len()];
1106 let selected = multi_select_with_custom_value(
1107 "hashtags for repository discovery",
1108 "hashtag",
1109 hashtags_default,
1110 selections,
1111 validate_hashtag,
1112 )?;
1113 show_multi_input_prompt_success("hashtags", &selected);
1114 selected
1115 };
1063 1116
1064 Ok(ResolvedFields { 1117 Ok(ResolvedFields {
1065 identifier, 1118 identifier,