git: Branch diff (#40188)

Release Notes:

- git: Adds the ability to view the diff of the current branch since
main

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Conrad Irwin
2025-10-23 22:38:40 -06:00
committed by GitHub
co-authored by Cole Miller
parent f11a3dcc97
commit f45a9b351d
18 changed files with 1362 additions and 777 deletions
+430 -147
View File
@@ -4,16 +4,15 @@ use crate::{
git_panel_settings::GitPanelSettings,
remote_button::{render_publish_button, render_push_button},
};
use anyhow::Result;
use anyhow::{Context as _, Result, anyhow};
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::{HashMap, HashSet};
use editor::{
Editor, EditorEvent, SelectionEffects,
Addon, Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
};
use futures::StreamExt;
use git::{
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -27,18 +26,23 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
use multi_buffer::{MultiBuffer, PathKey};
use project::{
Project, ProjectPath,
git_store::{GitStore, GitStoreEvent, Repository, RepositoryEvent},
git_store::{
Repository,
branch_diff::{self, BranchDiffEvent, DiffBase},
},
};
use settings::{Settings, SettingsStore};
use std::any::{Any, TypeId};
use std::ops::Range;
use std::sync::Arc;
use theme::ActiveTheme;
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt as _;
use util::{ResultExt as _, rel_path::RelPath};
use workspace::{
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
notifications::NotifyTaskExt,
searchable::SearchableItemHandle,
};
@@ -48,30 +52,24 @@ actions!(
/// Shows the diff between the working directory and the index.
Diff,
/// Adds files to the git staging area.
Add
Add,
/// Shows the diff between the working directory and your default
/// branch (typically main or master).
BranchDiff
]
);
pub struct ProjectDiff {
project: Entity<Project>,
multibuffer: Entity<MultiBuffer>,
branch_diff: Entity<branch_diff::BranchDiff>,
editor: Entity<Editor>,
git_store: Entity<GitStore>,
buffer_diff_subscriptions: HashMap<RepoPath, (Entity<BufferDiff>, Subscription)>,
buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
_task: Task<Result<()>>,
_git_store_subscription: Subscription,
}
#[derive(Debug)]
struct DiffBuffer {
path_key: PathKey,
buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
file_status: FileStatus,
_subscription: Subscription,
}
const CONFLICT_SORT_PREFIX: u64 = 1;
@@ -81,6 +79,7 @@ const NEW_SORT_PREFIX: u64 = 3;
impl ProjectDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
workspace.register_action(Self::deploy);
workspace.register_action(Self::deploy_branch_diff);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
});
@@ -96,6 +95,40 @@ impl ProjectDiff {
Self::deploy_at(workspace, None, window, cx)
}
fn deploy_branch_diff(
workspace: &mut Workspace,
_: &BranchDiff,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
telemetry::event!("Git Branch Diff Opened");
let project = workspace.project().clone();
let existing = workspace
.items_of_type::<Self>(cx)
.find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Merge { .. }));
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
return;
}
let workspace = cx.entity();
window
.spawn(cx, async move |cx| {
let this = cx
.update(|window, cx| {
Self::new_with_default_branch(project, workspace.clone(), window, cx)
})?
.await?;
workspace
.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(this), None, true, window, cx);
})
.ok();
anyhow::Ok(())
})
.detach_and_notify_err(window, cx);
}
pub fn deploy_at(
workspace: &mut Workspace,
entry: Option<GitStatusEntry>,
@@ -110,7 +143,10 @@ impl ProjectDiff {
"Action"
}
);
let project_diff = if let Some(existing) = workspace.item_of_type::<Self>(cx) {
let existing = workspace
.items_of_type::<Self>(cx)
.find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head));
let project_diff = if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
existing
} else {
@@ -139,11 +175,54 @@ impl ProjectDiff {
})
}
fn new_with_default_branch(
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
let Some(repo) = project.read(cx).git_store().read(cx).active_repository() else {
return Task::ready(Err(anyhow!("No active repository")));
};
let main_branch = repo.update(cx, |repo, _| repo.default_branch());
window.spawn(cx, async move |cx| {
let main_branch = main_branch
.await??
.context("Could not determine default branch")?;
let branch_diff = cx.new_window_entity(|window, cx| {
branch_diff::BranchDiff::new(
DiffBase::Merge {
base_ref: main_branch,
},
project.clone(),
window,
cx,
)
})?;
cx.new_window_entity(|window, cx| {
Self::new_impl(branch_diff, project, workspace, window, cx)
})
})
}
fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let branch_diff =
cx.new(|cx| branch_diff::BranchDiff::new(DiffBase::Head, project.clone(), window, cx));
Self::new_impl(branch_diff, project, workspace, window, cx)
}
fn new_impl(
branch_diff: Entity<branch_diff::BranchDiff>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
@@ -153,9 +232,25 @@ impl ProjectDiff {
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
diff_display_editor.disable_diagnostics(cx);
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
});
match branch_diff.read(cx).diff_base() {
DiffBase::Head => {
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
});
}
DiffBase::Merge { .. } => {
diff_display_editor.register_addon(BranchDiffAddon {
branch_diff: branch_diff.clone(),
});
diff_display_editor.start_temporary_diff_override();
diff_display_editor.set_render_diff_hunk_controls(
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
cx,
);
//
}
}
diff_display_editor
});
window.defer(cx, {
@@ -172,71 +267,71 @@ impl ProjectDiff {
cx.subscribe_in(&editor, window, Self::handle_editor_event)
.detach();
let git_store = project.read(cx).git_store().clone();
let git_store_subscription = cx.subscribe_in(
&git_store,
let branch_diff_subscription = cx.subscribe_in(
&branch_diff,
window,
move |this, _git_store, event, _window, _cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_)
| GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::StatusesChanged { full_scan: _ },
true,
)
| GitStoreEvent::ConflictsUpdated => {
*this.update_needed.borrow_mut() = ();
move |this, _git_store, event, window, cx| match event {
BranchDiffEvent::FileListChanged => {
this._task = window.spawn(cx, {
let this = cx.weak_entity();
async |cx| Self::refresh(this, cx).await
})
}
_ => {}
},
);
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
let mut was_collapse_untracked_diff =
GitPanelSettings::get_global(cx).collapse_untracked_diff;
cx.observe_global::<SettingsStore>(move |this, cx| {
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
let is_collapse_untracked_diff =
GitPanelSettings::get_global(cx).collapse_untracked_diff;
if is_sort_by_path != was_sort_by_path
|| is_collapse_untracked_diff != was_collapse_untracked_diff
{
*this.update_needed.borrow_mut() = ();
this._task = {
window.spawn(cx, {
let this = cx.weak_entity();
async |cx| Self::refresh(this, cx).await
})
}
}
was_sort_by_path = is_sort_by_path;
was_collapse_untracked_diff = is_collapse_untracked_diff;
})
.detach();
let (mut send, recv) = postage::watch::channel::<()>();
let worker = window.spawn(cx, {
let task = window.spawn(cx, {
let this = cx.weak_entity();
async |cx| Self::handle_status_updates(this, recv, cx).await
async |cx| Self::refresh(this, cx).await
});
// Kick off a refresh immediately
*send.borrow_mut() = ();
Self {
project,
git_store: git_store.clone(),
workspace: workspace.downgrade(),
branch_diff,
focus_handle,
editor,
multibuffer,
buffer_diff_subscriptions: Default::default(),
pending_scroll: None,
update_needed: send,
_task: worker,
_git_store_subscription: git_store_subscription,
_task: task,
_subscription: branch_diff_subscription,
}
}
pub fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase {
self.branch_diff.read(cx).diff_base()
}
pub fn move_to_entry(
&mut self,
entry: GitStatusEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(git_repo) = self.git_store.read(cx).active_repository() else {
let Some(git_repo) = self.branch_diff.read(cx).repo() else {
return;
};
let repo = git_repo.read(cx);
@@ -366,77 +461,28 @@ impl ProjectDiff {
}
}
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
let Some(repo) = self.git_store.read(cx).active_repository() else {
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
});
self.buffer_diff_subscriptions.clear();
return vec![];
};
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
let mut result = vec![];
repo.update(cx, |repo, cx| {
for entry in repo.cached_status() {
if !entry.status.has_changes() {
continue;
}
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path, cx)
else {
continue;
};
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0.clone());
previous_paths.remove(&path_key);
let load_buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
let project = self.project.clone();
result.push(cx.spawn(async move |_, cx| {
let buffer = load_buffer.await?;
let changes = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?;
Ok(DiffBuffer {
path_key,
buffer,
diff: changes,
file_status: entry.status,
})
}));
}
});
self.multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
self.buffer_diff_subscriptions
.remove(&path.path.clone().into());
multibuffer.remove_excerpts_for_path(path, cx);
}
});
result
}
fn register_buffer(
&mut self,
diff_buffer: DiffBuffer,
path_key: PathKey,
file_status: FileStatus,
buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let path_key = diff_buffer.path_key.clone();
let buffer = diff_buffer.buffer.clone();
let diff = diff_buffer.diff.clone();
let subscription = cx.subscribe(&diff, move |this, _, _, _| {
*this.update_needed.borrow_mut() = ();
if self.branch_diff.read(cx).diff_base().is_merge_base() {
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.add_diff(diff.clone(), cx);
});
}
let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| {
this._task = window.spawn(cx, {
let this = cx.weak_entity();
async |cx| Self::refresh(this, cx).await
})
});
self.buffer_diff_subscriptions
.insert(path_key.path.clone().into(), (diff.clone(), subscription));
.insert(path_key.path.clone(), (diff.clone(), subscription));
let conflict_addon = self
.editor
@@ -480,8 +526,8 @@ impl ProjectDiff {
});
}
if is_excerpt_newly_added
&& (diff_buffer.file_status.is_deleted()
|| (diff_buffer.file_status.is_untracked()
&& (file_status.is_deleted()
|| (file_status.is_untracked()
&& GitPanelSettings::get_global(cx).collapse_untracked_diff))
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
@@ -506,26 +552,51 @@ impl ProjectDiff {
}
}
pub async fn handle_status_updates(
this: WeakEntity<Self>,
mut recv: postage::watch::Receiver<()>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
while (recv.next().await).is_some() {
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
cx.update(|window, cx| {
this.update(cx, |this, cx| this.register_buffer(buffer, window, cx))
.ok();
})?;
pub async fn refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
let mut path_keys = Vec::new();
let buffers_to_load = this.update(cx, |this, cx| {
let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| {
let load_buffers = branch_diff.load_buffers(cx);
(branch_diff.repo().cloned(), load_buffers)
});
let mut previous_paths = this.multibuffer.read(cx).paths().collect::<HashSet<_>>();
if let Some(repo) = repo {
let repo = repo.read(cx);
path_keys = Vec::with_capacity(buffers_to_load.len());
for entry in buffers_to_load.iter() {
let sort_prefix = sort_prefix(&repo, &entry.repo_path, entry.file_status, cx);
let path_key =
PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0.clone());
previous_paths.remove(&path_key);
path_keys.push(path_key)
}
}
this.update(cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
})?;
this.multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
this.buffer_diff_subscriptions.remove(&path.path);
multibuffer.remove_excerpts_for_path(path, cx);
}
});
buffers_to_load
})?;
for (entry, path_key) in buffers_to_load.into_iter().zip(path_keys.into_iter()) {
if let Some((buffer, diff)) = entry.load.await.log_err() {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.register_buffer(path_key, entry.file_status, buffer, diff, window, cx)
})
.ok();
})?;
}
}
this.update(cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
})?;
Ok(())
}
@@ -594,8 +665,8 @@ impl Item for ProjectDiff {
Some("Project Diff".into())
}
fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
Label::new("Uncommitted Changes")
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
Label::new(self.tab_content_text(0, cx))
.color(if params.selected {
Color::Default
} else {
@@ -604,8 +675,11 @@ impl Item for ProjectDiff {
.into_any_element()
}
fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
"Uncommitted Changes".into()
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
match self.branch_diff.read(cx).diff_base() {
DiffBase::Head => "Uncommitted Changes".into(),
DiffBase::Merge { base_ref } => format!("Changes since {}", base_ref).into(),
}
}
fn telemetry_event_text(&self) -> Option<&'static str> {
@@ -802,30 +876,47 @@ impl SerializableItem for ProjectDiff {
}
fn deserialize(
_project: Entity<Project>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.entity();
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx))
})
let diff_base = persistence::PROJECT_DIFF_DB.get_diff_base(item_id, workspace_id)?;
let diff = cx.update(|window, cx| {
let branch_diff = cx
.new(|cx| branch_diff::BranchDiff::new(diff_base, project.clone(), window, cx));
let workspace = workspace.upgrade().context("workspace gone")?;
anyhow::Ok(
cx.new(|cx| ProjectDiff::new_impl(branch_diff, project, workspace, window, cx)),
)
})??;
Ok(diff)
})
}
fn serialize(
&mut self,
_workspace: &mut Workspace,
_item_id: workspace::ItemId,
workspace: &mut Workspace,
item_id: workspace::ItemId,
_closing: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
None
let workspace_id = workspace.database_id()?;
let diff_base = self.diff_base(cx).clone();
Some(cx.background_spawn({
async move {
persistence::PROJECT_DIFF_DB
.save_diff_base(item_id, workspace_id, diff_base.clone())
.await
}
}))
}
fn should_serialize(&self, _: &Self::Event) -> bool {
@@ -833,6 +924,80 @@ impl SerializableItem for ProjectDiff {
}
}
mod persistence {
use anyhow::Context as _;
use db::{
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
sqlez_macros::sql,
};
use project::git_store::branch_diff::DiffBase;
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
pub struct ProjectDiffDb(ThreadSafeConnection);
impl Domain for ProjectDiffDb {
const NAME: &str = stringify!(ProjectDiffDb);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE project_diffs(
workspace_id INTEGER,
item_id INTEGER UNIQUE,
diff_base TEXT,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
db::static_connection!(PROJECT_DIFF_DB, ProjectDiffDb, [WorkspaceDb]);
impl ProjectDiffDb {
pub async fn save_diff_base(
&self,
item_id: ItemId,
workspace_id: WorkspaceId,
diff_base: DiffBase,
) -> anyhow::Result<()> {
self.write(move |connection| {
let sql_stmt = sql!(
INSERT OR REPLACE INTO project_diffs(item_id, workspace_id, diff_base) VALUES (?, ?, ?)
);
let diff_base_str = serde_json::to_string(&diff_base)?;
let mut query = connection.exec_bound::<(ItemId, WorkspaceId, String)>(sql_stmt)?;
query((item_id, workspace_id, diff_base_str)).context(format!(
"exec_bound failed to execute or parse for: {}",
sql_stmt
))
})
.await
}
pub fn get_diff_base(
&self,
item_id: ItemId,
workspace_id: WorkspaceId,
) -> anyhow::Result<DiffBase> {
let sql_stmt =
sql!(SELECT diff_base FROM project_diffs WHERE item_id = ?AND workspace_id = ?);
let diff_base_str = self.select_row_bound::<(ItemId, WorkspaceId), String>(sql_stmt)?(
(item_id, workspace_id),
)
.context(::std::format!(
"Error in get_diff_base, select_row_bound failed to execute or parse for: {}",
sql_stmt
))?;
let Some(diff_base_str) = diff_base_str else {
return Ok(DiffBase::Head);
};
serde_json::from_str(&diff_base_str).context("deserializing diff base")
}
}
}
pub struct ProjectDiffToolbar {
project_diff: Option<WeakEntity<ProjectDiff>>,
workspace: WeakEntity<Workspace>,
@@ -897,6 +1062,7 @@ impl ToolbarItemView for ProjectDiffToolbar {
) -> ToolbarItemLocation {
self.project_diff = active_pane_item
.and_then(|item| item.act_as::<ProjectDiff>(cx))
.filter(|item| item.read(cx).diff_base(cx) == &DiffBase::Head)
.map(|entity| entity.downgrade());
if self.project_diff.is_some() {
ToolbarItemLocation::PrimaryRight
@@ -1366,18 +1532,42 @@ fn merge_anchor_ranges<'a>(
})
}
struct BranchDiffAddon {
branch_diff: Entity<branch_diff::BranchDiff>,
}
impl Addon for BranchDiffAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn override_status_for_buffer_id(
&self,
buffer_id: language::BufferId,
cx: &App,
) -> Option<FileStatus> {
self.branch_diff
.read(cx)
.status_for_buffer_id(buffer_id, cx)
}
}
#[cfg(test)]
mod tests {
use collections::HashMap;
use db::indoc;
use editor::test::editor_test_context::{EditorTestContext, assert_state_with_diff};
use git::status::{UnmergedStatus, UnmergedStatusCode};
use git::status::{TrackedStatus, UnmergedStatus, UnmergedStatusCode};
use gpui::TestAppContext;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use std::path::Path;
use unindent::Unindent as _;
use util::{path, rel_path::rel_path};
use util::{
path,
rel_path::{RelPath, rel_path},
};
use super::*;
@@ -2015,6 +2205,99 @@ mod tests {
);
}
#[gpui::test]
async fn test_branch_diff(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"a.txt": "C",
"b.txt": "new",
"c.txt": "in-merge-base-and-work-tree",
"d.txt": "created-in-head",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let diff = cx
.update(|window, cx| {
ProjectDiff::new_with_default_branch(project.clone(), workspace, window, cx)
})
.await
.unwrap();
cx.run_until_parked();
fs.set_head_for_repo(
Path::new(path!("/project/.git")),
&[("a.txt", "B".into()), ("d.txt", "created-in-head".into())],
"sha",
);
// fs.set_index_for_repo(dot_git, index_state);
fs.set_merge_base_content_for_repo(
Path::new(path!("/project/.git")),
&[
("a.txt", "A".into()),
("c.txt", "in-merge-base-and-work-tree".into()),
],
);
cx.run_until_parked();
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
assert_state_with_diff(
&editor,
cx,
&"
- A
+ ˇC
+ new
+ created-in-head"
.unindent(),
);
let statuses: HashMap<Arc<RelPath>, Option<FileStatus>> =
editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.all_buffers()
.iter()
.map(|buffer| {
(
buffer.read(cx).file().unwrap().path().clone(),
editor.status_for_buffer_id(buffer.read(cx).remote_id(), cx),
)
})
.collect()
});
assert_eq!(
statuses,
HashMap::from_iter([
(
rel_path("a.txt").into_arc(),
Some(FileStatus::Tracked(TrackedStatus {
index_status: git::status::StatusCode::Modified,
worktree_status: git::status::StatusCode::Modified
}))
),
(rel_path("b.txt").into_arc(), Some(FileStatus::Untracked)),
(
rel_path("d.txt").into_arc(),
Some(FileStatus::Tracked(TrackedStatus {
index_status: git::status::StatusCode::Added,
worktree_status: git::status::StatusCode::Added
}))
)
])
);
}
#[gpui::test]
async fn test_update_on_uncommit(cx: &mut TestAppContext) {
init_test(cx);