diff options
Diffstat (limited to 'src/bin/ngit/sub_commands/send.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/send.rs | 499 |
1 files changed, 417 insertions, 82 deletions
diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 8b49e37..3ae941f 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs | |||
| @@ -1,12 +1,32 @@ | |||
| 1 | use std::path::Path; | 1 | use std::{path::Path, str::FromStr, thread, time::Duration}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use console::Style; | 4 | use console::Style; |
| 5 | use ngit::{ | 5 | use ngit::{ |
| 6 | cli_interactor::{ | ||
| 7 | PromptChoiceParms, multi_select_with_custom_value, show_multi_input_prompt_success, | ||
| 8 | }, | ||
| 6 | client::{Params, send_events}, | 9 | client::{Params, send_events}, |
| 7 | git_events::{EventRefType, generate_cover_letter_and_patch_events}, | 10 | git::nostr_url::CloneUrl, |
| 11 | git_events::{ | ||
| 12 | EventRefType, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, | ||
| 13 | generate_cover_letter_and_patch_events, | ||
| 14 | }, | ||
| 15 | push::push_refs_and_generate_pr_or_pr_update_event, | ||
| 16 | repo_ref::{ | ||
| 17 | format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url, | ||
| 18 | is_grasp_server_in_list, normalize_grasp_server_url, | ||
| 19 | }, | ||
| 20 | utils::proposal_tip_is_pr_or_pr_update, | ||
| 21 | }; | ||
| 22 | use nostr::{ | ||
| 23 | ToBech32, | ||
| 24 | event::{Event, Kind}, | ||
| 25 | nips::{ | ||
| 26 | nip01::Coordinate, | ||
| 27 | nip19::{Nip19Coordinate, Nip19Event}, | ||
| 28 | }, | ||
| 8 | }; | 29 | }; |
| 9 | use nostr::{ToBech32, nips::nip19::Nip19Event}; | ||
| 10 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | 30 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; |
| 11 | 31 | ||
| 12 | use crate::{ | 32 | use crate::{ |
| @@ -60,12 +80,14 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 60 | fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; | 80 | fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; |
| 61 | } | 81 | } |
| 62 | 82 | ||
| 63 | let (root_proposal_id, mention_tags) = | 83 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; |
| 64 | get_root_proposal_id_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to) | 84 | |
| 85 | let (root_proposal, mention_tags) = | ||
| 86 | get_root_proposal_and_mentions_from_in_reply_to(git_repo.get_path()?, &args.in_reply_to) | ||
| 65 | .await?; | 87 | .await?; |
| 66 | 88 | ||
| 67 | if let Some(root_ref) = args.in_reply_to.first() { | 89 | if let Some(root_ref) = args.in_reply_to.first() { |
| 68 | if root_proposal_id.is_some() { | 90 | if root_proposal.is_some() { |
| 69 | println!("creating proposal revision for: {root_ref}"); | 91 | println!("creating proposal revision for: {root_ref}"); |
| 70 | } | 92 | } |
| 71 | } | 93 | } |
| @@ -104,41 +126,38 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 104 | let (first_commit_ahead, behind) = | 126 | let (first_commit_ahead, behind) = |
| 105 | git_repo.get_commits_ahead_behind(&main_tip, commits.last().context("no commits")?)?; | 127 | git_repo.get_commits_ahead_behind(&main_tip, commits.last().context("no commits")?)?; |
| 106 | 128 | ||
| 107 | // check proposal ahead of origin/main | 129 | check_commits_are_suitable_for_proposal( |
| 108 | if first_commit_ahead.len().gt(&1) && !Interactor::default().confirm( | 130 | &first_commit_ahead, |
| 109 | PromptConfirmParms::default() | 131 | &commits, |
| 110 | .with_prompt( | 132 | &behind, |
| 111 | format!("proposal builds on a commit {} ahead of '{main_branch_name}' - do you want to continue?", first_commit_ahead.len() - 1) | 133 | main_branch_name, |
| 112 | ) | 134 | &main_tip, |
| 113 | .with_default(false) | 135 | )?; |
| 114 | ).context("failed to get confirmation response from interactor confirm")? { | 136 | |
| 115 | bail!("aborting because selected commits were ahead of origin/master"); | 137 | let as_pr = { |
| 116 | } | 138 | if let Some(root_proposal) = &root_proposal { |
| 117 | 139 | proposal_tip_is_pr_or_pr_update(git_repo_path, &repo_ref, &root_proposal.id).await? | |
| 118 | // check if a selected commit is already in origin | 140 | } else { |
| 119 | if commits.iter().any(|c| c.eq(&main_tip)) { | 141 | false |
| 120 | if !Interactor::default().confirm( | ||
| 121 | PromptConfirmParms::default() | ||
| 122 | .with_prompt( | ||
| 123 | format!("proposal contains commit(s) already in '{main_branch_name}'. proceed anyway?") | ||
| 124 | ) | ||
| 125 | .with_default(false) | ||
| 126 | ).context("failed to get confirmation response from interactor confirm")? { | ||
| 127 | bail!("aborting as proposal contains commit(s) already in '{main_branch_name}'"); | ||
| 128 | } | 142 | } |
| 129 | } | 143 | } || git_repo.are_commits_too_big_for_patches(&commits); |
| 130 | // check proposal isn't behind origin/main | ||
| 131 | else if !behind.is_empty() && !Interactor::default().confirm( | ||
| 132 | PromptConfirmParms::default() | ||
| 133 | .with_prompt( | ||
| 134 | format!("proposal is {} behind '{main_branch_name}'. consider rebasing before submission. proceed anyway?", behind.len()) | ||
| 135 | ) | ||
| 136 | .with_default(false) | ||
| 137 | ).context("failed to get confirmation response from interactor confirm")? { | ||
| 138 | bail!("aborting so commits can be rebased"); | ||
| 139 | } | ||
| 140 | 144 | ||
| 141 | let title = if args.no_cover_letter { | 145 | let title = if as_pr { |
| 146 | match &args.title { | ||
| 147 | Some(t) => Some(t.clone()), | ||
| 148 | None => { | ||
| 149 | if root_proposal.is_none() { | ||
| 150 | Some( | ||
| 151 | Interactor::default() | ||
| 152 | .input(PromptInputParms::default().with_prompt("title"))? | ||
| 153 | .clone(), | ||
| 154 | ) | ||
| 155 | } else { | ||
| 156 | None | ||
| 157 | } | ||
| 158 | } | ||
| 159 | } | ||
| 160 | } else if args.no_cover_letter { | ||
| 142 | None | 161 | None |
| 143 | } else { | 162 | } else { |
| 144 | match &args.title { | 163 | match &args.title { |
| @@ -168,7 +187,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 168 | t.clone() | 187 | t.clone() |
| 169 | } else { | 188 | } else { |
| 170 | Interactor::default() | 189 | Interactor::default() |
| 171 | .input(PromptInputParms::default().with_prompt("cover letter description"))? | 190 | .input(PromptInputParms::default().with_prompt("description"))? |
| 172 | .clone() | 191 | .clone() |
| 173 | }, | 192 | }, |
| 174 | )) | 193 | )) |
| @@ -176,7 +195,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 176 | None | 195 | None |
| 177 | }; | 196 | }; |
| 178 | 197 | ||
| 179 | let (signer, user_ref, _) = login::login_or_signup( | 198 | let (signer, mut user_ref, _) = login::login_or_signup( |
| 180 | &Some(&git_repo), | 199 | &Some(&git_repo), |
| 181 | &extract_signer_cli_arguments(cli_args).unwrap_or(None), | 200 | &extract_signer_cli_arguments(cli_args).unwrap_or(None), |
| 182 | &cli_args.password, | 201 | &cli_args.password, |
| @@ -187,42 +206,316 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 187 | 206 | ||
| 188 | client.set_signer(signer.clone()).await; | 207 | client.set_signer(signer.clone()).await; |
| 189 | 208 | ||
| 190 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?; | ||
| 191 | |||
| 192 | // oldest first | 209 | // oldest first |
| 193 | commits.reverse(); | 210 | commits.reverse(); |
| 194 | 211 | ||
| 195 | let events = generate_cover_letter_and_patch_events( | 212 | let events = if as_pr { |
| 196 | cover_letter_title_description.clone(), | 213 | let mut to_try = vec![]; |
| 197 | &git_repo, | 214 | let mut tried = vec![]; |
| 198 | &commits, | 215 | let repo_grasps = repo_ref.grasp_servers(); |
| 199 | &signer, | 216 | // if the user already has a fork, or is a maintainer, use those git servers |
| 200 | &repo_ref, | 217 | let mut user_repo_ref = get_repo_ref_from_cache( |
| 201 | &root_proposal_id, | 218 | Some(git_repo_path), |
| 202 | &mention_tags, | 219 | &Nip19Coordinate { |
| 203 | ) | 220 | coordinate: Coordinate { |
| 204 | .await?; | 221 | kind: nostr::event::Kind::GitRepoAnnouncement, |
| 205 | 222 | public_key: user_ref.public_key, | |
| 206 | println!( | 223 | identifier: repo_ref.identifier.clone(), |
| 207 | "posting {} patch{} {} a covering letter...", | 224 | }, |
| 208 | if cover_letter_title_description.is_none() { | 225 | relays: vec![], |
| 209 | events.len() | 226 | }, |
| 210 | } else { | 227 | ) |
| 211 | events.len() - 1 | 228 | .await |
| 212 | }, | 229 | .ok(); |
| 213 | if cover_letter_title_description.is_none() && events.len().eq(&1) | 230 | if let Some(user_repo_ref) = &user_repo_ref { |
| 214 | || cover_letter_title_description.is_some() && events.len().eq(&2) | 231 | for url in &user_repo_ref.git_server { |
| 215 | { | 232 | if CloneUrl::from_str(url).is_ok() { |
| 216 | "" | 233 | to_try.push(url.clone()); |
| 217 | } else { | 234 | } |
| 218 | "es" | 235 | } |
| 219 | }, | 236 | } |
| 220 | if cover_letter_title_description.is_none() { | 237 | if !to_try.is_empty() || !repo_grasps.is_empty() { |
| 221 | "without" | 238 | println!( |
| 239 | "pushing proposal refs to {}", | ||
| 240 | if repo_ref.maintainers.contains(&user_ref.public_key) { | ||
| 241 | "repository git servers" | ||
| 242 | } else if to_try.is_empty() { | ||
| 243 | "repository grasp servers" | ||
| 244 | } else if repo_grasps.is_empty() { | ||
| 245 | "the git servers listed in your fork" | ||
| 246 | } else { | ||
| 247 | "the git servers listed in your fork and repository grasp servers" | ||
| 248 | } | ||
| 249 | ); | ||
| 222 | } else { | 250 | } else { |
| 223 | "with" | 251 | println!( |
| 252 | "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request." | ||
| 253 | ); | ||
| 254 | } | ||
| 255 | // also use repo grasp servers | ||
| 256 | for url in &repo_ref.git_server { | ||
| 257 | if is_grasp_server_in_list(url, &repo_grasps) && !to_try.contains(url) { | ||
| 258 | to_try.push(url.clone()); | ||
| 259 | } | ||
| 224 | } | 260 | } |
| 225 | ); | 261 | |
| 262 | let mut git_ref = None; | ||
| 263 | let events = loop { | ||
| 264 | let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( | ||
| 265 | &git_repo, | ||
| 266 | &repo_ref, | ||
| 267 | commits.last().context("no commits")?, | ||
| 268 | &user_ref, | ||
| 269 | root_proposal.as_ref(), | ||
| 270 | &cover_letter_title_description, | ||
| 271 | &to_try, | ||
| 272 | git_ref.clone(), | ||
| 273 | &signer, | ||
| 274 | &console::Term::stdout(), | ||
| 275 | ) | ||
| 276 | .await?; | ||
| 277 | for url in to_try { | ||
| 278 | tried.push(url); | ||
| 279 | } | ||
| 280 | to_try = vec![]; | ||
| 281 | if let Some(events) = events { | ||
| 282 | break events; | ||
| 283 | } | ||
| 284 | // fallback to creating user personal-fork on their grasp servers | ||
| 285 | let untried_user_grasp_servers: Vec<String> = user_ref | ||
| 286 | .grasp_list | ||
| 287 | .urls | ||
| 288 | .iter() | ||
| 289 | .map(std::string::ToString::to_string) | ||
| 290 | .filter(|g| { | ||
| 291 | // is a grasp server not in list of tried | ||
| 292 | !is_grasp_server_in_list(g, &tried) | ||
| 293 | }) | ||
| 294 | .collect(); | ||
| 295 | |||
| 296 | if untried_user_grasp_servers.is_empty() | ||
| 297 | && Interactor::default().choice( | ||
| 298 | PromptChoiceParms::default() | ||
| 299 | .with_prompt("choose alternative git server") | ||
| 300 | .dont_report() | ||
| 301 | .with_choices(vec![ | ||
| 302 | "choose grasp server(s)".to_string(), | ||
| 303 | "enter a git repo url with write permission".to_string(), | ||
| 304 | ]) | ||
| 305 | .with_default(0), | ||
| 306 | )? == 1 | ||
| 307 | { | ||
| 308 | loop { | ||
| 309 | let clone_url = Interactor::default() | ||
| 310 | .input( | ||
| 311 | PromptInputParms::default() | ||
| 312 | .with_prompt("git repo url with write permission"), | ||
| 313 | )? | ||
| 314 | .clone(); | ||
| 315 | if CloneUrl::from_str(&clone_url).is_ok() { | ||
| 316 | to_try.push(clone_url); | ||
| 317 | let mut git_ref_or_branch_name = Interactor::default() | ||
| 318 | .input( | ||
| 319 | PromptInputParms::default() | ||
| 320 | .with_prompt("ref / branch name") | ||
| 321 | .with_default( | ||
| 322 | git_ref.unwrap_or("refs/nostr/<event-id>".to_string()), | ||
| 323 | ), | ||
| 324 | )? | ||
| 325 | .clone(); | ||
| 326 | if !git_ref_or_branch_name.starts_with("refs/") { | ||
| 327 | git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}"); | ||
| 328 | } | ||
| 329 | git_ref = Some(git_ref_or_branch_name); | ||
| 330 | break; | ||
| 331 | } | ||
| 332 | println!("invalid clone url"); | ||
| 333 | } | ||
| 334 | continue; | ||
| 335 | } | ||
| 336 | |||
| 337 | let mut new_grasp_server_events: Vec<Event> = vec![]; | ||
| 338 | |||
| 339 | let grasp_servers = if untried_user_grasp_servers.is_empty() { | ||
| 340 | let default_choices: Vec<String> = client | ||
| 341 | .get_grasp_default_set() | ||
| 342 | .iter() | ||
| 343 | .filter(|g| !is_grasp_server_in_list(g, &tried)) | ||
| 344 | .cloned() | ||
| 345 | .collect(); | ||
| 346 | let selections = vec![true; default_choices.len()]; // all selected by default | ||
| 347 | let grasp_servers = multi_select_with_custom_value( | ||
| 348 | "alternative grasp server(s)", | ||
| 349 | "grasp server", | ||
| 350 | default_choices, | ||
| 351 | selections, | ||
| 352 | normalize_grasp_server_url, | ||
| 353 | )?; | ||
| 354 | show_multi_input_prompt_success("alternative grasp server(s)", &grasp_servers); | ||
| 355 | if grasp_servers.is_empty() { | ||
| 356 | // ask again | ||
| 357 | continue; | ||
| 358 | } | ||
| 359 | let normalised_grasp_servers: Vec<String> = grasp_servers | ||
| 360 | .iter() | ||
| 361 | .filter_map(|g| normalize_grasp_server_url(g).ok()) | ||
| 362 | .collect(); | ||
| 363 | // if any grasp servers not listed in user grasp list prompt to update | ||
| 364 | let grasp_servers_not_in_user_prefs: Vec<String> = normalised_grasp_servers | ||
| 365 | .iter() | ||
| 366 | .filter(|g| { | ||
| 367 | !user_ref.grasp_list.urls.contains( | ||
| 368 | // unwrap is safe as we constructed g | ||
| 369 | &nostr::Url::parse(&format_grasp_server_url_as_relay_url(g).unwrap()) | ||
| 370 | .unwrap(), | ||
| 371 | ) | ||
| 372 | }) | ||
| 373 | .cloned() | ||
| 374 | .collect(); | ||
| 375 | if !grasp_servers_not_in_user_prefs.is_empty() | ||
| 376 | && Interactor::default().confirm( | ||
| 377 | PromptConfirmParms::default() | ||
| 378 | .with_prompt( | ||
| 379 | "add these to your list of prefered grasp servers?".to_string(), | ||
| 380 | ) | ||
| 381 | .with_default(true), | ||
| 382 | )? | ||
| 383 | { | ||
| 384 | for g in &normalised_grasp_servers { | ||
| 385 | let as_url = nostr::Url::parse(&format_grasp_server_url_as_relay_url(g)?)?; | ||
| 386 | if !user_ref.grasp_list.urls.contains(&as_url) { | ||
| 387 | user_ref.grasp_list.urls.push(as_url); | ||
| 388 | } | ||
| 389 | } | ||
| 390 | new_grasp_server_events.push(user_ref.grasp_list.to_event(&signer).await?); | ||
| 391 | } | ||
| 392 | normalised_grasp_servers | ||
| 393 | } else { | ||
| 394 | untried_user_grasp_servers | ||
| 395 | }; | ||
| 396 | println!( | ||
| 397 | "{} personal-fork so we can push commits to your prefered grasp servers", | ||
| 398 | if user_repo_ref.is_some() { | ||
| 399 | "Updating" | ||
| 400 | } else { | ||
| 401 | "Creating a" | ||
| 402 | }, | ||
| 403 | ); | ||
| 404 | |||
| 405 | let grasp_servers_as_personal_clone_url: Vec<String> = grasp_servers | ||
| 406 | .iter() | ||
| 407 | .filter_map(|g| { | ||
| 408 | format_grasp_server_url_as_clone_url( | ||
| 409 | g, | ||
| 410 | &user_ref.public_key, | ||
| 411 | &repo_ref.identifier, | ||
| 412 | ) | ||
| 413 | .ok() | ||
| 414 | }) | ||
| 415 | .collect(); | ||
| 416 | |||
| 417 | // create personal-fork / update existing user repo and add these grasp servers | ||
| 418 | let updated_user_repo_ref = { | ||
| 419 | if let Some(mut user_repo_ref) = user_repo_ref { | ||
| 420 | for g in &grasp_servers_as_personal_clone_url { | ||
| 421 | user_repo_ref.add_grasp_server(g)?; | ||
| 422 | } | ||
| 423 | user_repo_ref | ||
| 424 | } else { | ||
| 425 | // clone repo_ref and reset as personal-fork | ||
| 426 | let mut user_repo_ref = repo_ref.clone(); | ||
| 427 | user_repo_ref.trusted_maintainer = user_ref.public_key; | ||
| 428 | user_repo_ref.maintainers = vec![user_ref.public_key]; | ||
| 429 | user_repo_ref.git_server = vec![]; | ||
| 430 | user_repo_ref.relays = vec![]; | ||
| 431 | if !user_repo_ref | ||
| 432 | .hashtags | ||
| 433 | .contains(&"personal-fork".to_string()) | ||
| 434 | { | ||
| 435 | user_repo_ref.hashtags.push("personal-fork".to_string()); | ||
| 436 | } | ||
| 437 | user_repo_ref | ||
| 438 | } | ||
| 439 | }; | ||
| 440 | // pubish event to my-relays and my-fork-relays | ||
| 441 | new_grasp_server_events.push(updated_user_repo_ref.to_event(&signer).await?); | ||
| 442 | send_events( | ||
| 443 | &client, | ||
| 444 | Some(git_repo_path), | ||
| 445 | new_grasp_server_events, | ||
| 446 | user_ref.relays.write(), | ||
| 447 | updated_user_repo_ref.relays.clone(), | ||
| 448 | !cli_args.disable_cli_spinners, | ||
| 449 | false, | ||
| 450 | ) | ||
| 451 | .await?; | ||
| 452 | user_repo_ref = Some(updated_user_repo_ref); | ||
| 453 | // wait a few seconds | ||
| 454 | let countdown_start = 5; | ||
| 455 | let term = console::Term::stdout(); | ||
| 456 | for i in (1..=countdown_start).rev() { | ||
| 457 | term.write_line( | ||
| 458 | format!( | ||
| 459 | "waiting {i}s grasp servers to create your repo before we push your data" | ||
| 460 | ) | ||
| 461 | .as_str(), | ||
| 462 | )?; | ||
| 463 | thread::sleep(Duration::new(1, 0)); // Sleep for 1 second | ||
| 464 | term.clear_last_lines(1)?; | ||
| 465 | } | ||
| 466 | term.flush().unwrap(); // Ensure the output is flushed to the terminal | ||
| 467 | |||
| 468 | // add grasp servers to to_try | ||
| 469 | for url in grasp_servers_as_personal_clone_url { | ||
| 470 | to_try.push(url); | ||
| 471 | } | ||
| 472 | // the loop with continue with the grasp servers | ||
| 473 | }; | ||
| 474 | println!( | ||
| 475 | "posting {}", | ||
| 476 | if events.iter().any(|e| e.kind.eq(&Kind::GitStatusClosed)) { | ||
| 477 | "proposal revision as new PR event, and a close status for the old patch" | ||
| 478 | } else if events.iter().any(|e| e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) { | ||
| 479 | "proposal revision as PR update event" | ||
| 480 | } else { | ||
| 481 | "proposal as PR event" | ||
| 482 | } | ||
| 483 | ); | ||
| 484 | events | ||
| 485 | } else { | ||
| 486 | let events = generate_cover_letter_and_patch_events( | ||
| 487 | cover_letter_title_description.clone(), | ||
| 488 | &git_repo, | ||
| 489 | &commits, | ||
| 490 | &signer, | ||
| 491 | &repo_ref, | ||
| 492 | &root_proposal.as_ref().map(|e| e.id.to_string()), | ||
| 493 | &mention_tags, | ||
| 494 | ) | ||
| 495 | .await?; | ||
| 496 | |||
| 497 | println!( | ||
| 498 | "posting {} patch{} {} a covering letter...", | ||
| 499 | if cover_letter_title_description.is_none() { | ||
| 500 | events.len() | ||
| 501 | } else { | ||
| 502 | events.len() - 1 | ||
| 503 | }, | ||
| 504 | if cover_letter_title_description.is_none() && events.len().eq(&1) | ||
| 505 | || cover_letter_title_description.is_some() && events.len().eq(&2) | ||
| 506 | { | ||
| 507 | "" | ||
| 508 | } else { | ||
| 509 | "es" | ||
| 510 | }, | ||
| 511 | if cover_letter_title_description.is_none() { | ||
| 512 | "without" | ||
| 513 | } else { | ||
| 514 | "with" | ||
| 515 | } | ||
| 516 | ); | ||
| 517 | events | ||
| 518 | }; | ||
| 226 | 519 | ||
| 227 | send_events( | 520 | send_events( |
| 228 | &client, | 521 | &client, |
| @@ -235,7 +528,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 235 | ) | 528 | ) |
| 236 | .await?; | 529 | .await?; |
| 237 | 530 | ||
| 238 | if root_proposal_id.is_none() { | 531 | if root_proposal.is_none() { |
| 239 | if let Some(event) = events.first() { | 532 | if let Some(event) = events.first() { |
| 240 | let event_bech32 = if let Some(relay) = repo_ref.relays.first() { | 533 | let event_bech32 = if let Some(relay) = repo_ref.relays.first() { |
| 241 | Nip19Event { | 534 | Nip19Event { |
| @@ -251,8 +544,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 251 | println!( | 544 | println!( |
| 252 | "{}", | 545 | "{}", |
| 253 | dim.apply_to(format!( | 546 | dim.apply_to(format!( |
| 254 | "view in gitworkshop.dev: https://gitworkshop.dev/repo/{}/proposal/{}", | 547 | "view in gitworkshop.dev: https://gitworkshop.dev/{}", |
| 255 | repo_ref.coordinate_with_hint().to_bech32()?, | ||
| 256 | &event_bech32, | 548 | &event_bech32, |
| 257 | )) | 549 | )) |
| 258 | ); | 550 | ); |
| @@ -269,6 +561,49 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 269 | Ok(()) | 561 | Ok(()) |
| 270 | } | 562 | } |
| 271 | 563 | ||
| 564 | fn check_commits_are_suitable_for_proposal( | ||
| 565 | first_commit_ahead: &[Sha1Hash], | ||
| 566 | commits: &[Sha1Hash], | ||
| 567 | behind: &[Sha1Hash], | ||
| 568 | main_branch_name: &str, | ||
| 569 | main_tip: &Sha1Hash, | ||
| 570 | ) -> Result<()> { | ||
| 571 | // check proposal ahead of origin/main | ||
| 572 | if first_commit_ahead.len().gt(&1) && !Interactor::default().confirm( | ||
| 573 | PromptConfirmParms::default() | ||
| 574 | .with_prompt( | ||
| 575 | format!("proposal builds on a commit {} ahead of '{main_branch_name}' - do you want to continue?", first_commit_ahead.len() - 1) | ||
| 576 | ) | ||
| 577 | .with_default(false) | ||
| 578 | ).context("failed to get confirmation response from interactor confirm")? { | ||
| 579 | bail!("aborting because selected commits were ahead of origin/master"); | ||
| 580 | } | ||
| 581 | |||
| 582 | // check if a selected commit is already in origin | ||
| 583 | if commits.iter().any(|c| c.eq(main_tip)) { | ||
| 584 | if !Interactor::default().confirm( | ||
| 585 | PromptConfirmParms::default() | ||
| 586 | .with_prompt( | ||
| 587 | format!("proposal contains commit(s) already in '{main_branch_name}'. proceed anyway?") | ||
| 588 | ) | ||
| 589 | .with_default(false) | ||
| 590 | ).context("failed to get confirmation response from interactor confirm")? { | ||
| 591 | bail!("aborting as proposal contains commit(s) already in '{main_branch_name}'"); | ||
| 592 | } | ||
| 593 | } | ||
| 594 | // check proposal isn't behind origin/main | ||
| 595 | else if !behind.is_empty() && !Interactor::default().confirm( | ||
| 596 | PromptConfirmParms::default() | ||
| 597 | .with_prompt( | ||
| 598 | format!("proposal is {} behind '{main_branch_name}'. consider rebasing before submission. proceed anyway?", behind.len()) | ||
| 599 | ) | ||
| 600 | .with_default(false) | ||
| 601 | ).context("failed to get confirmation response from interactor confirm")? { | ||
| 602 | bail!("aborting so commits can be rebased"); | ||
| 603 | } | ||
| 604 | Ok(()) | ||
| 605 | } | ||
| 606 | |||
| 272 | fn choose_commits(git_repo: &Repo, proposed_commits: Vec<Sha1Hash>) -> Result<Vec<Sha1Hash>> { | 607 | fn choose_commits(git_repo: &Repo, proposed_commits: Vec<Sha1Hash>) -> Result<Vec<Sha1Hash>> { |
| 273 | let mut proposed_commits = if proposed_commits.len().gt(&10) { | 608 | let mut proposed_commits = if proposed_commits.len().gt(&10) { |
| 274 | vec![] | 609 | vec![] |
| @@ -360,11 +695,11 @@ fn summarise_commit_for_selection(git_repo: &Repo, commit: &Sha1Hash) -> Result< | |||
| 360 | )) | 695 | )) |
| 361 | } | 696 | } |
| 362 | 697 | ||
| 363 | async fn get_root_proposal_id_and_mentions_from_in_reply_to( | 698 | async fn get_root_proposal_and_mentions_from_in_reply_to( |
| 364 | git_repo_path: &Path, | 699 | git_repo_path: &Path, |
| 365 | in_reply_to: &[String], | 700 | in_reply_to: &[String], |
| 366 | ) -> Result<(Option<String>, Vec<nostr::Tag>)> { | 701 | ) -> Result<(Option<Event>, Vec<nostr::Tag>)> { |
| 367 | let root_proposal_id = if let Some(first) = in_reply_to.first() { | 702 | let root_proposal = if let Some(first) = in_reply_to.first() { |
| 368 | match event_tag_from_nip19_or_hex(first, "in-reply-to", EventRefType::Root, true, false)? | 703 | match event_tag_from_nip19_or_hex(first, "in-reply-to", EventRefType::Root, true, false)? |
| 369 | .as_standardized() | 704 | .as_standardized() |
| 370 | { | 705 | { |
| @@ -382,8 +717,8 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( | |||
| 382 | .await?; | 717 | .await?; |
| 383 | 718 | ||
| 384 | if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) { | 719 | if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) { |
| 385 | if event_is_patch_set_root(first) { | 720 | if event_is_patch_set_root(first) || first.kind.eq(&KIND_PULL_REQUEST) { |
| 386 | Some(event_id.to_string()) | 721 | Some(first.clone()) |
| 387 | } else { | 722 | } else { |
| 388 | None | 723 | None |
| 389 | } | 724 | } |
| @@ -399,7 +734,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( | |||
| 399 | 734 | ||
| 400 | let mut mention_tags = vec![]; | 735 | let mut mention_tags = vec![]; |
| 401 | for (i, reply_to) in in_reply_to.iter().enumerate() { | 736 | for (i, reply_to) in in_reply_to.iter().enumerate() { |
| 402 | if i.ne(&0) || root_proposal_id.is_none() { | 737 | if i.ne(&0) || root_proposal.is_none() { |
| 403 | mention_tags.push( | 738 | mention_tags.push( |
| 404 | event_tag_from_nip19_or_hex( | 739 | event_tag_from_nip19_or_hex( |
| 405 | reply_to, | 740 | reply_to, |
| @@ -415,7 +750,7 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to( | |||
| 415 | } | 750 | } |
| 416 | } | 751 | } |
| 417 | 752 | ||
| 418 | Ok((root_proposal_id, mention_tags)) | 753 | Ok((root_proposal, mention_tags)) |
| 419 | } | 754 | } |
| 420 | 755 | ||
| 421 | // TODO | 756 | // TODO |