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-15 19:01:38 -05:00 committed by GitHub
parent 224f3d4746
commit a41d72ee81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1015 additions and 552 deletions

View file

@ -8,8 +8,7 @@ use anyhow::{Context as _, Result};
use db::kvp::KEY_VALUE_STORE;
use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorSettings, ShowScrollbar};
use git::repository::{GitFileStatus, RepoPath};
use git::status::GitStatusPair;
use git::{repository::RepoPath, status::FileStatus};
use gpui::*;
use language::Buffer;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
@ -72,7 +71,7 @@ pub struct GitListEntry {
depth: usize,
display_name: String,
repo_path: RepoPath,
status: GitStatusPair,
status: FileStatus,
is_staged: Option<bool>,
}
@ -665,7 +664,7 @@ impl GitPanel {
.skip(range.start)
.take(range.end - range.start)
{
let status = entry.status.clone();
let status = entry.status;
let filename = entry
.repo_path
.file_name()
@ -1072,22 +1071,23 @@ impl GitPanel {
let repo_path = entry_details.repo_path.clone();
let selected = self.selected_entry == Some(ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
// TODO revisit, maybe use a different status here?
let status = entry_details.status.combined();
let status = entry_details.status;
let mut label_color = cx.theme().colors().text;
if status_style == StatusStyle::LabelColor {
label_color = match status {
GitFileStatus::Added => cx.theme().status().created,
GitFileStatus::Modified => cx.theme().status().modified,
GitFileStatus::Conflict => cx.theme().status().conflict,
GitFileStatus::Deleted => cx.theme().colors().text_disabled,
// TODO: Should we even have this here?
GitFileStatus::Untracked => cx.theme().colors().text_placeholder,
label_color = if status.is_conflicted() {
cx.theme().status().conflict
} else if status.is_modified() {
cx.theme().status().modified
} else if status.is_deleted() {
cx.theme().colors().text_disabled
} else {
cx.theme().status().created
}
}
let path_color = matches!(status, GitFileStatus::Deleted)
let path_color = status
.is_deleted()
.then_some(cx.theme().colors().text_disabled)
.unwrap_or(cx.theme().colors().text_muted);
@ -1175,7 +1175,7 @@ impl GitPanel {
.child(
h_flex()
.text_color(label_color)
.when(status == GitFileStatus::Deleted, |this| this.line_through())
.when(status.is_deleted(), |this| this.line_through())
.when_some(repo_path.parent(), |this, parent| {
let parent_str = parent.to_string_lossy();
if !parent_str.is_empty() {

View file

@ -2,7 +2,8 @@ use ::settings::Settings;
use collections::HashMap;
use futures::channel::mpsc;
use futures::StreamExt as _;
use git::repository::{GitFileStatus, GitRepository, RepoPath};
use git::repository::{GitRepository, RepoPath};
use git::status::FileStatus;
use git_panel_settings::GitPanelSettings;
use gpui::{actions, AppContext, Hsla, Model};
use project::{Project, WorktreeId};
@ -223,17 +224,15 @@ const REMOVED_COLOR: Hsla = Hsla {
};
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
match status {
GitFileStatus::Added | GitFileStatus::Untracked => {
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
}
GitFileStatus::Modified => {
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
GitFileStatus::Deleted => {
Icon::new(IconName::SquareMinus).color(Color::Custom(REMOVED_COLOR))
}
}
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
let (icon_name, color) = if status.is_conflicted() {
(IconName::Warning, REMOVED_COLOR)
} else if status.is_deleted() {
(IconName::SquareMinus, REMOVED_COLOR)
} else if status.is_modified() {
(IconName::SquareDot, MODIFIED_COLOR)
} else {
(IconName::SquarePlus, ADDED_COLOR)
};
Icon::new(icon_name).color(Color::Custom(color))
}