upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-08-18 17:25:50 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-08-19 07:48:16 +0100
commitb7d4c5a81f0a008524dcc5b4f286f0cf700013c0 (patch)
tree9efc97803252d1021df767fe781c2fc91babf80b /src/bin
parent4d68e64ac4f08274aba6ff225bd89a60eb62e225 (diff)
feat(list): add PR fetch and checkout support
abstracted git remote helper fetch functions added support to `ngit list` to fetch PR data and checkout as proposal branch
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs526
-rw-r--r--src/bin/git_remote_nostr/list.rs6
-rw-r--r--src/bin/ngit/sub_commands/list.rs129
3 files changed, 113 insertions, 548 deletions
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs
index 2cc87da..b0d6cfe 100644
--- a/src/bin/git_remote_nostr/fetch.rs
+++ b/src/bin/git_remote_nostr/fetch.rs
@@ -2,27 +2,21 @@ use core::str;
2use std::{ 2use std::{
3 collections::{HashMap, HashSet}, 3 collections::{HashMap, HashSet},
4 io::Stdin, 4 io::Stdin,
5 sync::{Arc, Mutex},
6 time::Instant,
7}; 5};
8 6
9use anyhow::{Context, Result, anyhow, bail}; 7use anyhow::{Context, Result, bail};
10use auth_git2::GitAuthenticator;
11use git2::{Progress, Repository};
12use ngit::{ 8use ngit::{
13 cli_interactor::count_lines_per_msg_vec, 9 fetch::fetch_from_git_server,
14 git::{ 10 git::{Repo, RepoActions},
15 Repo, RepoActions, 11 git_events::{
16 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 12 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
17 utils::check_ssh_keys, 13 identify_clone_urls_for_oids_from_pr_pr_update_events, tag_value,
18 }, 14 },
19 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, tag_value},
20 login::get_curent_user, 15 login::get_curent_user,
21 repo_ref::{RepoRef, is_grasp_server_in_list}, 16 repo_ref::{RepoRef, is_grasp_server_in_list},
22 utils::{ 17 utils::{
23 Direction, find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, 18 find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch,
24 get_open_or_draft_proposals, get_read_protocols_to_try, join_with_and, 19 get_open_or_draft_proposals,
25 set_protocol_preference,
26 }, 20 },
27}; 21};
28use nostr::nips::nip19; 22use nostr::nips::nip19;
@@ -126,36 +120,6 @@ pub async fn run_fetch(
126 Ok(()) 120 Ok(())
127} 121}
128 122
129async fn identify_clone_urls_for_oids_from_pr_pr_update_events(
130 oids: Vec<&String>,
131 git_repo: &Repo,
132 repo_ref: &RepoRef,
133) -> Result<HashMap<String, Vec<String>>> {
134 let mut map: HashMap<String, Vec<String>> = HashMap::new();
135
136 let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?;
137
138 for (_, (_, events)) in open_and_draft_proposals {
139 for event in events {
140 if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind) {
141 if let Ok(c) = tag_value(&event, "c") {
142 if oids.contains(&&c) {
143 for tag in event.tags.as_slice() {
144 if tag.kind().eq(&nostr::event::TagKind::Clone) {
145 for clone_url in tag.as_slice().iter().skip(1) {
146 map.entry(c.clone()).or_default().push(clone_url.clone());
147 }
148 }
149 }
150 }
151 }
152 }
153 }
154 }
155
156 Ok(map)
157}
158
159pub fn make_commits_for_proposal( 123pub fn make_commits_for_proposal(
160 git_repo: &Repo, 124 git_repo: &Repo,
161 repo_ref: &RepoRef, 125 repo_ref: &RepoRef,
@@ -235,477 +199,3 @@ async fn fetch_open_or_draft_proposals_from_patches(
235 } 199 }
236 Ok(()) 200 Ok(())
237} 201}
238
239pub fn fetch_from_git_server(
240 git_repo: &Repo,
241 oids: &[String],
242 git_server_url: &str,
243 decoded_nostr_url: &NostrUrlDecoded,
244 term: &console::Term,
245 is_grasp_server: bool,
246) -> Result<()> {
247 let already_have_oids = oids
248 .iter()
249 .all(|oid| git_repo.does_commit_exist(oid).is_ok_and(|outcome| outcome));
250 if already_have_oids {
251 return Ok(());
252 }
253
254 let server_url = git_server_url.parse::<CloneUrl>()?;
255
256 let protocols_to_attempt =
257 get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_grasp_server);
258
259 let mut failed_protocols = vec![];
260 let mut success = false;
261 for protocol in &protocols_to_attempt {
262 term.write_line(
263 format!("fetching {} over {protocol}...", server_url.short_name(),).as_str(),
264 )?;
265
266 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?;
267 let res = fetch_from_git_server_url(
268 &git_repo.git_repo,
269 oids,
270 &formatted_url,
271 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol),
272 term,
273 );
274 if let Err(error) = res {
275 term.write_line(
276 format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(),
277 )?;
278 failed_protocols.push(protocol);
279 } else {
280 success = true;
281 if !failed_protocols.is_empty() {
282 term.write_line(format!("fetch: succeeded over {protocol}").as_str())?;
283 let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push);
284 }
285 break;
286 }
287 }
288 if success {
289 Ok(())
290 } else {
291 let error = anyhow!(
292 "{} failed over {}{}",
293 server_url.short_name(),
294 join_with_and(&failed_protocols),
295 if decoded_nostr_url.protocol.is_some() {
296 " and nostr url contains protocol override so no other protocols were attempted"
297 } else {
298 ""
299 },
300 );
301 term.write_line(format!("fetch: {error}").as_str())?;
302 Err(error)
303 }
304}
305
306#[allow(clippy::cast_precision_loss)]
307#[allow(clippy::float_cmp)]
308#[allow(clippy::needless_pass_by_value)]
309fn report_on_transfer_progress(
310 progress_stats: &Progress<'_>,
311 start_time: &Instant,
312 end_time: Option<&Instant>,
313) -> Vec<String> {
314 let mut report = vec![];
315 let total = progress_stats.total_objects() as f64;
316 if total == 0.0 {
317 return report;
318 }
319 let received = progress_stats.received_objects() as f64;
320 let percentage = ((received / total) * 100.0)
321 // always round down because 100% complete is misleading when its not complete
322 .floor();
323
324 let received_bytes = progress_stats.received_bytes() as f64;
325
326 let (size, unit) = if received_bytes >= (1024.0 * 1024.0) {
327 (received_bytes / (1024.0 * 1024.0), "MiB")
328 } else {
329 (received_bytes / 1024.0, "KiB")
330 };
331
332 let speed = {
333 let duration = if let Some(end_time) = end_time {
334 (*end_time - *start_time).as_millis() as f64
335 } else {
336 start_time.elapsed().as_millis() as f64
337 };
338
339 if duration > 0.0 {
340 (received_bytes / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds
341 } else {
342 0.0
343 }
344 };
345
346 // Format the output for receiving objects
347 report.push(format!(
348 "Receiving objects: {percentage}% ({received}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}",
349 if received == total {
350 ", done."
351 } else { ""},
352 ));
353 if received == total {
354 let indexed_deltas = progress_stats.indexed_deltas() as f64;
355 let total_deltas = progress_stats.total_deltas() as f64;
356 let percentage = ((indexed_deltas / total_deltas) * 100.0)
357 // always round down because 100% complete is misleading when its not complete
358 .floor();
359 if total_deltas > 0.0 {
360 report.push(format!(
361 "Resolving deltas: {percentage}% ({indexed_deltas}/{total_deltas}){}",
362 if indexed_deltas == total_deltas {
363 ", done."
364 } else {
365 ""
366 },
367 ));
368 }
369 }
370 report
371}
372
373struct FetchReporter<'a> {
374 remote_msgs: Vec<String>,
375 transfer_progress_msgs: Vec<String>,
376 term: &'a console::Term,
377 start_time: Option<Instant>,
378 end_time: Option<Instant>,
379}
380impl<'a> FetchReporter<'a> {
381 fn new(term: &'a console::Term) -> Self {
382 Self {
383 remote_msgs: vec![],
384 transfer_progress_msgs: vec![],
385 term,
386 start_time: None,
387 end_time: None,
388 }
389 }
390 fn write_all(&self, lines_to_clear: usize) {
391 let _ = self.term.clear_last_lines(lines_to_clear);
392 for msg in &self.remote_msgs {
393 let _ = self.term.write_line(format!("remote: {msg}").as_str());
394 }
395 for msg in &self.transfer_progress_msgs {
396 let _ = self.term.write_line(msg);
397 }
398 }
399 fn count_all_existing_lines(&self) -> usize {
400 let width = self.term.size().1;
401 count_lines_per_msg_vec(width, &self.remote_msgs, "remote: ".len())
402 + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0)
403 }
404 fn just_write_transfer_progress(&self, lines_to_clear: usize) {
405 let _ = self.term.clear_last_lines(lines_to_clear);
406 for msg in &self.transfer_progress_msgs {
407 let _ = self.term.write_line(msg);
408 }
409 }
410 fn just_count_transfer_progress(&self) -> usize {
411 let width = self.term.size().1;
412 count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0)
413 }
414 fn process_remote_msg(&mut self, data: &[u8]) {
415 if let Ok(data) = str::from_utf8(data) {
416 let data = data
417 .split(['\n', '\r'])
418 .map(str::trim)
419 .filter(|line| !line.trim().is_empty())
420 .collect::<Vec<&str>>();
421 for data in data {
422 let existing_lines = self.count_all_existing_lines();
423 let msg = data.to_string();
424 if let Some(last) = self.remote_msgs.last() {
425 // if previous line begins with x but doesnt finish with y then its part of the
426 // same msg
427 if (last.starts_with("Enume") && !last.ends_with(", done."))
428 || ((last.starts_with("Compre") || last.starts_with("Count"))
429 && !last.contains(')'))
430 {
431 let last = self.remote_msgs.pop().unwrap();
432 self.remote_msgs.push(format!("{last}{msg}"));
433 // if previous msg contains % and its not 100% then it
434 // should be overwritten
435 } else if (last.contains('%') && !last.contains("100%"))
436 // but also if the next message is identical with "", done." appended
437 || last == &msg.replace(", done.", "")
438 {
439 self.remote_msgs.pop();
440 self.remote_msgs.push(msg);
441 } else {
442 self.remote_msgs.push(msg);
443 }
444 } else {
445 self.remote_msgs.push(msg);
446 }
447 self.write_all(existing_lines);
448 }
449 }
450 }
451 fn process_transfer_progress_update(&mut self, progress_stats: &git2::Progress<'_>) {
452 if self.start_time.is_none() {
453 self.start_time = Some(Instant::now());
454 }
455 let existing_lines = self.just_count_transfer_progress();
456 let updated = report_on_transfer_progress(
457 progress_stats,
458 &self.start_time.unwrap(),
459 self.end_time.as_ref(),
460 );
461 if self.transfer_progress_msgs.len() <= updated.len() {
462 if self.end_time.is_none() && updated.first().is_some_and(|f| f.contains("100%")) {
463 self.end_time = Some(Instant::now());
464 }
465 // once "Resolving Deltas" is complete, deltas get reset to 0 and it stops
466 // reporting on it so we want to keep the old report
467 self.transfer_progress_msgs = updated;
468 }
469 self.just_write_transfer_progress(existing_lines);
470 }
471}
472
473fn fetch_from_git_server_url(
474 git_repo: &Repository,
475 oids: &[String],
476 git_server_url: &str,
477 dont_authenticate: bool,
478 term: &console::Term,
479) -> Result<()> {
480 if git_server_url.parse::<CloneUrl>()?.protocol() == ServerProtocol::Ssh && !check_ssh_keys() {
481 bail!("no ssh keys found");
482 }
483 let git_config = git_repo.config()?;
484 let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?;
485 let auth = GitAuthenticator::default();
486 let mut fetch_options = git2::FetchOptions::new();
487 let mut remote_callbacks = git2::RemoteCallbacks::new();
488 let fetch_reporter = Arc::new(Mutex::new(FetchReporter::new(term)));
489 remote_callbacks.sideband_progress({
490 let fetch_reporter = Arc::clone(&fetch_reporter);
491 move |data| {
492 let mut reporter = fetch_reporter.lock().unwrap();
493 reporter.process_remote_msg(data);
494 true
495 }
496 });
497 remote_callbacks.transfer_progress({
498 let fetch_reporter = Arc::clone(&fetch_reporter);
499 move |stats| {
500 let mut reporter = fetch_reporter.lock().unwrap();
501 reporter.process_transfer_progress_update(&stats);
502 true
503 }
504 });
505
506 if !dont_authenticate {
507 remote_callbacks.credentials(auth.credentials(&git_config));
508 }
509 fetch_options.remote_callbacks(remote_callbacks);
510
511 git_server_remote.download(oids, Some(&mut fetch_options))?;
512
513 git_server_remote.disconnect()?;
514 Ok(())
515}
516
517#[cfg(test)]
518mod tests {
519
520 use super::*;
521
522 fn pass_through_fetch_reporter_proces_remote_msg(msgs: Vec<&str>) -> Vec<String> {
523 let term = console::Term::stdout();
524 let mut reporter = FetchReporter::new(&term);
525 for msg in msgs {
526 reporter.process_remote_msg(msg.as_bytes());
527 }
528 reporter.remote_msgs
529 }
530
531 #[test]
532 fn logs_single_msg() {
533 assert_eq!(
534 pass_through_fetch_reporter_proces_remote_msg(vec![
535 "Enumerating objects: 23716, done.",
536 ]),
537 vec!["Enumerating objects: 23716, done."]
538 );
539 }
540
541 #[test]
542 fn logs_multiple_msgs() {
543 assert_eq!(
544 pass_through_fetch_reporter_proces_remote_msg(vec![
545 "Enumerating objects: 23716, done.",
546 "Counting objects: 0% (1/2195)",
547 ]),
548 vec![
549 "Enumerating objects: 23716, done.",
550 "Counting objects: 0% (1/2195)",
551 ]
552 );
553 }
554
555 mod ignores {
556 use super::*;
557
558 #[test]
559 fn empty_msgs() {
560 assert_eq!(
561 pass_through_fetch_reporter_proces_remote_msg(vec![
562 "Enumerating objects: 23716, done.",
563 "",
564 "Counting objects: 0% (1/2195)",
565 "",
566 ]),
567 vec![
568 "Enumerating objects: 23716, done.",
569 "Counting objects: 0% (1/2195)",
570 ]
571 );
572 }
573
574 #[test]
575 fn whitespace_msgs() {
576 assert_eq!(
577 pass_through_fetch_reporter_proces_remote_msg(vec![
578 "Enumerating objects: 23716, done.",
579 " ",
580 "Counting objects: 0% (1/2195)",
581 " \r\n \r",
582 ]),
583 vec![
584 "Enumerating objects: 23716, done.",
585 "Counting objects: 0% (1/2195)",
586 ]
587 );
588 }
589 }
590
591 mod splits {
592 use super::*;
593
594 #[test]
595 fn multiple_lines_in_single_msg() {
596 assert_eq!(
597 pass_through_fetch_reporter_proces_remote_msg(vec![
598 "Enumerating objects: 23716, done.\r\nCounting objects: 0% (1/2195)",
599 "",
600 ]),
601 vec![
602 "Enumerating objects: 23716, done.",
603 "Counting objects: 0% (1/2195)",
604 ]
605 );
606 }
607 }
608
609 mod joins_lines_sent_over_multiple_msgs {
610 use super::*;
611
612 #[test]
613 fn enumerating() {
614 assert_eq!(
615 pass_through_fetch_reporter_proces_remote_msg(vec![
616 "Enumerat",
617 "ing objec",
618 "ts: 23716, done.",
619 "Counting objects: 0% (1/2195)",
620 ]),
621 vec![
622 "Enumerating objects: 23716, done.",
623 "Counting objects: 0% (1/2195)",
624 ]
625 );
626 }
627 #[test]
628 fn counting() {
629 assert_eq!(
630 pass_through_fetch_reporter_proces_remote_msg(vec![
631 "Enumerating objects: 23716, done.",
632 "Counting obj",
633 "ects: 0% (1/2195)",
634 "Count",
635 "ing objects: 1% (22/",
636 "2195)",
637 ]),
638 vec![
639 "Enumerating objects: 23716, done.",
640 "Counting objects: 1% (22/2195)",
641 ]
642 );
643 }
644 #[test]
645 fn compressing() {
646 assert_eq!(
647 pass_through_fetch_reporter_proces_remote_msg(vec![
648 "Compress",
649 "ing obj",
650 "ect",
651 "s: 0% (1/56",
652 "0)"
653 ]),
654 vec!["Compressing objects: 0% (1/560)"]
655 );
656 }
657 }
658
659 #[test]
660 fn msgs_with_pc_and_not_100pc_are_replaced() {
661 assert_eq!(
662 pass_through_fetch_reporter_proces_remote_msg(vec![
663 "Enumerating objects: 23716, done.",
664 "Counting objects: 0% (1/2195)",
665 "Counting objects: 1% (22/2195)",
666 ]),
667 vec![
668 "Enumerating objects: 23716, done.",
669 "Counting objects: 1% (22/2195)",
670 ]
671 );
672 }
673 mod msgs_with_pc_100pc_are_not_replaced {
674 use super::*;
675
676 #[test]
677 fn when_next_msg_is_not_identical_but_with_done() {
678 assert_eq!(
679 pass_through_fetch_reporter_proces_remote_msg(vec![
680 "Enumerating objects: 23716, done.",
681 "Counting objects: 0% (1/2195)",
682 "Counting objects: 1% (22/2195)",
683 "Counting objects: 100% (2195/2195)",
684 "Compressing objects: 0% (1/560)"
685 ]),
686 vec![
687 "Enumerating objects: 23716, done.",
688 "Counting objects: 100% (2195/2195)",
689 "Compressing objects: 0% (1/560)"
690 ]
691 );
692 }
693
694 #[test]
695 fn but_is_when_next_msg_is_identical_but_with_done_appended() {
696 assert_eq!(
697 pass_through_fetch_reporter_proces_remote_msg(vec![
698 "Enumerating objects: 23716, done.",
699 "Counting objects: 0% (1/2195)",
700 "Counting objects: 1% (22/2195)",
701 "Counting objects: 100% (2195/2195)",
702 "Counting objects: 100% (2195/2195), done.",
703 ]),
704 vec![
705 "Enumerating objects: 23716, done.",
706 "Counting objects: 100% (2195/2195), done.",
707 ]
708 );
709 }
710 }
711}
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs
index f5eaee4..be83991 100644
--- a/src/bin/git_remote_nostr/list.rs
+++ b/src/bin/git_remote_nostr/list.rs
@@ -5,6 +5,7 @@ use client::get_state_from_cache;
5use git::RepoActions; 5use git::RepoActions;
6use ngit::{ 6use ngit::{
7 client, 7 client,
8 fetch::fetch_from_git_server,
8 git::{self}, 9 git::{self},
9 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value}, 10 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value},
10 list::{get_ahead_behind, list_from_remotes}, 11 list::{get_ahead_behind, list_from_remotes},
@@ -14,10 +15,7 @@ use ngit::{
14}; 15};
15use repo_ref::RepoRef; 16use repo_ref::RepoRef;
16 17
17use crate::{ 18use crate::{fetch::make_commits_for_proposal, git::Repo};
18 fetch::{fetch_from_git_server, make_commits_for_proposal},
19 git::Repo,
20};
21 19
22pub async fn run_list( 20pub async fn run_list(
23 git_repo: &Repo, 21 git_repo: &Repo,
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 0083c91..2de3151 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -1,4 +1,4 @@
1use std::{io::Write, ops::Add}; 1use std::{collections::HashSet, io::Write, ops::Add};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use ngit::{ 4use ngit::{
@@ -6,10 +6,12 @@ use ngit::{
6 Params, get_all_proposal_patch_pr_pr_update_events_from_cache, 6 Params, get_all_proposal_patch_pr_pr_update_events_from_cache,
7 get_proposals_and_revisions_from_cache, 7 get_proposals_and_revisions_from_cache,
8 }, 8 },
9 fetch::fetch_from_git_server,
9 git_events::{ 10 git_events::{
10 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch, 11 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, get_commit_id_from_patch,
11 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value, 12 get_pr_tip_event_or_most_recent_patch_with_ancestors, get_status, status_kinds, tag_value,
12 }, 13 },
14 repo_ref::{RepoRef, is_grasp_server_in_list},
13}; 15};
14use nostr_sdk::Kind; 16use nostr_sdk::Kind;
15 17
@@ -204,40 +206,65 @@ pub async fn launch() -> Result<()> {
204 .iter() 206 .iter()
205 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind)) 207 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
206 { 208 {
209 let branch_name = cover_letter.get_branch_name_with_pr_prefix_and_shorthand_id()?;
210 let local_branch_tip = git_repo.get_tip_of_branch(&branch_name).ok();
211 let proposal_tip_event = most_recent_proposal_patch_chain_or_pr_or_pr_update
212 .first()
213 .context("most_recent_proposal_patch_chain_or_pr_or_pr_update will always contain a event with c tag")?;
214 let proposal_tip = tag_value(proposal_tip_event, "c")?;
215
207 match Interactor::default().choice( 216 match Interactor::default().choice(
208 PromptChoiceParms::default() 217 PromptChoiceParms::default()
209 .with_prompt(
210 "this is new PR event kind which isn't supported in `ngit list` yet",
211 )
212 .with_default(0) 218 .with_default(0)
213 .with_choices( 219 .with_choices(vec![
214 if [Kind::GitStatusOpen, Kind::GitStatusDraft].contains(&selected_status) 220 if let Some(local_branch_tip) = local_branch_tip {
215 && git_repo 221 if local_branch_tip.to_string() == proposal_tip {
216 .get_first_nostr_remote_when_in_ngit_binary() 222 format!("checkout up-to-date proposal branch '{branch_name}'")
217 .await 223 } else {
218 .is_ok_and(|r| r.is_some()) 224 format!("checkout proposal branch and pull changes '{branch_name}'")
219 { 225 }
220 vec![
221 format!(
222 "I'll manually checkout the proposal at remote branch '{}'",
223 cover_letter
224 .get_branch_name_with_pr_prefix_and_shorthand_id()
225 .unwrap()
226 ),
227 // TODO fetch oids and follow similar logic for dealing with
228 // conflcts as with patches below
229 "back to proposals".to_string(),
230 ]
231 } else { 226 } else {
232 vec!["back to proposals".to_string()] 227 format!("create and checkout as branch '{branch_name}'")
233 }, 228 },
234 ), 229 "back to proposals".to_string(),
230 ]),
235 )? { 231 )? {
236 0 => continue, 232 0 => {
233 if let Some(local_branch_tip) = local_branch_tip {
234 git_repo
235 .checkout(&branch_name)
236 .context("cannot checkout existing proposal branch")?;
237 if local_branch_tip.to_string() == proposal_tip {
238 println!("checked out up-to-date proposal branch '{branch_name}'");
239 return Ok(());
240 }
241 if git_repo.does_commit_exist(&proposal_tip)? {
242 println!("checked out proposal branch and updated tip '{branch_name}'");
243 return Ok(());
244 }
245 }
246 fetch_oid_for_from_servers_for_pr(
247 &proposal_tip,
248 &git_repo,
249 &repo_ref,
250 proposal_tip_event,
251 )?;
252 git_repo.create_branch_at_commit(&branch_name, &proposal_tip)?;
253 git_repo.checkout(&branch_name)?;
254 if local_branch_tip.is_some() {
255 println!("created and checked out proposal branch '{branch_name}'");
256 } else {
257 println!("checked out proposal branch and pulled updates '{branch_name}'");
258 }
259 return Ok(());
260 }
261 1 => {
262 continue;
263 }
237 _ => { 264 _ => {
238 bail!("unexpected choice") 265 bail!("unexpected choice")
239 } 266 }
240 }; 267 }
241 } 268 }
242 269
243 let binding_patch_text_ref = format!( 270 let binding_patch_text_ref = format!(
@@ -739,6 +766,56 @@ pub async fn launch() -> Result<()> {
739 } 766 }
740} 767}
741 768
769fn fetch_oid_for_from_servers_for_pr(
770 oid: &str,
771 git_repo: &Repo,
772 repo_ref: &RepoRef,
773 pr_or_pr_update_event: &nostr::Event,
774) -> Result<()> {
775 let git_servers = {
776 let mut seen: HashSet<String> = HashSet::new();
777 let mut out: Vec<String> = vec![];
778 for tag in pr_or_pr_update_event.tags.as_slice() {
779 if tag.kind().eq(&nostr::event::TagKind::Clone) {
780 for clone_url in tag.as_slice().iter().skip(1) {
781 seen.insert(clone_url.clone());
782 }
783 }
784 }
785 for server in &repo_ref.git_server {
786 if seen.insert(server.clone()) {
787 out.push(server.clone());
788 }
789 }
790 out
791 };
792
793 let mut errors = vec![];
794 let term = console::Term::stderr();
795
796 for git_server_url in &git_servers {
797 if let Err(error) = fetch_from_git_server(
798 git_repo,
799 &[oid.to_string()],
800 git_server_url,
801 &repo_ref.to_nostr_git_url(&None),
802 &term,
803 is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()),
804 ) {
805 errors.push(error);
806 } else {
807 println!("fetched proposal git data from {git_server_url}");
808 break;
809 }
810 }
811 if !git_repo.does_commit_exist(oid)? {
812 bail!(
813 "cannot find proposal git data from proposal git server hint or repository git servers"
814 )
815 }
816 Ok(())
817}
818
742fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> { 819fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
743 println!("applying to current branch with `git am`"); 820 println!("applying to current branch with `git am`");
744 // TODO: add PATCH x/n to appended patches 821 // TODO: add PATCH x/n to appended patches