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
Generated
-1
@@ -7071,7 +7071,6 @@ dependencies = [
|
||||
"notifications",
|
||||
"panel",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"schemars 1.0.4",
|
||||
|
||||
@@ -1162,34 +1162,22 @@ impl BufferDiff {
|
||||
self.hunks_intersecting_range(start..end, buffer, cx)
|
||||
}
|
||||
|
||||
pub fn set_base_text_buffer(
|
||||
&mut self,
|
||||
base_buffer: Entity<language::Buffer>,
|
||||
buffer: text::BufferSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let base_buffer = base_buffer.read(cx);
|
||||
let language_registry = base_buffer.language_registry();
|
||||
let base_buffer = base_buffer.snapshot();
|
||||
self.set_base_text(base_buffer, language_registry, buffer, cx)
|
||||
}
|
||||
|
||||
/// Used in cases where the change set isn't derived from git.
|
||||
pub fn set_base_text(
|
||||
&mut self,
|
||||
base_buffer: language::BufferSnapshot,
|
||||
base_text: Option<Arc<String>>,
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
buffer: text::BufferSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let this = cx.weak_entity();
|
||||
let base_text = Arc::new(base_buffer.text());
|
||||
|
||||
let snapshot = BufferDiffSnapshot::new_with_base_text(
|
||||
buffer.clone(),
|
||||
Some(base_text),
|
||||
base_buffer.language().cloned(),
|
||||
base_text,
|
||||
language,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -347,6 +347,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetDefaultBranch>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUnstagedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenUncommittedDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtExpandMacro>)
|
||||
@@ -461,6 +462,8 @@ impl Server {
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::BreakpointsForFile>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitDiff>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetTreeDiff>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetBlobContent>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
|
||||
+19
-63
@@ -32,7 +32,6 @@ mod lsp_ext;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
mod proposed_changes_editor;
|
||||
mod rust_analyzer_ext;
|
||||
pub mod scroll;
|
||||
mod selections_collection;
|
||||
@@ -68,14 +67,12 @@ pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
|
||||
RowInfo, ToOffset, ToPoint,
|
||||
};
|
||||
pub use proposed_changes_editor::{
|
||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
};
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -847,6 +844,10 @@ pub trait Addon: 'static {
|
||||
None
|
||||
}
|
||||
|
||||
fn override_status_for_buffer_id(&self, _: BufferId, _: &App) -> Option<FileStatus> {
|
||||
None
|
||||
}
|
||||
|
||||
fn to_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
@@ -10641,6 +10642,20 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||
if let Some(status) = self
|
||||
.addons
|
||||
.iter()
|
||||
.find_map(|(_, addon)| addon.override_status_for_buffer_id(buffer_id, cx))
|
||||
{
|
||||
return Some(status);
|
||||
}
|
||||
self.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.status_for_buffer_id(buffer_id, cx)
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(
|
||||
&mut self,
|
||||
_: &OpenInTerminal,
|
||||
@@ -21011,65 +21026,6 @@ impl Editor {
|
||||
self.searchable
|
||||
}
|
||||
|
||||
fn open_proposed_changes_editor(
|
||||
&mut self,
|
||||
_: &OpenProposedChangesEditor,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace() else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
|
||||
let selections = self.selections.all::<usize>(&self.display_snapshot(cx));
|
||||
let multi_buffer = self.buffer.read(cx);
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in selections {
|
||||
for (buffer, range, _) in
|
||||
multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end)
|
||||
{
|
||||
let mut range = range.to_point(buffer);
|
||||
range.start.column = 0;
|
||||
range.end.column = buffer.line_len(range.end.row);
|
||||
new_selections_by_buffer
|
||||
.entry(multi_buffer.buffer(buffer.remote_id()).unwrap())
|
||||
.or_insert(Vec::new())
|
||||
.push(range)
|
||||
}
|
||||
}
|
||||
|
||||
let proposed_changes_buffers = new_selections_by_buffer
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
|
||||
.collect::<Vec<_>>();
|
||||
let proposed_changes_editor = cx.new(|cx| {
|
||||
ProposedChangesEditor::new(
|
||||
"Proposed changes",
|
||||
proposed_changes_buffers,
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(proposed_changes_editor),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_excerpts_in_split(
|
||||
&mut self,
|
||||
_: &OpenExcerptsSplit,
|
||||
|
||||
@@ -458,7 +458,6 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::toggle_code_actions);
|
||||
register_action(editor, window, Editor::open_excerpts);
|
||||
register_action(editor, window, Editor::open_excerpts_in_split);
|
||||
register_action(editor, window, Editor::open_proposed_changes_editor);
|
||||
register_action(editor, window, Editor::toggle_soft_wrap);
|
||||
register_action(editor, window, Editor::toggle_tab_bar);
|
||||
register_action(editor, window, Editor::toggle_line_numbers);
|
||||
@@ -3828,13 +3827,7 @@ impl EditorElement {
|
||||
let multi_buffer = editor.buffer.read(cx);
|
||||
let file_status = multi_buffer
|
||||
.all_diff_hunks_expanded()
|
||||
.then(|| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.status_for_buffer_id(for_excerpt.buffer_id, cx)
|
||||
})
|
||||
.then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
|
||||
.flatten();
|
||||
let indicator = multi_buffer
|
||||
.buffer(for_excerpt.buffer_id)
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::join_all};
|
||||
use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
|
||||
use language::{Buffer, BufferEvent, BufferRow, Capability};
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
|
||||
use text::{BufferId, ToOffset};
|
||||
use ui::{ButtonLike, KeyBinding, prelude::*};
|
||||
use workspace::{
|
||||
Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
item::SaveOptions, searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct ProposedChangesEditor {
|
||||
editor: Entity<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
title: SharedString,
|
||||
buffer_entries: Vec<BufferEntry>,
|
||||
_recalculate_diffs_task: Task<Option<()>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
||||
}
|
||||
|
||||
pub struct ProposedChangeLocation<T> {
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub ranges: Vec<Range<T>>,
|
||||
}
|
||||
|
||||
struct BufferEntry {
|
||||
base: Entity<Buffer>,
|
||||
branch: Entity<Buffer>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesEditorToolbar {
|
||||
current_editor: Option<Entity<ProposedChangesEditor>>,
|
||||
}
|
||||
|
||||
struct RecalculateDiff {
|
||||
buffer: Entity<Buffer>,
|
||||
debounce: bool,
|
||||
}
|
||||
|
||||
/// A provider of code semantics for branch buffers.
|
||||
///
|
||||
/// Requests in edited regions will return nothing, but requests in unchanged
|
||||
/// regions will be translated into the base buffer's coordinates.
|
||||
struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
|
||||
|
||||
impl ProposedChangesEditor {
|
||||
pub fn new<T: Clone + ToOffset>(
|
||||
title: impl Into<SharedString>,
|
||||
locations: Vec<ProposedChangeLocation<T>>,
|
||||
project: Option<Entity<Project>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
let mut this = Self {
|
||||
editor: cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, window, cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_completion_provider(None);
|
||||
editor.clear_code_action_providers();
|
||||
editor.set_semantics_provider(
|
||||
editor
|
||||
.semantics_provider()
|
||||
.map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
|
||||
);
|
||||
editor
|
||||
}),
|
||||
multibuffer,
|
||||
title: title.into(),
|
||||
buffer_entries: Vec::new(),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn_in(window, async move |this, cx| {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(recalculate_diff.buffer);
|
||||
|
||||
while recalculate_diff.debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(50))
|
||||
.await;
|
||||
let mut had_further_changes = false;
|
||||
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
|
||||
let next_recalculate_diff = next_recalculate_diff?;
|
||||
recalculate_diff.debounce &= next_recalculate_diff.debounce;
|
||||
buffers_to_diff.insert(next_recalculate_diff.buffer);
|
||||
had_further_changes = true;
|
||||
}
|
||||
if !had_further_changes {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let recalculate_diff_futures = this
|
||||
.update(cx, |this, cx| {
|
||||
buffers_to_diff
|
||||
.drain()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let base_buffer = buffer.base_buffer()?;
|
||||
let buffer = buffer.text_snapshot();
|
||||
let diff =
|
||||
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
|
||||
Some(diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text_buffer(base_buffer.clone(), buffer, cx)
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
join_all(recalculate_diff_futures).await;
|
||||
}
|
||||
None
|
||||
}),
|
||||
};
|
||||
this.reset_locations(locations, window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn branch_buffer_for_base(&self, base_buffer: &Entity<Buffer>) -> Option<Entity<Buffer>> {
|
||||
self.buffer_entries.iter().find_map(|entry| {
|
||||
if &entry.base == base_buffer {
|
||||
Some(entry.branch.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
|
||||
self.title = title;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reset_locations<T: Clone + ToOffset>(
|
||||
&mut self,
|
||||
locations: Vec<ProposedChangeLocation<T>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Undo all branch changes
|
||||
for entry in &self.buffer_entries {
|
||||
let base_version = entry.base.read(cx).version();
|
||||
entry.branch.update(cx, |buffer, cx| {
|
||||
let undo_counts = buffer
|
||||
.operations()
|
||||
.iter()
|
||||
.filter_map(|(timestamp, _)| {
|
||||
if !base_version.observed(*timestamp) {
|
||||
Some((*timestamp, u32::MAX))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
buffer.undo_operations(undo_counts, cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
|
||||
let mut buffer_entries = Vec::new();
|
||||
let mut new_diffs = Vec::new();
|
||||
for location in locations {
|
||||
let branch_buffer;
|
||||
if let Some(ix) = self
|
||||
.buffer_entries
|
||||
.iter()
|
||||
.position(|entry| entry.base == location.buffer)
|
||||
{
|
||||
let entry = self.buffer_entries.remove(ix);
|
||||
branch_buffer = entry.branch.clone();
|
||||
buffer_entries.push(entry);
|
||||
} else {
|
||||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
new_diffs.push(cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
|
||||
let _ = diff.set_base_text_buffer(
|
||||
location.buffer.clone(),
|
||||
branch_buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
);
|
||||
diff
|
||||
}));
|
||||
buffer_entries.push(BufferEntry {
|
||||
branch: branch_buffer.clone(),
|
||||
base: location.buffer.clone(),
|
||||
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
||||
});
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
branch_buffer,
|
||||
location
|
||||
.ranges
|
||||
.into_iter()
|
||||
.map(|range| ExcerptRange::new(range)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.buffer_entries = buffer_entries;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.refresh()
|
||||
});
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
for diff in new_diffs {
|
||||
buffer.add_diff(diff, cx)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub fn recalculate_all_buffer_diffs(&self) {
|
||||
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer: entry.branch.clone(),
|
||||
debounce: ix > 0,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
event: &BufferEvent,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
if let BufferEvent::Operation { .. } = event {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: true,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProposedChangesEditor {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.key_context("ProposedChangesEditor")
|
||||
.child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProposedChangesEditor {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
|
||||
|
||||
impl Item for ProposedChangesEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::Diff))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
self.title.clone()
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<gpui::AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::added_to_workspace(editor, workspace, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn std::any::Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: workspace::ItemNavHistory,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).can_save(cx)
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
options: SaveOptions,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::save(editor, options, project, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ProposedChangesEditorToolbar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
|
||||
if self.current_editor.is_some() {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProposedChangesEditorToolbar {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
|
||||
|
||||
match &self.current_editor {
|
||||
Some(editor) => {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx);
|
||||
|
||||
button_like.child(keybinding).on_click({
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
|
||||
}
|
||||
})
|
||||
}
|
||||
None => button_like.disabled(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
|
||||
|
||||
impl ToolbarItemView for ProposedChangesEditorToolbar {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::ItemHandle>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
self.current_editor =
|
||||
active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
|
||||
self.get_toolbar_item_location()
|
||||
}
|
||||
}
|
||||
|
||||
impl BranchBufferSemanticsProvider {
|
||||
fn to_base(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
positions: &[text::Anchor],
|
||||
cx: &App,
|
||||
) -> Option<Entity<Buffer>> {
|
||||
let base_buffer = buffer.read(cx).base_buffer()?;
|
||||
let version = base_buffer.read(cx).version();
|
||||
if positions
|
||||
.iter()
|
||||
.any(|position| !version.observed(position.timestamp))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(base_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl SemanticsProvider for BranchBufferSemanticsProvider {
|
||||
fn hover(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.hover(&buffer, position, cx)
|
||||
}
|
||||
|
||||
fn applicable_inlay_chunks(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
ranges: &[Range<text::Anchor>],
|
||||
cx: &mut App,
|
||||
) -> Vec<Range<BufferRow>> {
|
||||
self.0.applicable_inlay_chunks(buffer, ranges, cx)
|
||||
}
|
||||
|
||||
fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
|
||||
self.0.invalidate_inlay_hints(for_buffers, cx);
|
||||
}
|
||||
|
||||
fn inlay_hints(
|
||||
&self,
|
||||
invalidate: InvalidationStrategy,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: Vec<Range<text::Anchor>>,
|
||||
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
|
||||
cx: &mut App,
|
||||
) -> Option<HashMap<Range<BufferRow>, Task<anyhow::Result<CacheInlayHints>>>> {
|
||||
let positions = ranges
|
||||
.iter()
|
||||
.flat_map(|range| [range.start, range.end])
|
||||
.collect::<Vec<_>>();
|
||||
let buffer = self.to_base(&buffer, &positions, cx)?;
|
||||
self.0
|
||||
.inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
|
||||
}
|
||||
|
||||
fn inline_values(
|
||||
&self,
|
||||
_: Entity<Buffer>,
|
||||
_: Range<text::Anchor>,
|
||||
_: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
|
||||
if let Some(buffer) = self.to_base(buffer, &[], cx) {
|
||||
self.0.supports_inlay_hints(&buffer, cx)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn document_highlights(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.document_highlights(&buffer, position, cx)
|
||||
}
|
||||
|
||||
fn definitions(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
kind: crate::GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.definitions(&buffer, position, kind, cx)
|
||||
}
|
||||
|
||||
fn range_for_rename(
|
||||
&self,
|
||||
_: &Entity<Buffer>,
|
||||
_: text::Anchor,
|
||||
_: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Option<Range<text::Anchor>>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn perform_rename(
|
||||
&self,
|
||||
_: &Entity<Buffer>,
|
||||
_: text::Anchor,
|
||||
_: String,
|
||||
_: &mut App,
|
||||
) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,10 @@ use git::{
|
||||
AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository,
|
||||
GitRepositoryCheckpoint, PushOptions, Remote, RepoPath, ResetMode,
|
||||
},
|
||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||
status::{
|
||||
DiffTreeType, FileStatus, GitStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
|
||||
UnmergedStatus,
|
||||
},
|
||||
};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task, TaskLabel};
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
@@ -41,6 +44,9 @@ pub struct FakeGitRepositoryState {
|
||||
pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
|
||||
pub head_contents: HashMap<RepoPath, String>,
|
||||
pub index_contents: HashMap<RepoPath, String>,
|
||||
// everything in commit contents is in oids
|
||||
pub merge_base_contents: HashMap<RepoPath, Oid>,
|
||||
pub oids: HashMap<Oid, String>,
|
||||
pub blames: HashMap<RepoPath, Blame>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
@@ -60,6 +66,8 @@ impl FakeGitRepositoryState {
|
||||
branches: Default::default(),
|
||||
simulated_index_write_error_message: Default::default(),
|
||||
refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
|
||||
merge_base_contents: Default::default(),
|
||||
oids: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +118,13 @@ impl GitRepository for FakeGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_blob_content(&self, oid: git::Oid) -> BoxFuture<'_, Result<String>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
state.oids.get(&oid).cloned().context("oid does not exist")
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_commit(
|
||||
&self,
|
||||
_commit: String,
|
||||
@@ -140,6 +155,34 @@ impl GitRepository for FakeGitRepository {
|
||||
None
|
||||
}
|
||||
|
||||
fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
|
||||
let mut entries = HashMap::default();
|
||||
self.with_state_async(false, |state| {
|
||||
for (path, content) in &state.head_contents {
|
||||
let status = if let Some((oid, original)) = state
|
||||
.merge_base_contents
|
||||
.get(path)
|
||||
.map(|oid| (oid, &state.oids[oid]))
|
||||
{
|
||||
if original == content {
|
||||
continue;
|
||||
}
|
||||
TreeDiffStatus::Modified { old: *oid }
|
||||
} else {
|
||||
TreeDiffStatus::Added
|
||||
};
|
||||
entries.insert(path.clone(), status);
|
||||
}
|
||||
for (path, oid) in &state.merge_base_contents {
|
||||
if !entries.contains_key(path) {
|
||||
entries.insert(path.clone(), TreeDiffStatus::Deleted { old: *oid });
|
||||
}
|
||||
}
|
||||
Ok(TreeDiff { entries })
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
|
||||
self.with_state_async(false, |state| {
|
||||
Ok(revs
|
||||
@@ -523,7 +566,7 @@ impl GitRepository for FakeGitRepository {
|
||||
let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
let oid = Oid::random(&mut executor.rng());
|
||||
let oid = git::Oid::random(&mut executor.rng());
|
||||
let entry = fs.entry(&repository_dir_path)?;
|
||||
checkpoints.lock().insert(oid, entry);
|
||||
Ok(GitRepositoryCheckpoint { commit_sha: oid })
|
||||
@@ -579,7 +622,7 @@ impl GitRepository for FakeGitRepository {
|
||||
}
|
||||
|
||||
fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
|
||||
unimplemented!()
|
||||
async { Ok(Some("main".into())) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1752,6 +1752,26 @@ impl FakeFs {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_merge_base_content_for_repo(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
contents_by_path: &[(&str, String)],
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
use git::Oid;
|
||||
|
||||
state.merge_base_contents.clear();
|
||||
let oids = (1..)
|
||||
.map(|n| n.to_string())
|
||||
.map(|n| Oid::from_bytes(n.repeat(20).as_bytes()).unwrap());
|
||||
for ((path, content), oid) in contents_by_path.iter().zip(oids) {
|
||||
state.merge_base_contents.insert(repo_path(path), oid);
|
||||
state.oids.insert(oid, content.clone());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.blames.clear();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::commit::parse_git_diff_name_status;
|
||||
use crate::stash::GitStash;
|
||||
use crate::status::{GitStatus, StatusCode};
|
||||
use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff};
|
||||
use crate::{Oid, SHORT_SHA_LENGTH};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use collections::HashMap;
|
||||
@@ -350,6 +350,7 @@ pub trait GitRepository: Send + Sync {
|
||||
///
|
||||
/// Also returns `None` for symlinks.
|
||||
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
|
||||
fn load_blob_content(&self, oid: Oid) -> BoxFuture<'_, Result<String>>;
|
||||
|
||||
fn set_index_text(
|
||||
&self,
|
||||
@@ -379,6 +380,7 @@ pub trait GitRepository: Send + Sync {
|
||||
fn merge_message(&self) -> BoxFuture<'_, Option<String>>;
|
||||
|
||||
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>>;
|
||||
|
||||
@@ -908,6 +910,17 @@ impl GitRepository for RealGitRepository {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_blob_content(&self, oid: Oid) -> BoxFuture<'_, Result<String>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let repo = repo.lock();
|
||||
let content = repo.find_blob(oid.0)?.content().to_owned();
|
||||
Ok(String::from_utf8(content)?)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn set_index_text(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
@@ -1060,6 +1073,50 @@ impl GitRepository for RealGitRepository {
|
||||
})
|
||||
}
|
||||
|
||||
fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
let working_directory = match self.working_directory() {
|
||||
Ok(working_directory) => working_directory,
|
||||
Err(e) => return Task::ready(Err(e)).boxed(),
|
||||
};
|
||||
|
||||
let mut args = vec![
|
||||
OsString::from("--no-optional-locks"),
|
||||
OsString::from("diff-tree"),
|
||||
OsString::from("-r"),
|
||||
OsString::from("-z"),
|
||||
OsString::from("--no-renames"),
|
||||
];
|
||||
match request {
|
||||
DiffTreeType::MergeBase { base, head } => {
|
||||
args.push("--merge-base".into());
|
||||
args.push(OsString::from(base.as_str()));
|
||||
args.push(OsString::from(head.as_str()));
|
||||
}
|
||||
DiffTreeType::Since { base, head } => {
|
||||
args.push(OsString::from(base.as_str()));
|
||||
args.push(OsString::from(head.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(working_directory)
|
||||
.args(args)
|
||||
.output()
|
||||
.await?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout.parse()
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("git status failed: {stderr}");
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>> {
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
let working_directory = self.working_directory();
|
||||
@@ -1827,13 +1884,23 @@ impl GitRepository for RealGitRepository {
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
let output = git
|
||||
.run(&["symbolic-ref", "refs/remotes/origin/HEAD"])
|
||||
.await?;
|
||||
if let Ok(output) = git.run(&["symbolic-ref", "refs/remotes/origin/HEAD"]).await {
|
||||
return Ok(output
|
||||
.strip_prefix("refs/remotes/origin/")
|
||||
.map(|s| SharedString::from(s.to_owned())));
|
||||
}
|
||||
|
||||
Ok(output
|
||||
.strip_prefix("refs/remotes/origin/")
|
||||
.map(|s| SharedString::from(s.to_owned())))
|
||||
if let Ok(default_branch) = git.run(&["config", "init.defaultBranch"]).await {
|
||||
if git.run(&["rev-parse", &default_branch]).await.is_ok() {
|
||||
return Ok(Some(default_branch.into()));
|
||||
}
|
||||
}
|
||||
|
||||
if git.run(&["rev-parse", "master"]).await.is_ok() {
|
||||
return Ok(Some("master".into()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
+134
-3
@@ -1,5 +1,7 @@
|
||||
use crate::repository::RepoPath;
|
||||
use anyhow::Result;
|
||||
use crate::{Oid, repository::RepoPath};
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
@@ -190,7 +192,11 @@ impl FileStatus {
|
||||
}
|
||||
|
||||
pub fn is_deleted(self) -> bool {
|
||||
matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted)))
|
||||
let FileStatus::Tracked(tracked) = self else {
|
||||
return false;
|
||||
};
|
||||
tracked.index_status == StatusCode::Deleted && tracked.worktree_status != StatusCode::Added
|
||||
|| tracked.worktree_status == StatusCode::Deleted
|
||||
}
|
||||
|
||||
pub fn is_untracked(self) -> bool {
|
||||
@@ -486,3 +492,128 @@ impl Default for GitStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DiffTreeType {
|
||||
MergeBase {
|
||||
base: SharedString,
|
||||
head: SharedString,
|
||||
},
|
||||
Since {
|
||||
base: SharedString,
|
||||
head: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
impl DiffTreeType {
|
||||
pub fn base(&self) -> &SharedString {
|
||||
match self {
|
||||
DiffTreeType::MergeBase { base, .. } => base,
|
||||
DiffTreeType::Since { base, .. } => base,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn head(&self) -> &SharedString {
|
||||
match self {
|
||||
DiffTreeType::MergeBase { head, .. } => head,
|
||||
DiffTreeType::Since { head, .. } => head,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct TreeDiff {
|
||||
pub entries: HashMap<RepoPath, TreeDiffStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TreeDiffStatus {
|
||||
Added,
|
||||
Modified { old: Oid },
|
||||
Deleted { old: Oid },
|
||||
}
|
||||
|
||||
impl FromStr for TreeDiff {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut fields = s.split('\0');
|
||||
let mut parsed = HashMap::default();
|
||||
while let Some((status, path)) = fields.next().zip(fields.next()) {
|
||||
let path = RepoPath(RelPath::unix(path)?.into());
|
||||
|
||||
let mut fields = status.split(" ").skip(2);
|
||||
let old_sha = fields
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("expected to find old_sha"))?
|
||||
.to_owned()
|
||||
.parse()?;
|
||||
let _new_sha = fields
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("expected to find new_sha"))?;
|
||||
let status = fields
|
||||
.next()
|
||||
.and_then(|s| {
|
||||
if s.len() == 1 {
|
||||
s.as_bytes().first()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("expected to find status"))?;
|
||||
|
||||
let result = match StatusCode::from_byte(*status)? {
|
||||
StatusCode::Modified => TreeDiffStatus::Modified { old: old_sha },
|
||||
StatusCode::Added => TreeDiffStatus::Added,
|
||||
StatusCode::Deleted => TreeDiffStatus::Deleted { old: old_sha },
|
||||
_status => continue,
|
||||
};
|
||||
|
||||
parsed.insert(path, result);
|
||||
}
|
||||
|
||||
Ok(Self { entries: parsed })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
repository::RepoPath,
|
||||
status::{TreeDiff, TreeDiffStatus},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_tree_diff_parsing() {
|
||||
let input = ":000000 100644 0000000000000000000000000000000000000000 0062c311b8727c3a2e3cd7a41bc9904feacf8f98 A\x00.zed/settings.json\x00".to_owned() +
|
||||
":100644 000000 bb3e9ed2e97a8c02545bae243264d342c069afb3 0000000000000000000000000000000000000000 D\x00README.md\x00" +
|
||||
":100644 100644 42f097005a1f21eb2260fad02ec8c991282beee8 a437d85f63bb8c62bd78f83f40c506631fabf005 M\x00parallel.go\x00";
|
||||
|
||||
let output: TreeDiff = input.parse().unwrap();
|
||||
assert_eq!(
|
||||
output,
|
||||
TreeDiff {
|
||||
entries: [
|
||||
(
|
||||
RepoPath::new(".zed/settings.json").unwrap(),
|
||||
TreeDiffStatus::Added,
|
||||
),
|
||||
(
|
||||
RepoPath::new("README.md").unwrap(),
|
||||
TreeDiffStatus::Deleted {
|
||||
old: "bb3e9ed2e97a8c02545bae243264d342c069afb3".parse().unwrap()
|
||||
}
|
||||
),
|
||||
(
|
||||
RepoPath::new("parallel.go").unwrap(),
|
||||
TreeDiffStatus::Modified {
|
||||
old: "42f097005a1f21eb2260fad02ec8c991282beee8".parse().unwrap(),
|
||||
}
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ multi_buffer.workspace = true
|
||||
notifications.workspace = true
|
||||
panel.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
+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);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod branch_diff;
|
||||
mod conflict_set;
|
||||
pub mod git_traversal;
|
||||
|
||||
@@ -30,7 +31,8 @@ use git::{
|
||||
},
|
||||
stash::{GitStash, StashEntry},
|
||||
status::{
|
||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||
DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus,
|
||||
UnmergedStatus, UnmergedStatusCode,
|
||||
},
|
||||
};
|
||||
use gpui::{
|
||||
@@ -55,6 +57,7 @@ use std::{
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{self, AtomicU64},
|
||||
@@ -432,6 +435,8 @@ impl GitStore {
|
||||
client.add_entity_request_handler(Self::handle_askpass);
|
||||
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
|
||||
client.add_entity_request_handler(Self::handle_git_diff);
|
||||
client.add_entity_request_handler(Self::handle_tree_diff);
|
||||
client.add_entity_request_handler(Self::handle_get_blob_content);
|
||||
client.add_entity_request_handler(Self::handle_open_unstaged_diff);
|
||||
client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
|
||||
client.add_entity_message_handler(Self::handle_update_diff_bases);
|
||||
@@ -619,6 +624,52 @@ impl GitStore {
|
||||
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
||||
}
|
||||
|
||||
pub fn open_diff_since(
|
||||
&mut self,
|
||||
oid: Option<git::Oid>,
|
||||
buffer: Entity<Buffer>,
|
||||
repo: Entity<Repository>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
let content = match oid {
|
||||
None => None,
|
||||
Some(oid) => Some(
|
||||
repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))?
|
||||
.await?,
|
||||
),
|
||||
};
|
||||
let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx))?;
|
||||
|
||||
buffer_diff
|
||||
.update(cx, |buffer_diff, cx| {
|
||||
buffer_diff.set_base_text(
|
||||
content.map(Arc::new),
|
||||
buffer_snapshot.language().cloned(),
|
||||
Some(languages.clone()),
|
||||
buffer_snapshot.text,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let unstaged_diff = this
|
||||
.update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))?
|
||||
.await?;
|
||||
buffer_diff.update(cx, |buffer_diff, _| {
|
||||
buffer_diff.set_secondary_diff(unstaged_diff);
|
||||
})?;
|
||||
|
||||
this.update(cx, |_, cx| {
|
||||
cx.subscribe(&buffer_diff, Self::on_buffer_diff_event)
|
||||
.detach();
|
||||
})?;
|
||||
|
||||
Ok(buffer_diff)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_uncommitted_diff(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -2168,6 +2219,75 @@ impl GitStore {
|
||||
Ok(proto::GitDiffResponse { diff })
|
||||
}
|
||||
|
||||
async fn handle_tree_diff(
|
||||
this: Entity<Self>,
|
||||
request: TypedEnvelope<proto::GetTreeDiff>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::GetTreeDiffResponse> {
|
||||
let repository_id = RepositoryId(request.payload.repository_id);
|
||||
let diff_type = if request.payload.is_merge {
|
||||
DiffTreeType::MergeBase {
|
||||
base: request.payload.base.into(),
|
||||
head: request.payload.head.into(),
|
||||
}
|
||||
} else {
|
||||
DiffTreeType::Since {
|
||||
base: request.payload.base.into(),
|
||||
head: request.payload.head.into(),
|
||||
}
|
||||
};
|
||||
|
||||
let diff = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let repository = this.repositories().get(&repository_id)?;
|
||||
Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx)))
|
||||
})?
|
||||
.context("missing repository")?
|
||||
.await??;
|
||||
|
||||
Ok(proto::GetTreeDiffResponse {
|
||||
entries: diff
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|(path, status)| proto::TreeDiffStatus {
|
||||
path: path.0.to_proto(),
|
||||
status: match status {
|
||||
TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
|
||||
TreeDiffStatus::Modified { .. } => {
|
||||
proto::tree_diff_status::Status::Modified.into()
|
||||
}
|
||||
TreeDiffStatus::Deleted { .. } => {
|
||||
proto::tree_diff_status::Status::Deleted.into()
|
||||
}
|
||||
},
|
||||
oid: match status {
|
||||
TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => {
|
||||
Some(old.to_string())
|
||||
}
|
||||
TreeDiffStatus::Added => None,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_get_blob_content(
|
||||
this: Entity<Self>,
|
||||
request: TypedEnvelope<proto::GetBlobContent>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::GetBlobContentResponse> {
|
||||
let oid = git::Oid::from_str(&request.payload.oid)?;
|
||||
let repository_id = RepositoryId(request.payload.repository_id);
|
||||
let content = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let repository = this.repositories().get(&repository_id)?;
|
||||
Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx)))
|
||||
})?
|
||||
.context("missing repository")?
|
||||
.await?;
|
||||
Ok(proto::GetBlobContentResponse { content })
|
||||
}
|
||||
|
||||
async fn handle_open_unstaged_diff(
|
||||
this: Entity<Self>,
|
||||
request: TypedEnvelope<proto::OpenUnstagedDiff>,
|
||||
@@ -4303,6 +4423,62 @@ impl Repository {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff_tree(
|
||||
&mut self,
|
||||
diff_type: DiffTreeType,
|
||||
_cx: &App,
|
||||
) -> oneshot::Receiver<Result<TreeDiff>> {
|
||||
let repository_id = self.snapshot.id;
|
||||
self.send_job(None, move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local { backend, .. } => backend.diff_tree(diff_type).await,
|
||||
RepositoryState::Remote { client, project_id } => {
|
||||
let response = client
|
||||
.request(proto::GetTreeDiff {
|
||||
project_id: project_id.0,
|
||||
repository_id: repository_id.0,
|
||||
is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }),
|
||||
base: diff_type.base().to_string(),
|
||||
head: diff_type.head().to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let entries = response
|
||||
.entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let status = match entry.status() {
|
||||
proto::tree_diff_status::Status::Added => TreeDiffStatus::Added,
|
||||
proto::tree_diff_status::Status::Modified => {
|
||||
TreeDiffStatus::Modified {
|
||||
old: git::Oid::from_str(
|
||||
&entry.oid.context("missing oid").log_err()?,
|
||||
)
|
||||
.log_err()?,
|
||||
}
|
||||
}
|
||||
proto::tree_diff_status::Status::Deleted => {
|
||||
TreeDiffStatus::Deleted {
|
||||
old: git::Oid::from_str(
|
||||
&entry.oid.context("missing oid").log_err()?,
|
||||
)
|
||||
.log_err()?,
|
||||
}
|
||||
}
|
||||
};
|
||||
Some((
|
||||
RepoPath(RelPath::from_proto(&entry.path).log_err()?),
|
||||
status,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(TreeDiff { entries })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
|
||||
let id = self.id;
|
||||
self.send_job(None, move |repo, _cx| async move {
|
||||
@@ -4775,6 +4951,25 @@ impl Repository {
|
||||
|
||||
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
||||
}
|
||||
fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task<Result<String>> {
|
||||
let repository_id = self.snapshot.id;
|
||||
let rx = self.send_job(None, move |state, _| async move {
|
||||
match state {
|
||||
RepositoryState::Local { backend, .. } => backend.load_blob_content(oid).await,
|
||||
RepositoryState::Remote { client, project_id } => {
|
||||
let response = client
|
||||
.request(proto::GetBlobContent {
|
||||
project_id: project_id.to_proto(),
|
||||
repository_id: repository_id.0,
|
||||
oid: oid.to_string(),
|
||||
})
|
||||
.await?;
|
||||
Ok(response.content)
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
||||
}
|
||||
|
||||
fn paths_changed(
|
||||
&mut self,
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
repository::RepoPath,
|
||||
status::{DiffTreeType, FileStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus},
|
||||
};
|
||||
use gpui::{
|
||||
App, AsyncWindowContext, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
|
||||
use language::Buffer;
|
||||
use text::BufferId;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
Project,
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DiffBase {
|
||||
Head,
|
||||
Merge { base_ref: SharedString },
|
||||
}
|
||||
|
||||
impl DiffBase {
|
||||
pub fn is_merge_base(&self) -> bool {
|
||||
matches!(self, DiffBase::Merge { .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BranchDiff {
|
||||
diff_base: DiffBase,
|
||||
repo: Option<Entity<Repository>>,
|
||||
project: Entity<Project>,
|
||||
base_commit: Option<SharedString>,
|
||||
head_commit: Option<SharedString>,
|
||||
tree_diff: Option<TreeDiff>,
|
||||
_subscription: Subscription,
|
||||
update_needed: postage::watch::Sender<()>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
pub enum BranchDiffEvent {
|
||||
FileListChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<BranchDiffEvent> for BranchDiff {}
|
||||
|
||||
impl BranchDiff {
|
||||
pub fn new(
|
||||
source: DiffBase,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let git_store_subscription = cx.subscribe_in(
|
||||
&git_store,
|
||||
window,
|
||||
move |this, _git_store, event, _window, cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_)
|
||||
| GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: _ },
|
||||
true,
|
||||
)
|
||||
| GitStoreEvent::ConflictsUpdated => {
|
||||
cx.emit(BranchDiffEvent::FileListChanged);
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
let (send, recv) = postage::watch::channel::<()>();
|
||||
let worker = window.spawn(cx, {
|
||||
let this = cx.weak_entity();
|
||||
async |cx| Self::handle_status_updates(this, recv, cx).await
|
||||
});
|
||||
let repo = git_store.read(cx).active_repository();
|
||||
|
||||
Self {
|
||||
diff_base: source,
|
||||
repo,
|
||||
project,
|
||||
tree_diff: None,
|
||||
base_commit: None,
|
||||
head_commit: None,
|
||||
_subscription: git_store_subscription,
|
||||
_task: worker,
|
||||
update_needed: send,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_base(&self) -> &DiffBase {
|
||||
&self.diff_base
|
||||
}
|
||||
|
||||
pub async fn handle_status_updates(
|
||||
this: WeakEntity<Self>,
|
||||
mut recv: postage::watch::Receiver<()>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) {
|
||||
Self::reload_tree_diff(this.clone(), cx).await.log_err();
|
||||
while recv.next().await.is_some() {
|
||||
let Ok(needs_update) = this.update(cx, |this, cx| {
|
||||
let mut needs_update = false;
|
||||
let active_repo = this
|
||||
.project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.active_repository();
|
||||
if active_repo != this.repo {
|
||||
needs_update = true;
|
||||
this.repo = active_repo;
|
||||
} else if let Some(repo) = this.repo.as_ref() {
|
||||
repo.update(cx, |repo, _| {
|
||||
if let Some(branch) = &repo.branch
|
||||
&& let DiffBase::Merge { base_ref } = &this.diff_base
|
||||
&& let Some(commit) = branch.most_recent_commit.as_ref()
|
||||
&& &branch.ref_name == base_ref
|
||||
&& this.base_commit.as_ref() != Some(&commit.sha)
|
||||
{
|
||||
this.base_commit = Some(commit.sha.clone());
|
||||
needs_update = true;
|
||||
}
|
||||
|
||||
if repo.head_commit.as_ref().map(|c| &c.sha) != this.head_commit.as_ref() {
|
||||
this.head_commit = repo.head_commit.as_ref().map(|c| c.sha.clone());
|
||||
needs_update = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
needs_update
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
Self::reload_tree_diff(this.clone(), cx).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||
let (repo, path) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer_id, cx)?;
|
||||
if self.repo() == Some(&repo) {
|
||||
return self.merge_statuses(
|
||||
repo.read(cx)
|
||||
.status_for_path(&path)
|
||||
.map(|status| status.status),
|
||||
self.tree_diff
|
||||
.as_ref()
|
||||
.and_then(|diff| diff.entries.get(&path)),
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn merge_statuses(
|
||||
&self,
|
||||
diff_from_head: Option<FileStatus>,
|
||||
diff_from_merge_base: Option<&TreeDiffStatus>,
|
||||
) -> Option<FileStatus> {
|
||||
match (diff_from_head, diff_from_merge_base) {
|
||||
(None, None) => None,
|
||||
(Some(diff_from_head), None) => Some(diff_from_head),
|
||||
(Some(diff_from_head @ FileStatus::Unmerged(_)), _) => Some(diff_from_head),
|
||||
|
||||
// file does not exist in HEAD
|
||||
// but *does* exist in work-tree
|
||||
// and *does* exist in merge-base
|
||||
(
|
||||
Some(FileStatus::Untracked)
|
||||
| Some(FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: _,
|
||||
})),
|
||||
Some(_),
|
||||
) => Some(FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Modified,
|
||||
worktree_status: StatusCode::Modified,
|
||||
})),
|
||||
|
||||
// file exists in HEAD
|
||||
// but *does not* exist in work-tree
|
||||
(Some(diff_from_head), Some(diff_from_merge_base)) if diff_from_head.is_deleted() => {
|
||||
match diff_from_merge_base {
|
||||
TreeDiffStatus::Added => None, // unchanged, didn't exist in merge base or worktree
|
||||
_ => Some(diff_from_head),
|
||||
}
|
||||
}
|
||||
|
||||
// file exists in HEAD
|
||||
// and *does* exist in work-tree
|
||||
(Some(FileStatus::Tracked(_)), Some(tree_status)) => {
|
||||
Some(FileStatus::Tracked(TrackedStatus {
|
||||
index_status: match tree_status {
|
||||
TreeDiffStatus::Added { .. } => StatusCode::Added,
|
||||
_ => StatusCode::Modified,
|
||||
},
|
||||
worktree_status: match tree_status {
|
||||
TreeDiffStatus::Added => StatusCode::Added,
|
||||
_ => StatusCode::Modified,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
(_, Some(diff_from_merge_base)) => {
|
||||
Some(diff_status_to_file_status(diff_from_merge_base))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload_tree_diff(
|
||||
this: WeakEntity<Self>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let task = this.update(cx, |this, cx| {
|
||||
let DiffBase::Merge { base_ref } = this.diff_base.clone() else {
|
||||
return None;
|
||||
};
|
||||
let Some(repo) = this.repo.as_ref() else {
|
||||
this.tree_diff.take();
|
||||
return None;
|
||||
};
|
||||
repo.update(cx, |repo, cx| {
|
||||
Some(repo.diff_tree(
|
||||
DiffTreeType::MergeBase {
|
||||
base: base_ref,
|
||||
head: "HEAD".into(),
|
||||
},
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})?;
|
||||
let Some(task) = task else { return Ok(()) };
|
||||
|
||||
let diff = task.await??;
|
||||
this.update(cx, |this, cx| {
|
||||
this.tree_diff = Some(diff);
|
||||
cx.emit(BranchDiffEvent::FileListChanged);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn repo(&self) -> Option<&Entity<Repository>> {
|
||||
self.repo.as_ref()
|
||||
}
|
||||
|
||||
pub fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<DiffBuffer> {
|
||||
let mut output = Vec::default();
|
||||
let Some(repo) = self.repo.clone() else {
|
||||
return output;
|
||||
};
|
||||
|
||||
self.project.update(cx, |_project, cx| {
|
||||
let mut seen = HashSet::default();
|
||||
|
||||
for item in repo.read(cx).cached_status() {
|
||||
seen.insert(item.repo_path.clone());
|
||||
let branch_diff = self
|
||||
.tree_diff
|
||||
.as_ref()
|
||||
.and_then(|t| t.entries.get(&item.repo_path))
|
||||
.cloned();
|
||||
let status = self
|
||||
.merge_statuses(Some(item.status), branch_diff.as_ref())
|
||||
.unwrap();
|
||||
if !status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(project_path) =
|
||||
repo.read(cx).repo_path_to_project_path(&item.repo_path, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let task = Self::load_buffer(branch_diff, project_path, repo.clone(), cx);
|
||||
|
||||
output.push(DiffBuffer {
|
||||
repo_path: item.repo_path.clone(),
|
||||
load: task,
|
||||
file_status: item.status,
|
||||
});
|
||||
}
|
||||
let Some(tree_diff) = self.tree_diff.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (path, branch_diff) in tree_diff.entries.iter() {
|
||||
if seen.contains(&path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(project_path) = repo.read(cx).repo_path_to_project_path(&path, cx) else {
|
||||
continue;
|
||||
};
|
||||
let task =
|
||||
Self::load_buffer(Some(branch_diff.clone()), project_path, repo.clone(), cx);
|
||||
|
||||
let file_status = diff_status_to_file_status(branch_diff);
|
||||
|
||||
output.push(DiffBuffer {
|
||||
repo_path: path.clone(),
|
||||
load: task,
|
||||
file_status,
|
||||
});
|
||||
}
|
||||
});
|
||||
output
|
||||
}
|
||||
|
||||
fn load_buffer(
|
||||
branch_diff: Option<git::status::TreeDiffStatus>,
|
||||
project_path: crate::ProjectPath,
|
||||
repo: Entity<Repository>,
|
||||
cx: &Context<'_, Project>,
|
||||
) -> Task<Result<(Entity<Buffer>, Entity<BufferDiff>)>> {
|
||||
let task = cx.spawn(async move |project, cx| {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let languages = project.update(cx, |project, _cx| project.languages().clone())?;
|
||||
|
||||
let changes = if let Some(entry) = branch_diff {
|
||||
let oid = match entry {
|
||||
git::status::TreeDiffStatus::Added { .. } => None,
|
||||
git::status::TreeDiffStatus::Modified { old, .. }
|
||||
| git::status::TreeDiffStatus::Deleted { old } => Some(old),
|
||||
};
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.git_store().update(cx, |git_store, cx| {
|
||||
git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx)
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
} else {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
Ok((buffer, changes))
|
||||
});
|
||||
task
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_status_to_file_status(branch_diff: &git::status::TreeDiffStatus) -> FileStatus {
|
||||
let file_status = match branch_diff {
|
||||
git::status::TreeDiffStatus::Added { .. } => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: StatusCode::Added,
|
||||
}),
|
||||
git::status::TreeDiffStatus::Modified { .. } => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Modified,
|
||||
worktree_status: StatusCode::Modified,
|
||||
}),
|
||||
git::status::TreeDiffStatus::Deleted { .. } => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Deleted,
|
||||
}),
|
||||
};
|
||||
file_status
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DiffBuffer {
|
||||
pub repo_path: RepoPath,
|
||||
pub file_status: FileStatus,
|
||||
pub load: Task<Result<(Entity<Buffer>, Entity<BufferDiff>)>>,
|
||||
}
|
||||
@@ -472,3 +472,37 @@ message GetDefaultBranch {
|
||||
message GetDefaultBranchResponse {
|
||||
optional string branch = 1;
|
||||
}
|
||||
|
||||
message GetTreeDiff {
|
||||
uint64 project_id = 1;
|
||||
uint64 repository_id = 2;
|
||||
bool is_merge = 3;
|
||||
string base = 4;
|
||||
string head = 5;
|
||||
}
|
||||
|
||||
message GetTreeDiffResponse {
|
||||
repeated TreeDiffStatus entries = 1;
|
||||
}
|
||||
|
||||
message TreeDiffStatus {
|
||||
enum Status {
|
||||
ADDED = 0;
|
||||
MODIFIED = 1;
|
||||
DELETED = 2;
|
||||
}
|
||||
|
||||
Status status = 1;
|
||||
string path = 2;
|
||||
optional string oid = 3;
|
||||
}
|
||||
|
||||
message GetBlobContent {
|
||||
uint64 project_id = 1;
|
||||
uint64 repository_id = 2;
|
||||
string oid =3;
|
||||
}
|
||||
|
||||
message GetBlobContentResponse {
|
||||
string content = 1;
|
||||
}
|
||||
|
||||
@@ -421,7 +421,13 @@ message Envelope {
|
||||
RemoteStarted remote_started = 381;
|
||||
|
||||
GetDirectoryEnvironment get_directory_environment = 382;
|
||||
DirectoryEnvironment directory_environment = 383; // current max
|
||||
DirectoryEnvironment directory_environment = 383;
|
||||
|
||||
GetTreeDiff get_tree_diff = 384;
|
||||
GetTreeDiffResponse get_tree_diff_response = 385;
|
||||
|
||||
GetBlobContent get_blob_content = 386;
|
||||
GetBlobContentResponse get_blob_content_response = 387; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
||||
@@ -316,6 +316,10 @@ messages!(
|
||||
(PullWorkspaceDiagnostics, Background),
|
||||
(GetDefaultBranch, Background),
|
||||
(GetDefaultBranchResponse, Background),
|
||||
(GetTreeDiff, Background),
|
||||
(GetTreeDiffResponse, Background),
|
||||
(GetBlobContent, Background),
|
||||
(GetBlobContentResponse, Background),
|
||||
(GitClone, Background),
|
||||
(GitCloneResponse, Background),
|
||||
(ToggleLspLogs, Background),
|
||||
@@ -497,6 +501,8 @@ request_messages!(
|
||||
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
|
||||
(PullWorkspaceDiagnostics, Ack),
|
||||
(GetDefaultBranch, GetDefaultBranchResponse),
|
||||
(GetBlobContent, GetBlobContentResponse),
|
||||
(GetTreeDiff, GetTreeDiffResponse),
|
||||
(GitClone, GitCloneResponse),
|
||||
(ToggleLspLogs, Ack),
|
||||
(GetDirectoryEnvironment, DirectoryEnvironment),
|
||||
@@ -659,6 +665,8 @@ entity_messages!(
|
||||
GetDocumentDiagnostics,
|
||||
PullWorkspaceDiagnostics,
|
||||
GetDefaultBranch,
|
||||
GetTreeDiff,
|
||||
GetBlobContent,
|
||||
GitClone,
|
||||
GetAgentServerCommand,
|
||||
ExternalAgentsUpdated,
|
||||
|
||||
@@ -18,7 +18,6 @@ use breadcrumbs::Breadcrumbs;
|
||||
use client::zed_urls;
|
||||
use collections::VecDeque;
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::ProposedChangesEditorToolbar;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
|
||||
@@ -1035,8 +1034,6 @@ fn initialize_pane(
|
||||
)
|
||||
});
|
||||
toolbar.add_item(buffer_search_bar.clone(), window, cx);
|
||||
let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
|
||||
toolbar.add_item(proposed_change_bar, window, cx);
|
||||
let quick_action_bar =
|
||||
cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
|
||||
toolbar.add_item(quick_action_bar, window, cx);
|
||||
|
||||
Reference in New Issue
Block a user