feat: push checkout #14
@@ -235,6 +235,35 @@ impl CheckoutsStore {
|
||||
self.refresh(cx);
|
||||
}
|
||||
|
||||
/// The checkout at `path` was just pushed to the remote.
|
||||
///
|
||||
/// Its ready-to-push status is obsolete. Drop it from the cached statuses
|
||||
/// and notify observers right away, so the sidebar badge and the push
|
||||
/// banner update immediately instead of waiting for the next background
|
||||
/// pass, which re-scans and re-fetches the remote. The debounced refresh
|
||||
/// reconciles the remaining checkouts of the repository afterwards.
|
||||
pub fn checkout_pushed(&mut self, addr: &RepoAddr, path: &Path, cx: &mut Context<Self>) {
|
||||
let mut removed = false;
|
||||
|
||||
if let Some(statuses) = self.push_statuses.get_mut(addr) {
|
||||
let before = statuses.len();
|
||||
statuses.retain(|status| status.path.as_path() != path);
|
||||
removed = statuses.len() != before;
|
||||
|
||||
if removed && statuses.is_empty() {
|
||||
self.push_statuses.remove(addr);
|
||||
}
|
||||
}
|
||||
|
||||
if removed {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// The other checkouts of this repository still need re-deriving
|
||||
// against the remote, now that the pushed refs landed there.
|
||||
self.request_push_statuses(addr, cx);
|
||||
}
|
||||
|
||||
/// The ready-to-push statuses of `addr`.
|
||||
///
|
||||
/// Empty while none are known or nothing is unpushed.
|
||||
@@ -402,7 +431,7 @@ impl CheckoutsStore {
|
||||
// Keep the statuses current while any repository panel is open.
|
||||
this.update(cx, |this, cx| {
|
||||
if poll && this.refresh.idle() {
|
||||
this.refresh.debounce();
|
||||
this.refresh.poll();
|
||||
|
||||
// Open panels get the fast cadence.
|
||||
// Each cycle fetches every watched checkout's remote.
|
||||
@@ -414,7 +443,13 @@ impl CheckoutsStore {
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(delay).await;
|
||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
||||
this.update(cx, |this, cx| {
|
||||
// A request that arrived while the poll was pending
|
||||
// superseded it with its own debounce; skip the stale poll.
|
||||
if this.refresh.take_poll() {
|
||||
this.run_refresh(cx);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.push_task(task);
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
/// [`crate::RepoStore`], [`crate::RepoListStore`] and [`crate::CheckoutsStore`]
|
||||
/// re-query their inputs on a debounce timer with the same policy:
|
||||
/// a request arriving while a run is in flight is folded into a follow-up run,
|
||||
/// a request arriving while the debounce timer is pending is dropped by it.
|
||||
/// a request arriving while a request debounce is pending is dropped by it.
|
||||
///
|
||||
/// A slow poll cycle (`poll`) is different: it keeps a store's derived data
|
||||
/// fresh while nothing is happening, but it must never delay a real request.
|
||||
/// A request arriving while a poll is pending supersedes the poll with its own
|
||||
/// short debounce, so external events (a push landing, a settings change)
|
||||
/// propagate promptly instead of waiting out the poll interval.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RefreshGate {
|
||||
/// A run is in flight.
|
||||
@@ -12,6 +18,10 @@ pub struct RefreshGate {
|
||||
dirty: bool,
|
||||
/// The debounce timer is pending.
|
||||
debouncing: bool,
|
||||
/// The pending debounce is a poll cycle, not a request.
|
||||
///
|
||||
/// Polls wait for a quiet moment; requests supersede them.
|
||||
poll: bool,
|
||||
}
|
||||
|
||||
/// What a refresh request decided.
|
||||
@@ -41,28 +51,47 @@ impl RefreshGate {
|
||||
|
||||
/// A new refresh request arrived.
|
||||
///
|
||||
/// Folded into a follow-up run while one is in flight, dropped while the
|
||||
/// debounce timer is pending, otherwise starts the timer.
|
||||
/// Folded into a follow-up run while one is in flight or a request debounce
|
||||
/// is pending, superseding a slow poll with the request's own debounce,
|
||||
/// otherwise starts the debounce timer.
|
||||
pub fn request(&mut self) -> RefreshRequest {
|
||||
if self.running {
|
||||
self.dirty = true;
|
||||
RefreshRequest::Fold
|
||||
} else if self.debouncing {
|
||||
} else if self.debouncing && !self.poll {
|
||||
RefreshRequest::Fold
|
||||
} else {
|
||||
// A request supersedes a pending poll: start the short debounce.
|
||||
self.debouncing = true;
|
||||
self.poll = false;
|
||||
RefreshRequest::Schedule
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer was started without a request, e.g. a poll cycle.
|
||||
pub fn debounce(&mut self) {
|
||||
/// A slow poll timer was started without a request.
|
||||
pub fn poll(&mut self) {
|
||||
self.debouncing = true;
|
||||
self.poll = true;
|
||||
}
|
||||
|
||||
/// A poll timer fired. Whether it is still the scheduled pass and may run.
|
||||
///
|
||||
/// A request that arrived while the poll was pending superseded it with its
|
||||
/// own debounce, so the stale poll timer is skipped.
|
||||
pub fn take_poll(&mut self) -> bool {
|
||||
if self.debouncing && self.poll {
|
||||
self.debouncing = false;
|
||||
self.poll = false;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The debounce timer fired and the run starts now.
|
||||
pub fn begin(&mut self) {
|
||||
self.debouncing = false;
|
||||
self.poll = false;
|
||||
self.running = true;
|
||||
}
|
||||
|
||||
@@ -77,3 +106,83 @@ impl RefreshGate {
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RefreshGate, RefreshRequest};
|
||||
|
||||
#[test]
|
||||
fn request_starts_the_debounce_when_idle() {
|
||||
let mut gate = RefreshGate::default();
|
||||
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||
assert!(gate.debouncing());
|
||||
assert!(!gate.idle());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_folds_into_a_request_debounce() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.request();
|
||||
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_folds_into_a_running_run_and_runs_again() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.request();
|
||||
gate.begin();
|
||||
assert!(gate.running());
|
||||
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||
assert!(gate.finish());
|
||||
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_supersedes_a_pending_poll() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.poll();
|
||||
assert!(gate.debouncing());
|
||||
|
||||
// The request starts its own short debounce instead of waiting out the poll.
|
||||
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||
assert!(gate.debouncing());
|
||||
assert!(
|
||||
!gate.take_poll(),
|
||||
"the superseded poll timer must be skipped"
|
||||
);
|
||||
|
||||
// The request's own debounce still fires.
|
||||
gate.begin();
|
||||
assert!(gate.running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poll_timer_runs_when_not_superseded() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.poll();
|
||||
assert!(gate.take_poll());
|
||||
assert!(gate.idle());
|
||||
gate.begin();
|
||||
assert!(gate.running());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poll_keeps_scheduling_until_a_request_preempts() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.poll();
|
||||
assert!(gate.take_poll());
|
||||
gate.begin();
|
||||
gate.finish();
|
||||
gate.poll();
|
||||
gate.request();
|
||||
assert!(!gate.take_poll(), "preempted by the request");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_after_a_superseded_poll_is_folded_into_the_new_debounce() {
|
||||
let mut gate = RefreshGate::default();
|
||||
gate.poll();
|
||||
gate.request();
|
||||
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,6 +1133,7 @@ impl RepoStore {
|
||||
self.last_error = None;
|
||||
cx.notify();
|
||||
|
||||
let checkouts = CheckoutsStore::global(cx);
|
||||
let backend = Backend::global(cx);
|
||||
let push = backend.update(cx, |backend, cx| {
|
||||
backend.push_checkout(announcement, path.clone(), head, cx)
|
||||
@@ -1147,10 +1148,8 @@ impl RepoStore {
|
||||
match &result {
|
||||
Ok(()) => {
|
||||
// The remote moved, so recompute the ready-to-push statuses.
|
||||
let checkouts = CheckoutsStore::global(cx);
|
||||
checkouts.update(cx, |store, cx| {
|
||||
store.request_push_statuses(&addr, cx);
|
||||
cx.notify();
|
||||
store.checkout_pushed(&addr, &path, cx);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user