Represent git statuses more faithfully (#23082)

First, parse the output of `git status --porcelain=v1` into a
representation that can handle the full "grammar" and doesn't lose
information.

Second, as part of pushing this throughout the codebase, expand the use
of the existing `GitSummary` type to all the places where status
propagation is in play (i.e., anywhere we're dealing with a mix of files
and directories), and get rid of the previous `GitSummary ->
GitFileStatus` conversion.

- [x] Synchronize new representation over collab
  - [x] Update zed.proto
  - [x] Update DB models
- [x] Update `GitSummary` and summarization for the new `FileStatus`
- [x] Fix all tests
  - [x] worktree
  - [x] collab
- [x] Clean up `FILE_*` constants
- [x] New collab tests to exercise syncing of complex statuses
- [x] Run it locally and make sure it looks good

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Cole Miller
2025-01-16 00:01:38 +00:00
committed by GitHub
co-authored by Mikayla Conrad
parent 224f3d4746
commit a41d72ee81
24 changed files with 1015 additions and 552 deletions
+19 -23
View File
@@ -9,10 +9,7 @@ use std::{
use anyhow::{anyhow, Context as _};
use collections::{BTreeMap, HashMap};
use feature_flags::FeatureFlagAppExt;
use git::{
diff::{BufferDiff, DiffHunk},
repository::GitFileStatus,
};
use git::diff::{BufferDiff, DiffHunk};
use gpui::{
actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
@@ -54,7 +51,6 @@ struct ProjectDiffEditor {
#[derive(Debug)]
struct Changes {
_status: GitFileStatus,
buffer: Model<Buffer>,
hunks: Vec<DiffHunk>,
}
@@ -199,14 +195,13 @@ impl ProjectDiffEditor {
.repositories()
.iter()
.flat_map(|entry| {
entry.status().map(|git_entry| {
(git_entry.combined_status(), entry.join(git_entry.repo_path))
})
entry
.status()
.map(|git_entry| entry.join(git_entry.repo_path))
})
.filter_map(|(status, path)| {
.filter_map(|path| {
let id = snapshot.entry_for_path(&path)?.id;
Some((
status,
id,
ProjectPath {
worktree_id: snapshot.id(),
@@ -218,9 +213,9 @@ impl ProjectDiffEditor {
Some(
applicable_entries
.into_iter()
.map(|(status, entry_id, entry_path)| {
.map(|(entry_id, entry_path)| {
let open_task = project.open_path(entry_path.clone(), cx);
(status, entry_id, entry_path, open_task)
(entry_id, entry_path, open_task)
})
.collect::<Vec<_>>(),
)
@@ -234,15 +229,10 @@ impl ProjectDiffEditor {
let mut new_entries = Vec::new();
let mut buffers = HashMap::<
ProjectEntryId,
(
GitFileStatus,
text::BufferSnapshot,
Model<Buffer>,
BufferDiff,
),
(text::BufferSnapshot, Model<Buffer>, BufferDiff),
>::default();
let mut change_sets = Vec::new();
for (status, entry_id, entry_path, open_task) in open_tasks {
for (entry_id, entry_path, open_task) in open_tasks {
let Some(buffer) = open_task
.await
.and_then(|(_, opened_model)| {
@@ -272,7 +262,6 @@ impl ProjectDiffEditor {
buffers.insert(
entry_id,
(
status,
buffer.read(cx).text_snapshot(),
buffer,
change_set.read(cx).diff_to_buffer.clone(),
@@ -295,11 +284,10 @@ impl ProjectDiffEditor {
.background_executor()
.spawn(async move {
let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();
for (entry_id, (status, buffer_snapshot, buffer, buffer_diff)) in buffers {
for (entry_id, (buffer_snapshot, buffer, buffer_diff)) in buffers {
new_changes.insert(
entry_id,
Changes {
_status: status,
buffer,
hunks: buffer_diff
.hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
@@ -1107,6 +1095,7 @@ impl Render for ProjectDiffEditor {
#[cfg(test)]
mod tests {
use git::status::{StatusCode, TrackedStatus};
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use project::buffer_store::BufferChangeSet;
use serde_json::json;
@@ -1224,7 +1213,14 @@ mod tests {
});
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[(Path::new("file_a"), GitFileStatus::Modified)],
&[(
Path::new("file_a"),
TrackedStatus {
worktree_status: StatusCode::Modified,
index_status: StatusCode::Unmodified,
}
.into(),
)],
);
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+12 -16
View File
@@ -9,7 +9,7 @@ use anyhow::{anyhow, Context as _, Result};
use collections::HashSet;
use file_icons::FileIcons;
use futures::future::try_join_all;
use git::repository::GitFileStatus;
use git::status::GitSummary;
use gpui::{
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
@@ -27,8 +27,6 @@ use project::{
};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use std::{
any::TypeId,
borrow::Cow,
@@ -43,6 +41,7 @@ use theme::{Theme, ThemeSettings};
use ui::{h_flex, prelude::*, IconDecorationKind, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent};
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -621,10 +620,10 @@ impl Item for Editor {
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.snapshot()
.status_for_file(path.path);
.status_for_file(path.path)?;
Some(entry_git_aware_label_color(
git_status,
git_status.summary(),
entry.is_ignored,
params.selected,
))
@@ -1560,20 +1559,17 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
}
}
pub fn entry_git_aware_label_color(
git_status: Option<GitFileStatus>,
ignored: bool,
selected: bool,
) -> Color {
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
if ignored {
Color::Ignored
} else if git_status.conflict > 0 {
Color::Conflict
} else if git_status.modified > 0 {
Color::Modified
} else if git_status.added > 0 || git_status.untracked > 0 {
Color::Created
} else {
match git_status {
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
Some(GitFileStatus::Modified) => Color::Modified,
Some(GitFileStatus::Conflict) => Color::Conflict,
Some(GitFileStatus::Deleted) | None => entry_label_color(selected),
}
entry_label_color(selected)
}
}