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.
This commit is contained in:
Cole Miller 2026-05-25 12:15:12 -04:00 committed by GitHub
parent 6a747984d3
commit bcfbf669bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 304 additions and 198 deletions

View file

@ -489,7 +489,7 @@ impl GitRepository for FakeGitRepository {
})
}
fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
fn stash_entries(&self) -> BoxFuture<'static, Result<git::stash::GitStash>> {
self.with_state_async(false, |state| Ok(state.stash_entries.clone()))
}
@ -1127,7 +1127,7 @@ impl GitRepository for FakeGitRepository {
fn diff_stat(
&self,
path_prefixes: &[RepoPath],
) -> BoxFuture<'_, Result<git::status::GitDiffStat>> {
) -> BoxFuture<'static, Result<git::status::GitDiffStat>> {
fn count_lines(s: &str) -> u32 {
if s.is_empty() {
0

View file

@ -807,7 +807,7 @@ pub trait GitRepository: Send + Sync {
fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>;
fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>>;
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>>;
fn stash_entries(&self) -> BoxFuture<'static, Result<GitStash>>;
fn branches(&self) -> BoxFuture<'_, Result<BranchesScanResult>>;
@ -982,7 +982,7 @@ pub trait GitRepository: Send + Sync {
fn diff_stat(
&self,
path_prefixes: &[RepoPath],
) -> BoxFuture<'_, Result<crate::status::GitDiffStat>>;
) -> BoxFuture<'static, Result<crate::status::GitDiffStat>>;
/// Creates a checkpoint for the repository.
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
@ -1787,7 +1787,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>> {
fn stash_entries(&self) -> BoxFuture<'static, Result<GitStash>> {
let git_binary = self.git_binary_in_worktree();
self.executor
.spawn(async move {
@ -2142,7 +2142,7 @@ impl GitRepository for RealGitRepository {
fn diff_stat(
&self,
path_prefixes: &[RepoPath],
) -> BoxFuture<'_, Result<crate::status::GitDiffStat>> {
) -> BoxFuture<'static, Result<crate::status::GitDiffStat>> {
let path_prefixes = path_prefixes.to_vec();
let git_binary = self.git_binary_in_worktree();

View file

@ -586,7 +586,7 @@ pub struct DiffStat {
pub deleted: u32,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct GitDiffStat {
pub entries: Arc<[(RepoPath, DiffStat)]>,
}

View file

@ -302,6 +302,7 @@ pub struct RepositorySnapshot {
pub id: RepositoryId,
pub statuses_by_path: SumTree<StatusEntry>,
pub work_directory_abs_path: Arc<Path>,
pub dot_git_abs_path: Arc<Path>,
/// Absolute path to the directory holding this worktree's Git state.
///
/// For a linked worktree this is the worktree-specific directory under the
@ -381,6 +382,7 @@ pub struct Repository {
// and that should be examined during the next status scan.
paths_needing_status_update: Vec<Vec<RepoPath>>,
job_sender: mpsc::UnboundedSender<GitJob>,
_worker_task: Task<()>,
active_jobs: HashMap<JobId, JobInfo>,
job_debug_queue: job_debug_queue::GitJobDebugQueue,
pending_ops: SumTree<PendingOps>,
@ -391,14 +393,6 @@ pub struct Repository {
initial_graph_data: HashMap<(LogSource, LogOrder), InitialGitGraphData>,
commit_data_handler: CommitDataHandlerState,
commit_data: HashMap<Oid, CommitDataState>,
refetch_repo_state: Arc<
dyn Fn(
&mut Context<Self>,
) -> (
mpsc::UnboundedSender<GitJob>,
Shared<Task<Result<RepositoryState, String>>>,
),
>,
}
impl std::ops::Deref for Repository {
@ -547,14 +541,36 @@ impl GitStore {
let (mut watcher, _) = watcher.await;
while let Some(_) = watcher.next().await {
let Ok(_) = this.update(cx, |this, cx| {
for repo in this.repositories.values() {
repo.update(cx, |this, cx| {
if this.job_sender.is_closed() {
let (job_sender, state) = (this.refetch_repo_state)(cx);
this.repository_state = state;
this.job_sender = job_sender;
this.schedule_scan(None, cx);
}
let GitStoreState::Local {
project_environment,
fs,
..
} = &this.state
else {
return;
};
let project_environment = project_environment.downgrade();
let fs = fs.clone();
let repositories_to_respawn = this
.repositories
.iter()
.filter_map(|(repository_id, repo)| {
repo.read(cx)
.job_sender
.is_closed()
.then_some((*repository_id, repo.clone()))
})
.collect::<Vec<_>>();
for (repository_id, repo) in repositories_to_respawn {
let is_trusted = this.repository_is_trusted(repository_id, cx);
repo.update(cx, |repo, cx| {
repo.respawn_local_worker(
project_environment.clone(),
fs.clone(),
is_trusted,
cx,
);
repo.schedule_scan(None, cx);
})
}
cx.emit(GitStoreEvent::GlobalConfigurationUpdated);
@ -1599,6 +1615,21 @@ impl GitStore {
cx.emit(GitStoreEvent::JobsUpdated)
}
fn repository_is_trusted(&self, repository_id: RepositoryId, cx: &mut Context<Self>) -> bool {
let Some(worktree_ids) = self.worktree_ids.get(&repository_id) else {
return false;
};
let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) else {
return false;
};
worktree_ids.iter().any(|worktree_id| {
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust(&self.worktree_store, *worktree_id, cx)
})
})
}
/// Update our list of repositories and schedule git scans in response to a notification from a worktree,
fn update_repositories_from_worktree(
&mut self,
@ -1628,10 +1659,44 @@ impl GitStore {
.entry(repo_id)
.or_insert_with(HashSet::new)
.insert(worktree_id);
existing.update(cx, |existing, cx| {
existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
existing.schedule_scan(updates_tx.clone(), cx);
});
let path_changed = update.old_work_directory_abs_path.as_ref()
!= update.new_work_directory_abs_path.as_ref();
if path_changed
&& let Some(dot_git_abs_path) = update.dot_git_abs_path.clone()
&& let Some(repository_dir_abs_path) =
update.repository_dir_abs_path.clone()
&& let Some(common_dir_abs_path) = update.common_dir_abs_path.clone()
{
let is_trusted = TrustedWorktrees::try_get_global(cx)
.map(|trusted_worktrees| {
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust(
&self.worktree_store,
worktree_id,
cx,
)
})
})
.unwrap_or(false);
existing.update(cx, |existing, cx| {
existing.reinitialize_local_backend(
new_work_directory_abs_path,
dot_git_abs_path,
repository_dir_abs_path,
common_dir_abs_path,
project_environment.downgrade(),
fs.clone(),
is_trusted,
cx,
);
existing.schedule_scan(updates_tx.clone(), cx);
});
} else {
existing.update(cx, |existing, cx| {
existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
existing.schedule_scan(updates_tx.clone(), cx);
});
}
} else {
if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) {
worktree_ids.remove(&worktree_id);
@ -4107,11 +4172,14 @@ impl RepositorySnapshot {
id: RepositoryId,
work_directory_abs_path: Arc<Path>,
repository_dir_abs_path: Option<Arc<Path>>,
dot_git_abs_path: Option<Arc<Path>>,
common_dir_abs_path: Option<Arc<Path>>,
path_style: PathStyle,
) -> Self {
let repository_dir_abs_path =
repository_dir_abs_path.unwrap_or_else(|| work_directory_abs_path.join(".git").into());
let dot_git_abs_path =
dot_git_abs_path.unwrap_or_else(|| work_directory_abs_path.join(".git").into());
let common_dir_abs_path =
common_dir_abs_path.unwrap_or_else(|| repository_dir_abs_path.clone());
@ -4119,6 +4187,7 @@ impl RepositorySnapshot {
id,
statuses_by_path: Default::default(),
repository_dir_abs_path,
dot_git_abs_path,
common_dir_abs_path,
work_directory_abs_path,
branch: None,
@ -4406,7 +4475,7 @@ impl MergeDetails {
&mut self,
backend: &Arc<dyn GitRepository>,
current_conflicted_paths: Vec<RepoPath>,
) -> Result<bool> {
) -> bool {
log::debug!("load merge details");
self.message = backend.merge_message().await.map(SharedString::from);
let heads = backend
@ -4447,7 +4516,7 @@ impl MergeDetails {
keep
});
Ok(conflicts_changed)
conflicts_changed
}
}
@ -4477,6 +4546,66 @@ impl Repository {
.cloned()
}
fn respawn_local_worker(
&mut self,
project_environment: WeakEntity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
is_trusted: bool,
cx: &mut Context<Self>,
) {
let work_directory_abs_path = self.snapshot.work_directory_abs_path.clone();
let dot_git_abs_path = self.snapshot.dot_git_abs_path.clone();
let state = cx
.spawn(async move |_, cx| {
LocalRepositoryState::new(
work_directory_abs_path,
dot_git_abs_path,
project_environment,
fs,
is_trusted,
cx,
)
.await
.map_err(|err| err.to_string())
})
.shared();
self.job_sender.close_channel();
self._worker_task = Task::ready(());
self.active_jobs.clear();
self.job_debug_queue
.mark_unfinished_complete(job_debug_queue::CompletedJobStatus::Skipped);
cx.notify();
let (job_sender, worker_task) = Repository::spawn_local_git_worker(state.clone(), cx);
self.job_sender = job_sender;
self._worker_task = worker_task;
self.repository_state = cx
.spawn(async move |_, _| {
let state = state.await?;
Ok(RepositoryState::Local(state))
})
.shared();
}
fn reinitialize_local_backend(
&mut self,
work_directory_abs_path: Arc<Path>,
dot_git_abs_path: Arc<Path>,
repository_dir_abs_path: Arc<Path>,
common_dir_abs_path: Arc<Path>,
project_environment: WeakEntity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
is_trusted: bool,
cx: &mut Context<Self>,
) {
self.snapshot.work_directory_abs_path = work_directory_abs_path;
self.snapshot.dot_git_abs_path = dot_git_abs_path;
self.snapshot.repository_dir_abs_path = repository_dir_abs_path;
self.snapshot.common_dir_abs_path = common_dir_abs_path;
self.respawn_local_worker(project_environment, fs, is_trusted, cx);
}
fn local(
id: RepositoryId,
work_directory_abs_path: Arc<Path>,
@ -4491,64 +4620,35 @@ impl Repository {
) -> Self {
let snapshot = RepositorySnapshot::empty(
id,
work_directory_abs_path.clone(),
work_directory_abs_path,
Some(repository_dir_abs_path),
Some(dot_git_abs_path),
Some(common_dir_abs_path),
PathStyle::local(),
);
let refetch_repo_state = Arc::new(move |cx: &mut Context<Self>| {
let work_directory_abs_path = work_directory_abs_path.clone();
let dot_git_abs_path = dot_git_abs_path.clone();
let project_environment = project_environment.clone();
let fs = fs.clone();
let state = cx
.spawn(async move |_, cx| {
LocalRepositoryState::new(
work_directory_abs_path,
dot_git_abs_path,
project_environment,
fs,
is_trusted,
cx,
)
.await
.map_err(|err| err.to_string())
})
.shared();
let job_sender = Repository::spawn_local_git_worker(state.clone(), cx);
let state = cx
.spawn(async move |_, _| {
let state = state.await?;
Ok(RepositoryState::Local(state))
})
.shared();
(job_sender, state)
});
let (job_sender, state) = (refetch_repo_state)(cx);
cx.subscribe_self(Self::handle_subscribe_self).detach();
Repository {
let mut repo = Repository {
this: cx.weak_entity(),
git_store,
snapshot,
pending_ops: Default::default(),
repository_state: state,
repository_state: Task::ready(Err("not yet initialized".into())).shared(),
_worker_task: Task::ready(()),
commit_message_buffer: None,
askpass_delegates: Default::default(),
paths_needing_status_update: Default::default(),
latest_askpass_id: 0,
job_sender,
job_sender: mpsc::unbounded().0,
job_id: 0,
active_jobs: Default::default(),
job_debug_queue: job_debug_queue::GitJobDebugQueue::new(),
initial_graph_data: Default::default(),
commit_data: Default::default(),
commit_data_handler: CommitDataHandlerState::Closed,
refetch_repo_state,
}
};
repo.respawn_local_worker(project_environment, fs, is_trusted, cx);
cx.subscribe_self(Self::handle_subscribe_self).detach();
repo
}
fn remote(
@ -4566,21 +4666,14 @@ impl Repository {
id,
work_directory_abs_path,
repository_dir_abs_path,
None,
common_dir_abs_path,
path_style,
);
let refetch_repo_state = Arc::new(move |cx: &mut Context<Self>| {
let repository_state = RemoteRepositoryState {
project_id,
client: client.clone(),
};
let job_sender = Self::spawn_remote_git_worker(repository_state.clone(), cx);
let repository_state =
Task::ready(Ok(RepositoryState::Remote(repository_state))).shared();
(job_sender, repository_state)
});
let (job_sender, repository_state) = (refetch_repo_state)(cx);
let repository_state = RemoteRepositoryState { project_id, client };
let (job_sender, worker_task) = Self::spawn_remote_git_worker(repository_state.clone(), cx);
let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared();
cx.subscribe_self(Self::handle_subscribe_self).detach();
Self {
@ -4591,6 +4684,7 @@ impl Repository {
pending_ops: Default::default(),
paths_needing_status_update: Default::default(),
job_sender,
_worker_task: worker_task,
repository_state,
askpass_delegates: Default::default(),
latest_askpass_id: 0,
@ -4600,7 +4694,6 @@ impl Repository {
initial_graph_data: Default::default(),
commit_data: Default::default(),
commit_data_handler: CommitDataHandlerState::Closed,
refetch_repo_state,
}
}
@ -7807,7 +7900,7 @@ impl Repository {
let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else {
bail!("not a local repository")
};
let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await?;
let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await;
this.update(&mut cx, |this, cx| {
this.clear_pending_ops(cx);
});
@ -7824,11 +7917,13 @@ impl Repository {
fn spawn_local_git_worker(
state: Shared<Task<Result<LocalRepositoryState, String>>>,
cx: &mut Context<Self>,
) -> mpsc::UnboundedSender<GitJob> {
) -> (mpsc::UnboundedSender<GitJob>, Task<()>) {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
cx.spawn(async move |this, cx| {
let state = state.await.map_err(|err| anyhow::anyhow!(err))?;
let worker_task = cx.spawn(async move |this, cx| {
let Some(state) = state.await.log_err() else {
return;
};
if let Some(git_hosting_provider_registry) =
cx.update(|cx| GitHostingProviderRegistry::try_global(cx))
{
@ -7868,55 +7963,56 @@ impl Repository {
break;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
job_tx
(job_tx, worker_task)
}
fn spawn_remote_git_worker(
state: RemoteRepositoryState,
cx: &mut Context<Self>,
) -> mpsc::UnboundedSender<GitJob> {
) -> (mpsc::UnboundedSender<GitJob>, Task<()>) {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
cx.spawn(async move |this, cx| {
let state = RepositoryState::Remote(state);
let mut jobs = VecDeque::new();
loop {
while let Ok(next_job) = job_rx.try_recv() {
jobs.push_back(next_job);
}
if let Some(job) = jobs.pop_front() {
if let Some(current_key) = &job.key
&& jobs
.iter()
.any(|other_job| other_job.key.as_ref() == Some(current_key))
{
let skipped_job_id = job.id;
this.update(cx, |repo, _| {
repo.job_debug_queue.mark_complete(
skipped_job_id,
job_debug_queue::CompletedJobStatus::Skipped,
);
})
.ok();
continue;
let worker_task = cx.spawn(async move |this, cx| {
let result: Result<()> = async {
let state = RepositoryState::Remote(state);
let mut jobs = VecDeque::new();
loop {
while let Ok(next_job) = job_rx.try_recv() {
jobs.push_back(next_job);
}
(job.job)(state.clone(), cx).await;
} else if let Some(job) = job_rx.next().await {
jobs.push_back(job);
} else {
break;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
job_tx
if let Some(job) = jobs.pop_front() {
if let Some(current_key) = &job.key
&& jobs
.iter()
.any(|other_job| other_job.key.as_ref() == Some(current_key))
{
let skipped_job_id = job.id;
this.update(cx, |repo, _| {
repo.job_debug_queue.mark_complete(
skipped_job_id,
job_debug_queue::CompletedJobStatus::Skipped,
);
})
.ok();
continue;
}
(job.job)(state.clone(), cx).await;
} else if let Some(job) = job_rx.next().await {
jobs.push_back(job);
} else {
break;
}
}
anyhow::Ok(())
}
.await;
result.log_err();
});
(job_tx, worker_task)
}
fn load_staged_text(
@ -9173,7 +9269,7 @@ async fn compute_snapshot(
this: Entity<Repository>,
backend: Arc<dyn GitRepository>,
cx: &mut AsyncApp,
) -> Result<RepositorySnapshot> {
) -> RepositorySnapshot {
log::debug!("starting compute snapshot");
let (id, work_directory_abs_path, prev_snapshot) = this.update(cx, |this, _| {
@ -9185,56 +9281,41 @@ async fn compute_snapshot(
)
});
let branches_future = {
let backend = backend.clone();
async move { backend.branches().await.log_err().unwrap_or_default() }
};
let head_commit_future = {
let backend = backend.clone();
async move {
Ok(match backend.head_sha().await {
match backend.head_sha().await {
Some(head_sha) => backend.show(head_sha).await.log_err(),
None => None,
})
}
}
};
let (branches_scan, head_commit, all_worktrees) = cx
.background_spawn({
let backend = backend.clone();
async move {
futures::future::try_join3(
backend.branches(),
head_commit_future,
backend.worktrees(),
)
.await
}
})
.await?;
let worktrees_future = {
let backend = backend.clone();
async move { backend.worktrees().await.log_err().unwrap_or_default() }
};
let (branches, head_commit, all_worktrees) =
futures::future::join3(branches_future, head_commit_future, worktrees_future).await;
log::debug!("fetched branches, head commit, worktrees");
let branch_list_error = branches_scan.error;
let branch = branches_scan
.branches
.iter()
.find(|branch| branch.is_head)
.cloned();
let branch_list: Arc<[Branch]> = branches_scan.branches.into();
let BranchesScanResult {
branches,
error: branch_list_error,
} = branches;
let branch = branches.iter().find(|branch| branch.is_head).cloned();
let branch_list: Arc<[Branch]> = branches.into();
let linked_worktrees: Arc<[GitWorktree]> = all_worktrees
.into_iter()
.filter(|wt| wt.path != *work_directory_abs_path)
.collect();
let (remote_origin_url, remote_upstream_url) = cx
.background_spawn({
let backend = backend.clone();
async move {
Ok::<_, anyhow::Error>(
futures::future::join(
backend.remote_url("origin"),
backend.remote_url("upstream"),
)
.await,
)
}
})
.await?;
let remote_origin_url = backend.remote_url("origin").await;
let remote_upstream_url = backend.remote_url("upstream").await;
log::debug!("fetched remotes");
@ -9274,31 +9355,36 @@ async fn compute_snapshot(
this.snapshot.clone()
});
let (statuses, diff_stats, stash_entries) = cx
.background_spawn({
let backend = backend.clone();
let snapshot = snapshot.clone();
async move {
let diff_stat_future: BoxFuture<'_, Result<status::GitDiffStat>> =
if snapshot.head_commit.is_some() {
backend.diff_stat(&[])
} else {
future::ready(Ok(status::GitDiffStat {
entries: Arc::default(),
}))
.boxed()
};
futures::future::try_join3(
backend.status(&[RepoPath::from_rel_path(
&RelPath::new(".".as_ref(), PathStyle::local()).unwrap(),
)]),
diff_stat_future,
backend.stash_entries(),
)
let statuses_future = {
let backend = backend.clone();
async move {
backend
.status(&[RepoPath::from_rel_path(
&RelPath::new(".".as_ref(), PathStyle::local()).unwrap(),
)])
.await
.log_err()
.unwrap_or_default()
}
};
let diff_stat_future = {
let snapshot = snapshot.clone();
let backend = backend.clone();
async move {
if snapshot.head_commit.is_some() {
backend.diff_stat(&[]).await.log_err().unwrap_or_default()
} else {
Default::default()
}
})
.await?;
}
};
let stash_entries_future = {
let backend = backend.clone();
async move { backend.stash_entries().await.log_err().unwrap_or_default() }
};
let (statuses, diff_stats, stash_entries) =
futures::future::join3(statuses_future, diff_stat_future, stash_entries_future).await;
log::debug!("fetched statuses, diff stats, stash entries");
let diff_stat_map: HashMap<&RepoPath, DiffStat> =
@ -9318,20 +9404,19 @@ async fn compute_snapshot(
(),
);
let merge_details = cx
let (merge_details, conflicts_changed) = cx
.background_spawn({
let backend = backend.clone();
let mut merge_details = snapshot.merge.clone();
async move {
let conflicts_changed = merge_details.update(&backend, conflicted_paths).await?;
Ok::<_, anyhow::Error>((merge_details, conflicts_changed))
let conflicts_changed = merge_details.update(&backend, conflicted_paths).await;
(merge_details, conflicts_changed)
}
})
.await?;
let (merge_details, conflicts_changed) = merge_details;
.await;
log::debug!("new merge details: {merge_details:?}");
Ok(this.update(cx, |this, cx| {
this.update(cx, |this, cx| {
if conflicts_changed || statuses_by_path != this.snapshot.statuses_by_path {
cx.emit(RepositoryEvent::StatusesChanged);
}
@ -9345,7 +9430,7 @@ async fn compute_snapshot(
this.snapshot.stash_entries = stash_entries;
this.snapshot.clone()
}))
})
}
fn status_from_proto(

View file

@ -80,6 +80,18 @@ impl GitJobDebugQueue {
});
}
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) {

View file

@ -11229,6 +11229,10 @@ async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
)
.unwrap();
tree.flush_fs_events(cx).await;
project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;
cx.executor().run_until_parked();
repository.read_with(cx, |repository, _| {
assert_eq!(

View file

@ -4446,6 +4446,9 @@ impl BackgroundScanner {
}
if !dot_git_abs_paths.contains(&dot_git_abs_path) {
log::debug!(
"detected update within git repo at {dot_git_abs_path:?}: {abs_path:?}"
);
dot_git_abs_paths.push(dot_git_abs_path);
}
}
@ -4526,7 +4529,7 @@ impl BackgroundScanner {
.is_some_and(|entry| entry.kind == EntryKind::Dir)
});
if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory");
log::debug!("filtering event {relative_path:?} within unloaded directory");
skip_ix(&mut ranges_to_drop, ix);
continue;
}
@ -4567,13 +4570,15 @@ impl BackgroundScanner {
self.state.lock().await.snapshot.scan_id += 1;
let (scan_job_tx, scan_job_rx) = async_channel::unbounded();
log::debug!(
"received fs events {:?}",
relative_paths
.iter()
.map(|event_root| &event_root.path)
.collect::<Vec<_>>()
);
if !relative_paths.is_empty() {
log::debug!(
"will update project paths {:?}",
relative_paths
.iter()
.map(|event_root| &event_root.path)
.collect::<Vec<_>>()
);
}
self.reload_entries_for_paths(
&root_path,
&root_canonical_path,