Cargo.lock 🔗
@@ -7071,7 +7071,6 @@ dependencies = [
"notifications",
"panel",
"picker",
- "postage",
"pretty_assertions",
"project",
"schemars 1.0.4",
Conrad Irwin and Cole Miller created
Release Notes:
- git: Adds the ability to view the diff of the current branch since
main
---------
Co-authored-by: Cole Miller <cole@zed.dev>
Cargo.lock | 1
crates/buffer_diff/src/buffer_diff.rs | 20
crates/collab/src/rpc.rs | 3
crates/editor/src/editor.rs | 82 --
crates/editor/src/element.rs | 9
crates/editor/src/proposed_changes_editor.rs | 523 -------------------
crates/fs/src/fake_git_repo.rs | 49 +
crates/fs/src/fs.rs | 20
crates/git/src/repository.rs | 81 ++
crates/git/src/status.rs | 137 +++++
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/project_diff.rs | 577 ++++++++++++++++-----
crates/project/src/git_store.rs | 197 +++++++
crates/project/src/git_store/branch_diff.rs | 386 ++++++++++++++
crates/proto/proto/git.proto | 34 +
crates/proto/proto/zed.proto | 8
crates/proto/src/proto.rs | 8
crates/zed/src/zed.rs | 3
18 files changed, 1,362 insertions(+), 777 deletions(-)
@@ -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>)
@@ -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())));
+ }
+
+ 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(output
- .strip_prefix("refs/remotes/origin/")
- .map(|s| SharedString::from(s.to_owned())))
+ Ok(None)
})
.boxed()
}
@@ -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
@@ -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);