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,
|
"work_directory_id" INT8 NOT NULL,
|
||||||
"repo_path" VARCHAR NOT NULL,
|
"repo_path" VARCHAR NOT NULL,
|
||||||
"status" INT8 NOT NULL,
|
"status" INT8 NOT NULL,
|
||||||
|
"status_kind" INT4 NOT NULL,
|
||||||
|
"first_status" INT4 NULL,
|
||||||
|
"second_status" INT4 NULL,
|
||||||
"scan_id" INT8 NOT NULL,
|
"scan_id" INT8 NOT NULL,
|
||||||
"is_deleted" BOOL NOT NULL,
|
"is_deleted" BOOL NOT NULL,
|
||||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
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 time::PrimitiveDateTime;
|
||||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||||
|
use worktree_repository_statuses::StatusKind;
|
||||||
use worktree_settings_file::LocalSettingsKind;
|
use worktree_settings_file::LocalSettingsKind;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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(
|
update.updated_repositories.iter().flat_map(
|
||||||
|repository: &proto::RepositoryEntry| {
|
|repository: &proto::RepositoryEntry| {
|
||||||
repository.updated_statuses.iter().map(|status_entry| {
|
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 {
|
worktree_repository_statuses::ActiveModel {
|
||||||
project_id: ActiveValue::set(project_id),
|
project_id: ActiveValue::set(project_id),
|
||||||
worktree_id: ActiveValue::set(worktree_id),
|
worktree_id: ActiveValue::set(worktree_id),
|
||||||
|
@ -368,8 +370,11 @@ impl Database {
|
||||||
),
|
),
|
||||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||||
is_deleted: ActiveValue::set(false),
|
is_deleted: ActiveValue::set(false),
|
||||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
repo_path: ActiveValue::set(repo_path),
|
||||||
status: ActiveValue::set(status_entry.status as i64),
|
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([
|
.update_columns([
|
||||||
worktree_repository_statuses::Column::ScanId,
|
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(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
|
@ -759,10 +766,7 @@ impl Database {
|
||||||
let mut updated_statuses = Vec::new();
|
let mut updated_statuses = Vec::new();
|
||||||
while let Some(status_entry) = repository_statuses.next().await {
|
while let Some(status_entry) = repository_statuses.next().await {
|
||||||
let status_entry: worktree_repository_statuses::Model = status_entry?;
|
let status_entry: worktree_repository_statuses::Model = status_entry?;
|
||||||
updated_statuses.push(proto::StatusEntry {
|
updated_statuses.push(db_status_to_proto(status_entry)?);
|
||||||
repo_path: status_entry.repo_path,
|
|
||||||
status: status_entry.status as i32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
worktree.repository_entries.insert(
|
worktree.repository_entries.insert(
|
||||||
|
|
|
@ -732,10 +732,7 @@ impl Database {
|
||||||
if db_status.is_deleted {
|
if db_status.is_deleted {
|
||||||
removed_statuses.push(db_status.repo_path);
|
removed_statuses.push(db_status.repo_path);
|
||||||
} else {
|
} else {
|
||||||
updated_statuses.push(proto::StatusEntry {
|
updated_statuses.push(db_status_to_proto(db_status)?);
|
||||||
repo_path: db_status.repo_path,
|
|
||||||
status: db_status.status as i32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,26 @@ pub struct Model {
|
||||||
pub work_directory_id: i64,
|
pub work_directory_id: i64,
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub repo_path: String,
|
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: 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 scan_id: i64,
|
||||||
pub is_deleted: bool,
|
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)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ use client::{User, RECEIVE_TIMEOUT};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||||
use futures::{channel::mpsc, StreamExt as _};
|
use futures::{channel::mpsc, StreamExt as _};
|
||||||
use git::repository::GitFileStatus;
|
|
||||||
|
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
||||||
TestAppContext, UpdateGlobal,
|
TestAppContext, UpdateGlobal,
|
||||||
|
@ -2889,11 +2890,20 @@ async fn test_git_status_sync(
|
||||||
const A_TXT: &str = "a.txt";
|
const A_TXT: &str = "a.txt";
|
||||||
const B_TXT: &str = "b.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(
|
client_a.fs().set_status_for_repo_via_git_operation(
|
||||||
Path::new("/dir/.git"),
|
Path::new("/dir/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new(A_TXT), GitFileStatus::Added),
|
(Path::new(A_TXT), A_STATUS_START),
|
||||||
(Path::new(B_TXT), GitFileStatus::Added),
|
(Path::new(B_TXT), B_STATUS_START),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2913,7 +2923,7 @@ async fn test_git_status_sync(
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_status(
|
fn assert_status(
|
||||||
file: &impl AsRef<Path>,
|
file: &impl AsRef<Path>,
|
||||||
status: Option<GitFileStatus>,
|
status: Option<FileStatus>,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) {
|
) {
|
||||||
|
@ -2926,20 +2936,29 @@ async fn test_git_status_sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
project_local.read_with(cx_a, |project, cx| {
|
project_local.read_with(cx_a, |project, cx| {
|
||||||
assert_status(&Path::new(A_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(GitFileStatus::Added), project, cx);
|
assert_status(&Path::new(B_TXT), Some(B_STATUS_START), project, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
project_remote.read_with(cx_b, |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(A_TXT), Some(A_STATUS_START), project, cx);
|
||||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), 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(
|
client_a.fs().set_status_for_repo_via_working_copy_change(
|
||||||
Path::new("/dir/.git"),
|
Path::new("/dir/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new(A_TXT), GitFileStatus::Modified),
|
(Path::new(A_TXT), A_STATUS_END),
|
||||||
(Path::new(B_TXT), GitFileStatus::Modified),
|
(Path::new(B_TXT), B_STATUS_END),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2949,33 +2968,13 @@ async fn test_git_status_sync(
|
||||||
// Smoke test status reading
|
// Smoke test status reading
|
||||||
|
|
||||||
project_local.read_with(cx_a, |project, cx| {
|
project_local.read_with(cx_a, |project, cx| {
|
||||||
assert_status(
|
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||||
&Path::new(A_TXT),
|
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_status(
|
|
||||||
&Path::new(B_TXT),
|
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
project_remote.read_with(cx_b, |project, cx| {
|
project_remote.read_with(cx_b, |project, cx| {
|
||||||
assert_status(
|
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||||
&Path::new(A_TXT),
|
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_status(
|
|
||||||
&Path::new(B_TXT),
|
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// And synchronization while joining
|
// And synchronization while joining
|
||||||
|
@ -2983,18 +2982,8 @@ async fn test_git_status_sync(
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
|
|
||||||
project_remote_c.read_with(cx_c, |project, cx| {
|
project_remote_c.read_with(cx_c, |project, cx| {
|
||||||
assert_status(
|
assert_status(&Path::new(A_TXT), Some(A_STATUS_END), project, cx);
|
||||||
&Path::new(A_TXT),
|
assert_status(&Path::new(B_TXT), Some(B_STATUS_END), project, cx);
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_status(
|
|
||||||
&Path::new(B_TXT),
|
|
||||||
Some(GitFileStatus::Modified),
|
|
||||||
project,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use call::ActiveCall;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use editor::Bias;
|
use editor::Bias;
|
||||||
use fs::{FakeFs, Fs as _};
|
use fs::{FakeFs, Fs as _};
|
||||||
use git::repository::GitFileStatus;
|
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
|
||||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||||
use language::{
|
use language::{
|
||||||
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
|
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
|
||||||
|
@ -127,7 +127,7 @@ enum GitOperation {
|
||||||
},
|
},
|
||||||
WriteGitStatuses {
|
WriteGitStatuses {
|
||||||
repo_path: PathBuf,
|
repo_path: PathBuf,
|
||||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
statuses: Vec<(PathBuf, FileStatus)>,
|
||||||
git_operation: bool,
|
git_operation: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1458,17 +1458,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||||
|
|
||||||
let statuses = file_paths
|
let statuses = file_paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|paths| {
|
.map(|path| (path, gen_status(rng)))
|
||||||
(
|
|
||||||
paths,
|
|
||||||
match rng.gen_range(0..3_u32) {
|
|
||||||
0 => GitFileStatus::Added,
|
|
||||||
1 => GitFileStatus::Modified,
|
|
||||||
2 => GitFileStatus::Conflict,
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let git_operation = rng.gen::<bool>();
|
let git_operation = rng.gen::<bool>();
|
||||||
|
@ -1613,3 +1603,41 @@ fn gen_file_name(rng: &mut StdRng) -> String {
|
||||||
}
|
}
|
||||||
name
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,10 +9,7 @@ use std::{
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use git::{
|
use git::diff::{BufferDiff, DiffHunk};
|
||||||
diff::{BufferDiff, DiffHunk},
|
|
||||||
repository::GitFileStatus,
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
|
actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||||
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
|
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
|
||||||
|
@ -54,7 +51,6 @@ struct ProjectDiffEditor {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Changes {
|
struct Changes {
|
||||||
_status: GitFileStatus,
|
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
hunks: Vec<DiffHunk>,
|
hunks: Vec<DiffHunk>,
|
||||||
}
|
}
|
||||||
|
@ -199,14 +195,13 @@ impl ProjectDiffEditor {
|
||||||
.repositories()
|
.repositories()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|entry| {
|
.flat_map(|entry| {
|
||||||
entry.status().map(|git_entry| {
|
entry
|
||||||
(git_entry.combined_status(), entry.join(git_entry.repo_path))
|
.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;
|
let id = snapshot.entry_for_path(&path)?.id;
|
||||||
Some((
|
Some((
|
||||||
status,
|
|
||||||
id,
|
id,
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id: snapshot.id(),
|
worktree_id: snapshot.id(),
|
||||||
|
@ -218,9 +213,9 @@ impl ProjectDiffEditor {
|
||||||
Some(
|
Some(
|
||||||
applicable_entries
|
applicable_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(status, entry_id, entry_path)| {
|
.map(|(entry_id, entry_path)| {
|
||||||
let open_task = project.open_path(entry_path.clone(), cx);
|
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<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
|
@ -234,15 +229,10 @@ impl ProjectDiffEditor {
|
||||||
let mut new_entries = Vec::new();
|
let mut new_entries = Vec::new();
|
||||||
let mut buffers = HashMap::<
|
let mut buffers = HashMap::<
|
||||||
ProjectEntryId,
|
ProjectEntryId,
|
||||||
(
|
(text::BufferSnapshot, Model<Buffer>, BufferDiff),
|
||||||
GitFileStatus,
|
|
||||||
text::BufferSnapshot,
|
|
||||||
Model<Buffer>,
|
|
||||||
BufferDiff,
|
|
||||||
),
|
|
||||||
>::default();
|
>::default();
|
||||||
let mut change_sets = Vec::new();
|
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
|
let Some(buffer) = open_task
|
||||||
.await
|
.await
|
||||||
.and_then(|(_, opened_model)| {
|
.and_then(|(_, opened_model)| {
|
||||||
|
@ -272,7 +262,6 @@ impl ProjectDiffEditor {
|
||||||
buffers.insert(
|
buffers.insert(
|
||||||
entry_id,
|
entry_id,
|
||||||
(
|
(
|
||||||
status,
|
|
||||||
buffer.read(cx).text_snapshot(),
|
buffer.read(cx).text_snapshot(),
|
||||||
buffer,
|
buffer,
|
||||||
change_set.read(cx).diff_to_buffer.clone(),
|
change_set.read(cx).diff_to_buffer.clone(),
|
||||||
|
@ -295,11 +284,10 @@ impl ProjectDiffEditor {
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();
|
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(
|
new_changes.insert(
|
||||||
entry_id,
|
entry_id,
|
||||||
Changes {
|
Changes {
|
||||||
_status: status,
|
|
||||||
buffer,
|
buffer,
|
||||||
hunks: buffer_diff
|
hunks: buffer_diff
|
||||||
.hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
|
.hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
|
||||||
|
@ -1107,6 +1095,7 @@ impl Render for ProjectDiffEditor {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use git::status::{StatusCode, TrackedStatus};
|
||||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||||
use project::buffer_store::BufferChangeSet;
|
use project::buffer_store::BufferChangeSet;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -1224,7 +1213,14 @@ mod tests {
|
||||||
});
|
});
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/.git"),
|
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()
|
cx.executor()
|
||||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||||
|
|
|
@ -9,7 +9,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use git::repository::GitFileStatus;
|
use git::status::GitSummary;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
|
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
|
||||||
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
|
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
|
||||||
|
@ -27,8 +27,6 @@ use project::{
|
||||||
};
|
};
|
||||||
use rpc::proto::{self, update_view, PeerId};
|
use rpc::proto::{self, update_view, PeerId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
@ -43,6 +41,7 @@ use theme::{Theme, ThemeSettings};
|
||||||
use ui::{h_flex, prelude::*, IconDecorationKind, Label};
|
use ui::{h_flex, prelude::*, IconDecorationKind, Label};
|
||||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||||
use workspace::item::{BreadcrumbText, FollowEvent};
|
use workspace::item::{BreadcrumbText, FollowEvent};
|
||||||
|
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||||
|
@ -621,10 +620,10 @@ impl Item for Editor {
|
||||||
.worktree_for_id(path.worktree_id, cx)?
|
.worktree_for_id(path.worktree_id, cx)?
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.snapshot()
|
.snapshot()
|
||||||
.status_for_file(path.path);
|
.status_for_file(path.path)?;
|
||||||
|
|
||||||
Some(entry_git_aware_label_color(
|
Some(entry_git_aware_label_color(
|
||||||
git_status,
|
git_status.summary(),
|
||||||
entry.is_ignored,
|
entry.is_ignored,
|
||||||
params.selected,
|
params.selected,
|
||||||
))
|
))
|
||||||
|
@ -1560,20 +1559,17 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry_git_aware_label_color(
|
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
|
||||||
git_status: Option<GitFileStatus>,
|
|
||||||
ignored: bool,
|
|
||||||
selected: bool,
|
|
||||||
) -> Color {
|
|
||||||
if ignored {
|
if ignored {
|
||||||
Color::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 {
|
} else {
|
||||||
match git_status {
|
entry_label_color(selected)
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ mod mac_watcher;
|
||||||
pub mod fs_watcher;
|
pub mod fs_watcher;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use git::status::FileStatus;
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
@ -41,7 +43,7 @@ use util::ResultExt;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use collections::{btree_map, BTreeMap};
|
use collections::{btree_map, BTreeMap};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use git::repository::{FakeGitRepositoryState, GitFileStatus};
|
use git::repository::FakeGitRepositoryState;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -1285,11 +1287,11 @@ impl FakeFs {
|
||||||
pub fn set_status_for_repo_via_working_copy_change(
|
pub fn set_status_for_repo_via_working_copy_change(
|
||||||
&self,
|
&self,
|
||||||
dot_git: &Path,
|
dot_git: &Path,
|
||||||
statuses: &[(&Path, GitFileStatus)],
|
statuses: &[(&Path, FileStatus)],
|
||||||
) {
|
) {
|
||||||
self.with_git_state(dot_git, false, |state| {
|
self.with_git_state(dot_git, false, |state| {
|
||||||
state.worktree_statuses.clear();
|
state.statuses.clear();
|
||||||
state.worktree_statuses.extend(
|
state.statuses.extend(
|
||||||
statuses
|
statuses
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(path, content)| ((**path).into(), *content)),
|
.map(|(path, content)| ((**path).into(), *content)),
|
||||||
|
@ -1305,11 +1307,11 @@ impl FakeFs {
|
||||||
pub fn set_status_for_repo_via_git_operation(
|
pub fn set_status_for_repo_via_git_operation(
|
||||||
&self,
|
&self,
|
||||||
dot_git: &Path,
|
dot_git: &Path,
|
||||||
statuses: &[(&Path, GitFileStatus)],
|
statuses: &[(&Path, FileStatus)],
|
||||||
) {
|
) {
|
||||||
self.with_git_state(dot_git, true, |state| {
|
self.with_git_state(dot_git, true, |state| {
|
||||||
state.worktree_statuses.clear();
|
state.statuses.clear();
|
||||||
state.worktree_statuses.extend(
|
state.statuses.extend(
|
||||||
statuses
|
statuses
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(path, content)| ((**path).into(), *content)),
|
.map(|(path, content)| ((**path).into(), *content)),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::status::GitStatusPair;
|
use crate::status::FileStatus;
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
@ -7,7 +7,6 @@ use git2::BranchType;
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -294,7 +293,7 @@ pub struct FakeGitRepositoryState {
|
||||||
pub event_emitter: smol::channel::Sender<PathBuf>,
|
pub event_emitter: smol::channel::Sender<PathBuf>,
|
||||||
pub index_contents: HashMap<PathBuf, String>,
|
pub index_contents: HashMap<PathBuf, String>,
|
||||||
pub blames: HashMap<PathBuf, Blame>,
|
pub blames: HashMap<PathBuf, Blame>,
|
||||||
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
|
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>,
|
||||||
}
|
}
|
||||||
|
@ -312,7 +311,7 @@ impl FakeGitRepositoryState {
|
||||||
event_emitter,
|
event_emitter,
|
||||||
index_contents: Default::default(),
|
index_contents: Default::default(),
|
||||||
blames: Default::default(),
|
blames: Default::default(),
|
||||||
worktree_statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
current_branch_name: Default::default(),
|
current_branch_name: Default::default(),
|
||||||
branches: Default::default(),
|
branches: Default::default(),
|
||||||
}
|
}
|
||||||
|
@ -349,20 +348,14 @@ impl GitRepository for FakeGitRepository {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
|
|
||||||
let mut entries = state
|
let mut entries = state
|
||||||
.worktree_statuses
|
.statuses
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(repo_path, status_worktree)| {
|
.filter_map(|(repo_path, status)| {
|
||||||
if path_prefixes
|
if path_prefixes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|path_prefix| repo_path.0.starts_with(path_prefix))
|
.any(|path_prefix| repo_path.0.starts_with(path_prefix))
|
||||||
{
|
{
|
||||||
Some((
|
Some((repo_path.to_owned(), *status))
|
||||||
repo_path.to_owned(),
|
|
||||||
GitStatusPair {
|
|
||||||
index_status: None,
|
|
||||||
worktree_status: Some(*status_worktree),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -461,51 +454,6 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
pub enum GitFileStatus {
|
|
||||||
Added,
|
|
||||||
Modified,
|
|
||||||
// TODO conflicts should be represented by the GitStatusPair
|
|
||||||
Conflict,
|
|
||||||
Deleted,
|
|
||||||
Untracked,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitFileStatus {
|
|
||||||
pub fn merge(
|
|
||||||
this: Option<GitFileStatus>,
|
|
||||||
other: Option<GitFileStatus>,
|
|
||||||
prefer_other: bool,
|
|
||||||
) -> Option<GitFileStatus> {
|
|
||||||
if prefer_other {
|
|
||||||
return other;
|
|
||||||
}
|
|
||||||
|
|
||||||
match (this, other) {
|
|
||||||
(Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
|
|
||||||
Some(GitFileStatus::Conflict)
|
|
||||||
}
|
|
||||||
(Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
|
|
||||||
Some(GitFileStatus::Modified)
|
|
||||||
}
|
|
||||||
(Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
|
|
||||||
Some(GitFileStatus::Added)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_byte(byte: u8) -> Option<Self> {
|
|
||||||
match byte {
|
|
||||||
b'M' => Some(GitFileStatus::Modified),
|
|
||||||
b'A' => Some(GitFileStatus::Added),
|
|
||||||
b'D' => Some(GitFileStatus::Deleted),
|
|
||||||
b'?' => Some(GitFileStatus::Untracked),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
|
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
|
||||||
LazyLock::new(|| RepoPath(Path::new("").into()));
|
LazyLock::new(|| RepoPath(Path::new("").into()));
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,316 @@
|
||||||
use crate::repository::{GitFileStatus, RepoPath};
|
use crate::repository::RepoPath;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{path::Path, process::Stdio, sync::Arc};
|
use std::{path::Path, process::Stdio, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct GitStatusPair {
|
pub enum FileStatus {
|
||||||
// Not both `None`.
|
Untracked,
|
||||||
pub index_status: Option<GitFileStatus>,
|
Ignored,
|
||||||
pub worktree_status: Option<GitFileStatus>,
|
Unmerged(UnmergedStatus),
|
||||||
|
Tracked(TrackedStatus),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitStatusPair {
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub fn is_staged(&self) -> Option<bool> {
|
pub struct UnmergedStatus {
|
||||||
match (self.index_status, self.worktree_status) {
|
pub first_head: UnmergedStatusCode,
|
||||||
(Some(_), None) => Some(true),
|
pub second_head: UnmergedStatusCode,
|
||||||
(None, Some(_)) => Some(false),
|
}
|
||||||
(Some(GitFileStatus::Untracked), Some(GitFileStatus::Untracked)) => Some(false),
|
|
||||||
(Some(_), Some(_)) => None,
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
(None, None) => unreachable!(),
|
pub enum UnmergedStatusCode {
|
||||||
|
Added,
|
||||||
|
Deleted,
|
||||||
|
Updated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct TrackedStatus {
|
||||||
|
pub index_status: StatusCode,
|
||||||
|
pub worktree_status: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum StatusCode {
|
||||||
|
Modified,
|
||||||
|
TypeChanged,
|
||||||
|
Added,
|
||||||
|
Deleted,
|
||||||
|
Renamed,
|
||||||
|
Copied,
|
||||||
|
Unmodified,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UnmergedStatus> for FileStatus {
|
||||||
|
fn from(value: UnmergedStatus) -> Self {
|
||||||
|
FileStatus::Unmerged(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TrackedStatus> for FileStatus {
|
||||||
|
fn from(value: TrackedStatus) -> Self {
|
||||||
|
FileStatus::Tracked(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStatus {
|
||||||
|
pub const fn worktree(worktree_status: StatusCode) -> Self {
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
worktree_status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a FileStatus Code from a byte pair, as described in
|
||||||
|
/// https://git-scm.com/docs/git-status#_output
|
||||||
|
///
|
||||||
|
/// NOTE: That instead of '', we use ' ' to denote no change
|
||||||
|
fn from_bytes(bytes: [u8; 2]) -> anyhow::Result<Self> {
|
||||||
|
let status = match bytes {
|
||||||
|
[b'?', b'?'] => FileStatus::Untracked,
|
||||||
|
[b'!', b'!'] => FileStatus::Ignored,
|
||||||
|
[b'A', b'A'] => UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Added,
|
||||||
|
second_head: UnmergedStatusCode::Added,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
[b'D', b'D'] => UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Added,
|
||||||
|
second_head: UnmergedStatusCode::Added,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
[x, b'U'] => UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::from_byte(x)?,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
[b'U', y] => UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::from_byte(y)?,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
[x, y] => TrackedStatus {
|
||||||
|
index_status: StatusCode::from_byte(x)?,
|
||||||
|
worktree_status: StatusCode::from_byte(y)?,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_staged(self) -> Option<bool> {
|
||||||
|
match self {
|
||||||
|
FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||||
|
(StatusCode::Unmodified, _) => Some(false),
|
||||||
|
(_, StatusCode::Unmodified) => Some(true),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO reconsider uses of this
|
pub fn is_conflicted(self) -> bool {
|
||||||
pub fn combined(&self) -> GitFileStatus {
|
match self {
|
||||||
self.index_status.or(self.worktree_status).unwrap()
|
FileStatus::Unmerged { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_ignored(self) -> bool {
|
||||||
|
match self {
|
||||||
|
FileStatus::Ignored => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_modified(self) -> bool {
|
||||||
|
match self {
|
||||||
|
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||||
|
(StatusCode::Modified, _) | (_, StatusCode::Modified) => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_created(self) -> bool {
|
||||||
|
match self {
|
||||||
|
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||||
|
(StatusCode::Added, _) | (_, StatusCode::Added) => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_deleted(self) -> bool {
|
||||||
|
match self {
|
||||||
|
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||||
|
(StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_untracked(self) -> bool {
|
||||||
|
match self {
|
||||||
|
FileStatus::Untracked => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary(self) -> GitSummary {
|
||||||
|
match self {
|
||||||
|
FileStatus::Ignored => GitSummary::UNCHANGED,
|
||||||
|
FileStatus::Untracked => GitSummary::UNTRACKED,
|
||||||
|
FileStatus::Unmerged(_) => GitSummary::CONFLICT,
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status,
|
||||||
|
worktree_status,
|
||||||
|
}) => index_status.summary() + worktree_status.summary(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusCode {
|
||||||
|
fn from_byte(byte: u8) -> anyhow::Result<Self> {
|
||||||
|
match byte {
|
||||||
|
b'M' => Ok(StatusCode::Modified),
|
||||||
|
b'T' => Ok(StatusCode::TypeChanged),
|
||||||
|
b'A' => Ok(StatusCode::Added),
|
||||||
|
b'D' => Ok(StatusCode::Deleted),
|
||||||
|
b'R' => Ok(StatusCode::Renamed),
|
||||||
|
b'C' => Ok(StatusCode::Copied),
|
||||||
|
b' ' => Ok(StatusCode::Unmodified),
|
||||||
|
_ => Err(anyhow!("Invalid status code: {byte}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(self) -> GitSummary {
|
||||||
|
match self {
|
||||||
|
StatusCode::Modified | StatusCode::TypeChanged => GitSummary::MODIFIED,
|
||||||
|
StatusCode::Added => GitSummary::ADDED,
|
||||||
|
StatusCode::Deleted => GitSummary::DELETED,
|
||||||
|
StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
|
||||||
|
GitSummary::UNCHANGED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnmergedStatusCode {
|
||||||
|
fn from_byte(byte: u8) -> anyhow::Result<Self> {
|
||||||
|
match byte {
|
||||||
|
b'A' => Ok(UnmergedStatusCode::Added),
|
||||||
|
b'D' => Ok(UnmergedStatusCode::Deleted),
|
||||||
|
b'U' => Ok(UnmergedStatusCode::Updated),
|
||||||
|
_ => Err(anyhow!("Invalid unmerged status code: {byte}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
||||||
|
pub struct GitSummary {
|
||||||
|
pub added: usize,
|
||||||
|
pub modified: usize,
|
||||||
|
pub conflict: usize,
|
||||||
|
pub untracked: usize,
|
||||||
|
pub deleted: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitSummary {
|
||||||
|
pub const ADDED: Self = Self {
|
||||||
|
added: 1,
|
||||||
|
..Self::UNCHANGED
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MODIFIED: Self = Self {
|
||||||
|
modified: 1,
|
||||||
|
..Self::UNCHANGED
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CONFLICT: Self = Self {
|
||||||
|
conflict: 1,
|
||||||
|
..Self::UNCHANGED
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DELETED: Self = Self {
|
||||||
|
deleted: 1,
|
||||||
|
..Self::UNCHANGED
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const UNTRACKED: Self = Self {
|
||||||
|
untracked: 1,
|
||||||
|
..Self::UNCHANGED
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const UNCHANGED: Self = Self {
|
||||||
|
added: 0,
|
||||||
|
modified: 0,
|
||||||
|
conflict: 0,
|
||||||
|
untracked: 0,
|
||||||
|
deleted: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FileStatus> for GitSummary {
|
||||||
|
fn from(status: FileStatus) -> Self {
|
||||||
|
status.summary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Summary for GitSummary {
|
||||||
|
type Context = ();
|
||||||
|
|
||||||
|
fn zero(_: &Self::Context) -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_summary(&mut self, rhs: &Self, _: &Self::Context) {
|
||||||
|
*self += *rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add<Self> for GitSummary {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(mut self, rhs: Self) -> Self {
|
||||||
|
self += rhs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::AddAssign for GitSummary {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.added += rhs.added;
|
||||||
|
self.modified += rhs.modified;
|
||||||
|
self.conflict += rhs.conflict;
|
||||||
|
self.untracked += rhs.untracked;
|
||||||
|
self.deleted += rhs.deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for GitSummary {
|
||||||
|
type Output = GitSummary;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
GitSummary {
|
||||||
|
added: self.added - rhs.added,
|
||||||
|
modified: self.modified - rhs.modified,
|
||||||
|
conflict: self.conflict - rhs.conflict,
|
||||||
|
untracked: self.untracked - rhs.untracked,
|
||||||
|
deleted: self.deleted - rhs.deleted,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GitStatus {
|
pub struct GitStatus {
|
||||||
pub entries: Arc<[(RepoPath, GitStatusPair)]>,
|
pub entries: Arc<[(RepoPath, FileStatus)]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitStatus {
|
impl GitStatus {
|
||||||
|
@ -77,20 +359,10 @@ impl GitStatus {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let path = &entry[3..];
|
let path = &entry[3..];
|
||||||
let status = entry[0..2].as_bytes();
|
let status = entry[0..2].as_bytes().try_into().unwrap();
|
||||||
let index_status = GitFileStatus::from_byte(status[0]);
|
let status = FileStatus::from_bytes(status).log_err()?;
|
||||||
let worktree_status = GitFileStatus::from_byte(status[1]);
|
|
||||||
if (index_status, worktree_status) == (None, None) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let path = RepoPath(Path::new(path).into());
|
let path = RepoPath(Path::new(path).into());
|
||||||
Some((
|
Some((path, status))
|
||||||
path,
|
|
||||||
GitStatusPair {
|
|
||||||
index_status,
|
|
||||||
worktree_status,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
||||||
|
|
|
@ -8,8 +8,7 @@ use anyhow::{Context as _, Result};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::scroll::ScrollbarAutoHide;
|
use editor::scroll::ScrollbarAutoHide;
|
||||||
use editor::{Editor, EditorSettings, ShowScrollbar};
|
use editor::{Editor, EditorSettings, ShowScrollbar};
|
||||||
use git::repository::{GitFileStatus, RepoPath};
|
use git::{repository::RepoPath, status::FileStatus};
|
||||||
use git::status::GitStatusPair;
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
|
@ -72,7 +71,7 @@ pub struct GitListEntry {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
display_name: String,
|
display_name: String,
|
||||||
repo_path: RepoPath,
|
repo_path: RepoPath,
|
||||||
status: GitStatusPair,
|
status: FileStatus,
|
||||||
is_staged: Option<bool>,
|
is_staged: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,7 +664,7 @@ impl GitPanel {
|
||||||
.skip(range.start)
|
.skip(range.start)
|
||||||
.take(range.end - range.start)
|
.take(range.end - range.start)
|
||||||
{
|
{
|
||||||
let status = entry.status.clone();
|
let status = entry.status;
|
||||||
let filename = entry
|
let filename = entry
|
||||||
.repo_path
|
.repo_path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -1072,22 +1071,23 @@ impl GitPanel {
|
||||||
let repo_path = entry_details.repo_path.clone();
|
let repo_path = entry_details.repo_path.clone();
|
||||||
let selected = self.selected_entry == Some(ix);
|
let selected = self.selected_entry == Some(ix);
|
||||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||||
// TODO revisit, maybe use a different status here?
|
let status = entry_details.status;
|
||||||
let status = entry_details.status.combined();
|
|
||||||
|
|
||||||
let mut label_color = cx.theme().colors().text;
|
let mut label_color = cx.theme().colors().text;
|
||||||
if status_style == StatusStyle::LabelColor {
|
if status_style == StatusStyle::LabelColor {
|
||||||
label_color = match status {
|
label_color = if status.is_conflicted() {
|
||||||
GitFileStatus::Added => cx.theme().status().created,
|
cx.theme().status().conflict
|
||||||
GitFileStatus::Modified => cx.theme().status().modified,
|
} else if status.is_modified() {
|
||||||
GitFileStatus::Conflict => cx.theme().status().conflict,
|
cx.theme().status().modified
|
||||||
GitFileStatus::Deleted => cx.theme().colors().text_disabled,
|
} else if status.is_deleted() {
|
||||||
// TODO: Should we even have this here?
|
cx.theme().colors().text_disabled
|
||||||
GitFileStatus::Untracked => cx.theme().colors().text_placeholder,
|
} 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)
|
.then_some(cx.theme().colors().text_disabled)
|
||||||
.unwrap_or(cx.theme().colors().text_muted);
|
.unwrap_or(cx.theme().colors().text_muted);
|
||||||
|
|
||||||
|
@ -1175,7 +1175,7 @@ impl GitPanel {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.text_color(label_color)
|
.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| {
|
.when_some(repo_path.parent(), |this, parent| {
|
||||||
let parent_str = parent.to_string_lossy();
|
let parent_str = parent.to_string_lossy();
|
||||||
if !parent_str.is_empty() {
|
if !parent_str.is_empty() {
|
||||||
|
|
|
@ -2,7 +2,8 @@ use ::settings::Settings;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::StreamExt as _;
|
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 git_panel_settings::GitPanelSettings;
|
||||||
use gpui::{actions, AppContext, Hsla, Model};
|
use gpui::{actions, AppContext, Hsla, Model};
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
|
@ -223,17 +224,15 @@ const REMOVED_COLOR: Hsla = Hsla {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Add updated status colors to theme
|
// TODO: Add updated status colors to theme
|
||||||
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
|
||||||
match status {
|
let (icon_name, color) = if status.is_conflicted() {
|
||||||
GitFileStatus::Added | GitFileStatus::Untracked => {
|
(IconName::Warning, REMOVED_COLOR)
|
||||||
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
|
} else if status.is_deleted() {
|
||||||
}
|
(IconName::SquareMinus, REMOVED_COLOR)
|
||||||
GitFileStatus::Modified => {
|
} else if status.is_modified() {
|
||||||
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
(IconName::SquareDot, MODIFIED_COLOR)
|
||||||
}
|
} else {
|
||||||
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
|
(IconName::SquarePlus, ADDED_COLOR)
|
||||||
GitFileStatus::Deleted => {
|
};
|
||||||
Icon::new(IconName::SquareMinus).color(Color::Custom(REMOVED_COLOR))
|
Icon::new(icon_name).color(Color::Custom(color))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,17 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use editor::items::entry_git_aware_label_color;
|
use editor::items::entry_git_aware_label_color;
|
||||||
|
use file_icons::FileIcons;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
|
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
|
||||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
|
||||||
Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use persistence::IMAGE_VIEWER;
|
use persistence::IMAGE_VIEWER;
|
||||||
use theme::Theme;
|
|
||||||
use ui::prelude::*;
|
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
|
||||||
use project::{image_store::ImageItemEvent, ImageItem, Project, ProjectPath};
|
use project::{image_store::ImageItemEvent, ImageItem, Project, ProjectPath};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use theme::Theme;
|
||||||
|
use ui::prelude::*;
|
||||||
use util::paths::PathExt;
|
use util::paths::PathExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
|
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
|
||||||
|
@ -101,7 +100,9 @@ impl Item for ImageView {
|
||||||
let git_status = self
|
let git_status = self
|
||||||
.project
|
.project
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.project_path_git_status(&project_path, cx);
|
.project_path_git_status(&project_path, cx)
|
||||||
|
.map(|status| status.summary())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
self.project
|
self.project
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
|
@ -1982,7 +1982,7 @@ impl OutlinePanel {
|
||||||
let is_expanded = !self
|
let is_expanded = !self
|
||||||
.collapsed_entries
|
.collapsed_entries
|
||||||
.contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
|
.contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
|
||||||
let color = entry_git_aware_label_color(None, false, is_active);
|
let color = entry_label_color(is_active);
|
||||||
let icon = if has_outlines {
|
let icon = if has_outlines {
|
||||||
FileIcons::get_chevron_icon(is_expanded, cx)
|
FileIcons::get_chevron_icon(is_expanded, cx)
|
||||||
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
||||||
|
@ -2086,7 +2086,7 @@ impl OutlinePanel {
|
||||||
}) => {
|
}) => {
|
||||||
let name = self.entry_name(worktree_id, entry, cx);
|
let name = self.entry_name(worktree_id, entry, cx);
|
||||||
let color =
|
let color =
|
||||||
entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
|
entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
|
||||||
let icon = if settings.file_icons {
|
let icon = if settings.file_icons {
|
||||||
FileIcons::get_icon(&entry.path, cx)
|
FileIcons::get_icon(&entry.path, cx)
|
||||||
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
||||||
|
@ -2114,7 +2114,7 @@ impl OutlinePanel {
|
||||||
directory.entry.id,
|
directory.entry.id,
|
||||||
));
|
));
|
||||||
let color = entry_git_aware_label_color(
|
let color = entry_git_aware_label_color(
|
||||||
directory.entry.git_status,
|
directory.entry.git_summary,
|
||||||
directory.entry.is_ignored,
|
directory.entry.is_ignored,
|
||||||
is_active,
|
is_active,
|
||||||
);
|
);
|
||||||
|
@ -2210,7 +2210,8 @@ impl OutlinePanel {
|
||||||
let git_status = folded_dir
|
let git_status = folded_dir
|
||||||
.entries
|
.entries
|
||||||
.first()
|
.first()
|
||||||
.and_then(|entry| entry.git_status);
|
.map(|entry| entry.git_summary)
|
||||||
|
.unwrap_or_default();
|
||||||
let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
|
let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
|
||||||
let icon = if settings.folder_icons {
|
let icon = if settings.folder_icons {
|
||||||
FileIcons::get_folder_icon(is_expanded, cx)
|
FileIcons::get_folder_icon(is_expanded, cx)
|
||||||
|
@ -2556,7 +2557,10 @@ impl OutlinePanel {
|
||||||
match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
|
match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let entry = GitEntry {
|
let entry = GitEntry {
|
||||||
git_status: worktree.status_for_file(&entry.path),
|
git_summary: worktree
|
||||||
|
.status_for_file(&entry.path)
|
||||||
|
.map(|status| status.summary())
|
||||||
|
.unwrap_or_default(),
|
||||||
entry,
|
entry,
|
||||||
};
|
};
|
||||||
let mut traversal = worktree
|
let mut traversal = worktree
|
||||||
|
|
|
@ -39,10 +39,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::{GitFileStatus, GitRepository},
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
||||||
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
||||||
|
@ -1449,7 +1446,7 @@ impl Project {
|
||||||
&self,
|
&self,
|
||||||
project_path: &ProjectPath,
|
project_path: &ProjectPath,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<GitFileStatus> {
|
) -> Option<FileStatus> {
|
||||||
self.worktree_for_id(project_path.worktree_id, cx)
|
self.worktree_for_id(project_path.worktree_id, cx)
|
||||||
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
|
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use editor::{
|
||||||
Editor, EditorEvent, EditorSettings, ShowScrollbar,
|
Editor, EditorEvent, EditorSettings, ShowScrollbar,
|
||||||
};
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use git::repository::GitFileStatus;
|
use git::status::GitSummary;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
|
actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
|
||||||
AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
|
AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
|
||||||
|
@ -145,7 +145,7 @@ struct EntryDetails {
|
||||||
is_cut: bool,
|
is_cut: bool,
|
||||||
filename_text_color: Color,
|
filename_text_color: Color,
|
||||||
diagnostic_severity: Option<DiagnosticSeverity>,
|
diagnostic_severity: Option<DiagnosticSeverity>,
|
||||||
git_status: Option<GitFileStatus>,
|
git_status: GitSummary,
|
||||||
is_private: bool,
|
is_private: bool,
|
||||||
worktree_id: WorktreeId,
|
worktree_id: WorktreeId,
|
||||||
canonical_path: Option<Box<Path>>,
|
canonical_path: Option<Box<Path>>,
|
||||||
|
@ -1584,9 +1584,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
&& entry.is_file()
|
&& entry.is_file()
|
||||||
&& entry
|
&& entry.git_summary.modified > 0
|
||||||
.git_status
|
|
||||||
.is_some_and(|status| matches!(status, GitFileStatus::Modified))
|
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1664,9 +1662,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
&& entry.is_file()
|
&& entry.is_file()
|
||||||
&& entry
|
&& entry.git_summary.modified > 0
|
||||||
.git_status
|
|
||||||
.is_some_and(|status| matches!(status, GitFileStatus::Modified))
|
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -2417,7 +2413,7 @@ impl ProjectPanel {
|
||||||
char_bag: entry.char_bag,
|
char_bag: entry.char_bag,
|
||||||
is_fifo: entry.is_fifo,
|
is_fifo: entry.is_fifo,
|
||||||
},
|
},
|
||||||
git_status: entry.git_status,
|
git_summary: entry.git_summary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||||
|
@ -2815,7 +2811,9 @@ impl ProjectPanel {
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
for entry in visible_worktree_entries[entry_range].iter() {
|
for entry in visible_worktree_entries[entry_range].iter() {
|
||||||
let status = git_status_setting.then_some(entry.git_status).flatten();
|
let status = git_status_setting
|
||||||
|
.then_some(entry.git_summary)
|
||||||
|
.unwrap_or_default();
|
||||||
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
|
||||||
let icon = match entry.kind {
|
let icon = match entry.kind {
|
||||||
EntryKind::File => {
|
EntryKind::File => {
|
||||||
|
|
|
@ -1784,7 +1784,9 @@ message RepositoryEntry {
|
||||||
|
|
||||||
message StatusEntry {
|
message StatusEntry {
|
||||||
string repo_path = 1;
|
string repo_path = 1;
|
||||||
GitStatus status = 2;
|
// Can be removed once collab's min version is >=0.171.0.
|
||||||
|
GitStatus simple_status = 2;
|
||||||
|
GitFileStatus status = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GitStatus {
|
enum GitStatus {
|
||||||
|
@ -1792,6 +1794,31 @@ enum GitStatus {
|
||||||
Modified = 1;
|
Modified = 1;
|
||||||
Conflict = 2;
|
Conflict = 2;
|
||||||
Deleted = 3;
|
Deleted = 3;
|
||||||
|
Updated = 4;
|
||||||
|
TypeChanged = 5;
|
||||||
|
Renamed = 6;
|
||||||
|
Copied = 7;
|
||||||
|
Unmodified = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GitFileStatus {
|
||||||
|
oneof variant {
|
||||||
|
Untracked untracked = 1;
|
||||||
|
Ignored ignored = 2;
|
||||||
|
Unmerged unmerged = 3;
|
||||||
|
Tracked tracked = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Untracked {}
|
||||||
|
message Ignored {}
|
||||||
|
message Unmerged {
|
||||||
|
GitStatus first_head = 1;
|
||||||
|
GitStatus second_head = 2;
|
||||||
|
}
|
||||||
|
message Tracked {
|
||||||
|
GitStatus index_status = 1;
|
||||||
|
GitStatus worktree_status = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferState {
|
message BufferState {
|
||||||
|
|
|
@ -362,7 +362,10 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
.and_then(|path| {
|
.and_then(|path| {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let entry = project.entry_for_path(path, cx)?;
|
let entry = project.entry_for_path(path, cx)?;
|
||||||
let git_status = project.project_path_git_status(path, cx);
|
let git_status = project
|
||||||
|
.project_path_git_status(path, cx)
|
||||||
|
.map(|status| status.summary())
|
||||||
|
.unwrap_or_default();
|
||||||
Some((entry, git_status))
|
Some((entry, git_status))
|
||||||
})
|
})
|
||||||
.map(|(entry, git_status)| {
|
.map(|(entry, git_status)| {
|
||||||
|
|
|
@ -18,11 +18,12 @@ use futures::{
|
||||||
FutureExt as _, Stream, StreamExt,
|
FutureExt as _, Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
use git::GitHostingProviderRegistry;
|
|
||||||
use git::{
|
use git::{
|
||||||
repository::{GitFileStatus, GitRepository, RepoPath},
|
repository::{GitRepository, RepoPath},
|
||||||
status::GitStatusPair,
|
status::{
|
||||||
COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||||
|
},
|
||||||
|
GitHostingProviderRegistry, COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
||||||
|
@ -239,10 +240,7 @@ impl RepositoryEntry {
|
||||||
updated_statuses: self
|
updated_statuses: self
|
||||||
.statuses_by_path
|
.statuses_by_path
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| proto::StatusEntry {
|
.map(|entry| entry.to_proto())
|
||||||
repo_path: entry.repo_path.to_string_lossy().to_string(),
|
|
||||||
status: status_pair_to_proto(entry.status.clone()),
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
removed_statuses: Default::default(),
|
removed_statuses: Default::default(),
|
||||||
}
|
}
|
||||||
|
@ -266,7 +264,7 @@ impl RepositoryEntry {
|
||||||
current_new_entry = new_statuses.next();
|
current_new_entry = new_statuses.next();
|
||||||
}
|
}
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
if new_entry.combined_status() != old_entry.combined_status() {
|
if new_entry.status != old_entry.status {
|
||||||
updated_statuses.push(new_entry.to_proto());
|
updated_statuses.push(new_entry.to_proto());
|
||||||
}
|
}
|
||||||
current_old_entry = old_statuses.next();
|
current_old_entry = old_statuses.next();
|
||||||
|
@ -2361,13 +2359,13 @@ impl Snapshot {
|
||||||
Some(removed_entry.path)
|
Some(removed_entry.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<GitFileStatus> {
|
pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<FileStatus> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
self.repository_for_path(path).and_then(|repo| {
|
self.repository_for_path(path).and_then(|repo| {
|
||||||
let repo_path = repo.relativize(path).unwrap();
|
let repo_path = repo.relativize(path).unwrap();
|
||||||
repo.statuses_by_path
|
repo.statuses_by_path
|
||||||
.get(&PathKey(repo_path.0), &())
|
.get(&PathKey(repo_path.0), &())
|
||||||
.map(|entry| entry.combined_status())
|
.map(|entry| entry.status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3633,41 +3631,41 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, GitRepositoryChange)]>;
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct StatusEntry {
|
pub struct StatusEntry {
|
||||||
pub repo_path: RepoPath,
|
pub repo_path: RepoPath,
|
||||||
pub status: GitStatusPair,
|
pub status: FileStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusEntry {
|
impl StatusEntry {
|
||||||
// TODO revisit uses of this
|
|
||||||
pub fn combined_status(&self) -> GitFileStatus {
|
|
||||||
self.status.combined()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index_status(&self) -> Option<GitFileStatus> {
|
|
||||||
self.status.index_status
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn worktree_status(&self) -> Option<GitFileStatus> {
|
|
||||||
self.status.worktree_status
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_staged(&self) -> Option<bool> {
|
pub fn is_staged(&self) -> Option<bool> {
|
||||||
self.status.is_staged()
|
self.status.is_staged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto(&self) -> proto::StatusEntry {
|
fn to_proto(&self) -> proto::StatusEntry {
|
||||||
|
let simple_status = match self.status {
|
||||||
|
FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
|
||||||
|
FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status,
|
||||||
|
worktree_status,
|
||||||
|
}) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
|
||||||
|
worktree_status
|
||||||
|
} else {
|
||||||
|
index_status
|
||||||
|
}),
|
||||||
|
};
|
||||||
proto::StatusEntry {
|
proto::StatusEntry {
|
||||||
repo_path: self.repo_path.to_proto(),
|
repo_path: self.repo_path.to_proto(),
|
||||||
status: status_pair_to_proto(self.status.clone()),
|
simple_status,
|
||||||
|
status: Some(status_to_proto(self.status)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
||||||
let repo_path = RepoPath(Path::new(&value.repo_path).into());
|
let repo_path = RepoPath(Path::new(&value.repo_path).into());
|
||||||
let status = status_pair_from_proto(value.status)
|
let status = status_from_proto(value.simple_status, value.status)?;
|
||||||
.ok_or_else(|| anyhow!("Unable to parse status value {}", value.status))?;
|
|
||||||
Ok(Self { repo_path, status })
|
Ok(Self { repo_path, status })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3734,43 +3732,13 @@ impl sum_tree::KeyedItem for RepositoryEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sum_tree::Summary for GitStatuses {
|
|
||||||
type Context = ();
|
|
||||||
|
|
||||||
fn zero(_: &Self::Context) -> Self {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_summary(&mut self, rhs: &Self, _: &Self::Context) {
|
|
||||||
*self += *rhs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sum_tree::Item for StatusEntry {
|
impl sum_tree::Item for StatusEntry {
|
||||||
type Summary = PathSummary<GitStatuses>;
|
type Summary = PathSummary<GitSummary>;
|
||||||
|
|
||||||
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
||||||
PathSummary {
|
PathSummary {
|
||||||
max_path: self.repo_path.0.clone(),
|
max_path: self.repo_path.0.clone(),
|
||||||
item_summary: match self.combined_status() {
|
item_summary: self.status.summary(),
|
||||||
GitFileStatus::Added => GitStatuses {
|
|
||||||
added: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
GitFileStatus::Modified => GitStatuses {
|
|
||||||
modified: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
GitFileStatus::Conflict => GitStatuses {
|
|
||||||
conflict: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
GitFileStatus::Deleted => Default::default(),
|
|
||||||
GitFileStatus::Untracked => GitStatuses {
|
|
||||||
untracked: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3783,69 +3751,12 @@ impl sum_tree::KeyedItem for StatusEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
|
||||||
pub struct GitStatuses {
|
|
||||||
added: usize,
|
|
||||||
modified: usize,
|
|
||||||
conflict: usize,
|
|
||||||
untracked: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitStatuses {
|
|
||||||
pub fn to_status(&self) -> Option<GitFileStatus> {
|
|
||||||
if self.conflict > 0 {
|
|
||||||
Some(GitFileStatus::Conflict)
|
|
||||||
} else if self.modified > 0 {
|
|
||||||
Some(GitFileStatus::Modified)
|
|
||||||
} else if self.added > 0 || self.untracked > 0 {
|
|
||||||
Some(GitFileStatus::Added)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Add<Self> for GitStatuses {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn add(self, rhs: Self) -> Self {
|
|
||||||
GitStatuses {
|
|
||||||
added: self.added + rhs.added,
|
|
||||||
modified: self.modified + rhs.modified,
|
|
||||||
conflict: self.conflict + rhs.conflict,
|
|
||||||
untracked: self.untracked + rhs.untracked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::AddAssign for GitStatuses {
|
|
||||||
fn add_assign(&mut self, rhs: Self) {
|
|
||||||
self.added += rhs.added;
|
|
||||||
self.modified += rhs.modified;
|
|
||||||
self.conflict += rhs.conflict;
|
|
||||||
self.untracked += rhs.untracked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Sub for GitStatuses {
|
|
||||||
type Output = GitStatuses;
|
|
||||||
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
GitStatuses {
|
|
||||||
added: self.added - rhs.added,
|
|
||||||
modified: self.modified - rhs.modified,
|
|
||||||
conflict: self.conflict - rhs.conflict,
|
|
||||||
untracked: self.untracked - rhs.untracked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> sum_tree::Dimension<'a, PathSummary<GitStatuses>> for GitStatuses {
|
|
||||||
fn zero(_cx: &()) -> Self {
|
fn zero(_cx: &()) -> Self {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_summary(&mut self, summary: &'a PathSummary<GitStatuses>, _: &()) {
|
fn add_summary(&mut self, summary: &'a PathSummary<GitSummary>, _: &()) {
|
||||||
*self += summary.item_summary
|
*self += summary.item_summary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4851,7 +4762,7 @@ impl BackgroundScanner {
|
||||||
|
|
||||||
changed_path_statuses.push(Edit::Insert(StatusEntry {
|
changed_path_statuses.push(Edit::Insert(StatusEntry {
|
||||||
repo_path: repo_path.clone(),
|
repo_path: repo_path.clone(),
|
||||||
status: status.clone(),
|
status: *status,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5280,7 +5191,7 @@ impl BackgroundScanner {
|
||||||
new_entries_by_path.insert_or_replace(
|
new_entries_by_path.insert_or_replace(
|
||||||
StatusEntry {
|
StatusEntry {
|
||||||
repo_path: repo_path.clone(),
|
repo_path: repo_path.clone(),
|
||||||
status: status.clone(),
|
status: *status,
|
||||||
},
|
},
|
||||||
&(),
|
&(),
|
||||||
);
|
);
|
||||||
|
@ -5695,14 +5606,14 @@ impl<'a> Default for TraversalProgress<'a> {
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct GitEntryRef<'a> {
|
pub struct GitEntryRef<'a> {
|
||||||
pub entry: &'a Entry,
|
pub entry: &'a Entry,
|
||||||
pub git_status: Option<GitFileStatus>,
|
pub git_summary: GitSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GitEntryRef<'a> {
|
impl<'a> GitEntryRef<'a> {
|
||||||
pub fn to_owned(&self) -> GitEntry {
|
pub fn to_owned(&self) -> GitEntry {
|
||||||
GitEntry {
|
GitEntry {
|
||||||
entry: self.entry.clone(),
|
entry: self.entry.clone(),
|
||||||
git_status: self.git_status,
|
git_summary: self.git_summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5724,14 +5635,14 @@ impl<'a> AsRef<Entry> for GitEntryRef<'a> {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct GitEntry {
|
pub struct GitEntry {
|
||||||
pub entry: Entry,
|
pub entry: Entry,
|
||||||
pub git_status: Option<GitFileStatus>,
|
pub git_summary: GitSummary,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitEntry {
|
impl GitEntry {
|
||||||
pub fn to_ref(&self) -> GitEntryRef {
|
pub fn to_ref(&self) -> GitEntryRef {
|
||||||
GitEntryRef {
|
GitEntryRef {
|
||||||
entry: &self.entry,
|
entry: &self.entry,
|
||||||
git_status: self.git_status,
|
git_summary: self.git_summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5753,7 +5664,7 @@ impl AsRef<Entry> for GitEntry {
|
||||||
/// Walks the worktree entries and their associated git statuses.
|
/// Walks the worktree entries and their associated git statuses.
|
||||||
pub struct GitTraversal<'a> {
|
pub struct GitTraversal<'a> {
|
||||||
traversal: Traversal<'a>,
|
traversal: Traversal<'a>,
|
||||||
current_entry_status: Option<GitFileStatus>,
|
current_entry_summary: Option<GitSummary>,
|
||||||
repo_location: Option<(
|
repo_location: Option<(
|
||||||
&'a RepositoryEntry,
|
&'a RepositoryEntry,
|
||||||
Cursor<'a, StatusEntry, PathProgress<'a>>,
|
Cursor<'a, StatusEntry, PathProgress<'a>>,
|
||||||
|
@ -5762,7 +5673,7 @@ pub struct GitTraversal<'a> {
|
||||||
|
|
||||||
impl<'a> GitTraversal<'a> {
|
impl<'a> GitTraversal<'a> {
|
||||||
fn synchronize_statuses(&mut self, reset: bool) {
|
fn synchronize_statuses(&mut self, reset: bool) {
|
||||||
self.current_entry_status = None;
|
self.current_entry_summary = None;
|
||||||
|
|
||||||
let Some(entry) = self.traversal.cursor.item() else {
|
let Some(entry) = self.traversal.cursor.item() else {
|
||||||
return;
|
return;
|
||||||
|
@ -5787,14 +5698,16 @@ impl<'a> GitTraversal<'a> {
|
||||||
if entry.is_dir() {
|
if entry.is_dir() {
|
||||||
let mut statuses = statuses.clone();
|
let mut statuses = statuses.clone();
|
||||||
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
|
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
|
||||||
let summary: GitStatuses =
|
let summary =
|
||||||
statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &());
|
statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &());
|
||||||
|
|
||||||
self.current_entry_status = summary.to_status();
|
self.current_entry_summary = Some(summary);
|
||||||
} else if entry.is_file() {
|
} else if entry.is_file() {
|
||||||
// For a file entry, park the cursor on the corresponding status
|
// For a file entry, park the cursor on the corresponding status
|
||||||
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
||||||
self.current_entry_status = Some(statuses.item().unwrap().combined_status());
|
self.current_entry_summary = Some(statuses.item().unwrap().status.into());
|
||||||
|
} else {
|
||||||
|
self.current_entry_summary = Some(GitSummary::zero(&()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5830,10 +5743,9 @@ impl<'a> GitTraversal<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry(&self) -> Option<GitEntryRef<'a>> {
|
pub fn entry(&self) -> Option<GitEntryRef<'a>> {
|
||||||
Some(GitEntryRef {
|
let entry = self.traversal.cursor.item()?;
|
||||||
entry: self.traversal.cursor.item()?,
|
let git_summary = self.current_entry_summary.unwrap_or_default();
|
||||||
git_status: self.current_entry_status,
|
Some(GitEntryRef { entry, git_summary })
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5884,7 +5796,7 @@ impl<'a> Traversal<'a> {
|
||||||
pub fn with_git_statuses(self) -> GitTraversal<'a> {
|
pub fn with_git_statuses(self) -> GitTraversal<'a> {
|
||||||
let mut this = GitTraversal {
|
let mut this = GitTraversal {
|
||||||
traversal: self,
|
traversal: self,
|
||||||
current_entry_status: None,
|
current_entry_summary: None,
|
||||||
repo_location: None,
|
repo_location: None,
|
||||||
};
|
};
|
||||||
this.synchronize_statuses(true);
|
this.synchronize_statuses(true);
|
||||||
|
@ -6003,10 +5915,10 @@ impl<'a, 'b, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> SeekTarget<'a, PathSummary<GitStatuses>, (TraversalProgress<'a>, GitStatuses)>
|
impl<'a, 'b> SeekTarget<'a, PathSummary<GitSummary>, (TraversalProgress<'a>, GitSummary)>
|
||||||
for PathTarget<'b>
|
for PathTarget<'b>
|
||||||
{
|
{
|
||||||
fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering {
|
fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering {
|
||||||
self.cmp_path(&cursor_location.0.max_path)
|
self.cmp_path(&cursor_location.0.max_path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6159,28 +6071,135 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO pass the status pair all the way through
|
fn status_from_proto(
|
||||||
fn status_pair_from_proto(proto: i32) -> Option<GitStatusPair> {
|
simple_status: i32,
|
||||||
let proto = proto::GitStatus::from_i32(proto)?;
|
status: Option<proto::GitFileStatus>,
|
||||||
let worktree_status = match proto {
|
) -> anyhow::Result<FileStatus> {
|
||||||
proto::GitStatus::Added => GitFileStatus::Added,
|
use proto::git_file_status::Variant;
|
||||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
|
||||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
let Some(variant) = status.and_then(|status| status.variant) else {
|
||||||
proto::GitStatus::Deleted => GitFileStatus::Deleted,
|
let code = proto::GitStatus::from_i32(simple_status)
|
||||||
|
.ok_or_else(|| anyhow!("Invalid git status code: {simple_status}"))?;
|
||||||
|
let result = match code {
|
||||||
|
proto::GitStatus::Added => TrackedStatus {
|
||||||
|
worktree_status: StatusCode::Added,
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
proto::GitStatus::Modified => TrackedStatus {
|
||||||
|
worktree_status: StatusCode::Modified,
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
proto::GitStatus::Conflict => UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
proto::GitStatus::Deleted => TrackedStatus {
|
||||||
|
worktree_status: StatusCode::Deleted,
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
_ => return Err(anyhow!("Invalid code for simple status: {simple_status}")),
|
||||||
|
};
|
||||||
|
return Ok(result);
|
||||||
};
|
};
|
||||||
Some(GitStatusPair {
|
|
||||||
index_status: None,
|
let result = match variant {
|
||||||
worktree_status: Some(worktree_status),
|
Variant::Untracked(_) => FileStatus::Untracked,
|
||||||
})
|
Variant::Ignored(_) => FileStatus::Ignored,
|
||||||
|
Variant::Unmerged(unmerged) => {
|
||||||
|
let [first_head, second_head] =
|
||||||
|
[unmerged.first_head, unmerged.second_head].map(|head| {
|
||||||
|
let code = proto::GitStatus::from_i32(head)
|
||||||
|
.ok_or_else(|| anyhow!("Invalid git status code: {head}"))?;
|
||||||
|
let result = match code {
|
||||||
|
proto::GitStatus::Added => UnmergedStatusCode::Added,
|
||||||
|
proto::GitStatus::Updated => UnmergedStatusCode::Updated,
|
||||||
|
proto::GitStatus::Deleted => UnmergedStatusCode::Deleted,
|
||||||
|
_ => return Err(anyhow!("Invalid code for unmerged status: {code:?}")),
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
});
|
||||||
|
let [first_head, second_head] = [first_head?, second_head?];
|
||||||
|
UnmergedStatus {
|
||||||
|
first_head,
|
||||||
|
second_head,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Variant::Tracked(tracked) => {
|
||||||
|
let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status]
|
||||||
|
.map(|status| {
|
||||||
|
let code = proto::GitStatus::from_i32(status)
|
||||||
|
.ok_or_else(|| anyhow!("Invalid git status code: {status}"))?;
|
||||||
|
let result = match code {
|
||||||
|
proto::GitStatus::Modified => StatusCode::Modified,
|
||||||
|
proto::GitStatus::TypeChanged => StatusCode::TypeChanged,
|
||||||
|
proto::GitStatus::Added => StatusCode::Added,
|
||||||
|
proto::GitStatus::Deleted => StatusCode::Deleted,
|
||||||
|
proto::GitStatus::Renamed => StatusCode::Renamed,
|
||||||
|
proto::GitStatus::Copied => StatusCode::Copied,
|
||||||
|
proto::GitStatus::Unmodified => StatusCode::Unmodified,
|
||||||
|
_ => return Err(anyhow!("Invalid code for tracked status: {code:?}")),
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
|
});
|
||||||
|
let [index_status, worktree_status] = [index_status?, worktree_status?];
|
||||||
|
TrackedStatus {
|
||||||
|
index_status,
|
||||||
|
worktree_status,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_pair_to_proto(status: GitStatusPair) -> i32 {
|
fn status_to_proto(status: FileStatus) -> proto::GitFileStatus {
|
||||||
match status.combined() {
|
use proto::git_file_status::{Tracked, Unmerged, Variant};
|
||||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
|
||||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
let variant = match status {
|
||||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
FileStatus::Untracked => Variant::Untracked(Default::default()),
|
||||||
GitFileStatus::Deleted => proto::GitStatus::Deleted as i32,
|
FileStatus::Ignored => Variant::Ignored(Default::default()),
|
||||||
GitFileStatus::Untracked => proto::GitStatus::Added as i32, // TODO
|
FileStatus::Unmerged(UnmergedStatus {
|
||||||
|
first_head,
|
||||||
|
second_head,
|
||||||
|
}) => Variant::Unmerged(Unmerged {
|
||||||
|
first_head: unmerged_status_to_proto(first_head),
|
||||||
|
second_head: unmerged_status_to_proto(second_head),
|
||||||
|
}),
|
||||||
|
FileStatus::Tracked(TrackedStatus {
|
||||||
|
index_status,
|
||||||
|
worktree_status,
|
||||||
|
}) => Variant::Tracked(Tracked {
|
||||||
|
index_status: tracked_status_to_proto(index_status),
|
||||||
|
worktree_status: tracked_status_to_proto(worktree_status),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
proto::GitFileStatus {
|
||||||
|
variant: Some(variant),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 {
|
||||||
|
match code {
|
||||||
|
UnmergedStatusCode::Added => proto::GitStatus::Added as _,
|
||||||
|
UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _,
|
||||||
|
UnmergedStatusCode::Updated => proto::GitStatus::Updated as _,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracked_status_to_proto(code: StatusCode) -> i32 {
|
||||||
|
match code {
|
||||||
|
StatusCode::Added => proto::GitStatus::Added as _,
|
||||||
|
StatusCode::Deleted => proto::GitStatus::Deleted as _,
|
||||||
|
StatusCode::Modified => proto::GitStatus::Modified as _,
|
||||||
|
StatusCode::Renamed => proto::GitStatus::Renamed as _,
|
||||||
|
StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _,
|
||||||
|
StatusCode::Copied => proto::GitStatus::Copied as _,
|
||||||
|
StatusCode::Unmodified => proto::GitStatus::Unmodified as _,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,12 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||||
use git::{repository::GitFileStatus, GITIGNORE};
|
use git::{
|
||||||
|
status::{
|
||||||
|
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||||
|
},
|
||||||
|
GITIGNORE,
|
||||||
|
};
|
||||||
use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
|
use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
|
@ -738,7 +743,10 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
fs.set_status_for_repo_via_working_copy_change(
|
fs.set_status_for_repo_via_working_copy_change(
|
||||||
Path::new("/root/tree/.git"),
|
Path::new("/root/tree/.git"),
|
||||||
&[(Path::new("tracked-dir/tracked-file2"), GitFileStatus::Added)],
|
&[(
|
||||||
|
Path::new("tracked-dir/tracked-file2"),
|
||||||
|
FileStatus::worktree(StatusCode::Added),
|
||||||
|
)],
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.create_file(
|
fs.create_file(
|
||||||
|
@ -766,7 +774,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||||
assert_entry_git_state(
|
assert_entry_git_state(
|
||||||
tree,
|
tree,
|
||||||
"tracked-dir/tracked-file2",
|
"tracked-dir/tracked-file2",
|
||||||
Some(GitFileStatus::Added),
|
Some(StatusCode::Added),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
|
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
|
||||||
|
@ -822,14 +830,14 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
fs.set_status_for_repo_via_working_copy_change(
|
fs.set_status_for_repo_via_working_copy_change(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[(Path::new("b.txt"), GitFileStatus::Added)],
|
&[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
cx.read(|cx| {
|
cx.read(|cx| {
|
||||||
let tree = tree.read(cx);
|
let tree = tree.read(cx);
|
||||||
assert_entry_git_state(tree, "a.xml", None, true);
|
assert_entry_git_state(tree, "a.xml", None, true);
|
||||||
assert_entry_git_state(tree, "b.txt", Some(GitFileStatus::Added), false);
|
assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1492,7 +1500,10 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||||
// detected.
|
// detected.
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[(Path::new("b/c.txt"), GitFileStatus::Modified)],
|
&[(
|
||||||
|
Path::new("b/c.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
)],
|
||||||
);
|
);
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
@ -1501,9 +1512,9 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), Some(GitFileStatus::Modified)),
|
(Path::new(""), GitSummary::MODIFIED),
|
||||||
(Path::new("a.txt"), None),
|
(Path::new("a.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("b/c.txt"), GitSummary::MODIFIED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2142,6 +2153,11 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||||
|
first_head: UnmergedStatusCode::Updated,
|
||||||
|
second_head: UnmergedStatusCode::Updated,
|
||||||
|
});
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2183,11 +2199,11 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/a")),
|
tree.status_for_file(Path::new("projects/project1/a")),
|
||||||
Some(GitFileStatus::Modified)
|
Some(FileStatus::worktree(StatusCode::Modified)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project1/b")),
|
tree.status_for_file(Path::new("projects/project1/b")),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2204,11 +2220,11 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/a")),
|
tree.status_for_file(Path::new("projects/project2/a")),
|
||||||
Some(GitFileStatus::Modified)
|
Some(FileStatus::worktree(StatusCode::Modified)),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new("projects/project2/b")),
|
tree.status_for_file(Path::new("projects/project2/b")),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2387,11 +2403,11 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2405,7 +2421,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(A_TXT)),
|
snapshot.status_for_file(project_path.join(A_TXT)),
|
||||||
Some(GitFileStatus::Modified)
|
Some(FileStatus::worktree(StatusCode::Modified)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2421,7 +2437,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||||
|
@ -2443,11 +2459,11 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(E_TXT)),
|
snapshot.status_for_file(project_path.join(E_TXT)),
|
||||||
Some(GitFileStatus::Modified)
|
Some(FileStatus::worktree(StatusCode::Modified)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2482,7 +2498,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
let snapshot = tree.snapshot();
|
let snapshot = tree.snapshot();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
|
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2506,7 +2522,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||||
.join(Path::new(renamed_dir_name))
|
.join(Path::new(renamed_dir_name))
|
||||||
.join(RENAMED_FILE)
|
.join(RENAMED_FILE)
|
||||||
),
|
),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2559,11 +2575,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
assert_eq!(entries.len(), 3);
|
assert_eq!(entries.len(), 3);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
|
assert_eq!(
|
||||||
|
entries[0].status,
|
||||||
|
FileStatus::worktree(StatusCode::Modified)
|
||||||
|
);
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
|
assert_eq!(entries[1].status, FileStatus::Untracked);
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Deleted));
|
assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
|
||||||
});
|
});
|
||||||
|
|
||||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||||
|
@ -2581,14 +2600,20 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
|
assert_eq!(
|
||||||
|
entries[0].status,
|
||||||
|
FileStatus::worktree(StatusCode::Modified)
|
||||||
|
);
|
||||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||||
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
|
assert_eq!(entries[1].status, FileStatus::Untracked);
|
||||||
// Status updated
|
// Status updated
|
||||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
||||||
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Modified));
|
assert_eq!(
|
||||||
|
entries[2].status,
|
||||||
|
FileStatus::worktree(StatusCode::Modified)
|
||||||
|
);
|
||||||
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
||||||
assert_eq!(entries[3].worktree_status(), Some(GitFileStatus::Deleted));
|
assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
|
||||||
});
|
});
|
||||||
|
|
||||||
git_add("a.txt", &repo);
|
git_add("a.txt", &repo);
|
||||||
|
@ -2621,7 +2646,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||||
&entries
|
&entries
|
||||||
);
|
);
|
||||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||||
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Deleted));
|
assert_eq!(entries[0].status, FileStatus::worktree(StatusCode::Deleted));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2692,7 +2717,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||||
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot.status_for_file("d/e.txt"),
|
snapshot.status_for_file("d/e.txt"),
|
||||||
Some(GitFileStatus::Untracked)
|
Some(FileStatus::Untracked)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2744,17 +2769,20 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
(
|
||||||
(Path::new("z.txt"), GitFileStatus::Added),
|
Path::new("x2.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
),
|
||||||
|
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/y/.git"),
|
Path::new("/root/x/y/.git"),
|
||||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
&[(Path::new("y1.txt"), CONFLICT)],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -2780,25 +2808,25 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
||||||
assert_eq!(entry.git_status, None);
|
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
||||||
assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
|
assert_eq!(entry.git_summary, GitSummary::MODIFIED);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
|
||||||
assert_eq!(entry.git_status, Some(GitFileStatus::Conflict));
|
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
||||||
assert_eq!(entry.git_status, None);
|
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
||||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
assert_eq!(entry.git_summary, GitSummary::ADDED);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
||||||
assert_eq!(entry.git_status, None);
|
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||||
let entry = traversal.next().unwrap();
|
let entry = traversal.next().unwrap();
|
||||||
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
||||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
assert_eq!(entry.git_summary, GitSummary::ADDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -2834,9 +2862,15 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/.git"),
|
Path::new("/root/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new("a/b/c1.txt"), GitFileStatus::Added),
|
(
|
||||||
(Path::new("a/d/e2.txt"), GitFileStatus::Modified),
|
Path::new("a/b/c1.txt"),
|
||||||
(Path::new("g/h2.txt"), GitFileStatus::Conflict),
|
FileStatus::worktree(StatusCode::Added),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Path::new("a/d/e2.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
),
|
||||||
|
(Path::new("g/h2.txt"), CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2859,52 +2893,58 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
(
|
||||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
Path::new(""),
|
||||||
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
|
GitSummary::CONFLICT + GitSummary::MODIFIED + GitSummary::ADDED,
|
||||||
|
),
|
||||||
|
(Path::new("g"), GitSummary::CONFLICT),
|
||||||
|
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
(
|
||||||
(Path::new("a"), Some(GitFileStatus::Modified)),
|
Path::new(""),
|
||||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
GitSummary::CONFLICT + GitSummary::ADDED + GitSummary::MODIFIED,
|
||||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
),
|
||||||
(Path::new("a/b/c2.txt"), None),
|
(Path::new("a"), GitSummary::ADDED + GitSummary::MODIFIED),
|
||||||
(Path::new("a/d"), Some(GitFileStatus::Modified)),
|
(Path::new("a/b"), GitSummary::ADDED),
|
||||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
|
||||||
(Path::new("f"), None),
|
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("f/no-status.txt"), None),
|
(Path::new("a/d"), GitSummary::MODIFIED),
|
||||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("f"), GitSummary::UNCHANGED),
|
||||||
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||||
|
(Path::new("g"), GitSummary::CONFLICT),
|
||||||
|
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
(Path::new("a/b"), GitSummary::ADDED),
|
||||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
|
||||||
(Path::new("a/b/c2.txt"), None),
|
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("a/d"), Some(GitFileStatus::Modified)),
|
(Path::new("a/d"), GitSummary::MODIFIED),
|
||||||
(Path::new("a/d/e1.txt"), None),
|
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("f"), None),
|
(Path::new("f"), GitSummary::UNCHANGED),
|
||||||
(Path::new("f/no-status.txt"), None),
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
(Path::new("g"), GitSummary::CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
(Path::new("a/b/c1.txt"), GitSummary::ADDED),
|
||||||
(Path::new("a/b/c2.txt"), None),
|
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("a/d/e1.txt"), None),
|
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("f/no-status.txt"), None),
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2937,18 +2977,24 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
|
||||||
|
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[(Path::new("x1.txt"), GitFileStatus::Added)],
|
&[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/y/.git"),
|
Path::new("/root/y/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new("y1.txt"), GitFileStatus::Conflict),
|
(Path::new("y1.txt"), CONFLICT),
|
||||||
(Path::new("y2.txt"), GitFileStatus::Modified),
|
(
|
||||||
|
Path::new("y2.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
|
&[(
|
||||||
|
Path::new("z2.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
)],
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -2971,48 +3017,48 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
(Path::new("x"), GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
(Path::new("x/x1.txt"), GitSummary::ADDED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
(Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
|
||||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
||||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("y/y2.txt"), GitSummary::MODIFIED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
(Path::new("z"), GitSummary::MODIFIED),
|
||||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("z/z2.txt"), GitSummary::MODIFIED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
(Path::new("x"), GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
(Path::new("x/x1.txt"), GitSummary::ADDED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
(Path::new("x"), GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
(Path::new("x/x1.txt"), GitSummary::ADDED),
|
||||||
(Path::new("x/x2.txt"), None),
|
(Path::new("x/x2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
(Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
|
||||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
||||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("y/y2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
(Path::new("z"), GitSummary::MODIFIED),
|
||||||
(Path::new("z/z1.txt"), None),
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("z/z2.txt"), GitSummary::MODIFIED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3047,18 +3093,21 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/.git"),
|
Path::new("/root/x/.git"),
|
||||||
&[
|
&[
|
||||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
(
|
||||||
(Path::new("z.txt"), GitFileStatus::Added),
|
Path::new("x2.txt"),
|
||||||
|
FileStatus::worktree(StatusCode::Modified),
|
||||||
|
),
|
||||||
|
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/x/y/.git"),
|
Path::new("/root/x/y/.git"),
|
||||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
&[(Path::new("y1.txt"), CONFLICT)],
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.set_status_for_repo_via_git_operation(
|
fs.set_status_for_repo_via_git_operation(
|
||||||
Path::new("/root/z/.git"),
|
Path::new("/root/z/.git"),
|
||||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
|
@ -3082,17 +3131,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y2.txt"), None),
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
(Path::new("z"), GitSummary::ADDED),
|
||||||
(Path::new("z/z1.txt"), None),
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
(Path::new("z/z2.txt"), GitSummary::ADDED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3100,9 +3149,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
|
||||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3110,13 +3159,13 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), None),
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("x/x2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y2.txt"), None),
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
(Path::new("x/z.txt"), GitSummary::ADDED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3124,9 +3173,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), None),
|
(Path::new(""), GitSummary::UNCHANGED),
|
||||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), None),
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3134,17 +3183,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||||
check_git_statuses(
|
check_git_statuses(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&[
|
&[
|
||||||
(Path::new(""), None),
|
(Path::new(""), GitSummary::UNCHANGED),
|
||||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
(Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
|
||||||
(Path::new("x/x1.txt"), None),
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
(Path::new("x/x2.txt"), GitSummary::MODIFIED),
|
||||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||||
(Path::new("x/y/y2.txt"), None),
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
(Path::new("x/z.txt"), GitSummary::ADDED),
|
||||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
(Path::new("z"), GitSummary::ADDED),
|
||||||
(Path::new("z/z1.txt"), None),
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
(Path::new("z/z2.txt"), GitSummary::ADDED),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3173,7 +3222,7 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
|
fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
|
||||||
let mut traversal = snapshot
|
let mut traversal = snapshot
|
||||||
.traverse_from_path(true, true, false, "".as_ref())
|
.traverse_from_path(true, true, false, "".as_ref())
|
||||||
.with_git_statuses();
|
.with_git_statuses();
|
||||||
|
@ -3182,8 +3231,8 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<G
|
||||||
.map(|&(path, _)| {
|
.map(|&(path, _)| {
|
||||||
let git_entry = traversal
|
let git_entry = traversal
|
||||||
.find(|git_entry| &*git_entry.path == path)
|
.find(|git_entry| &*git_entry.path == path)
|
||||||
.expect("Traversal has no entry for {path:?}");
|
.unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
|
||||||
(path, git_entry.git_status)
|
(path, git_entry.git_summary)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(found_statuses, expected_statuses);
|
assert_eq!(found_statuses, expected_statuses);
|
||||||
|
@ -3330,14 +3379,21 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||||
fn assert_entry_git_state(
|
fn assert_entry_git_state(
|
||||||
tree: &Worktree,
|
tree: &Worktree,
|
||||||
path: &str,
|
path: &str,
|
||||||
git_status: Option<GitFileStatus>,
|
worktree_status: Option<StatusCode>,
|
||||||
is_ignored: bool,
|
is_ignored: bool,
|
||||||
) {
|
) {
|
||||||
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||||
|
let status = tree.status_for_file(Path::new(path));
|
||||||
|
let expected = worktree_status.map(|worktree_status| {
|
||||||
|
TrackedStatus {
|
||||||
|
worktree_status,
|
||||||
|
index_status: StatusCode::Unmodified,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree.status_for_file(Path::new(path)),
|
status, expected,
|
||||||
git_status,
|
"expected {path} to have git status: {expected:?}"
|
||||||
"expected {path} to have git status: {git_status:?}"
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
entry.is_ignored, is_ignored,
|
entry.is_ignored, is_ignored,
|
||||||
|
|
|
@ -21,6 +21,7 @@ OPTIONS
|
||||||
-2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled.
|
-2, -3, -4, ... Spawn multiple Zed instances, with their windows tiled.
|
||||||
--top Arrange the Zed windows so they take up the top half of the screen.
|
--top Arrange the Zed windows so they take up the top half of the screen.
|
||||||
--stable Use stable Zed release installed on local machine for all instances (except for the first one).
|
--stable Use stable Zed release installed on local machine for all instances (except for the first one).
|
||||||
|
--preview Like --stable, but uses the locally-installed preview release instead.
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const { spawn, execSync, execFileSync } = require("child_process");
|
const { spawn, execSync, execFileSync } = require("child_process");
|
||||||
|
@ -48,6 +49,7 @@ let instanceCount = 1;
|
||||||
let isReleaseMode = false;
|
let isReleaseMode = false;
|
||||||
let isTop = false;
|
let isTop = false;
|
||||||
let othersOnStable = false;
|
let othersOnStable = false;
|
||||||
|
let othersOnPreview = false;
|
||||||
let isStateful = false;
|
let isStateful = false;
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
@ -68,6 +70,8 @@ while (args.length > 0) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else if (arg === "--stable") {
|
} else if (arg === "--stable") {
|
||||||
othersOnStable = true;
|
othersOnStable = true;
|
||||||
|
} else if (arg === "--preview") {
|
||||||
|
othersOnPreview = true;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -172,6 +176,8 @@ setTimeout(() => {
|
||||||
let binaryPath = zedBinary;
|
let binaryPath = zedBinary;
|
||||||
if (i != 0 && othersOnStable) {
|
if (i != 0 && othersOnStable) {
|
||||||
binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
|
binaryPath = "/Applications/Zed.app/Contents/MacOS/zed";
|
||||||
|
} else if (i != 0 && othersOnPreview) {
|
||||||
|
binaryPath = "/Applications/Zed Preview.app/Contents/MacOS/zed";
|
||||||
}
|
}
|
||||||
spawn(binaryPath, i == 0 ? args : [], {
|
spawn(binaryPath, i == 0 ? args : [], {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue