Files
zed/crates/project/src/git_store/job_debug_queue.rs
T
Cole Miller 16ede263e4 git: Degrade gracefully when refreshing git state (#57292)
This PR changes the git store's `compute_snapshot`, which runs to update
state that depends on the contents of `.git`, to degrade gracefully when
fetching individual pieces of state fails. For example, when fetching
the list of branches fails, instead of returning early from the function
(leaving the previous git state snapshot in place with stale state), we
continue with an empty list of branches. This prevents failures of
individual git commands from making the entire git UI get stuck
indefinitely.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- Fixed an issue where failing to fetch branches using the git CLI would
prevent other git-related state from being updated.
2026-05-25 16:15:12 +00:00

237 lines
7.3 KiB
Rust

use std::{collections::VecDeque, time::Instant};
use gpui::SharedString;
use super::JobId;
pub struct GitJobDebugQueue {
pending: VecDeque<PendingJob>,
running: VecDeque<RunningJob>,
completed: VecDeque<CompletedJob>,
}
const MAX_COMPLETED_JOBS: usize = 500;
#[derive(Clone, Debug)]
pub struct PendingJob {
pub id: JobId,
pub description: SharedString,
pub key: Option<SharedString>,
pub enqueued_at: Instant,
}
#[derive(Clone, Debug)]
pub struct RunningJob {
pub id: JobId,
pub description: SharedString,
pub key: Option<SharedString>,
pub enqueued_at: Instant,
pub started_at: Instant,
}
#[derive(Clone, Debug)]
pub struct CompletedJob {
pub id: JobId,
pub description: SharedString,
pub key: Option<SharedString>,
pub enqueued_at: Instant,
pub started_at: Option<Instant>,
pub completed_at: Instant,
pub status: CompletedJobStatus,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompletedJobStatus {
Finished,
Skipped,
}
impl GitJobDebugQueue {
pub fn new() -> Self {
Self {
pending: VecDeque::new(),
running: VecDeque::new(),
completed: VecDeque::new(),
}
}
pub fn add(&mut self, id: JobId, description: &'static str, key: Option<SharedString>) {
self.pending.push_back(PendingJob {
id,
description: description.into(),
key,
enqueued_at: Instant::now(),
});
}
pub fn mark_running(&mut self, id: JobId) {
let Some(index) = self.pending.iter().position(|job| job.id == id) else {
return;
};
// Safe to unwrap: `index` was just found by `position()`, so it's in bounds.
let pending = self.pending.remove(index).unwrap();
self.running.push_back(RunningJob {
id: pending.id,
description: pending.description,
key: pending.key,
enqueued_at: pending.enqueued_at,
started_at: Instant::now(),
});
}
pub fn mark_unfinished_complete(&mut self, status: CompletedJobStatus) {
let ids = self
.pending
.iter()
.map(|job| job.id)
.chain(self.running.iter().map(|job| job.id))
.collect::<Vec<_>>();
for id in ids {
self.mark_complete(id, status);
}
}
pub fn mark_complete(&mut self, id: JobId, status: CompletedJobStatus) {
let (enqueued_at, started_at, description, key) =
if let Some(index) = self.running.iter().position(|job| job.id == id) {
let running = self.running.remove(index).unwrap();
(
running.enqueued_at,
Some(running.started_at),
running.description,
running.key,
)
} else if let Some(index) = self.pending.iter().position(|job| job.id == id) {
let pending = self.pending.remove(index).unwrap();
(pending.enqueued_at, None, pending.description, pending.key)
} else {
return;
};
self.completed.push_back(CompletedJob {
id,
description,
key,
enqueued_at,
started_at,
completed_at: Instant::now(),
status,
});
while self.completed.len() > MAX_COMPLETED_JOBS {
self.completed.pop_front();
}
}
pub fn to_debug_string(&self) -> String {
serde_json::to_string_pretty(&self.to_debug_value()).unwrap_or_default()
}
pub fn to_debug_value(&self) -> serde_json::Value {
let mut entries = Vec::new();
let mut pending_count = 0u64;
let mut running_count = 0u64;
let mut finished_count = 0u64;
let mut skipped_count = 0u64;
for job in &self.pending {
pending_count += 1;
entries.push((job.enqueued_at, self.format_pending(job)));
}
for job in &self.running {
running_count += 1;
entries.push((job.enqueued_at, self.format_running(job)));
}
for job in &self.completed {
match job.status {
CompletedJobStatus::Finished => finished_count += 1,
CompletedJobStatus::Skipped => skipped_count += 1,
}
entries.push((job.enqueued_at, self.format_completed(job)));
}
entries.sort_by_key(|(enqueued_at, _)| *enqueued_at);
let json_entries: Vec<serde_json::Value> =
entries.into_iter().map(|(_, json)| json).collect();
serde_json::json!({
"summary": {
"pending": pending_count,
"running": running_count,
"finished": finished_count,
"skipped": skipped_count,
},
"entries": json_entries,
})
}
fn format_pending(&self, job: &PendingJob) -> serde_json::Value {
serde_json::json!({
"id": job.id,
"description": job.description.as_ref(),
"key": job.key.as_ref().map(|k| k.as_ref()),
"status": "Pending",
"enqueued": format!("{} ago", format_duration(job.enqueued_at.elapsed())),
})
}
fn format_running(&self, job: &RunningJob) -> serde_json::Value {
serde_json::json!({
"id": job.id,
"description": job.description.as_ref(),
"key": job.key.as_ref().map(|k| k.as_ref()),
"status": "Running",
"enqueued": format!("{} ago", format_duration(job.enqueued_at.elapsed())),
"wait_time": format_duration(job.started_at.duration_since(job.enqueued_at)),
"run_time": format!("{} (still running)", format_duration(job.started_at.elapsed())),
})
}
fn format_completed(&self, job: &CompletedJob) -> serde_json::Value {
let status = match job.status {
CompletedJobStatus::Finished => "Finished",
CompletedJobStatus::Skipped => "Skipped",
};
let (wait_time, run_time) = if let Some(started) = job.started_at {
let wait = format_duration(started.duration_since(job.enqueued_at));
let run = format_duration(job.completed_at.duration_since(started));
(wait, Some(run))
} else {
let wait = format!(
"{} (skipped)",
format_duration(job.completed_at.duration_since(job.enqueued_at))
);
(wait, None)
};
serde_json::json!({
"id": job.id,
"description": job.description.as_ref(),
"key": job.key.as_ref().map(|k| k.as_ref()),
"status": status,
"enqueued": format!("{} ago", format_duration(job.enqueued_at.elapsed())),
"wait_time": wait_time,
"run_time": run_time,
})
}
}
fn format_duration(duration: std::time::Duration) -> String {
let secs = duration.as_secs_f64();
if secs < 0.001 {
format!("{:.0}us", secs * 1_000_000.0)
} else if secs < 1.0 {
format!("{:.0}ms", secs * 1000.0)
} else if secs < 60.0 {
format!("{:.0}s", secs)
} else if secs < 3600.0 {
format!("{:.0}m", secs / 60.0)
} else {
format!("{:.0}h", secs / 3600.0)
}
}