Clear pending staged/unstaged diff hunks hunks when writing to the git index fails (#26173)
Release Notes: - Git Beta: Fixed a bug where discarding a hunk in the project diff view performed two concurrent saves of the buffer. - Git Beta: Fixed an issue where diff hunks appeared in the wrong state after failing to write to the git index.
This commit is contained in:
parent
d3c68650c0
commit
314ad5dd5f
14 changed files with 534 additions and 143 deletions
|
@ -663,11 +663,13 @@ impl std::fmt::Debug for BufferDiff {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum BufferDiffEvent {
|
pub enum BufferDiffEvent {
|
||||||
DiffChanged {
|
DiffChanged {
|
||||||
changed_range: Option<Range<text::Anchor>>,
|
changed_range: Option<Range<text::Anchor>>,
|
||||||
},
|
},
|
||||||
LanguageChanged,
|
LanguageChanged,
|
||||||
|
HunksStagedOrUnstaged(Option<Rope>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
||||||
|
@ -762,6 +764,17 @@ impl BufferDiff {
|
||||||
self.secondary_diff.clone()
|
self.secondary_diff.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if let Some(secondary_diff) = &self.secondary_diff {
|
||||||
|
secondary_diff.update(cx, |diff, _| {
|
||||||
|
diff.inner.pending_hunks.clear();
|
||||||
|
});
|
||||||
|
cx.emit(BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stage_or_unstage_hunks(
|
pub fn stage_or_unstage_hunks(
|
||||||
&mut self,
|
&mut self,
|
||||||
stage: bool,
|
stage: bool,
|
||||||
|
@ -784,6 +797,9 @@ impl BufferDiff {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||||
|
new_index_text.clone(),
|
||||||
|
));
|
||||||
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
||||||
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
||||||
cx.emit(BufferDiffEvent::DiffChanged {
|
cx.emit(BufferDiffEvent::DiffChanged {
|
||||||
|
@ -900,6 +916,14 @@ impl BufferDiff {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hunks<'a>(
|
||||||
|
&'a self,
|
||||||
|
buffer_snapshot: &'a text::BufferSnapshot,
|
||||||
|
cx: &'a App,
|
||||||
|
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||||
|
self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hunks_intersecting_range<'a>(
|
pub fn hunks_intersecting_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: Range<text::Anchor>,
|
range: Range<text::Anchor>,
|
||||||
|
|
|
@ -7843,7 +7843,7 @@ impl Editor {
|
||||||
for hunk in &hunks {
|
for hunk in &hunks {
|
||||||
self.prepare_restore_change(&mut revert_changes, hunk, cx);
|
self.prepare_restore_change(&mut revert_changes, hunk, cx);
|
||||||
}
|
}
|
||||||
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), window, cx);
|
self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
|
||||||
}
|
}
|
||||||
drop(chunk_by);
|
drop(chunk_by);
|
||||||
if !revert_changes.is_empty() {
|
if !revert_changes.is_empty() {
|
||||||
|
@ -13657,13 +13657,13 @@ impl Editor {
|
||||||
pub fn toggle_staged_selected_diff_hunks(
|
pub fn toggle_staged_selected_diff_hunks(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &::git::ToggleStaged,
|
_: &::git::ToggleStaged,
|
||||||
window: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect();
|
||||||
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
|
let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
|
||||||
self.stage_or_unstage_diff_hunks(stage, &ranges, window, cx);
|
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage_and_next(
|
pub fn stage_and_next(
|
||||||
|
@ -13687,16 +13687,53 @@ impl Editor {
|
||||||
pub fn stage_or_unstage_diff_hunks(
|
pub fn stage_or_unstage_diff_hunks(
|
||||||
&mut self,
|
&mut self,
|
||||||
stage: bool,
|
stage: bool,
|
||||||
ranges: &[Range<Anchor>],
|
ranges: Vec<Range<Anchor>>,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
|
||||||
let chunk_by = self
|
cx.spawn(|this, mut cx| async move {
|
||||||
.diff_hunks_in_ranges(&ranges, &snapshot)
|
task.await?;
|
||||||
.chunk_by(|hunk| hunk.buffer_id);
|
this.update(&mut cx, |this, cx| {
|
||||||
for (buffer_id, hunks) in &chunk_by {
|
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||||
self.do_stage_or_unstage(stage, buffer_id, hunks, window, cx);
|
let chunk_by = this
|
||||||
|
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||||
|
.chunk_by(|hunk| hunk.buffer_id);
|
||||||
|
for (buffer_id, hunks) in &chunk_by {
|
||||||
|
this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_buffers_for_ranges_if_needed(
|
||||||
|
&mut self,
|
||||||
|
ranges: &[Range<Anchor>],
|
||||||
|
cx: &mut Context<'_, Editor>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let multibuffer = self.buffer.read(cx);
|
||||||
|
let snapshot = multibuffer.read(cx);
|
||||||
|
let buffer_ids: HashSet<_> = ranges
|
||||||
|
.iter()
|
||||||
|
.flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
|
||||||
|
.collect();
|
||||||
|
drop(snapshot);
|
||||||
|
|
||||||
|
let mut buffers = HashSet::default();
|
||||||
|
for buffer_id in buffer_ids {
|
||||||
|
if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
|
||||||
|
let buffer = buffer_entity.read(cx);
|
||||||
|
if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
|
||||||
|
{
|
||||||
|
buffers.insert(buffer_entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(project) = &self.project {
|
||||||
|
project.update(cx, |project, cx| project.save_buffers(buffers, cx))
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13709,7 +13746,7 @@ impl Editor {
|
||||||
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
|
||||||
|
|
||||||
if ranges.iter().any(|range| range.start != range.end) {
|
if ranges.iter().any(|range| range.start != range.end) {
|
||||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13728,7 +13765,7 @@ impl Editor {
|
||||||
if run_twice {
|
if run_twice {
|
||||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||||
}
|
}
|
||||||
self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx);
|
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
||||||
self.go_to_next_hunk(&GoToHunk, window, cx);
|
self.go_to_next_hunk(&GoToHunk, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13737,31 +13774,16 @@ impl Editor {
|
||||||
stage: bool,
|
stage: bool,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) -> Option<()> {
|
||||||
let Some(project) = self.project.as_ref() else {
|
let project = self.project.as_ref()?;
|
||||||
return;
|
let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
|
||||||
};
|
let diff = self.buffer.read(cx).diff_for(buffer_id)?;
|
||||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(diff) = self.buffer.read(cx).diff_for(buffer_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
let file_exists = buffer_snapshot
|
let file_exists = buffer_snapshot
|
||||||
.file()
|
.file()
|
||||||
.is_some_and(|file| file.disk_state().exists());
|
.is_some_and(|file| file.disk_state().exists());
|
||||||
let Some((repo, path)) = project
|
diff.update(cx, |diff, cx| {
|
||||||
.read(cx)
|
|
||||||
.repository_and_path_for_buffer_id(buffer_id, cx)
|
|
||||||
else {
|
|
||||||
log::debug!("no git repo for buffer id");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_index_text = diff.update(cx, |diff, cx| {
|
|
||||||
diff.stage_or_unstage_hunks(
|
diff.stage_or_unstage_hunks(
|
||||||
stage,
|
stage,
|
||||||
&hunks
|
&hunks
|
||||||
|
@ -13777,20 +13799,7 @@ impl Editor {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
None
|
||||||
if file_exists {
|
|
||||||
let buffer_store = project.read(cx).buffer_store().clone();
|
|
||||||
buffer_store
|
|
||||||
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let recv = repo
|
|
||||||
.read(cx)
|
|
||||||
.set_index_text(&path, new_index_text.map(|rope| rope.to_string()));
|
|
||||||
|
|
||||||
cx.background_spawn(async move { recv.await? })
|
|
||||||
.detach_and_notify_err(window, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||||
|
@ -16305,7 +16314,7 @@ fn get_uncommitted_diff_for_buffer(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let diffs = futures::future::join_all(tasks).await;
|
let diffs = future::join_all(tasks).await;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, cx| {
|
.update(&mut cx, |buffer, cx| {
|
||||||
for diff in diffs.into_iter().flatten() {
|
for diff in diffs.into_iter().flatten() {
|
||||||
|
|
|
@ -77,7 +77,7 @@ use ui::{
|
||||||
POPOVER_Y_PADDING,
|
POPOVER_Y_PADDING,
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use util::{debug_panic, maybe, RangeExt, ResultExt};
|
use util::{debug_panic, RangeExt, ResultExt};
|
||||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
use workspace::{item::Item, notifications::NotifyTaskExt};
|
||||||
|
|
||||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||||
|
@ -2676,24 +2676,21 @@ impl EditorElement {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
let file_status = maybe!({
|
let editor = self.editor.read(cx);
|
||||||
let project = self.editor.read(cx).project.as_ref()?.read(cx);
|
let file_status = editor
|
||||||
let (repo, path) =
|
.buffer
|
||||||
project.repository_and_path_for_buffer_id(for_excerpt.buffer_id, cx)?;
|
|
||||||
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
|
|
||||||
Some(status.status)
|
|
||||||
})
|
|
||||||
.filter(|_| {
|
|
||||||
self.editor
|
|
||||||
.read(cx)
|
|
||||||
.buffer
|
|
||||||
.read(cx)
|
|
||||||
.all_diff_hunks_expanded()
|
|
||||||
});
|
|
||||||
|
|
||||||
let include_root = self
|
|
||||||
.editor
|
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
.all_diff_hunks_expanded()
|
||||||
|
.then(|| {
|
||||||
|
editor
|
||||||
|
.project
|
||||||
|
.as_ref()?
|
||||||
|
.read(cx)
|
||||||
|
.status_for_buffer_id(for_excerpt.buffer_id, cx)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let include_root = editor
|
||||||
.project
|
.project
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||||
|
@ -2705,7 +2702,7 @@ impl EditorElement {
|
||||||
let parent_path = path.as_ref().and_then(|path| {
|
let parent_path = path.as_ref().and_then(|path| {
|
||||||
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||||
});
|
});
|
||||||
let focus_handle = self.editor.focus_handle(cx);
|
let focus_handle = editor.focus_handle(cx);
|
||||||
let colors = cx.theme().colors();
|
let colors = cx.theme().colors();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
@ -2778,8 +2775,7 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.children(
|
.children(
|
||||||
self.editor
|
editor
|
||||||
.read(cx)
|
|
||||||
.addons
|
.addons
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|addon| {
|
.filter_map(|addon| {
|
||||||
|
@ -8822,12 +8818,11 @@ fn diff_hunk_controls(
|
||||||
})
|
})
|
||||||
.on_click({
|
.on_click({
|
||||||
let editor = editor.clone();
|
let editor = editor.clone();
|
||||||
move |_event, window, cx| {
|
move |_event, _window, cx| {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.stage_or_unstage_diff_hunks(
|
editor.stage_or_unstage_diff_hunks(
|
||||||
true,
|
true,
|
||||||
&[hunk_range.start..hunk_range.start],
|
vec![hunk_range.start..hunk_range.start],
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -8850,12 +8845,11 @@ fn diff_hunk_controls(
|
||||||
})
|
})
|
||||||
.on_click({
|
.on_click({
|
||||||
let editor = editor.clone();
|
let editor = editor.clone();
|
||||||
move |_event, window, cx| {
|
move |_event, _window, cx| {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.stage_or_unstage_diff_hunks(
|
editor.stage_or_unstage_diff_hunks(
|
||||||
false,
|
false,
|
||||||
&[hunk_range.start..hunk_range.start],
|
vec![hunk_range.start..hunk_range.start],
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1448,6 +1448,12 @@ impl FakeFs {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
|
||||||
|
self.with_git_state(dot_git, true, |state| {
|
||||||
|
state.simulated_index_write_error_message = message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut queue = collections::VecDeque::new();
|
let mut queue = collections::VecDeque::new();
|
||||||
|
|
|
@ -862,6 +862,7 @@ pub struct FakeGitRepositoryState {
|
||||||
pub statuses: HashMap<RepoPath, FileStatus>,
|
pub statuses: HashMap<RepoPath, FileStatus>,
|
||||||
pub current_branch_name: Option<String>,
|
pub current_branch_name: Option<String>,
|
||||||
pub branches: HashSet<String>,
|
pub branches: HashSet<String>,
|
||||||
|
pub simulated_index_write_error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeGitRepository {
|
impl FakeGitRepository {
|
||||||
|
@ -881,6 +882,7 @@ impl FakeGitRepositoryState {
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
current_branch_name: Default::default(),
|
current_branch_name: Default::default(),
|
||||||
branches: Default::default(),
|
branches: Default::default(),
|
||||||
|
simulated_index_write_error_message: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -900,6 +902,9 @@ impl GitRepository for FakeGitRepository {
|
||||||
|
|
||||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
|
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||||
|
return Err(anyhow::anyhow!(message));
|
||||||
|
}
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
state.index_contents.insert(path.clone(), content);
|
state.index_contents.insert(path.clone(), content);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -307,6 +307,13 @@ impl GitPanel {
|
||||||
this.active_repository = git_store.read(cx).active_repository();
|
this.active_repository = git_store.read(cx).active_repository();
|
||||||
this.schedule_update(true, window, cx);
|
this.schedule_update(true, window, cx);
|
||||||
}
|
}
|
||||||
|
GitEvent::IndexWriteError(error) => {
|
||||||
|
this.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.show_error(error, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -19,7 +19,10 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
||||||
use multi_buffer::{MultiBuffer, PathKey};
|
use multi_buffer::{MultiBuffer, PathKey};
|
||||||
use project::{git::GitStore, Project, ProjectPath};
|
use project::{
|
||||||
|
git::{GitEvent, GitStore},
|
||||||
|
Project, ProjectPath,
|
||||||
|
};
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{prelude::*, vertical_divider, Tooltip};
|
use ui::{prelude::*, vertical_divider, Tooltip};
|
||||||
|
@ -141,8 +144,13 @@ impl ProjectDiff {
|
||||||
let git_store_subscription = cx.subscribe_in(
|
let git_store_subscription = cx.subscribe_in(
|
||||||
&git_store,
|
&git_store,
|
||||||
window,
|
window,
|
||||||
move |this, _git_store, _event, _window, _cx| {
|
move |this, _git_store, event, _window, _cx| match event {
|
||||||
*this.update_needed.borrow_mut() = ();
|
GitEvent::ActiveRepositoryChanged
|
||||||
|
| GitEvent::FileSystemUpdated
|
||||||
|
| GitEvent::GitStateUpdated => {
|
||||||
|
*this.update_needed.borrow_mut() = ();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1017,9 +1025,6 @@ mod tests {
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.git_restore(&Default::default(), window, cx);
|
editor.git_restore(&Default::default(), window, cx);
|
||||||
});
|
});
|
||||||
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
|
|
||||||
state.statuses = HashMap::default();
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
assert_state_with_diff(&editor, cx, &"ˇ".unindent());
|
assert_state_with_diff(&editor, cx, &"ˇ".unindent());
|
||||||
|
|
|
@ -250,6 +250,7 @@ impl DiffState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
|
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
|
||||||
|
_ => {}
|
||||||
}),
|
}),
|
||||||
diff,
|
diff,
|
||||||
}
|
}
|
||||||
|
|
|
@ -339,6 +339,7 @@ enum OpenBuffer {
|
||||||
|
|
||||||
pub enum BufferStoreEvent {
|
pub enum BufferStoreEvent {
|
||||||
BufferAdded(Entity<Buffer>),
|
BufferAdded(Entity<Buffer>),
|
||||||
|
BufferDiffAdded(Entity<BufferDiff>),
|
||||||
BufferDropped(BufferId),
|
BufferDropped(BufferId),
|
||||||
BufferChangedFilePath {
|
BufferChangedFilePath {
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
|
@ -1522,11 +1523,12 @@ impl BufferStore {
|
||||||
if let Some(OpenBuffer::Complete { diff_state, .. }) =
|
if let Some(OpenBuffer::Complete { diff_state, .. }) =
|
||||||
this.opened_buffers.get_mut(&buffer_id)
|
this.opened_buffers.get_mut(&buffer_id)
|
||||||
{
|
{
|
||||||
|
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
||||||
|
cx.emit(BufferStoreEvent::BufferDiffAdded(diff.clone()));
|
||||||
diff_state.update(cx, |diff_state, cx| {
|
diff_state.update(cx, |diff_state, cx| {
|
||||||
diff_state.language = language;
|
diff_state.language = language;
|
||||||
diff_state.language_registry = language_registry;
|
diff_state.language_registry = language_registry;
|
||||||
|
|
||||||
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
|
|
||||||
match kind {
|
match kind {
|
||||||
DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
|
DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
|
||||||
DiffKind::Uncommitted => {
|
DiffKind::Uncommitted => {
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
use crate::buffer_store::BufferStore;
|
use crate::{
|
||||||
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
buffer_store::{BufferStore, BufferStoreEvent},
|
||||||
use crate::{Project, ProjectPath};
|
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||||
|
Project, ProjectItem, ProjectPath,
|
||||||
|
};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
use buffer_diff::BufferDiffEvent;
|
||||||
use client::ProjectId;
|
use client::ProjectId;
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::{
|
||||||
use futures::StreamExt as _;
|
channel::{mpsc, oneshot},
|
||||||
use git::repository::{Branch, CommitDetails, PushOptions, Remote, RemoteCommandOutput, ResetMode};
|
StreamExt as _,
|
||||||
use git::repository::{GitRepository, RepoPath};
|
};
|
||||||
|
use git::{
|
||||||
|
repository::{
|
||||||
|
Branch, CommitDetails, GitRepository, PushOptions, Remote, RemoteCommandOutput, RepoPath,
|
||||||
|
ResetMode,
|
||||||
|
},
|
||||||
|
status::FileStatus,
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageRegistry};
|
use language::{Buffer, LanguageRegistry};
|
||||||
use rpc::proto::{git_reset, ToProto};
|
use rpc::{
|
||||||
use rpc::{proto, AnyProtoClient, TypedEnvelope};
|
proto::{self, git_reset, ToProto},
|
||||||
|
AnyProtoClient, TypedEnvelope,
|
||||||
|
};
|
||||||
use settings::WorktreeId;
|
use settings::WorktreeId;
|
||||||
use std::collections::VecDeque;
|
use std::{
|
||||||
use std::future::Future;
|
collections::VecDeque,
|
||||||
use std::path::{Path, PathBuf};
|
future::Future,
|
||||||
use std::sync::Arc;
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use text::BufferId;
|
use text::BufferId;
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
|
||||||
|
@ -30,7 +44,7 @@ pub struct GitStore {
|
||||||
repositories: Vec<Entity<Repository>>,
|
repositories: Vec<Entity<Repository>>,
|
||||||
active_index: Option<usize>,
|
active_index: Option<usize>,
|
||||||
update_sender: mpsc::UnboundedSender<GitJob>,
|
update_sender: mpsc::UnboundedSender<GitJob>,
|
||||||
_subscription: Subscription,
|
_subscriptions: [Subscription; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Repository {
|
pub struct Repository {
|
||||||
|
@ -54,10 +68,12 @@ pub enum GitRepo {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum GitEvent {
|
pub enum GitEvent {
|
||||||
ActiveRepositoryChanged,
|
ActiveRepositoryChanged,
|
||||||
FileSystemUpdated,
|
FileSystemUpdated,
|
||||||
GitStateUpdated,
|
GitStateUpdated,
|
||||||
|
IndexWriteError(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GitJob {
|
struct GitJob {
|
||||||
|
@ -81,7 +97,10 @@ impl GitStore {
|
||||||
cx: &mut Context<'_, Self>,
|
cx: &mut Context<'_, Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let update_sender = Self::spawn_git_worker(cx);
|
let update_sender = Self::spawn_git_worker(cx);
|
||||||
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
|
let _subscriptions = [
|
||||||
|
cx.subscribe(worktree_store, Self::on_worktree_store_event),
|
||||||
|
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
|
||||||
|
];
|
||||||
|
|
||||||
GitStore {
|
GitStore {
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -90,7 +109,7 @@ impl GitStore {
|
||||||
repositories: Vec::new(),
|
repositories: Vec::new(),
|
||||||
active_index: None,
|
active_index: None,
|
||||||
update_sender,
|
update_sender,
|
||||||
_subscription,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,10 +246,82 @@ impl GitStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_buffer_store_event(
|
||||||
|
&mut self,
|
||||||
|
_: Entity<BufferStore>,
|
||||||
|
event: &BufferStoreEvent,
|
||||||
|
cx: &mut Context<'_, Self>,
|
||||||
|
) {
|
||||||
|
if let BufferStoreEvent::BufferDiffAdded(diff) = event {
|
||||||
|
cx.subscribe(diff, Self::on_buffer_diff_event).detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_buffer_diff_event(
|
||||||
|
this: &mut GitStore,
|
||||||
|
diff: Entity<buffer_diff::BufferDiff>,
|
||||||
|
event: &BufferDiffEvent,
|
||||||
|
cx: &mut Context<'_, GitStore>,
|
||||||
|
) {
|
||||||
|
if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
|
||||||
|
let buffer_id = diff.read(cx).buffer_id;
|
||||||
|
if let Some((repo, path)) = this.repository_and_path_for_buffer_id(buffer_id, cx) {
|
||||||
|
let recv = repo
|
||||||
|
.read(cx)
|
||||||
|
.set_index_text(&path, new_index_text.as_ref().map(|rope| rope.to_string()));
|
||||||
|
let diff = diff.downgrade();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Some(result) = cx.background_spawn(async move { recv.await.ok() }).await
|
||||||
|
{
|
||||||
|
if let Err(error) = result {
|
||||||
|
diff.update(&mut cx, |diff, cx| {
|
||||||
|
diff.clear_pending_hunks(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
this.update(&mut cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error)))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
|
pub fn all_repositories(&self) -> Vec<Entity<Repository>> {
|
||||||
self.repositories.clone()
|
self.repositories.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||||
|
let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
|
||||||
|
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
|
||||||
|
Some(status.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository_and_path_for_buffer_id(
|
||||||
|
&self,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<(Entity<Repository>, RepoPath)> {
|
||||||
|
let buffer = self.buffer_store.read(cx).get(buffer_id)?;
|
||||||
|
let path = buffer.read(cx).project_path(cx)?;
|
||||||
|
let mut result: Option<(Entity<Repository>, RepoPath)> = None;
|
||||||
|
for repo_handle in &self.repositories {
|
||||||
|
let repo = repo_handle.read(cx);
|
||||||
|
if repo.worktree_id == path.worktree_id {
|
||||||
|
if let Ok(relative_path) = repo.repository_entry.relativize(&path.path) {
|
||||||
|
if result
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|(result, _)| !repo.contains_sub_repo(result, cx))
|
||||||
|
{
|
||||||
|
result = Some((repo_handle.clone(), relative_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {
|
fn spawn_git_worker(cx: &mut Context<'_, GitStore>) -> mpsc::UnboundedSender<GitJob> {
|
||||||
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
|
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
|
||||||
|
|
||||||
|
@ -658,9 +749,8 @@ impl GitStore {
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Result<Entity<Repository>> {
|
) -> Result<Entity<Repository>> {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let repository_handle = this
|
this.repositories
|
||||||
.all_repositories()
|
.iter()
|
||||||
.into_iter()
|
|
||||||
.find(|repository_handle| {
|
.find(|repository_handle| {
|
||||||
repository_handle.read(cx).worktree_id == worktree_id
|
repository_handle.read(cx).worktree_id == worktree_id
|
||||||
&& repository_handle
|
&& repository_handle
|
||||||
|
@ -669,8 +759,8 @@ impl GitStore {
|
||||||
.work_directory_id()
|
.work_directory_id()
|
||||||
== work_directory_id
|
== work_directory_id
|
||||||
})
|
})
|
||||||
.context("missing repository handle")?;
|
.context("missing repository handle")
|
||||||
anyhow::Ok(repository_handle)
|
.cloned()
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1297,7 +1387,7 @@ impl Repository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_index_text(
|
fn set_index_text(
|
||||||
&self,
|
&self,
|
||||||
path: &RepoPath,
|
path: &RepoPath,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
|
|
|
@ -3184,7 +3184,7 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BufferStoreEvent::BufferDropped(_) => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,7 @@ use futures::{
|
||||||
pub use image_store::{ImageItem, ImageStore};
|
pub use image_store::{ImageItem, ImageStore};
|
||||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||||
|
|
||||||
use ::git::{
|
use ::git::{blame::Blame, repository::GitRepository, status::FileStatus};
|
||||||
blame::Blame,
|
|
||||||
repository::{GitRepository, RepoPath},
|
|
||||||
status::FileStatus,
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
|
AnyEntity, App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter,
|
||||||
Hsla, SharedString, Task, WeakEntity, Window,
|
Hsla, SharedString, Task, WeakEntity, Window,
|
||||||
|
@ -2276,7 +2272,6 @@ impl Project {
|
||||||
BufferStoreEvent::BufferAdded(buffer) => {
|
BufferStoreEvent::BufferAdded(buffer) => {
|
||||||
self.register_buffer(buffer, cx).log_err();
|
self.register_buffer(buffer, cx).log_err();
|
||||||
}
|
}
|
||||||
BufferStoreEvent::BufferChangedFilePath { .. } => {}
|
|
||||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||||
if let Some(ref ssh_client) = self.ssh_client {
|
if let Some(ref ssh_client) = self.ssh_client {
|
||||||
ssh_client
|
ssh_client
|
||||||
|
@ -2289,6 +2284,7 @@ impl Project {
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4336,35 +4332,8 @@ impl Project {
|
||||||
self.git_store.read(cx).all_repositories()
|
self.git_store.read(cx).all_repositories()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repository_and_path_for_buffer_id(
|
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||||
&self,
|
self.git_store.read(cx).status_for_buffer_id(buffer_id, cx)
|
||||||
buffer_id: BufferId,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<(Entity<Repository>, RepoPath)> {
|
|
||||||
let path = self
|
|
||||||
.buffer_for_id(buffer_id, cx)?
|
|
||||||
.read(cx)
|
|
||||||
.project_path(cx)?;
|
|
||||||
|
|
||||||
let mut found: Option<(Entity<Repository>, RepoPath)> = None;
|
|
||||||
for repo_handle in self.git_store.read(cx).all_repositories() {
|
|
||||||
let repo = repo_handle.read(cx);
|
|
||||||
if repo.worktree_id != path.worktree_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Ok(relative_path) = repo.repository_entry.relativize(&path.path) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if found
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|(found, _)| repo.contains_sub_repo(found, cx))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
found = Some((repo_handle.clone(), relative_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
found
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::{task_inventory::TaskContexts, Event, *};
|
use crate::{task_inventory::TaskContexts, Event, *};
|
||||||
use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{
|
||||||
|
assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
|
||||||
|
};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::{future, StreamExt};
|
use futures::{future, StreamExt};
|
||||||
use gpui::{App, SemanticVersion, UpdateGlobal};
|
use gpui::{App, SemanticVersion, UpdateGlobal};
|
||||||
|
@ -5786,7 +5788,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||||
unstaged_diff.update(cx, |unstaged_diff, cx| {
|
unstaged_diff.update(cx, |unstaged_diff, cx| {
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
assert_hunks(
|
assert_hunks(
|
||||||
unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
|
unstaged_diff.hunks(&snapshot, cx),
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&unstaged_diff.base_text_string().unwrap(),
|
&unstaged_diff.base_text_string().unwrap(),
|
||||||
&[
|
&[
|
||||||
|
@ -6008,6 +6010,271 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||||
|
use DiffHunkSecondaryStatus::*;
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let committed_contents = r#"
|
||||||
|
zero
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
four
|
||||||
|
five
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
let file_contents = r#"
|
||||||
|
one
|
||||||
|
TWO
|
||||||
|
three
|
||||||
|
FOUR
|
||||||
|
five
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background_executor.clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
".git": {},
|
||||||
|
"file.txt": file_contents.clone()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
fs.set_head_for_repo(
|
||||||
|
"/dir/.git".as_ref(),
|
||||||
|
&[("file.txt".into(), committed_contents.clone())],
|
||||||
|
);
|
||||||
|
fs.set_index_for_repo(
|
||||||
|
"/dir/.git".as_ref(),
|
||||||
|
&[("file.txt".into(), committed_contents.clone())],
|
||||||
|
);
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_local_buffer("/dir/file.txt", cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||||
|
let uncommitted_diff = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut diff_events = cx.events(&uncommitted_diff);
|
||||||
|
|
||||||
|
// The hunks are initially unstaged.
|
||||||
|
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&snapshot, cx),
|
||||||
|
&snapshot,
|
||||||
|
&diff.base_text_string().unwrap(),
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
0..0,
|
||||||
|
"zero\n",
|
||||||
|
"",
|
||||||
|
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1..2,
|
||||||
|
"two\n",
|
||||||
|
"TWO\n",
|
||||||
|
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
"four\n",
|
||||||
|
"FOUR\n",
|
||||||
|
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stage a hunk. It appears as optimistically staged.
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
let range =
|
||||||
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
|
||||||
|
let hunks = diff
|
||||||
|
.hunks_intersecting_range(range, &snapshot, cx)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
|
||||||
|
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&snapshot, cx),
|
||||||
|
&snapshot,
|
||||||
|
&diff.base_text_string().unwrap(),
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
0..0,
|
||||||
|
"zero\n",
|
||||||
|
"",
|
||||||
|
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1..2,
|
||||||
|
"two\n",
|
||||||
|
"TWO\n",
|
||||||
|
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
"four\n",
|
||||||
|
"FOUR\n",
|
||||||
|
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The diff emits a change event for the range of the staged hunk.
|
||||||
|
assert!(matches!(
|
||||||
|
diff_events.next().await.unwrap(),
|
||||||
|
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||||
|
));
|
||||||
|
let event = diff_events.next().await.unwrap();
|
||||||
|
if let BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: Some(changed_range),
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let changed_range = changed_range.to_point(&snapshot);
|
||||||
|
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected event {event:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the write to the index completes, it appears as staged.
|
||||||
|
cx.run_until_parked();
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&snapshot, cx),
|
||||||
|
&snapshot,
|
||||||
|
&diff.base_text_string().unwrap(),
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
0..0,
|
||||||
|
"zero\n",
|
||||||
|
"",
|
||||||
|
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
"four\n",
|
||||||
|
"FOUR\n",
|
||||||
|
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The diff emits a change event for the changed index text.
|
||||||
|
let event = diff_events.next().await.unwrap();
|
||||||
|
if let BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: Some(changed_range),
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let changed_range = changed_range.to_point(&snapshot);
|
||||||
|
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected event {event:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a problem writing to the git index.
|
||||||
|
fs.set_error_message_for_index_write(
|
||||||
|
"/dir/.git".as_ref(),
|
||||||
|
Some("failed to write git index".into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stage another hunk.
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
let range =
|
||||||
|
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
|
||||||
|
let hunks = diff
|
||||||
|
.hunks_intersecting_range(range, &snapshot, cx)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
|
||||||
|
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&snapshot, cx),
|
||||||
|
&snapshot,
|
||||||
|
&diff.base_text_string().unwrap(),
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
0..0,
|
||||||
|
"zero\n",
|
||||||
|
"",
|
||||||
|
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
"four\n",
|
||||||
|
"FOUR\n",
|
||||||
|
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert!(matches!(
|
||||||
|
diff_events.next().await.unwrap(),
|
||||||
|
BufferDiffEvent::HunksStagedOrUnstaged(_)
|
||||||
|
));
|
||||||
|
let event = diff_events.next().await.unwrap();
|
||||||
|
if let BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: Some(changed_range),
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let changed_range = changed_range.to_point(&snapshot);
|
||||||
|
assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected event {event:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the write fails, the hunk returns to being unstaged.
|
||||||
|
cx.run_until_parked();
|
||||||
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&snapshot, cx),
|
||||||
|
&snapshot,
|
||||||
|
&diff.base_text_string().unwrap(),
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
0..0,
|
||||||
|
"zero\n",
|
||||||
|
"",
|
||||||
|
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||||
|
(
|
||||||
|
3..4,
|
||||||
|
"four\n",
|
||||||
|
"FOUR\n",
|
||||||
|
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let event = diff_events.next().await.unwrap();
|
||||||
|
if let BufferDiffEvent::DiffChanged {
|
||||||
|
changed_range: Some(changed_range),
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let changed_range = changed_range.to_point(&snapshot);
|
||||||
|
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected event {event:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -6065,7 +6332,7 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
||||||
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
|
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
assert_hunks(
|
assert_hunks(
|
||||||
uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
|
uncommitted_diff.hunks(&snapshot, cx),
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&uncommitted_diff.base_text_string().unwrap(),
|
&uncommitted_diff.base_text_string().unwrap(),
|
||||||
&[(
|
&[(
|
||||||
|
|
|
@ -70,6 +70,10 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||||
self.0.insert_or_replace(MapEntry { key, value }, &());
|
self.0.insert_or_replace(MapEntry { key, value }, &());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.0 = SumTree::default();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, key: &K) -> Option<V> {
|
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||||
let mut removed = None;
|
let mut removed = None;
|
||||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(&());
|
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(&());
|
||||||
|
@ -157,6 +161,14 @@ impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||||
self.0.iter().map(|entry| &entry.value)
|
self.0.iter().map(|entry| &entry.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn first(&self) -> Option<(&K, &V)> {
|
||||||
|
self.0.first().map(|entry| (&entry.key, &entry.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last(&self) -> Option<(&K, &V)> {
|
||||||
|
self.0.last().map(|entry| (&entry.key, &entry.value))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
|
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
|
||||||
let edits = other
|
let edits = other
|
||||||
.iter()
|
.iter()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue