Compare commits
26 Commits
ordered-mu
...
cole/faith
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e30de5b0 | ||
|
|
b70f1cf626 | ||
|
|
cfbbf856be | ||
|
|
2cc6212068 | ||
|
|
c74a75357a | ||
|
|
480c9509a8 | ||
|
|
7c9bff5d00 | ||
|
|
145b4d7c08 | ||
|
|
92ebd77c13 | ||
|
|
2e12209d7d | ||
|
|
1cae20a132 | ||
|
|
eb2bb603f3 | ||
|
|
f60d4bb2d0 | ||
|
|
effb35a807 | ||
|
|
f1f9f2fa34 | ||
|
|
9e4cd5a63b | ||
|
|
81c7d1face | ||
|
|
990c85fadc | ||
|
|
2723e16842 | ||
|
|
61aedf2009 | ||
|
|
1c4a2e6d6e | ||
|
|
27fa5d6c94 | ||
|
|
c08e3f3cc7 | ||
|
|
f2adfc99da | ||
|
|
b3441c84d8 | ||
|
|
a32fae4fc1 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -5113,6 +5113,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -5123,6 +5124,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
|
||||
@@ -682,6 +682,38 @@
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && !CommitEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "git_panel::Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"space": "git::ToggleStaged",
|
||||
"cmd-shift-space": "git::StageAll",
|
||||
"ctrl-shift-space": "git::UnstageAll",
|
||||
"alt-down": "git_panel::FocusEditor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && CommitEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-up": "git_panel::FocusChanges",
|
||||
"escape": "git_panel::FocusChanges",
|
||||
"cmd-enter": "git::CommitChanges",
|
||||
"cmd-alt-enter": "git::CommitAllChanges"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -13,7 +13,8 @@ use client::{User, RECEIVE_TIMEOUT};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::{channel::mpsc, StreamExt as _};
|
||||
use git::repository::GitFileStatus;
|
||||
|
||||
use git::status::{FileStatus, StatusCode, TrackedStatus};
|
||||
use gpui::{
|
||||
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
||||
TestAppContext, UpdateGlobal,
|
||||
@@ -2858,6 +2859,16 @@ async fn test_git_branch_name(
|
||||
});
|
||||
}
|
||||
|
||||
const FILE_MODIFIED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
|
||||
const FILE_ADDED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Added,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status_sync(
|
||||
executor: BackgroundExecutor,
|
||||
@@ -2892,8 +2903,8 @@ async fn test_git_status_sync(
|
||||
client_a.fs().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(Path::new(A_TXT), GitFileStatus::Added),
|
||||
(Path::new(B_TXT), GitFileStatus::Added),
|
||||
(Path::new(A_TXT), FILE_ADDED),
|
||||
(Path::new(B_TXT), FILE_ADDED),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2913,7 +2924,7 @@ async fn test_git_status_sync(
|
||||
#[track_caller]
|
||||
fn assert_status(
|
||||
file: &impl AsRef<Path>,
|
||||
status: Option<GitFileStatus>,
|
||||
status: Option<FileStatus>,
|
||||
project: &Project,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
@@ -2926,20 +2937,20 @@ async fn test_git_status_sync(
|
||||
}
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(A_TXT), Some(FILE_ADDED), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(FILE_ADDED), project, cx);
|
||||
});
|
||||
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
|
||||
assert_status(&Path::new(A_TXT), Some(FILE_ADDED), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(FILE_ADDED), project, cx);
|
||||
});
|
||||
|
||||
client_a.fs().set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(Path::new(B_TXT), GitFileStatus::Modified),
|
||||
(Path::new(A_TXT), FILE_MODIFIED),
|
||||
(Path::new(B_TXT), FILE_MODIFIED),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2949,33 +2960,13 @@ async fn test_git_status_sync(
|
||||
// Smoke test status reading
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
});
|
||||
|
||||
project_remote.read_with(cx_b, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
});
|
||||
|
||||
// And synchronization while joining
|
||||
@@ -2983,18 +2974,8 @@ async fn test_git_status_sync(
|
||||
executor.run_until_parked();
|
||||
|
||||
project_remote_c.read_with(cx_c, |project, cx| {
|
||||
assert_status(
|
||||
&Path::new(A_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(
|
||||
&Path::new(B_TXT),
|
||||
Some(GitFileStatus::Modified),
|
||||
project,
|
||||
cx,
|
||||
);
|
||||
assert_status(&Path::new(A_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
assert_status(&Path::new(B_TXT), Some(FILE_MODIFIED), project, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use call::ActiveCall;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use editor::Bias;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use git::repository::GitFileStatus;
|
||||
use git::status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode};
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use language::{
|
||||
range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
|
||||
@@ -29,6 +29,19 @@ use std::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const FILE_MODIFIED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
const FILE_ADDED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Added,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
const FILE_CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Updated,
|
||||
});
|
||||
|
||||
#[gpui::test(
|
||||
iterations = 100,
|
||||
on_failure = "crate::tests::save_randomized_test_plan"
|
||||
@@ -127,7 +140,7 @@ enum GitOperation {
|
||||
},
|
||||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
statuses: Vec<(PathBuf, FileStatus)>,
|
||||
git_operation: bool,
|
||||
},
|
||||
}
|
||||
@@ -1221,7 +1234,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
id,
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
assert_eq!(guest_snapshot.repositories().iter().collect::<Vec<_>>(), host_snapshot.repositories().iter().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
@@ -1462,9 +1475,9 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
(
|
||||
paths,
|
||||
match rng.gen_range(0..3_u32) {
|
||||
0 => GitFileStatus::Added,
|
||||
1 => GitFileStatus::Modified,
|
||||
2 => GitFileStatus::Conflict,
|
||||
0 => FILE_ADDED,
|
||||
1 => FILE_MODIFIED,
|
||||
2 => FILE_CONFLICT,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -9,10 +9,7 @@ use std::{
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use git::{
|
||||
diff::{BufferDiff, DiffHunk},
|
||||
repository::GitFileStatus,
|
||||
};
|
||||
use git::diff::{BufferDiff, DiffHunk};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
|
||||
@@ -54,7 +51,6 @@ struct ProjectDiffEditor {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Changes {
|
||||
_status: GitFileStatus,
|
||||
buffer: Model<Buffer>,
|
||||
hunks: Vec<DiffHunk>,
|
||||
}
|
||||
@@ -197,15 +193,15 @@ impl ProjectDiffEditor {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let applicable_entries = snapshot
|
||||
.repositories()
|
||||
.iter()
|
||||
.flat_map(|entry| {
|
||||
entry.status().map(|git_entry| {
|
||||
(git_entry.status, entry.join(git_entry.repo_path))
|
||||
})
|
||||
entry
|
||||
.status()
|
||||
.map(|git_entry| entry.join(git_entry.repo_path))
|
||||
})
|
||||
.filter_map(|(status, path)| {
|
||||
.filter_map(|path| {
|
||||
let id = snapshot.entry_for_path(&path)?.id;
|
||||
Some((
|
||||
status,
|
||||
id,
|
||||
ProjectPath {
|
||||
worktree_id: snapshot.id(),
|
||||
@@ -217,9 +213,9 @@ impl ProjectDiffEditor {
|
||||
Some(
|
||||
applicable_entries
|
||||
.into_iter()
|
||||
.map(|(status, entry_id, entry_path)| {
|
||||
.map(|(entry_id, entry_path)| {
|
||||
let open_task = project.open_path(entry_path.clone(), cx);
|
||||
(status, entry_id, entry_path, open_task)
|
||||
(entry_id, entry_path, open_task)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
@@ -233,15 +229,10 @@ impl ProjectDiffEditor {
|
||||
let mut new_entries = Vec::new();
|
||||
let mut buffers = HashMap::<
|
||||
ProjectEntryId,
|
||||
(
|
||||
GitFileStatus,
|
||||
text::BufferSnapshot,
|
||||
Model<Buffer>,
|
||||
BufferDiff,
|
||||
),
|
||||
(text::BufferSnapshot, Model<Buffer>, BufferDiff),
|
||||
>::default();
|
||||
let mut change_sets = Vec::new();
|
||||
for (status, entry_id, entry_path, open_task) in open_tasks {
|
||||
for (entry_id, entry_path, open_task) in open_tasks {
|
||||
let Some(buffer) = open_task
|
||||
.await
|
||||
.and_then(|(_, opened_model)| {
|
||||
@@ -271,7 +262,6 @@ impl ProjectDiffEditor {
|
||||
buffers.insert(
|
||||
entry_id,
|
||||
(
|
||||
status,
|
||||
buffer.read(cx).text_snapshot(),
|
||||
buffer,
|
||||
change_set.read(cx).diff_to_buffer.clone(),
|
||||
@@ -294,11 +284,10 @@ impl ProjectDiffEditor {
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();
|
||||
for (entry_id, (status, buffer_snapshot, buffer, buffer_diff)) in buffers {
|
||||
for (entry_id, (buffer_snapshot, buffer, buffer_diff)) in buffers {
|
||||
new_changes.insert(
|
||||
entry_id,
|
||||
Changes {
|
||||
_status: status,
|
||||
buffer,
|
||||
hunks: buffer_diff
|
||||
.hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
|
||||
@@ -1106,6 +1095,7 @@ impl Render for ProjectDiffEditor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use git::status::{StatusCode, TrackedStatus};
|
||||
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||
use project::buffer_store::BufferChangeSet;
|
||||
use serde_json::json;
|
||||
@@ -1223,7 +1213,14 @@ mod tests {
|
||||
});
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("file_a"), GitFileStatus::Modified)],
|
||||
&[(
|
||||
Path::new("file_a"),
|
||||
TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Unmodified,
|
||||
}
|
||||
.into(),
|
||||
)],
|
||||
);
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
|
||||
@@ -9,7 +9,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use file_icons::FileIcons;
|
||||
use futures::future::try_join_all;
|
||||
use git::repository::GitFileStatus;
|
||||
use git::status::GitSummary;
|
||||
use gpui::{
|
||||
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
|
||||
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
|
||||
@@ -27,8 +27,6 @@ use project::{
|
||||
};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
@@ -43,6 +41,7 @@ use theme::{Theme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, IconDecorationKind, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent};
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
@@ -621,10 +620,10 @@ impl Item for Editor {
|
||||
.worktree_for_id(path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.status_for_file(path.path);
|
||||
.status_for_file(path.path)?;
|
||||
|
||||
Some(entry_git_aware_label_color(
|
||||
git_status,
|
||||
git_status.summary(),
|
||||
entry.is_ignored,
|
||||
params.selected,
|
||||
))
|
||||
@@ -1560,20 +1559,17 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entry_git_aware_label_color(
|
||||
git_status: Option<GitFileStatus>,
|
||||
ignored: bool,
|
||||
selected: bool,
|
||||
) -> Color {
|
||||
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
|
||||
if ignored {
|
||||
Color::Ignored
|
||||
} else if git_status.conflict > 0 {
|
||||
Color::Conflict
|
||||
} else if git_status.modified > 0 {
|
||||
Color::Modified
|
||||
} else if git_status.added > 0 || git_status.untracked > 0 {
|
||||
Color::Created
|
||||
} else {
|
||||
match git_status {
|
||||
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
|
||||
Some(GitFileStatus::Modified) => Color::Modified,
|
||||
Some(GitFileStatus::Conflict) => Color::Conflict,
|
||||
Some(GitFileStatus::Deleted) | None => entry_label_color(selected),
|
||||
}
|
||||
entry_label_color(selected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ mod mac_watcher;
|
||||
pub mod fs_watcher;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use git::status::FileStatus;
|
||||
use git::GitHostingProviderRegistry;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
@@ -44,7 +45,7 @@ use util::ResultExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use git::repository::{FakeGitRepositoryState, GitFileStatus};
|
||||
use git::repository::FakeGitRepositoryState;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -1301,11 +1302,11 @@ impl FakeFs {
|
||||
pub fn set_status_for_repo_via_working_copy_change(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
statuses: &[(&Path, FileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, false, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
state.statuses.clear();
|
||||
state.statuses.extend(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, content)| ((**path).into(), *content)),
|
||||
@@ -1321,11 +1322,11 @@ impl FakeFs {
|
||||
pub fn set_status_for_repo_via_git_operation(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
statuses: &[(&Path, FileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
state.statuses.clear();
|
||||
state.statuses.extend(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, content)| ((**path).into(), *content)),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::status::FileStatus;
|
||||
use crate::GitHostingProviderRegistry;
|
||||
use crate::{blame::Blame, status::GitStatus};
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use git2::BranchType;
|
||||
use gpui::SharedString;
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Borrow;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use util::command::new_std_command;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||
@@ -51,6 +52,8 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
/// Returns the path to the repository, typically the `.git` folder.
|
||||
fn dot_git_dir(&self) -> PathBuf;
|
||||
|
||||
fn update_index(&self, stage: &[RepoPath], unstage: &[RepoPath]) -> Result<()>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn GitRepository {
|
||||
@@ -152,7 +155,7 @@ impl GitRepository for RealGitRepository {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => match e.code() {
|
||||
git2::ErrorCode::NotFound => Ok(false),
|
||||
_ => Err(anyhow::anyhow!(e)),
|
||||
_ => Err(anyhow!(e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -196,7 +199,7 @@ impl GitRepository for RealGitRepository {
|
||||
repo.set_head(
|
||||
revision
|
||||
.name()
|
||||
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
|
||||
.ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -228,6 +231,36 @@ impl GitRepository for RealGitRepository {
|
||||
self.hosting_provider_registry.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn update_index(&self, stage: &[RepoPath], unstage: &[RepoPath]) -> Result<()> {
|
||||
let working_directory = self
|
||||
.repository
|
||||
.lock()
|
||||
.workdir()
|
||||
.context("failed to read git work directory")?
|
||||
.to_path_buf();
|
||||
if !stage.is_empty() {
|
||||
let add = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["add", "--"])
|
||||
.args(stage.iter().map(|p| p.as_ref()))
|
||||
.status()?;
|
||||
if !add.success() {
|
||||
return Err(anyhow!("Failed to stage files: {add}"));
|
||||
}
|
||||
}
|
||||
if !unstage.is_empty() {
|
||||
let rm = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["restore", "--staged", "--"])
|
||||
.args(unstage.iter().map(|p| p.as_ref()))
|
||||
.status()?;
|
||||
if !rm.success() {
|
||||
return Err(anyhow!("Failed to unstage files: {rm}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -241,7 +274,7 @@ pub struct FakeGitRepositoryState {
|
||||
pub event_emitter: smol::channel::Sender<PathBuf>,
|
||||
pub index_contents: HashMap<PathBuf, String>,
|
||||
pub blames: HashMap<PathBuf, Blame>,
|
||||
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
|
||||
pub statuses: HashMap<RepoPath, FileStatus>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
}
|
||||
@@ -259,7 +292,7 @@ impl FakeGitRepositoryState {
|
||||
event_emitter,
|
||||
index_contents: Default::default(),
|
||||
blames: Default::default(),
|
||||
worktree_statuses: Default::default(),
|
||||
statuses: Default::default(),
|
||||
current_branch_name: Default::default(),
|
||||
branches: Default::default(),
|
||||
}
|
||||
@@ -296,7 +329,7 @@ impl GitRepository for FakeGitRepository {
|
||||
let state = self.state.lock();
|
||||
|
||||
let mut entries = state
|
||||
.worktree_statuses
|
||||
.statuses
|
||||
.iter()
|
||||
.filter_map(|(repo_path, status)| {
|
||||
if path_prefixes
|
||||
@@ -309,7 +342,7 @@ impl GitRepository for FakeGitRepository {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
||||
|
||||
Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
@@ -363,6 +396,10 @@ impl GitRepository for FakeGitRepository {
|
||||
.with_context(|| format!("failed to get blame for {:?}", path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn update_index(&self, _stage: &[RepoPath], _unstage: &[RepoPath]) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
|
||||
@@ -394,40 +431,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,
|
||||
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 static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
|
||||
LazyLock::new(|| RepoPath(Path::new("").into()));
|
||||
|
||||
@@ -453,6 +456,12 @@ impl RepoPath {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RepoPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.to_string_lossy().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for RepoPath {
|
||||
fn from(value: &Path) -> Self {
|
||||
RepoPath::new(value.into())
|
||||
|
||||
@@ -1,10 +1,286 @@
|
||||
use crate::repository::{GitFileStatus, RepoPath};
|
||||
use crate::repository::RepoPath;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, process::Stdio, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum FileStatus {
|
||||
Untracked,
|
||||
Ignored,
|
||||
Unmerged(UnmergedStatus),
|
||||
Tracked(TrackedStatus),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct UnmergedStatus {
|
||||
pub first_head: UnmergedStatusCode,
|
||||
pub second_head: UnmergedStatusCode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
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 {
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_conflicted(self) -> bool {
|
||||
match self {
|
||||
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 {
|
||||
let summary = if self.is_conflicted() {
|
||||
GitSummary {
|
||||
conflict: 1,
|
||||
..Default::default()
|
||||
}
|
||||
} else if self.is_untracked() {
|
||||
GitSummary {
|
||||
untracked: 1,
|
||||
..Default::default()
|
||||
}
|
||||
} else if self.is_modified() {
|
||||
GitSummary {
|
||||
modified: 1,
|
||||
..Default::default()
|
||||
}
|
||||
} else if self.is_created() {
|
||||
GitSummary {
|
||||
added: 1,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
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}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// TODO add a deleted count
|
||||
}
|
||||
|
||||
impl GitSummary {
|
||||
pub fn is_modified(self) -> bool {
|
||||
self.modified > 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(self, rhs: Self) -> Self {
|
||||
GitSummary {
|
||||
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 GitSummary {
|
||||
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 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitStatus {
|
||||
pub entries: Arc<[(RepoPath, GitFileStatus)]>,
|
||||
pub entries: Arc<[(RepoPath, FileStatus)]>,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
@@ -20,6 +296,7 @@ impl GitStatus {
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--untracked-files=all",
|
||||
"--no-renames",
|
||||
"-z",
|
||||
])
|
||||
.args(path_prefixes.iter().map(|path_prefix| {
|
||||
@@ -47,36 +324,22 @@ impl GitStatus {
|
||||
let mut entries = stdout
|
||||
.split('\0')
|
||||
.filter_map(|entry| {
|
||||
if entry.is_char_boundary(3) {
|
||||
let (status, path) = entry.split_at(3);
|
||||
let status = status.trim();
|
||||
Some((
|
||||
RepoPath(Path::new(path).into()),
|
||||
match status {
|
||||
"A" => GitFileStatus::Added,
|
||||
"M" => GitFileStatus::Modified,
|
||||
"D" => GitFileStatus::Deleted,
|
||||
"??" => GitFileStatus::Untracked,
|
||||
_ => return None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let sep = entry.get(2..3)?;
|
||||
if sep != " " {
|
||||
return None;
|
||||
};
|
||||
let path = &entry[3..];
|
||||
let status = entry[0..2].as_bytes().try_into().unwrap();
|
||||
let status = FileStatus::from_bytes(status).log_err()?;
|
||||
let path = RepoPath(Path::new(path).into());
|
||||
Some((path, status))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
|
||||
Ok(Self {
|
||||
entries: entries.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
|
||||
self.entries
|
||||
.binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path))
|
||||
.ok()
|
||||
.map(|index| self.entries[index].1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GitStatus {
|
||||
|
||||
@@ -17,6 +17,7 @@ anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
@@ -27,6 +28,7 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sum_tree.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
### General
|
||||
|
||||
- [x] Disable staging and committing actions for read-only projects
|
||||
|
||||
### List
|
||||
|
||||
- [x] Add uniform list
|
||||
- [x] Git status item
|
||||
- [ ] Directory item
|
||||
- [x] Scrollbar
|
||||
- [ ] Add indent size setting
|
||||
- [ ] Add tree settings
|
||||
|
||||
### List Items
|
||||
|
||||
- [x] Checkbox for staging
|
||||
- [x] Git status icon
|
||||
- [ ] Context menu
|
||||
- [ ] Discard Changes
|
||||
- ---
|
||||
- [ ] Ignore
|
||||
- [ ] Ignore directory
|
||||
- ---
|
||||
- [ ] Copy path
|
||||
- [ ] Copy relative path
|
||||
- ---
|
||||
- [ ] Reveal in Finder
|
||||
|
||||
### Commit Editor
|
||||
|
||||
- [ ] Add commit editor
|
||||
- [ ] Add commit message placeholder & add commit message to store
|
||||
- [ ] Add a way to get the current collaborators & automatically add them to the commit message as co-authors
|
||||
- [ ] Add action to clear commit message
|
||||
- [x] Swap commit button between "Commit" and "Commit All" based on modifier key
|
||||
|
||||
### Component Updates
|
||||
|
||||
- [ ] ChangedLineCount (new)
|
||||
- takes `lines_added: usize, lines_removed: usize`, returns a added/removed badge
|
||||
- [x] GitStatusIcon (new)
|
||||
- [ ] Checkbox
|
||||
- update checkbox design
|
||||
- [ ] ScrollIndicator
|
||||
- shows a gradient overlay when more content is available to be scrolled
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,56 +1,283 @@
|
||||
use ::settings::Settings;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{actions, AppContext, Context, Global, Hsla, Model};
|
||||
use collections::HashMap;
|
||||
use futures::{future::FusedFuture, select, FutureExt};
|
||||
use git::repository::{GitRepository, RepoPath};
|
||||
use git::status::FileStatus;
|
||||
use gpui::{actions, AppContext, Context, Global, Hsla, Model, ModelContext};
|
||||
use project::{Project, WorktreeId};
|
||||
use settings::GitPanelSettings;
|
||||
use std::sync::mpsc;
|
||||
use std::{
|
||||
pin::{pin, Pin},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use sum_tree::SumTree;
|
||||
use ui::{Color, Icon, IconName, IntoElement, SharedString};
|
||||
use worktree::RepositoryEntry;
|
||||
|
||||
pub mod git_panel;
|
||||
mod settings;
|
||||
|
||||
const GIT_TASK_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
actions!(
|
||||
git_ui,
|
||||
git,
|
||||
[
|
||||
StageFile,
|
||||
UnstageFile,
|
||||
ToggleStaged,
|
||||
// Revert actions are currently in the editor crate:
|
||||
// editor::RevertFile,
|
||||
// editor::RevertSelectedHunks
|
||||
StageAll,
|
||||
UnstageAll,
|
||||
RevertAll,
|
||||
CommitStagedChanges,
|
||||
CommitChanges,
|
||||
CommitAllChanges,
|
||||
ClearMessage
|
||||
ClearCommitMessage
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
GitPanelSettings::register(cx);
|
||||
let git_state = cx.new_model(|_cx| GitState::new());
|
||||
let git_state = cx.new_model(GitState::new);
|
||||
cx.set_global(GlobalGitState(git_state));
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum GitViewMode {
|
||||
#[default]
|
||||
List,
|
||||
Tree,
|
||||
}
|
||||
|
||||
struct GlobalGitState(Model<GitState>);
|
||||
|
||||
impl Global for GlobalGitState {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum StatusAction {
|
||||
Stage,
|
||||
Unstage,
|
||||
}
|
||||
|
||||
pub struct GitState {
|
||||
/// The current commit message being composed.
|
||||
commit_message: Option<SharedString>,
|
||||
|
||||
/// When a git repository is selected, this is used to track which repository's changes
|
||||
/// are currently being viewed or modified in the UI.
|
||||
active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
|
||||
|
||||
updater_tx: mpsc::Sender<(Arc<dyn GitRepository>, Vec<RepoPath>, StatusAction)>,
|
||||
|
||||
all_repositories: HashMap<WorktreeId, SumTree<RepositoryEntry>>,
|
||||
|
||||
list_view_mode: GitViewMode,
|
||||
}
|
||||
|
||||
impl GitState {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cx: &mut ModelContext<'_, Self>) -> Self {
|
||||
let (updater_tx, updater_rx) = mpsc::channel();
|
||||
cx.spawn(|_, cx| async move {
|
||||
// Long-running task to periodically update git indices based on messages from the panel.
|
||||
|
||||
// We read messages from the channel in batches that refer to the same repository.
|
||||
// When we read a message whose repository is different from the current batch's repository,
|
||||
// the batch is finished, and since we can't un-receive this last message, we save it
|
||||
// to begin the next batch.
|
||||
let mut leftover_message: Option<(
|
||||
Arc<dyn GitRepository>,
|
||||
Vec<RepoPath>,
|
||||
StatusAction,
|
||||
)> = None;
|
||||
let mut git_task = None;
|
||||
loop {
|
||||
let mut timer = cx.background_executor().timer(GIT_TASK_DEBOUNCE).fuse();
|
||||
let _result = {
|
||||
let mut task: Pin<&mut dyn FusedFuture<Output = anyhow::Result<()>>> =
|
||||
match git_task.as_mut() {
|
||||
Some(task) => pin!(task),
|
||||
// If no git task is running, just wait for the timeout.
|
||||
None => pin!(std::future::pending().fuse()),
|
||||
};
|
||||
select! {
|
||||
result = task => {
|
||||
// Task finished.
|
||||
git_task = None;
|
||||
Some(result)
|
||||
}
|
||||
_ = timer => None,
|
||||
}
|
||||
};
|
||||
|
||||
// TODO handle failure of the git command
|
||||
|
||||
if git_task.is_none() {
|
||||
// No git task running now; let's see if we should launch a new one.
|
||||
let mut to_stage = Vec::new();
|
||||
let mut to_unstage = Vec::new();
|
||||
let mut current_repo = leftover_message.as_ref().map(|msg| msg.0.clone());
|
||||
for (git_repo, paths, action) in leftover_message
|
||||
.take()
|
||||
.into_iter()
|
||||
.chain(updater_rx.try_iter())
|
||||
{
|
||||
if current_repo
|
||||
.as_ref()
|
||||
.map_or(false, |repo| !Arc::ptr_eq(repo, &git_repo))
|
||||
{
|
||||
// End of a batch, save this for the next one.
|
||||
leftover_message = Some((git_repo.clone(), paths, action));
|
||||
break;
|
||||
} else if current_repo.is_none() {
|
||||
// Start of a batch.
|
||||
current_repo = Some(git_repo);
|
||||
}
|
||||
|
||||
if action == StatusAction::Stage {
|
||||
to_stage.extend(paths);
|
||||
} else {
|
||||
to_unstage.extend(paths);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle the same path being staged and unstaged
|
||||
|
||||
if to_stage.is_empty() && to_unstage.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(git_repo) = current_repo {
|
||||
git_task = Some(
|
||||
cx.background_executor()
|
||||
.spawn(async move { git_repo.update_index(&to_stage, &to_unstage) })
|
||||
.fuse(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
GitState {
|
||||
commit_message: None,
|
||||
active_repository: None,
|
||||
updater_tx,
|
||||
list_view_mode: GitViewMode::default(),
|
||||
all_repositories: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_message(&mut self, message: Option<SharedString>) {
|
||||
self.commit_message = message;
|
||||
}
|
||||
|
||||
pub fn clear_message(&mut self) {
|
||||
self.commit_message = None;
|
||||
}
|
||||
|
||||
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
|
||||
cx.global::<GlobalGitState>().0.clone()
|
||||
}
|
||||
|
||||
pub fn activate_repository(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
active_repository: RepositoryEntry,
|
||||
git_repo: Arc<dyn GitRepository>,
|
||||
) {
|
||||
self.active_repository = Some((worktree_id, active_repository, git_repo));
|
||||
}
|
||||
|
||||
pub fn active_repository(
|
||||
&self,
|
||||
) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||
self.active_repository.as_ref()
|
||||
}
|
||||
|
||||
pub fn commit_message(&mut self, message: Option<SharedString>) {
|
||||
self.commit_message = message;
|
||||
}
|
||||
|
||||
pub fn clear_commit_message(&mut self) {
|
||||
self.commit_message = None;
|
||||
}
|
||||
|
||||
pub fn stage_entry(&mut self, repo_path: RepoPath) {
|
||||
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||
let _ = self
|
||||
.updater_tx
|
||||
.send((git_repo.clone(), vec![repo_path], StatusAction::Stage));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unstage_entry(&mut self, repo_path: RepoPath) {
|
||||
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||
let _ =
|
||||
self.updater_tx
|
||||
.send((git_repo.clone(), vec![repo_path], StatusAction::Unstage));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_entries(&mut self, entries: Vec<RepoPath>) {
|
||||
if let Some((_, _, git_repo)) = self.active_repository.as_ref() {
|
||||
let _ = self
|
||||
.updater_tx
|
||||
.send((git_repo.clone(), entries, StatusAction::Stage));
|
||||
}
|
||||
}
|
||||
|
||||
fn act_on_all(&mut self, action: StatusAction) {
|
||||
if let Some((_, active_repository, git_repo)) = self.active_repository.as_ref() {
|
||||
let _ = self.updater_tx.send((
|
||||
git_repo.clone(),
|
||||
active_repository
|
||||
.status()
|
||||
.map(|entry| entry.repo_path)
|
||||
.collect(),
|
||||
action,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage_all(&mut self) {
|
||||
self.act_on_all(StatusAction::Stage);
|
||||
}
|
||||
|
||||
pub fn unstage_all(&mut self) {
|
||||
self.act_on_all(StatusAction::Unstage);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn first_worktree_repository(
|
||||
project: &Model<Project>,
|
||||
worktree_id: WorktreeId,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.and_then(|worktree| {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let repo = snapshot.repositories().iter().next()?.clone();
|
||||
let git_repo = worktree
|
||||
.read(cx)
|
||||
.as_local()?
|
||||
.get_local_repo(&repo)?
|
||||
.repo()
|
||||
.clone();
|
||||
Some((repo, git_repo))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn first_repository_in_project(
|
||||
project: &Model<Project>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
||||
project.read(cx).worktrees(cx).next().and_then(|worktree| {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let repo = snapshot.repositories().iter().next()?.clone();
|
||||
let git_repo = worktree
|
||||
.read(cx)
|
||||
.as_local()?
|
||||
.get_local_repo(&repo)?
|
||||
.repo()
|
||||
.clone();
|
||||
Some((snapshot.id(), repo, git_repo))
|
||||
})
|
||||
}
|
||||
|
||||
const ADDED_COLOR: Hsla = Hsla {
|
||||
@@ -73,17 +300,15 @@ const REMOVED_COLOR: Hsla = Hsla {
|
||||
};
|
||||
|
||||
// TODO: Add updated status colors to theme
|
||||
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
|
||||
match status {
|
||||
GitFileStatus::Added | GitFileStatus::Untracked => {
|
||||
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
|
||||
}
|
||||
GitFileStatus::Modified => {
|
||||
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
|
||||
}
|
||||
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
|
||||
GitFileStatus::Deleted => {
|
||||
Icon::new(IconName::SquareMinus).color(Color::Custom(REMOVED_COLOR))
|
||||
}
|
||||
}
|
||||
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
|
||||
let (icon_name, color) = if status.is_conflicted() {
|
||||
(IconName::Warning, REMOVED_COLOR)
|
||||
} else if status.is_deleted() {
|
||||
(IconName::SquareMinus, REMOVED_COLOR)
|
||||
} else if status.is_modified() {
|
||||
(IconName::SquareDot, MODIFIED_COLOR)
|
||||
} else {
|
||||
(IconName::SquarePlus, ADDED_COLOR)
|
||||
};
|
||||
Icon::new(icon_name).color(Color::Custom(color))
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use editor::items::entry_git_aware_label_color;
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, EventEmitter,
|
||||
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ObjectFit, ParentElement,
|
||||
Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use persistence::IMAGE_VIEWER;
|
||||
use theme::Theme;
|
||||
use ui::prelude::*;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use project::{image_store::ImageItemEvent, ImageItem, Project, ProjectPath};
|
||||
use settings::Settings;
|
||||
use theme::Theme;
|
||||
use ui::prelude::*;
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams},
|
||||
@@ -101,7 +100,9 @@ impl Item for ImageView {
|
||||
let git_status = self
|
||||
.project
|
||||
.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
|
||||
.read(cx)
|
||||
|
||||
@@ -1982,7 +1982,7 @@ impl OutlinePanel {
|
||||
let is_expanded = !self
|
||||
.collapsed_entries
|
||||
.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 {
|
||||
FileIcons::get_chevron_icon(is_expanded, cx)
|
||||
.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 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 {
|
||||
FileIcons::get_icon(&entry.path, cx)
|
||||
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
||||
@@ -2114,7 +2114,7 @@ impl OutlinePanel {
|
||||
directory.entry.id,
|
||||
));
|
||||
let color = entry_git_aware_label_color(
|
||||
directory.entry.git_status,
|
||||
directory.entry.git_summary,
|
||||
directory.entry.is_ignored,
|
||||
is_active,
|
||||
);
|
||||
@@ -2210,7 +2210,8 @@ impl OutlinePanel {
|
||||
let git_status = folded_dir
|
||||
.entries
|
||||
.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 icon = if settings.folder_icons {
|
||||
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() {
|
||||
Some(entry) => {
|
||||
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,
|
||||
};
|
||||
let mut traversal = worktree
|
||||
|
||||
@@ -39,10 +39,7 @@ use futures::{
|
||||
pub use image_store::{ImageItem, ImageStore};
|
||||
use image_store::{ImageItemEvent, ImageStoreEvent};
|
||||
|
||||
use git::{
|
||||
blame::Blame,
|
||||
repository::{GitFileStatus, GitRepository},
|
||||
};
|
||||
use git::{blame::Blame, repository::GitRepository, status::FileStatus};
|
||||
use gpui::{
|
||||
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
|
||||
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
|
||||
@@ -1449,7 +1446,7 @@ impl Project {
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
cx: &AppContext,
|
||||
) -> Option<GitFileStatus> {
|
||||
) -> Option<FileStatus> {
|
||||
self.worktree_for_id(project_path.worktree_id, cx)
|
||||
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use editor::{
|
||||
Editor, EditorEvent, EditorSettings, ShowScrollbar,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use git::repository::GitFileStatus;
|
||||
use git::status::GitSummary;
|
||||
use gpui::{
|
||||
actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
|
||||
AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
|
||||
@@ -145,7 +145,7 @@ struct EntryDetails {
|
||||
is_cut: bool,
|
||||
filename_text_color: Color,
|
||||
diagnostic_severity: Option<DiagnosticSeverity>,
|
||||
git_status: Option<GitFileStatus>,
|
||||
git_status: GitSummary,
|
||||
is_private: bool,
|
||||
worktree_id: WorktreeId,
|
||||
canonical_path: Option<Box<Path>>,
|
||||
@@ -1584,9 +1584,7 @@ impl ProjectPanel {
|
||||
}
|
||||
}))
|
||||
&& entry.is_file()
|
||||
&& entry
|
||||
.git_status
|
||||
.is_some_and(|status| matches!(status, GitFileStatus::Modified))
|
||||
&& entry.git_summary.is_modified()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -1664,9 +1662,7 @@ impl ProjectPanel {
|
||||
}
|
||||
}))
|
||||
&& entry.is_file()
|
||||
&& entry
|
||||
.git_status
|
||||
.is_some_and(|status| matches!(status, GitFileStatus::Modified))
|
||||
&& entry.git_summary.is_modified()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -2417,7 +2413,7 @@ impl ProjectPanel {
|
||||
char_bag: entry.char_bag,
|
||||
is_fifo: entry.is_fifo,
|
||||
},
|
||||
git_status: entry.git_status,
|
||||
git_summary: entry.git_summary,
|
||||
});
|
||||
}
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
@@ -2815,7 +2811,9 @@ impl ProjectPanel {
|
||||
.collect()
|
||||
});
|
||||
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 icon = match entry.kind {
|
||||
EntryKind::File => {
|
||||
|
||||
@@ -362,7 +362,10 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||
.and_then(|path| {
|
||||
let project = self.project.read(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))
|
||||
})
|
||||
.map(|(entry, git_status)| {
|
||||
|
||||
@@ -487,6 +487,7 @@ impl RenderOnce for ButtonLike {
|
||||
self.base
|
||||
.h_flex()
|
||||
.id(self.id.clone())
|
||||
.font_ui(cx)
|
||||
.group("")
|
||||
.flex_none()
|
||||
.h(self.height.unwrap_or(self.size.rems().into()))
|
||||
|
||||
@@ -18,10 +18,12 @@ use futures::{
|
||||
FutureExt as _, Stream, StreamExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::GitHostingProviderRegistry;
|
||||
use git::{
|
||||
repository::{GitFileStatus, GitRepository, RepoPath},
|
||||
COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
||||
repository::{GitRepository, RepoPath},
|
||||
status::{
|
||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||
},
|
||||
GitHostingProviderRegistry, COOKIES, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE,
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
||||
@@ -193,8 +195,8 @@ pub struct RepositoryEntry {
|
||||
/// - my_sub_folder_1/project_root/changed_file_1
|
||||
/// - my_sub_folder_2/changed_file_2
|
||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||
pub(crate) work_directory_id: ProjectEntryId,
|
||||
pub(crate) work_directory: WorkDirectory,
|
||||
pub work_directory_id: ProjectEntryId,
|
||||
pub work_directory: WorkDirectory,
|
||||
pub(crate) branch: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
@@ -225,6 +227,12 @@ impl RepositoryEntry {
|
||||
self.statuses_by_path.iter().cloned()
|
||||
}
|
||||
|
||||
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
|
||||
self.statuses_by_path
|
||||
.get(&PathKey(path.0.clone()), &())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: self.work_directory_id.to_proto(),
|
||||
@@ -234,7 +242,7 @@ impl RepositoryEntry {
|
||||
.iter()
|
||||
.map(|entry| proto::StatusEntry {
|
||||
repo_path: entry.repo_path.to_string_lossy().to_string(),
|
||||
status: git_status_to_proto(entry.status),
|
||||
status: status_to_proto(entry.status),
|
||||
})
|
||||
.collect(),
|
||||
removed_statuses: Default::default(),
|
||||
@@ -2354,7 +2362,7 @@ impl Snapshot {
|
||||
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();
|
||||
self.repository_for_path(path).and_then(|repo| {
|
||||
let repo_path = repo.relativize(path).unwrap();
|
||||
@@ -2574,8 +2582,8 @@ impl Snapshot {
|
||||
.map(|repo| repo.status().collect())
|
||||
}
|
||||
|
||||
pub fn repositories(&self) -> impl Iterator<Item = &RepositoryEntry> {
|
||||
self.repositories.iter()
|
||||
pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
|
||||
&self.repositories
|
||||
}
|
||||
|
||||
/// Get the repository whose work directory corresponds to the given path.
|
||||
@@ -2609,7 +2617,7 @@ impl Snapshot {
|
||||
entries: impl 'a + Iterator<Item = &'a Entry>,
|
||||
) -> impl 'a + Iterator<Item = (&'a Entry, Option<&'a RepositoryEntry>)> {
|
||||
let mut containing_repos = Vec::<&RepositoryEntry>::new();
|
||||
let mut repositories = self.repositories().peekable();
|
||||
let mut repositories = self.repositories().iter().peekable();
|
||||
entries.map(move |entry| {
|
||||
while let Some(repository) = containing_repos.last() {
|
||||
if repository.directory_contains(&entry.path) {
|
||||
@@ -3626,14 +3634,18 @@ pub type UpdatedGitRepositoriesSet = Arc<[(Arc<Path>, GitRepositoryChange)]>;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StatusEntry {
|
||||
pub repo_path: RepoPath,
|
||||
pub status: GitFileStatus,
|
||||
pub status: FileStatus,
|
||||
}
|
||||
|
||||
impl StatusEntry {
|
||||
pub fn is_staged(&self) -> Option<bool> {
|
||||
self.status.is_staged()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> proto::StatusEntry {
|
||||
proto::StatusEntry {
|
||||
repo_path: self.repo_path.to_proto(),
|
||||
status: git_status_to_proto(self.status),
|
||||
status: status_to_proto(self.status),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3641,11 +3653,10 @@ impl StatusEntry {
|
||||
impl TryFrom<proto::StatusEntry> for StatusEntry {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
repo_path: RepoPath(Path::new(&value.repo_path).into()),
|
||||
status: git_status_from_proto(Some(value.status))
|
||||
.ok_or_else(|| anyhow!("Unable to parse status value {}", value.status))?,
|
||||
})
|
||||
let repo_path = RepoPath(Path::new(&value.repo_path).into());
|
||||
let status = status_from_proto(value.status)
|
||||
.ok_or_else(|| anyhow!("Unable to parse status value {}", value.status))?;
|
||||
Ok(Self { repo_path, status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3711,43 +3722,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 {
|
||||
type Summary = PathSummary<GitStatuses>;
|
||||
type Summary = PathSummary<GitSummary>;
|
||||
|
||||
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
||||
PathSummary {
|
||||
max_path: self.repo_path.0.clone(),
|
||||
item_summary: match self.status {
|
||||
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()
|
||||
},
|
||||
},
|
||||
item_summary: self.status.summary(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3760,69 +3741,12 @@ impl sum_tree::KeyedItem for StatusEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
|
||||
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 {
|
||||
impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
|
||||
fn zero(_cx: &()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &'a PathSummary<GitStatuses>, _: &()) {
|
||||
fn add_summary(&mut self, summary: &'a PathSummary<GitSummary>, _: &()) {
|
||||
*self += summary.item_summary
|
||||
}
|
||||
}
|
||||
@@ -4820,8 +4744,8 @@ impl BackgroundScanner {
|
||||
|
||||
for (repo_path, status) in &*status.entries {
|
||||
paths.remove_repo_path(repo_path);
|
||||
if cursor.seek_forward(&PathTarget::Path(&repo_path), Bias::Left, &()) {
|
||||
if cursor.item().unwrap().status == *status {
|
||||
if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left, &()) {
|
||||
if &cursor.item().unwrap().status == status {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -5672,14 +5596,14 @@ impl<'a> Default for TraversalProgress<'a> {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GitEntryRef<'a> {
|
||||
pub entry: &'a Entry,
|
||||
pub git_status: Option<GitFileStatus>,
|
||||
pub git_summary: GitSummary,
|
||||
}
|
||||
|
||||
impl<'a> GitEntryRef<'a> {
|
||||
pub fn to_owned(&self) -> GitEntry {
|
||||
GitEntry {
|
||||
entry: self.entry.clone(),
|
||||
git_status: self.git_status,
|
||||
git_summary: self.git_summary,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5701,14 +5625,14 @@ impl<'a> AsRef<Entry> for GitEntryRef<'a> {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GitEntry {
|
||||
pub entry: Entry,
|
||||
pub git_status: Option<GitFileStatus>,
|
||||
pub git_summary: GitSummary,
|
||||
}
|
||||
|
||||
impl GitEntry {
|
||||
pub fn to_ref(&self) -> GitEntryRef {
|
||||
GitEntryRef {
|
||||
entry: &self.entry,
|
||||
git_status: self.git_status,
|
||||
git_summary: self.git_summary,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5730,7 +5654,7 @@ impl AsRef<Entry> for GitEntry {
|
||||
/// Walks the worktree entries and their associated git statuses.
|
||||
pub struct GitTraversal<'a> {
|
||||
traversal: Traversal<'a>,
|
||||
current_entry_status: Option<GitFileStatus>,
|
||||
current_entry_summary: Option<GitSummary>,
|
||||
repo_location: Option<(
|
||||
&'a RepositoryEntry,
|
||||
Cursor<'a, StatusEntry, PathProgress<'a>>,
|
||||
@@ -5739,7 +5663,7 @@ pub struct GitTraversal<'a> {
|
||||
|
||||
impl<'a> GitTraversal<'a> {
|
||||
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 {
|
||||
return;
|
||||
@@ -5764,14 +5688,16 @@ impl<'a> GitTraversal<'a> {
|
||||
if entry.is_dir() {
|
||||
let mut statuses = statuses.clone();
|
||||
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, &());
|
||||
|
||||
self.current_entry_status = summary.to_status();
|
||||
self.current_entry_summary = Some(summary);
|
||||
} else if entry.is_file() {
|
||||
// For a file entry, park the cursor on the corresponding status
|
||||
if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
|
||||
self.current_entry_status = Some(statuses.item().unwrap().status);
|
||||
self.current_entry_summary = Some(statuses.item().unwrap().status.into());
|
||||
} else {
|
||||
self.current_entry_summary = Some(GitSummary::zero(&()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5807,10 +5733,9 @@ impl<'a> GitTraversal<'a> {
|
||||
}
|
||||
|
||||
pub fn entry(&self) -> Option<GitEntryRef<'a>> {
|
||||
Some(GitEntryRef {
|
||||
entry: self.traversal.cursor.item()?,
|
||||
git_status: self.current_entry_status,
|
||||
})
|
||||
let entry = self.traversal.cursor.item()?;
|
||||
let git_summary = self.current_entry_summary.unwrap_or_default();
|
||||
Some(GitEntryRef { entry, git_summary })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5861,7 +5786,7 @@ impl<'a> Traversal<'a> {
|
||||
pub fn with_git_statuses(self) -> GitTraversal<'a> {
|
||||
let mut this = GitTraversal {
|
||||
traversal: self,
|
||||
current_entry_status: None,
|
||||
current_entry_summary: None,
|
||||
repo_location: None,
|
||||
};
|
||||
this.synchronize_statuses(true);
|
||||
@@ -5980,10 +5905,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>
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -6136,25 +6061,46 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
|
||||
}
|
||||
}
|
||||
|
||||
fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
||||
git_status.and_then(|status| {
|
||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
||||
proto::GitStatus::Added => GitFileStatus::Added,
|
||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||
proto::GitStatus::Deleted => GitFileStatus::Deleted,
|
||||
})
|
||||
})
|
||||
// TODO transmit file statuses with full fidelity
|
||||
|
||||
fn status_from_proto(proto: i32) -> Option<FileStatus> {
|
||||
let proto = proto::GitStatus::from_i32(proto)?;
|
||||
let status = match proto {
|
||||
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(),
|
||||
};
|
||||
Some(status)
|
||||
}
|
||||
|
||||
fn git_status_to_proto(status: GitFileStatus) -> i32 {
|
||||
match status {
|
||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||
GitFileStatus::Deleted => proto::GitStatus::Deleted as i32,
|
||||
GitFileStatus::Untracked => proto::GitStatus::Added as i32, // TODO
|
||||
}
|
||||
fn status_to_proto(status: FileStatus) -> i32 {
|
||||
let proto = if status.is_conflicted() {
|
||||
proto::GitStatus::Conflict
|
||||
} else if status.is_deleted() {
|
||||
proto::GitStatus::Modified
|
||||
} else if status.is_modified() {
|
||||
proto::GitStatus::Modified
|
||||
} else {
|
||||
proto::GitStatus::Added
|
||||
};
|
||||
proto as i32
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -4,7 +4,12 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
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 parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
@@ -738,7 +743,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
|
||||
fs.set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/root/tree/.git"),
|
||||
&[(Path::new("tracked-dir/tracked-file2"), GitFileStatus::Added)],
|
||||
&[(Path::new("tracked-dir/tracked-file2"), FILE_ADDED)],
|
||||
);
|
||||
|
||||
fs.create_file(
|
||||
@@ -766,7 +771,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
assert_entry_git_state(
|
||||
tree,
|
||||
"tracked-dir/tracked-file2",
|
||||
Some(GitFileStatus::Added),
|
||||
Some(StatusCode::Added),
|
||||
false,
|
||||
);
|
||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
|
||||
@@ -822,14 +827,14 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
|
||||
|
||||
fs.set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("b.txt"), GitFileStatus::Added)],
|
||||
&[(Path::new("b.txt"), FILE_ADDED)],
|
||||
);
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
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 +1497,7 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||
// detected.
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[(Path::new("b/c.txt"), GitFileStatus::Modified)],
|
||||
&[(Path::new("b/c.txt"), FILE_MODIFIED)],
|
||||
);
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -1501,9 +1506,9 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Modified)),
|
||||
(Path::new("a.txt"), None),
|
||||
(Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new(""), MODIFIED),
|
||||
(Path::new("a.txt"), UNCHANGED),
|
||||
(Path::new("b/c.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -2142,6 +2147,24 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
const FILE_MODIFIED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Modified,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
const FILE_UNTRACKED: FileStatus = FileStatus::Untracked;
|
||||
const FILE_ADDED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Added,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
const FILE_CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
second_head: UnmergedStatusCode::Updated,
|
||||
});
|
||||
const FILE_DELETED: FileStatus = FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Deleted,
|
||||
index_status: StatusCode::Unmodified,
|
||||
});
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2179,15 +2202,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories().next().unwrap();
|
||||
let repo = tree.repositories().iter().next().unwrap();
|
||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project1/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
Some(FILE_MODIFIED),
|
||||
);
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project1/b")),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2200,15 +2223,15 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories().next().unwrap();
|
||||
let repo = tree.repositories().iter().next().unwrap();
|
||||
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project2/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
Some(FILE_MODIFIED),
|
||||
);
|
||||
assert_eq!(
|
||||
tree.status_for_file(Path::new("projects/project2/b")),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2380,18 +2403,18 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories().count(), 1);
|
||||
let repo_entry = snapshot.repositories().next().unwrap();
|
||||
assert_eq!(snapshot.repositories().iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories().iter().next().unwrap();
|
||||
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
|
||||
assert!(repo_entry.location_in_repo.is_none());
|
||||
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(B_TXT)),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2405,7 +2428,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(A_TXT)),
|
||||
Some(GitFileStatus::Modified)
|
||||
Some(FILE_MODIFIED),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2421,7 +2444,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(F_TXT)),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
|
||||
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
|
||||
@@ -2443,11 +2466,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(B_TXT)),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(E_TXT)),
|
||||
Some(GitFileStatus::Modified)
|
||||
Some(FILE_MODIFIED),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2482,7 +2505,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(
|
||||
snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2506,7 +2529,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
|
||||
.join(Path::new(renamed_dir_name))
|
||||
.join(RENAMED_FILE)
|
||||
),
|
||||
Some(GitFileStatus::Untracked)
|
||||
Some(FILE_UNTRACKED),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2554,16 +2577,16 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
let repo = snapshot.repositories().iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(entries.len(), 3);
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[0].status, FILE_MODIFIED);
|
||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
||||
assert_eq!(entries[1].status, FILE_UNTRACKED);
|
||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
|
||||
assert_eq!(entries[2].status, GitFileStatus::Deleted);
|
||||
assert_eq!(entries[2].status, FILE_DELETED);
|
||||
});
|
||||
|
||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||
@@ -2576,19 +2599,19 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repository = snapshot.repositories().next().unwrap();
|
||||
let repository = snapshot.repositories().iter().next().unwrap();
|
||||
let entries = repository.status().collect::<Vec<_>>();
|
||||
|
||||
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[0].status, FILE_MODIFIED);
|
||||
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
|
||||
assert_eq!(entries[1].status, GitFileStatus::Untracked);
|
||||
assert_eq!(entries[1].status, FILE_UNTRACKED);
|
||||
// Status updated
|
||||
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
|
||||
assert_eq!(entries[2].status, GitFileStatus::Modified);
|
||||
assert_eq!(entries[2].status, FILE_MODIFIED);
|
||||
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
|
||||
assert_eq!(entries[3].status, GitFileStatus::Deleted);
|
||||
assert_eq!(entries[3].status, FILE_DELETED);
|
||||
});
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
@@ -2609,7 +2632,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
let repo = snapshot.repositories().iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// Deleting an untracked entry, b.txt, should leave no status
|
||||
@@ -2621,7 +2644,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
&entries
|
||||
);
|
||||
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
|
||||
assert_eq!(entries[0].status, GitFileStatus::Deleted);
|
||||
assert_eq!(entries[0].status, FILE_DELETED);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2676,8 +2699,8 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||
// Ensure that the git status is loaded correctly
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories().count(), 1);
|
||||
let repo = snapshot.repositories().next().unwrap();
|
||||
assert_eq!(snapshot.repositories().iter().count(), 1);
|
||||
let repo = snapshot.repositories().iter().next().unwrap();
|
||||
// Path is blank because the working directory of
|
||||
// the git repository is located at the root of the project
|
||||
assert_eq!(repo.path.as_ref(), Path::new(""));
|
||||
@@ -2690,10 +2713,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("d/e.txt"),
|
||||
Some(GitFileStatus::Untracked)
|
||||
);
|
||||
assert_eq!(snapshot.status_for_file("d/e.txt"), Some(FILE_UNTRACKED));
|
||||
});
|
||||
|
||||
// Now we simulate FS events, but ONLY in the .git folder that's outside
|
||||
@@ -2707,7 +2727,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
|
||||
assert!(snapshot.repositories().next().is_some());
|
||||
assert!(snapshot.repositories().iter().next().is_some());
|
||||
|
||||
assert_eq!(snapshot.status_for_file("c.txt"), None);
|
||||
assert_eq!(snapshot.status_for_file("d/e.txt"), None);
|
||||
@@ -2744,17 +2764,17 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[
|
||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("z.txt"), GitFileStatus::Added),
|
||||
(Path::new("x2.txt"), FILE_MODIFIED),
|
||||
(Path::new("z.txt"), FILE_ADDED),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/y/.git"),
|
||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
||||
&[(Path::new("y1.txt"), FILE_CONFLICT)],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
||||
&[(Path::new("z2.txt"), FILE_ADDED)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
@@ -2780,25 +2800,25 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
|
||||
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
assert_eq!(entry.git_summary, UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
|
||||
assert_eq!(entry.git_summary, MODIFIED);
|
||||
let entry = traversal.next().unwrap();
|
||||
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, CONFLICT);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
assert_eq!(entry.git_summary, UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
||||
assert_eq!(entry.git_status, None);
|
||||
assert_eq!(entry.git_summary, UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
||||
assert_eq!(entry.git_status, Some(GitFileStatus::Added));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2834,9 +2854,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/.git"),
|
||||
&[
|
||||
(Path::new("a/b/c1.txt"), GitFileStatus::Added),
|
||||
(Path::new("a/d/e2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("g/h2.txt"), GitFileStatus::Conflict),
|
||||
(Path::new("a/b/c1.txt"), FILE_ADDED),
|
||||
(Path::new("a/d/e2.txt"), FILE_MODIFIED),
|
||||
(Path::new("g/h2.txt"), FILE_CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2859,52 +2879,52 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new(""), CONFLICT + MODIFIED + ADDED),
|
||||
(Path::new("g"), CONFLICT),
|
||||
(Path::new("g/h2.txt"), CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("a"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("a/b/c2.txt"), None),
|
||||
(Path::new("a/d"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("f"), None),
|
||||
(Path::new("f/no-status.txt"), None),
|
||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new(""), CONFLICT + ADDED + MODIFIED),
|
||||
(Path::new("a"), ADDED + MODIFIED),
|
||||
(Path::new("a/b"), ADDED),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), UNCHANGED),
|
||||
(Path::new("a/d"), MODIFIED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f"), UNCHANGED),
|
||||
(Path::new("f/no-status.txt"), UNCHANGED),
|
||||
(Path::new("g"), CONFLICT),
|
||||
(Path::new("g/h2.txt"), CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b"), Some(GitFileStatus::Added)),
|
||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("a/b/c2.txt"), None),
|
||||
(Path::new("a/d"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("a/d/e1.txt"), None),
|
||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("f"), None),
|
||||
(Path::new("f/no-status.txt"), None),
|
||||
(Path::new("g"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("a/b"), ADDED),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), UNCHANGED),
|
||||
(Path::new("a/d"), MODIFIED),
|
||||
(Path::new("a/d/e1.txt"), UNCHANGED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f"), UNCHANGED),
|
||||
(Path::new("f/no-status.txt"), UNCHANGED),
|
||||
(Path::new("g"), CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("a/b/c2.txt"), None),
|
||||
(Path::new("a/d/e1.txt"), None),
|
||||
(Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("f/no-status.txt"), None),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), UNCHANGED),
|
||||
(Path::new("a/d/e1.txt"), UNCHANGED),
|
||||
(Path::new("a/d/e2.txt"), MODIFIED),
|
||||
(Path::new("f/no-status.txt"), UNCHANGED),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -2937,18 +2957,18 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[(Path::new("x1.txt"), GitFileStatus::Added)],
|
||||
&[(Path::new("x1.txt"), FILE_ADDED)],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/y/.git"),
|
||||
&[
|
||||
(Path::new("y1.txt"), GitFileStatus::Conflict),
|
||||
(Path::new("y2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("y1.txt"), FILE_CONFLICT),
|
||||
(Path::new("y2.txt"), FILE_MODIFIED),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Modified)],
|
||||
&[(Path::new("z2.txt"), FILE_MODIFIED)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
@@ -2968,51 +2988,45 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("y"), CONFLICT + MODIFIED),
|
||||
(Path::new("y/y1.txt"), CONFLICT),
|
||||
(Path::new("y/y2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z"), MODIFIED),
|
||||
(Path::new("z/z2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
||||
],
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x/x2.txt"), None),
|
||||
(Path::new("y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x"), ADDED),
|
||||
(Path::new("x/x1.txt"), ADDED),
|
||||
(Path::new("x/x2.txt"), UNCHANGED),
|
||||
(Path::new("y"), CONFLICT + MODIFIED),
|
||||
(Path::new("y/y1.txt"), CONFLICT),
|
||||
(Path::new("y/y2.txt"), MODIFIED),
|
||||
(Path::new("z"), MODIFIED),
|
||||
(Path::new("z/z1.txt"), UNCHANGED),
|
||||
(Path::new("z/z2.txt"), MODIFIED),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -3047,18 +3061,18 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/.git"),
|
||||
&[
|
||||
(Path::new("x2.txt"), GitFileStatus::Modified),
|
||||
(Path::new("z.txt"), GitFileStatus::Added),
|
||||
(Path::new("x2.txt"), FILE_MODIFIED),
|
||||
(Path::new("z.txt"), FILE_ADDED),
|
||||
],
|
||||
);
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/x/y/.git"),
|
||||
&[(Path::new("y1.txt"), GitFileStatus::Conflict)],
|
||||
&[(Path::new("y1.txt"), FILE_CONFLICT)],
|
||||
);
|
||||
|
||||
fs.set_status_for_repo_via_git_operation(
|
||||
Path::new("/root/z/.git"),
|
||||
&[(Path::new("z2.txt"), GitFileStatus::Added)],
|
||||
&[(Path::new("z2.txt"), FILE_ADDED)],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
@@ -3082,17 +3096,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&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/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
(Path::new("x/y"), CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), UNCHANGED),
|
||||
],
|
||||
);
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z"), ADDED),
|
||||
(Path::new("z/z1.txt"), UNCHANGED),
|
||||
(Path::new("z/z2.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3100,9 +3114,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/y"), CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), CONFLICT),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3110,13 +3124,13 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), UNCHANGED),
|
||||
(Path::new("x/x2.txt"), MODIFIED),
|
||||
(Path::new("x/y"), CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), UNCHANGED),
|
||||
(Path::new("x/z.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3124,9 +3138,9 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), None),
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
(Path::new(""), UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), UNCHANGED),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3134,17 +3148,17 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&[
|
||||
(Path::new(""), None),
|
||||
(Path::new("x"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/x1.txt"), None),
|
||||
(Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
|
||||
(Path::new("x/y"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
|
||||
(Path::new("x/y/y2.txt"), None),
|
||||
(Path::new("x/z.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z"), Some(GitFileStatus::Added)),
|
||||
(Path::new("z/z1.txt"), None),
|
||||
(Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
|
||||
(Path::new(""), UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), UNCHANGED),
|
||||
(Path::new("x/x2.txt"), MODIFIED),
|
||||
(Path::new("x/y"), CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), CONFLICT),
|
||||
(Path::new("x/y/y2.txt"), UNCHANGED),
|
||||
(Path::new("x/z.txt"), ADDED),
|
||||
(Path::new("z"), ADDED),
|
||||
(Path::new("z/z1.txt"), UNCHANGED),
|
||||
(Path::new("z/z2.txt"), ADDED),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -3173,7 +3187,7 @@ async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[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
|
||||
.traverse_from_path(true, true, false, "".as_ref())
|
||||
.with_git_statuses();
|
||||
@@ -3182,13 +3196,41 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<G
|
||||
.map(|&(path, _)| {
|
||||
let git_entry = traversal
|
||||
.find(|git_entry| &*git_entry.path == path)
|
||||
.expect("Traversal has no entry for {path:?}");
|
||||
(path, git_entry.git_status)
|
||||
.expect(&format!("Traversal has no entry for {path:?}"));
|
||||
(path, git_entry.git_summary)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(found_statuses, expected_statuses);
|
||||
}
|
||||
|
||||
const ADDED: GitSummary = GitSummary {
|
||||
added: 1,
|
||||
modified: 0,
|
||||
conflict: 0,
|
||||
untracked: 0,
|
||||
};
|
||||
|
||||
const MODIFIED: GitSummary = GitSummary {
|
||||
added: 0,
|
||||
modified: 1,
|
||||
conflict: 0,
|
||||
untracked: 0,
|
||||
};
|
||||
|
||||
const CONFLICT: GitSummary = GitSummary {
|
||||
added: 0,
|
||||
modified: 0,
|
||||
conflict: 1,
|
||||
untracked: 0,
|
||||
};
|
||||
|
||||
const UNCHANGED: GitSummary = GitSummary {
|
||||
added: 0,
|
||||
modified: 0,
|
||||
conflict: 0,
|
||||
untracked: 0,
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
git2::Repository::init(path).expect("Failed to initialize git repository")
|
||||
@@ -3330,14 +3372,21 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
fn assert_entry_git_state(
|
||||
tree: &Worktree,
|
||||
path: &str,
|
||||
git_status: Option<GitFileStatus>,
|
||||
worktree_status: Option<StatusCode>,
|
||||
is_ignored: bool,
|
||||
) {
|
||||
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!(
|
||||
tree.status_for_file(Path::new(path)),
|
||||
git_status,
|
||||
"expected {path} to have git status: {git_status:?}"
|
||||
status, expected,
|
||||
"expected {path} to have git status: {expected:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.is_ignored, is_ignored,
|
||||
|
||||
Reference in New Issue
Block a user