diff options
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 59 |
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). | ||
| 229 | fn 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. | ||
| 247 | fn 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, |