@@ -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,
@@ -55,6 +56,7 @@ pub struct FakeGitRepositoryState {
pub refs: HashMap<String, String>,
pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
pub worktrees: Vec<Worktree>,
+ pub stash_entries: GitStash,
}
impl FakeGitRepositoryState {
@@ -76,6 +78,7 @@ impl FakeGitRepositoryState {
remotes: HashMap::default(),
graph_commits: Vec::new(),
worktrees: Vec::new(),
+ stash_entries: Default::default(),
}
}
}
@@ -382,13 +385,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| {
@@ -406,7 +409,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(_, _) => {}
_ => {}
}
@@ -2388,9 +2394,8 @@ impl GitGraph {
let local_y = position_y - canvas_bounds.origin.y;
if local_y >= px(0.) && local_y < canvas_bounds.size.height {
- let row_in_viewport = (local_y / self.row_height).floor() as usize;
- let scroll_rows = (scroll_offset_y / self.row_height).floor() as usize;
- let absolute_row = scroll_rows + row_in_viewport;
+ let absolute_y = local_y + scroll_offset_y;
+ let absolute_row = (absolute_y / self.row_height).floor() as usize;
if absolute_row < self.graph_data.commits.len() {
return Some(absolute_row);
@@ -3745,8 +3750,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())
@@ -3919,11 +3924,220 @@ 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]);
+ }
+
+ #[gpui::test]
+ async fn test_git_graph_row_at_position_rounding(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ Path::new("/project"),
+ serde_json::json!({
+ ".git": {},
+ "file.txt": "content",
+ }),
+ )
+ .await;
+
+ let mut rng = StdRng::seed_from_u64(42);
+ let commits = generate_random_commit_dag(&mut rng, 10, false);
+ fs.set_graph_commits(Path::new("/project/.git"), commits.clone());
+
+ 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();
+
+ git_graph.update(cx, |graph, cx| {
+ assert!(
+ graph.graph_data.commits.len() >= 10,
+ "graph should load dummy commits"
+ );
+
+ graph.row_height = px(20.0);
+ let origin_y = px(100.0);
+ graph.graph_canvas_bounds.set(Some(Bounds {
+ origin: point(px(0.0), origin_y),
+ size: gpui::size(px(100.0), px(1000.0)),
+ }));
+
+ graph.table_interaction_state.update(cx, |state, _| {
+ state.set_scroll_offset(point(px(0.0), px(-15.0)))
+ });
+ let pos_y = origin_y + px(10.0);
+ let absolute_calc_row = graph.row_at_position(pos_y, cx);
+
+ assert_eq!(
+ absolute_calc_row,
+ Some(1),
+ "Row calculation should yield absolute row exactly"
+ );
+ });
+ }
}
@@ -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> },
@@ -3576,6 +3578,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(),
@@ -3938,11 +3941,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();
@@ -5484,7 +5493,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 {
@@ -6248,7 +6257,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;
@@ -7164,7 +7173,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()
@@ -7187,14 +7197,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,
@@ -7203,8 +7215,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 {