mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
git_graph: Refresh UI when stash/branch list has changed (#53094)
### Summary This PR fixes an issue where the git graph wouldn't refresh its state correctly unless HEAD changed. Now repository emits events when the branch list has changed, and invalidates the graph data cache when the stash or branch list has changed. I also renamed the event `Repository::BranchedChanged` to `Repository::HeadChanged` as well. 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) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #53080 Release Notes: - N/A or Added/Fixed/Improved ...
This commit is contained in:
parent
4b1e0a30b8
commit
7748047051
6 changed files with 186 additions and 20 deletions
|
|
@ -10,6 +10,7 @@ use git::{
|
|||
GRAPH_CHUNK_SIZE, GitRepository, GitRepositoryCheckpoint, InitialGraphCommitData, LogOrder,
|
||||
LogSource, PushOptions, Remote, RepoPath, ResetMode, SearchCommitArgs, Worktree,
|
||||
},
|
||||
stash::GitStash,
|
||||
status::{
|
||||
DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
|
||||
UnmergedStatus,
|
||||
|
|
@ -53,6 +54,7 @@ pub struct FakeGitRepositoryState {
|
|||
pub simulated_create_worktree_error: Option<String>,
|
||||
pub refs: HashMap<String, String>,
|
||||
pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
|
||||
pub stash_entries: GitStash,
|
||||
}
|
||||
|
||||
impl FakeGitRepositoryState {
|
||||
|
|
@ -72,6 +74,7 @@ impl FakeGitRepositoryState {
|
|||
oids: Default::default(),
|
||||
remotes: HashMap::default(),
|
||||
graph_commits: Vec::new(),
|
||||
stash_entries: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -378,13 +381,13 @@ impl GitRepository for FakeGitRepository {
|
|||
}
|
||||
|
||||
fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
|
||||
async { Ok(git::stash::GitStash::default()) }.boxed()
|
||||
self.with_state_async(false, |state| Ok(state.stash_entries.clone()))
|
||||
}
|
||||
|
||||
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
let current_branch = &state.current_branch_name;
|
||||
Ok(state
|
||||
let mut branches = state
|
||||
.branches
|
||||
.iter()
|
||||
.map(|branch_name| {
|
||||
|
|
@ -402,7 +405,11 @@ impl GitRepository for FakeGitRepository {
|
|||
upstream: None,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
.collect::<Vec<_>>();
|
||||
// compute snapshot expects these to be sorted by ref_name
|
||||
// because that's what git itself does
|
||||
branches.sort_by(|a, b| a.ref_name.cmp(&b.ref_name));
|
||||
Ok(branches)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1148,7 +1148,7 @@ impl GitGraph {
|
|||
}
|
||||
}
|
||||
}
|
||||
RepositoryEvent::BranchChanged => {
|
||||
RepositoryEvent::HeadChanged | RepositoryEvent::BranchListChanged => {
|
||||
self.pending_select_sha = None;
|
||||
// Only invalidate if we scanned atleast once,
|
||||
// meaning we are not inside the initial repo loading state
|
||||
|
|
@ -1157,6 +1157,12 @@ impl GitGraph {
|
|||
self.invalidate_state(cx);
|
||||
}
|
||||
}
|
||||
RepositoryEvent::StashEntriesChanged if self.log_source == LogSource::All => {
|
||||
self.pending_select_sha = None;
|
||||
if repository.read(cx).scan_id > 1 {
|
||||
self.invalidate_state(cx);
|
||||
}
|
||||
}
|
||||
RepositoryEvent::GraphEvent(_, _) => {}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -3737,8 +3743,8 @@ mod tests {
|
|||
assert!(
|
||||
observed_repository_events
|
||||
.iter()
|
||||
.any(|event| matches!(event, RepositoryEvent::BranchChanged)),
|
||||
"initial repository scan should emit BranchChanged"
|
||||
.any(|event| matches!(event, RepositoryEvent::HeadChanged)),
|
||||
"initial repository scan should emit HeadChanged"
|
||||
);
|
||||
let commit_count_after = repository.read_with(cx, |repo, _| {
|
||||
repo.get_graph_data(crate::LogSource::default(), crate::LogOrder::default())
|
||||
|
|
@ -3856,11 +3862,148 @@ mod tests {
|
|||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let commit_count_after_switch_back =
|
||||
// Verify graph data is reloaded from repository cache on switch back
|
||||
let reloaded_commit_count =
|
||||
git_graph.read_with(&*cx, |graph, _| graph.graph_data.commits.len());
|
||||
assert_eq!(
|
||||
initial_commit_count, commit_count_after_switch_back,
|
||||
"graph_data should be repopulated from cache after switching back to the same repo"
|
||||
reloaded_commit_count,
|
||||
commits.len(),
|
||||
"graph data should be reloaded after switching back"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_graph_data_reloaded_after_stash_change(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
Path::new("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": "content",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_head = Oid::from_bytes(&[1; 20]).unwrap();
|
||||
let initial_stash = Oid::from_bytes(&[2; 20]).unwrap();
|
||||
let updated_head = Oid::from_bytes(&[3; 20]).unwrap();
|
||||
let updated_stash = Oid::from_bytes(&[4; 20]).unwrap();
|
||||
|
||||
fs.set_graph_commits(
|
||||
Path::new("/project/.git"),
|
||||
vec![
|
||||
Arc::new(InitialGraphCommitData {
|
||||
sha: initial_head,
|
||||
parents: smallvec![initial_stash],
|
||||
ref_names: vec!["HEAD".into(), "refs/heads/main".into()],
|
||||
}),
|
||||
Arc::new(InitialGraphCommitData {
|
||||
sha: initial_stash,
|
||||
parents: smallvec![],
|
||||
ref_names: vec!["refs/stash".into()],
|
||||
}),
|
||||
],
|
||||
);
|
||||
fs.with_git_state(Path::new("/project/.git"), true, |state| {
|
||||
state.stash_entries = git::stash::GitStash {
|
||||
entries: vec![git::stash::StashEntry {
|
||||
index: 0,
|
||||
oid: initial_stash,
|
||||
message: "initial stash".to_string(),
|
||||
branch: Some("main".to_string()),
|
||||
timestamp: 1,
|
||||
}]
|
||||
.into(),
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let repository = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.active_repository(cx)
|
||||
.expect("should have a repository")
|
||||
});
|
||||
|
||||
let (multi_workspace, cx) = cx.add_window_view(|window, cx| {
|
||||
workspace::MultiWorkspace::test_new(project.clone(), window, cx)
|
||||
});
|
||||
let workspace_weak =
|
||||
multi_workspace.read_with(&*cx, |multi, _| multi.workspace().downgrade());
|
||||
let git_graph = cx.new_window_entity(|window, cx| {
|
||||
GitGraph::new(
|
||||
repository.read(cx).id,
|
||||
project.read(cx).git_store().clone(),
|
||||
workspace_weak,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let initial_shas = git_graph.read_with(&*cx, |graph, _| {
|
||||
graph
|
||||
.graph_data
|
||||
.commits
|
||||
.iter()
|
||||
.map(|commit| commit.data.sha)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(initial_shas, vec![initial_head, initial_stash]);
|
||||
|
||||
fs.set_graph_commits(
|
||||
Path::new("/project/.git"),
|
||||
vec![
|
||||
Arc::new(InitialGraphCommitData {
|
||||
sha: updated_head,
|
||||
parents: smallvec![updated_stash],
|
||||
ref_names: vec!["HEAD".into(), "refs/heads/main".into()],
|
||||
}),
|
||||
Arc::new(InitialGraphCommitData {
|
||||
sha: updated_stash,
|
||||
parents: smallvec![],
|
||||
ref_names: vec!["refs/stash".into()],
|
||||
}),
|
||||
],
|
||||
);
|
||||
fs.with_git_state(Path::new("/project/.git"), true, |state| {
|
||||
state.stash_entries = git::stash::GitStash {
|
||||
entries: vec![git::stash::StashEntry {
|
||||
index: 0,
|
||||
oid: updated_stash,
|
||||
message: "updated stash".to_string(),
|
||||
branch: Some("main".to_string()),
|
||||
timestamp: 1,
|
||||
}]
|
||||
.into(),
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.draw(
|
||||
point(px(0.), px(0.)),
|
||||
gpui::size(px(1200.), px(800.)),
|
||||
|_, _| git_graph.clone().into_any_element(),
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let reloaded_shas = git_graph.read_with(&*cx, |graph, _| {
|
||||
graph
|
||||
.graph_data
|
||||
.commits
|
||||
.iter()
|
||||
.map(|commit| commit.data.sha)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(reloaded_shas, vec![updated_head, updated_stash]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -780,7 +780,7 @@ impl GitPanel {
|
|||
move |this, _git_store, event, window, cx| match event {
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged | RepositoryEvent::BranchChanged,
|
||||
RepositoryEvent::StatusesChanged | RepositoryEvent::HeadChanged,
|
||||
true,
|
||||
)
|
||||
| GitStoreEvent::RepositoryAdded
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ pub struct RepositorySnapshot {
|
|||
pub original_repo_abs_path: Arc<Path>,
|
||||
pub path_style: PathStyle,
|
||||
pub branch: Option<Branch>,
|
||||
pub branch_list: Arc<[Branch]>,
|
||||
pub head_commit: Option<CommitDetails>,
|
||||
pub scan_id: u64,
|
||||
pub merge: MergeDetails,
|
||||
|
|
@ -428,7 +429,8 @@ pub enum GitGraphEvent {
|
|||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RepositoryEvent {
|
||||
StatusesChanged,
|
||||
BranchChanged,
|
||||
HeadChanged,
|
||||
BranchListChanged,
|
||||
StashEntriesChanged,
|
||||
GitWorktreeListChanged,
|
||||
PendingOpsChanged { pending_ops: SumTree<PendingOps> },
|
||||
|
|
@ -3686,6 +3688,7 @@ impl RepositorySnapshot {
|
|||
.unwrap_or_else(|| work_directory_abs_path.clone()),
|
||||
work_directory_abs_path,
|
||||
branch: None,
|
||||
branch_list: Arc::from([]),
|
||||
head_commit: None,
|
||||
scan_id: 0,
|
||||
merge: Default::default(),
|
||||
|
|
@ -4048,11 +4051,17 @@ impl Repository {
|
|||
.shared();
|
||||
|
||||
cx.subscribe_self(move |this, event: &RepositoryEvent, _| match event {
|
||||
RepositoryEvent::BranchChanged => {
|
||||
RepositoryEvent::HeadChanged | RepositoryEvent::BranchListChanged => {
|
||||
if this.scan_id > 1 {
|
||||
this.initial_graph_data.clear();
|
||||
}
|
||||
}
|
||||
RepositoryEvent::StashEntriesChanged => {
|
||||
if this.scan_id > 1 {
|
||||
this.initial_graph_data
|
||||
.retain(|(log_source, _), _| *log_source != LogSource::All);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
|
@ -5594,7 +5603,7 @@ impl Repository {
|
|||
log::info!("head branch after scan is {branch:?}");
|
||||
let snapshot = this.update(&mut cx, |this, cx| {
|
||||
this.snapshot.branch = branch;
|
||||
cx.emit(RepositoryEvent::BranchChanged);
|
||||
cx.emit(RepositoryEvent::HeadChanged);
|
||||
this.snapshot.clone()
|
||||
})?;
|
||||
if let Some(updates_tx) = updates_tx {
|
||||
|
|
@ -6380,7 +6389,7 @@ impl Repository {
|
|||
.as_ref()
|
||||
.map(proto_to_commit_details);
|
||||
if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit {
|
||||
cx.emit(RepositoryEvent::BranchChanged)
|
||||
cx.emit(RepositoryEvent::HeadChanged)
|
||||
}
|
||||
self.snapshot.branch = new_branch;
|
||||
self.snapshot.head_commit = new_head_commit;
|
||||
|
|
@ -7318,7 +7327,8 @@ async fn compute_snapshot(
|
|||
}
|
||||
})
|
||||
.await?;
|
||||
let branch = branches.into_iter().find(|branch| branch.is_head);
|
||||
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()
|
||||
|
|
@ -7341,14 +7351,16 @@ async fn compute_snapshot(
|
|||
.await?;
|
||||
|
||||
let snapshot = this.update(cx, |this, cx| {
|
||||
let branch_changed =
|
||||
let head_changed =
|
||||
branch != this.snapshot.branch || head_commit != this.snapshot.head_commit;
|
||||
let branch_list_changed = *branch_list != *this.snapshot.branch_list;
|
||||
let worktrees_changed = *linked_worktrees != *this.snapshot.linked_worktrees;
|
||||
|
||||
this.snapshot = RepositorySnapshot {
|
||||
id,
|
||||
work_directory_abs_path,
|
||||
branch,
|
||||
branch_list: branch_list.clone(),
|
||||
head_commit,
|
||||
remote_origin_url,
|
||||
remote_upstream_url,
|
||||
|
|
@ -7357,8 +7369,12 @@ async fn compute_snapshot(
|
|||
..prev_snapshot
|
||||
};
|
||||
|
||||
if branch_changed {
|
||||
cx.emit(RepositoryEvent::BranchChanged);
|
||||
if head_changed {
|
||||
cx.emit(RepositoryEvent::HeadChanged);
|
||||
}
|
||||
|
||||
if branch_list_changed {
|
||||
cx.emit(RepositoryEvent::BranchListChanged);
|
||||
}
|
||||
|
||||
if worktrees_changed {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl BranchDiff {
|
|||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
event_repo_id,
|
||||
RepositoryEvent::StatusesChanged | RepositoryEvent::BranchChanged,
|
||||
RepositoryEvent::StatusesChanged | RepositoryEvent::HeadChanged,
|
||||
_,
|
||||
) => this
|
||||
.repo
|
||||
|
|
|
|||
|
|
@ -11161,7 +11161,7 @@ async fn test_odd_events_for_ignored_dirs(
|
|||
assert_eq!(
|
||||
repository_updates.lock().drain(..).collect::<Vec<_>>(),
|
||||
vec![
|
||||
RepositoryEvent::BranchChanged,
|
||||
RepositoryEvent::HeadChanged,
|
||||
RepositoryEvent::StatusesChanged,
|
||||
RepositoryEvent::StatusesChanged,
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue