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:
co-authored by
Cole Miller
parent
f11a3dcc97
commit
f45a9b351d
+430
-147
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user