tenseleyflow/fussr / 4785061

Browse files

port upstream/remote modal to fetch functioanlity

Authored by espadonne
SHA
478506124afcfc84492c1f1398e46dab8ea8d018
Parents
38ae8b2
Tree
ba83fd1

5 changed files

StatusFile+-
M src/app.rs 61 5
M src/git.rs 23 0
M src/main.rs 59 0
M src/types.rs 10 0
M src/ui.rs 91 1
src/app.rsmodified
@@ -370,12 +370,68 @@ impl App {
370370
         Ok(())
371371
     }
372372
 
373
-    /// Fetch from remote
373
+    /// Fetch from remote - shows remote selector if multiple remotes
374374
     pub fn fetch(&mut self) -> Result<()> {
375
-        self.set_status("Fetching...".to_string());
376
-        self.repo.fetch()?;
377
-        self.set_status("Fetch complete".to_string());
378
-        self.refresh_files()
375
+        let remotes = self.repo.get_remotes();
376
+        if remotes.len() <= 1 {
377
+            // Single or no remote - just fetch
378
+            self.set_status("Fetching...".to_string());
379
+            self.repo.fetch()?;
380
+            self.set_status("Fetch complete".to_string());
381
+            self.refresh_files()
382
+        } else {
383
+            // Multiple remotes - show selector
384
+            self.input_mode = InputMode::Fetch {
385
+                remotes,
386
+                selected: 0,
387
+                status: crate::types::FetchStatus::SelectRemote,
388
+            };
389
+            Ok(())
390
+        }
391
+    }
392
+
393
+    /// Execute fetch from selected remote
394
+    pub fn fetch_from_remote(&mut self, remote: &str) -> Result<()> {
395
+        if let InputMode::Fetch { status, .. } = &mut self.input_mode {
396
+            *status = crate::types::FetchStatus::Fetching;
397
+        }
398
+
399
+        match self.repo.fetch_from_remote(remote) {
400
+            Ok(()) => {
401
+                if let InputMode::Fetch { status, .. } = &mut self.input_mode {
402
+                    *status = crate::types::FetchStatus::Success;
403
+                }
404
+                self.set_status(format!("Fetched from {}", remote));
405
+                self.refresh_files()?;
406
+                Ok(())
407
+            }
408
+            Err(e) => {
409
+                let msg = e.to_string();
410
+                if let InputMode::Fetch { status, .. } = &mut self.input_mode {
411
+                    *status = crate::types::FetchStatus::Failed(msg);
412
+                }
413
+                Ok(())
414
+            }
415
+        }
416
+    }
417
+
418
+    /// Close fetch modal
419
+    pub fn close_fetch(&mut self) {
420
+        self.input_mode = InputMode::Navigation;
421
+    }
422
+
423
+    /// Force show fetch modal (for testing)
424
+    pub fn show_fetch_modal(&mut self) {
425
+        let remotes = self.repo.get_remotes();
426
+        if remotes.is_empty() {
427
+            self.set_status("No remotes configured".to_string());
428
+            return;
429
+        }
430
+        self.input_mode = InputMode::Fetch {
431
+            remotes,
432
+            selected: 0,
433
+            status: crate::types::FetchStatus::SelectRemote,
434
+        };
379435
     }
380436
 
381437
     /// Pull from remote - shows remote selector if no upstream configured
src/git.rsmodified
@@ -343,6 +343,29 @@ impl GitRepo {
343343
         }
344344
     }
345345
 
346
+    /// Fetch from a specific remote
347
+    pub fn fetch_from_remote(&self, remote: &str) -> Result<()> {
348
+        let output = Command::new("git")
349
+            .args(["fetch", remote])
350
+            .stdout(std::process::Stdio::piped())
351
+            .stderr(std::process::Stdio::piped())
352
+            .output()?;
353
+
354
+        if output.status.success() {
355
+            Ok(())
356
+        } else {
357
+            let stderr = String::from_utf8_lossy(&output.stderr);
358
+            let msg = if stderr.contains("Could not read from remote") {
359
+                format!("Cannot reach '{}' - check connection/auth", remote)
360
+            } else if stderr.contains("does not appear to be a git repository") {
361
+                format!("Remote '{}' not found", remote)
362
+            } else {
363
+                format!("Fetch from '{}' failed", remote)
364
+            };
365
+            Err(FussrError::Git(git2::Error::from_str(&msg)))
366
+        }
367
+    }
368
+
346369
     /// Pull from remote (captures output to not corrupt TUI)
347370
     pub fn pull(&self) -> Result<()> {
348371
         let output = Command::new("git")
src/main.rsmodified
@@ -93,6 +93,7 @@ fn run_event_loop(
9393
                     InputMode::Search { .. } => handle_search_key(app, key.code)?,
9494
                     InputMode::Push { .. } => handle_push_key(app, key.code)?,
9595
                     InputMode::Pull { .. } => handle_pull_key(app, key.code)?,
96
+                    InputMode::Fetch { .. } => handle_fetch_key(app, key.code)?,
9697
                     InputMode::Confirm { .. } => handle_confirm_key(app, key.code)?,
9798
                 }
9899
 
@@ -204,6 +205,10 @@ fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers)
204205
                 Err(e) => app.set_status(format!("Fetch failed: {}", e)),
205206
             }
206207
         }
208
+        KeyCode::Char('F') if app.mode == AppMode::Git => {
209
+            // Debug: Force show fetch modal regardless of remote count
210
+            app.show_fetch_modal();
211
+        }
207212
         KeyCode::Char('l') if app.mode == AppMode::Git => {
208213
             match app.pull() {
209214
                 Ok(()) => {}
@@ -444,6 +449,60 @@ fn handle_pull_key(app: &mut App, code: KeyCode) -> Result<()> {
444449
     Ok(())
445450
 }
446451
 
452
+/// Handle keys in fetch mode
453
+fn handle_fetch_key(app: &mut App, code: KeyCode) -> Result<()> {
454
+    use crate::types::FetchStatus;
455
+
456
+    let status = if let InputMode::Fetch { status, .. } = &app.input_mode {
457
+        status.clone()
458
+    } else {
459
+        return Ok(());
460
+    };
461
+
462
+    match status {
463
+        FetchStatus::SelectRemote => {
464
+            match code {
465
+                KeyCode::Esc => app.close_fetch(),
466
+                KeyCode::Char('j') | KeyCode::Down => {
467
+                    if let InputMode::Fetch { remotes, selected, .. } = &mut app.input_mode {
468
+                        if *selected + 1 < remotes.len() {
469
+                            *selected += 1;
470
+                        }
471
+                    }
472
+                }
473
+                KeyCode::Char('k') | KeyCode::Up => {
474
+                    if let InputMode::Fetch { selected, .. } = &mut app.input_mode {
475
+                        if *selected > 0 {
476
+                            *selected -= 1;
477
+                        }
478
+                    }
479
+                }
480
+                KeyCode::Enter => {
481
+                    let remote = if let InputMode::Fetch { remotes, selected, .. } = &app.input_mode {
482
+                        remotes.get(*selected).cloned()
483
+                    } else {
484
+                        None
485
+                    };
486
+
487
+                    if let Some(remote) = remote {
488
+                        app.fetch_from_remote(&remote)?;
489
+                    }
490
+                }
491
+                _ => {}
492
+            }
493
+        }
494
+        FetchStatus::Fetching => {
495
+            // Don't respond to keys while fetching
496
+        }
497
+        FetchStatus::Success | FetchStatus::Failed(_) => {
498
+            // Any key closes the modal
499
+            app.close_fetch();
500
+        }
501
+    }
502
+
503
+    Ok(())
504
+}
505
+
447506
 /// Handle keys in search mode (legacy - not currently used)
448507
 fn handle_search_key(app: &mut App, code: KeyCode) -> Result<()> {
449508
     match code {
src/types.rsmodified
@@ -198,6 +198,15 @@ pub enum PullStatus {
198198
     Failed(String),
199199
 }
200200
 
201
+/// Status of fetch operation
202
+#[derive(Debug, Clone, PartialEq, Eq)]
203
+pub enum FetchStatus {
204
+    SelectRemote,
205
+    Fetching,
206
+    Success,
207
+    Failed(String),
208
+}
209
+
201210
 /// Input mode for special states
202211
 #[derive(Debug, Clone, PartialEq, Eq)]
203212
 pub enum InputMode {
@@ -207,6 +216,7 @@ pub enum InputMode {
207216
     Commit { buffer: String, cursor: usize, amend: bool, status: CommitStatus },
208217
     Push { remotes: Vec<String>, selected: usize, status: PushStatus },
209218
     Pull { remotes: Vec<String>, selected: usize, status: PullStatus },
219
+    Fetch { remotes: Vec<String>, selected: usize, status: FetchStatus },
210220
     Confirm { message: String, action: ConfirmAction },
211221
 }
212222
 
src/ui.rsmodified
@@ -1,5 +1,5 @@
11
 use crate::app::App;
2
-use crate::types::{AppMode, CommitStatus, InputMode, PullStatus, PushStatus, SelectableItem};
2
+use crate::types::{AppMode, CommitStatus, FetchStatus, InputMode, PullStatus, PushStatus, SelectableItem};
33
 use ratatui::{
44
     layout::{Constraint, Direction, Layout, Rect},
55
     style::{Color, Modifier, Style},
@@ -38,6 +38,11 @@ pub fn draw(frame: &mut Frame, app: &App) {
3838
         draw_pull_modal(frame, remotes, *selected, status, &app.branch_name);
3939
     }
4040
 
41
+    // Draw modal overlay if in fetch mode
42
+    if let InputMode::Fetch { remotes, selected, status } = &app.input_mode {
43
+        draw_fetch_modal(frame, remotes, *selected, status);
44
+    }
45
+
4146
     // Draw modal overlay if in confirm mode
4247
     if let InputMode::Confirm { message, .. } = &app.input_mode {
4348
         draw_confirm_modal(frame, message);
@@ -296,6 +301,91 @@ fn draw_pull_modal(frame: &mut Frame, remotes: &[String], selected: usize, statu
296301
     frame.render_widget(widget, modal_area);
297302
 }
298303
 
304
+/// Draw fetch remote selection modal
305
+fn draw_fetch_modal(frame: &mut Frame, remotes: &[String], selected: usize, status: &FetchStatus) {
306
+    let area = frame.area();
307
+
308
+    let modal_height = match status {
309
+        FetchStatus::SelectRemote => (remotes.len() + 4).min(12) as u16,
310
+        _ => 5,
311
+    };
312
+    let modal_width = 50.min(area.width.saturating_sub(4));
313
+    let x = (area.width.saturating_sub(modal_width)) / 2;
314
+    let y = (area.height.saturating_sub(modal_height)) / 2;
315
+
316
+    let modal_area = Rect::new(x, y, modal_width, modal_height);
317
+
318
+    frame.render_widget(Clear, modal_area);
319
+
320
+    let (title, border_color) = match status {
321
+        FetchStatus::SelectRemote => (" Fetch - Select Remote ", Color::Cyan),
322
+        FetchStatus::Fetching => (" Fetching... ", Color::Yellow),
323
+        FetchStatus::Success => (" ✓ Fetched ", Color::Green),
324
+        FetchStatus::Failed(_) => (" ✗ Fetch Failed ", Color::Red),
325
+    };
326
+
327
+    let block = Block::default()
328
+        .title(title)
329
+        .borders(Borders::ALL)
330
+        .border_style(Style::default().fg(border_color));
331
+
332
+    let content: Vec<Line> = match status {
333
+        FetchStatus::SelectRemote => {
334
+            let mut lines = vec![
335
+                Line::from(Span::styled(
336
+                    "  Select remote to fetch from:",
337
+                    Style::default().fg(Color::White),
338
+                )),
339
+                Line::from(""),
340
+            ];
341
+            for (i, remote) in remotes.iter().enumerate() {
342
+                let style = if i == selected {
343
+                    Style::default().add_modifier(Modifier::REVERSED)
344
+                } else {
345
+                    Style::default()
346
+                };
347
+                lines.push(Line::from(Span::styled(format!("    {}", remote), style)));
348
+            }
349
+            lines.push(Line::from(""));
350
+            lines.push(Line::from(Span::styled(
351
+                "  j/k:select Enter:fetch ESC:cancel",
352
+                Style::default().fg(Color::DarkGray),
353
+            )));
354
+            lines
355
+        }
356
+        FetchStatus::Fetching => {
357
+            vec![
358
+                Line::from(""),
359
+                Line::from(Span::styled(
360
+                    "  Fetching from remote...",
361
+                    Style::default().fg(Color::Yellow),
362
+                )),
363
+            ]
364
+        }
365
+        FetchStatus::Success => {
366
+            vec![
367
+                Line::from(""),
368
+                Line::from(Span::styled(
369
+                    "  ✓ Fetch complete!",
370
+                    Style::default().fg(Color::Green),
371
+                )),
372
+            ]
373
+        }
374
+        FetchStatus::Failed(msg) => {
375
+            vec![
376
+                Line::from(""),
377
+                Line::from(Span::styled(
378
+                    format!("  ✗ {}", msg),
379
+                    Style::default().fg(Color::Red),
380
+                )),
381
+            ]
382
+        }
383
+    };
384
+
385
+    let widget = Paragraph::new(content).block(block);
386
+    frame.render_widget(widget, modal_area);
387
+}
388
+
299389
 /// Draw confirmation modal
300390
 fn draw_confirm_modal(frame: &mut Frame, message: &str) {
301391
     let area = frame.area();