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:
parent
224f3d4746
commit
a41d72ee81
24 changed files with 1015 additions and 552 deletions
|
@ -112,6 +112,9 @@ CREATE TABLE "worktree_repository_statuses" (
|
|||
"work_directory_id" INT8 NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
"status_kind" INT4 NOT NULL,
|
||||
"first_status" INT4 NULL,
|
||||
"second_status" INT4 NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
ALTER TABLE worktree_repository_statuses
|
||||
ADD COLUMN status_kind INTEGER,
|
||||
ADD COLUMN first_status INTEGER,
|
||||
ADD COLUMN second_status INTEGER;
|
||||
|
||||
UPDATE worktree_repository_statuses
|
||||
SET
|
||||
status_kind = 0;
|
||||
|
||||
ALTER TABLE worktree_repository_statuses
|
||||
ALTER COLUMN status_kind
|
||||
SET
|
||||
NOT NULL;
|
|
@ -35,6 +35,7 @@ use std::{
|
|||
};
|
||||
use time::PrimitiveDateTime;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
use worktree_repository_statuses::StatusKind;
|
||||
use worktree_settings_file::LocalSettingsKind;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -805,3 +806,92 @@ impl LocalSettingsKind {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn db_status_to_proto(
|
||||
entry: worktree_repository_statuses::Model,
|
||||
) -> anyhow::Result<proto::StatusEntry> {
|
||||
use proto::git_file_status::{Tracked, Unmerged, Variant};
|
||||
|
||||
let (simple_status, variant) =
|
||||
match (entry.status_kind, entry.first_status, entry.second_status) {
|
||||
(StatusKind::Untracked, None, None) => (
|
||||
proto::GitStatus::Added as i32,
|
||||
Variant::Untracked(Default::default()),
|
||||
),
|
||||
(StatusKind::Ignored, None, None) => (
|
||||
proto::GitStatus::Added as i32,
|
||||
Variant::Ignored(Default::default()),
|
||||
),
|
||||
(StatusKind::Unmerged, Some(first_head), Some(second_head)) => (
|
||||
proto::GitStatus::Conflict as i32,
|
||||
Variant::Unmerged(Unmerged {
|
||||
first_head,
|
||||
second_head,
|
||||
}),
|
||||
),
|
||||
(StatusKind::Tracked, Some(index_status), Some(worktree_status)) => {
|
||||
let simple_status = if worktree_status != proto::GitStatus::Unmodified as i32 {
|
||||
worktree_status
|
||||
} else if index_status != proto::GitStatus::Unmodified as i32 {
|
||||
index_status
|
||||
} else {
|
||||
proto::GitStatus::Unmodified as i32
|
||||
};
|
||||
(
|
||||
simple_status,
|
||||
Variant::Tracked(Tracked {
|
||||
index_status,
|
||||
worktree_status,
|
||||
}),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"Unexpected combination of status fields: {entry:?}"
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(proto::StatusEntry {
|
||||
repo_path: entry.repo_path,
|
||||
simple_status,
|
||||
status: Some(proto::GitFileStatus {
|
||||
variant: Some(variant),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn proto_status_to_db(
|
||||
status_entry: proto::StatusEntry,
|
||||
) -> (String, StatusKind, Option<i32>, Option<i32>) {
|
||||
use proto::git_file_status::{Tracked, Unmerged, Variant};
|
||||
|
||||
let (status_kind, first_status, second_status) = status_entry
|
||||
.status
|
||||
.clone()
|
||||
.and_then(|status| status.variant)
|
||||
.map_or(
|
||||
(StatusKind::Untracked, None, None),
|
||||
|variant| match variant {
|
||||
Variant::Untracked(_) => (StatusKind::Untracked, None, None),
|
||||
Variant::Ignored(_) => (StatusKind::Ignored, None, None),
|
||||
Variant::Unmerged(Unmerged {
|
||||
first_head,
|
||||
second_head,
|
||||
}) => (StatusKind::Unmerged, Some(first_head), Some(second_head)),
|
||||
Variant::Tracked(Tracked {
|
||||
index_status,
|
||||
worktree_status,
|
||||
}) => (
|
||||
StatusKind::Tracked,
|
||||
Some(index_status),
|
||||
Some(worktree_status),
|
||||
),
|
||||
},
|
||||
);
|
||||
(
|
||||
status_entry.repo_path,
|
||||
status_kind,
|
||||
first_status,
|
||||
second_status,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -360,6 +360,8 @@ impl Database {
|
|||
update.updated_repositories.iter().flat_map(
|
||||
|repository: &proto::RepositoryEntry| {
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
let (repo_path, status_kind, first_status, second_status) =
|
||||
proto_status_to_db(status_entry.clone());
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
|
@ -368,8 +370,11 @@ impl Database {
|
|||
),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
repo_path: ActiveValue::set(repo_path),
|
||||
status: ActiveValue::set(0),
|
||||
status_kind: ActiveValue::set(status_kind),
|
||||
first_status: ActiveValue::set(first_status),
|
||||
second_status: ActiveValue::set(second_status),
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -384,7 +389,9 @@ impl Database {
|
|||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
worktree_repository_statuses::Column::StatusKind,
|
||||
worktree_repository_statuses::Column::FirstStatus,
|
||||
worktree_repository_statuses::Column::SecondStatus,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
|
@ -759,10 +766,7 @@ impl Database {
|
|||
let mut updated_statuses = Vec::new();
|
||||
while let Some(status_entry) = repository_statuses.next().await {
|
||||
let status_entry: worktree_repository_statuses::Model = status_entry?;
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: status_entry.repo_path,
|
||||
status: status_entry.status as i32,
|
||||
});
|
||||
updated_statuses.push(db_status_to_proto(status_entry)?);
|
||||
}
|
||||
|
||||
worktree.repository_entries.insert(
|
||||
|
|
|
@ -732,10 +732,7 @@ impl Database {
|
|||
if db_status.is_deleted {
|
||||
removed_statuses.push(db_status.repo_path);
|
||||
} else {
|
||||
updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status.repo_path,
|
||||
status: db_status.status as i32,
|
||||
});
|
||||
updated_statuses.push(db_status_to_proto(db_status)?);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,11 +12,26 @@ pub struct Model {
|
|||
pub work_directory_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub repo_path: String,
|
||||
/// Old single-code status field, no longer used but kept here to mirror the DB schema.
|
||||
pub status: i64,
|
||||
pub status_kind: StatusKind,
|
||||
/// For unmerged entries, this is the `first_head` status. For tracked entries, this is the `index_status`.
|
||||
pub first_status: Option<i32>,
|
||||
/// For unmerged entries, this is the `second_head` status. For tracked entries, this is the `worktree_status`.
|
||||
pub second_status: Option<i32>,
|
||||
pub scan_id: i64,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(rs_type = "i32", db_type = "Integer")]
|
||||
pub enum StatusKind {
|
||||
Untracked = 0,
|
||||
Ignored = 1,
|
||||
Unmerged = 2,
|
||||
Tracked = 3,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ use client::{User, RECEIVE_TIMEOUT};
|
|||
use collections::{HashMap, HashSet};
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::{channel::mpsc, StreamExt as _};
|
||||
use git::repository::GitFileStatus;
|
||||
|
||||
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
|
||||
use gpui::{
|
||||
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
||||
TestAppContext, UpdateGlobal,
|
||||
|
@ -2889,11 +2890,20 @@ async fn test_git_status_sync(
|
|||
const A_TXT: &str = "a.txt";
|
||||
const B_TXT: &str = "b.txt";
|
||||
|
||||
const A_STATUS_START: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: StatusCode::Modified,
|
||||
});
|
||||
const B_STATUS_START: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Deleted,
|
||||
});
|
||||
|
||||
client_a.fs().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(Path::new(A_TXT), GitFileStatus::Added),
|
||||
(Path::new(B_TXT), GitFileStatus::Added),
|
||||
(Path::new(A_TXT), A_STATUS_START),
|
||||
(Path::new(B_TXT), B_STATUS_START),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -2913,7 +2923,7 @@ async fn test_git_status_sync(
|
|||
#[track_caller]
|
||||
fn assert_status(
|
||||
file: &impl AsRef<Path>,
|
||||
status: Option<GitFileStatus>,
|
||||
status: Option<FileStatus>,
|
||||
project: &Project,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
|
@ -2926,20 +2936,29 @@ async fn test_git_status_sync(
|
|||
}
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
|
||||
});
|
||||
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(A_TXT), Some(A_STATUS_START), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
|
||||
});
|
||||
|
||||
const A_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Added,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
});
|
||||
const B_STATUS_END: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
});
|
||||
|
||||
client_a.fs().set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(Path::new(B_TXT), GitFileStatus::Modified),
|
||||
(Path::new(A_TXT), A_STATUS_END),
|
||||
(Path::new(B_TXT), B_STATUS_END),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -2949,33 +2968,13 @@ async fn test_git_status_sync(
|
|||
// Smoke test status reading
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||
});
|
||||
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||
});
|
||||
|
||||
// And synchronization while joining
|
||||
|
@ -2983,18 +2982,8 @@ async fn test_git_status_sync(
|
|||
executor.run_until_parked();
|
||||
|
||||
project_remote_c.read_with(cx_c, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use call::ActiveCall;
|
|||
use collections::{BTreeMap, HashMap};
|
||||
use editor::Bias;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use git::repository::GitFileStatus;
|
||||
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use language::{
|
||||
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
|
||||
|
@ -127,7 +127,7 @@ enum GitOperation {
|
|||
},
|
||||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
statuses: Vec<(PathBuf, FileStatus)>,
|
||||
git_operation: bool,
|
||||
},
|
||||
}
|
||||
|
@ -1458,17 +1458,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
|||
|
||||
let statuses = file_paths
|
||||
.into_iter()
|
||||
.map(|paths| {
|
||||
(
|
||||
paths,
|
||||
match rng.gen_range(0..3_u32) {
|
||||
0 => GitFileStatus::Added,
|
||||
1 => GitFileStatus::Modified,
|
||||
2 => GitFileStatus::Conflict,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|path| (path, gen_status(rng)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let git_operation = rng.gen::<bool>();
|
||||
|
@ -1613,3 +1603,41 @@ fn gen_file_name(rng: &mut StdRng) -> String {
|
|||
}
|
||||
name
|
||||
}
|
||||
|
||||
fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
fn gen_status_code(rng: &mut StdRng) -> StatusCode {
|
||||
match rng.gen_range(0..7) {
|
||||
0 => StatusCode::Modified,
|
||||
1 => StatusCode::TypeChanged,
|
||||
2 => StatusCode::Added,
|
||||
3 => StatusCode::Deleted,
|
||||
4 => StatusCode::Renamed,
|
||||
5 => StatusCode::Copied,
|
||||
6 => StatusCode::Unmodified,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
|
||||
match rng.gen_range(0..3) {
|
||||
0 => UnmergedStatusCode::Updated,
|
||||
1 => UnmergedStatusCode::Added,
|
||||
2 => UnmergedStatusCode::Deleted,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
match rng.gen_range(0..4) {
|
||||
0 => FileStatus::Untracked,
|
||||
1 => FileStatus::Ignored,
|
||||
2 => FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: gen_unmerged_status_code(rng),
|
||||
second_head: gen_unmerged_status_code(rng),
|
||||
}),
|
||||
3 => FileStatus::Tracked(TrackedStatus {
|
||||
index_status: gen_status_code(rng),
|
||||
worktree_status: gen_status_code(rng),
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue