From ef88d50c22f586b459a29d13e0dda87c78dee74f Mon Sep 17 00:00:00 2001 From: Lars Jansen Date: Sun, 1 Nov 2026 11:29:38 +0100 Subject: [PATCH] fix: run what is asked for while git is busy next, instead of dropping it --- CONTRIBUTING.md | 5 +++-- crates/app/src/app.rs | 16 +++++++++++++--- crates/app/src/tests.rs | 17 ++++++++++++++++- crates/app/src/ui.rs | 5 ++--- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fcc339b..def79ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,8 +68,9 @@ Its tests run against real repositories made with `git` in a temporary folder - `app.rs` holds the state. Views never change it directly: they push an `Action`, which `Workspace::apply` carries out after the frame. Anything that touches the repository runs on a - background thread and is followed by a reload, so the window never waits. The tab on screen - also reloads by itself when its watcher saw a change. + background thread and is followed by a reload, so the window never waits; what the user asks for + in the meantime runs next. The tab on screen also reloads by itself when its watcher saw a + change. - `start.rs` (open, clone and create), `ui.rs` (tabs and toolbar), `graph.rs`, `sidebar.rs`, `details.rs` and `diff.rs` draw the views, and `theme.rs` has the colours. diff --git a/crates/app/src/app.rs b/crates/app/src/app.rs index 2e007be..5ae9395 100644 --- a/crates/app/src/app.rs +++ b/crates/app/src/app.rs @@ -180,6 +180,8 @@ pub struct Workspace { /// The graph rows on screen. pub visible: Range, job: Option, + /// What the user asked for while a job ran, to run after it, in order. + queue: Vec, /// Set when something changed since the last reload began. changed: Arc, /// Keeps `changed` up to date. @@ -207,6 +209,7 @@ impl Workspace { scroll_to: None, visible: 0..0, job: None, + queue: Vec::new(), changed: Arc::default(), watch: None, quiet_until: Instant::now(), @@ -294,7 +297,10 @@ impl Workspace { } fn start(&mut self, ctx: &egui::Context, op: Option) { - if self.job.is_none() { + if self.job.is_some() { + // A change waits its turn; a reload follows the running job anyway. + self.queue.extend(op); + } else { self.spawn(ctx, op, false); } } @@ -322,8 +328,8 @@ impl Workspace { self.job = Some(Job { op, outcome, started: Instant::now() }); } - /// Takes in the result of the background job, once it is there. - pub fn poll(&mut self) { + /// Takes in the result of the background job, once it is there, and starts the next. + pub fn poll(&mut self, ctx: &egui::Context) { let Some(job) = &self.job else { return }; let (failed, snapshot, watch) = match job.outcome.try_recv() { Err(TryRecvError::Empty) => return, @@ -332,6 +338,10 @@ impl Workspace { }; let job = self.job.take().expect("a job is running"); self.quiet_until = Instant::now() + job.started.elapsed().max(Duration::from_millis(100)); + if !self.queue.is_empty() { + let next = self.queue.remove(0); + self.spawn(ctx, Some(next), false); + } let op = job.op; if failed.is_none() && matches!(op, Some(Op::Commit { .. })) { self.summary.clear(); diff --git a/crates/app/src/tests.rs b/crates/app/src/tests.rs index 23d04ab..f75c9f5 100644 --- a/crates/app/src/tests.rs +++ b/crates/app/src/tests.rs @@ -11,7 +11,7 @@ use egui_kittest::kittest::Queryable; use gitcraft_core::git::{Git, Op}; use tempfile::TempDir; -use crate::app::Selection; +use crate::app::{Action, Selection}; use crate::start::percent; use crate::ui::{App, KEYS}; @@ -288,6 +288,21 @@ fn discard_single_lines_and_hunks() { assert!(harness.query_by_label("Uncommitted changes").is_none()); } +#[test] +fn what_is_asked_for_while_git_is_busy_runs_next() { + let (_dir, git) = repo(); + fs::write(git.dir().join("file.txt"), "changed").unwrap(); + let mut harness = app(Some(git.dir())); + let ctx = harness.ctx.clone(); + let ws = harness.state_mut().workspace_mut().unwrap(); + ws.apply(&ctx, Action::Refresh); + ws.apply(&ctx, Action::Run(Op::Stage(vec!["file.txt".into()]))); + ws.apply(&ctx, Action::Run(Op::Commit { message: "Second commit".into(), amend: false })); + settle(&mut harness); + + assert_eq!(log(&git), "Second commit\nFirst commit\n"); +} + #[test] fn changes_made_outside_show_up_by_themselves() { let (_dir, git) = repo(); diff --git a/crates/app/src/ui.rs b/crates/app/src/ui.rs index 76348db..c9c8a88 100644 --- a/crates/app/src/ui.rs +++ b/crates/app/src/ui.rs @@ -97,7 +97,7 @@ impl App { for path in std::mem::take(&mut self.pending) { self.open(&ctx, &path); } - self.tabs.iter_mut().for_each(Workspace::poll); + self.tabs.iter_mut().for_each(|ws| ws.poll(&ctx)); // A tab out of sight reloads when it is shown. if let Some(ws) = self.workspace_mut() { ws.reload_if_changed(&ctx); @@ -269,8 +269,7 @@ fn confirm(ctx: &egui::Context, ws: &mut Workspace, actions: &mut Vec) { ui.strong(&confirm.question); ui.label("This cannot be undone."); ui.horizontal(|ui| { - let verb = Button::new(egui::RichText::new(confirm.verb).color(theme::ERROR)); - if ui.add_enabled(ws.busy().is_none(), verb).clicked() { + if ui.button(egui::RichText::new(confirm.verb).color(theme::ERROR)).clicked() { actions.push(Action::Run(confirm.op.clone())); done = true; }