This reduces the size of cursor by a usize when the summary does not
require a context making Cursor usages and constructions slightly more
efficient.
This change is a bit annoying though, as Rust has no means of
specializing, so this uses a `ContextlessSummary` trait with a blanket
impl while turning the `Context` into a GAT `Context<'a>`. This means
`Summary` implies are a bit more verbose now while contextless ones are
slimmer. It does come with the downside that the lifetime in the GAT is
always considered invariant, so some lifetime splitting occurred due to
that.
```
push/4096 time: [352.65 µs 360.87 µs 367.80 µs]
thrpt: [10.621 MiB/s 10.825 MiB/s 11.077 MiB/s]
change:
time: [-2.6633% -1.3640% -0.0561%] (p = 0.05 < 0.05)
thrpt: [+0.0561% +1.3828% +2.7361%]
Change within noise threshold.
Found 16 outliers among 100 measurements (16.00%)
7 (7.00%) low severe
3 (3.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe
push/65536 time: [1.2917 ms 1.2949 ms 1.2979 ms]
thrpt: [48.156 MiB/s 48.267 MiB/s 48.387 MiB/s]
change:
time: [+1.4428% +1.9844% +2.5299%] (p = 0.00 < 0.05)
thrpt: [-2.4675% -1.9458% -1.4223%]
Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
1 (1.00%) low severe
1 (1.00%) low mild
1 (1.00%) high severe
append/4096 time: [677.87 ns 678.87 ns 679.83 ns]
thrpt: [5.6112 GiB/s 5.6192 GiB/s 5.6274 GiB/s]
change:
time: [-0.8924% -0.5017% -0.1705%] (p = 0.00 < 0.05)
thrpt: [+0.1708% +0.5043% +0.9004%]
Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) low mild
1 (1.00%) high mild
append/65536 time: [9.3275 µs 9.3406 µs 9.3536 µs]
thrpt: [6.5253 GiB/s 6.5344 GiB/s 6.5435 GiB/s]
change:
time: [+0.5409% +0.7215% +0.9054%] (p = 0.00 < 0.05)
thrpt: [-0.8973% -0.7163% -0.5380%]
Change within noise threshold.
slice/4096 time: [27.673 µs 27.791 µs 27.907 µs]
thrpt: [139.97 MiB/s 140.56 MiB/s 141.16 MiB/s]
change:
time: [-1.1065% -0.6725% -0.2429%] (p = 0.00 < 0.05)
thrpt: [+0.2435% +0.6770% +1.1189%]
Change within noise threshold.
Found 5 outliers among 100 measurements (5.00%)
4 (4.00%) low mild
1 (1.00%) high mild
slice/65536 time: [507.55 µs 517.40 µs 535.60 µs]
thrpt: [116.69 MiB/s 120.80 MiB/s 123.14 MiB/s]
change:
time: [-1.3489% +0.0599% +2.2591%] (p = 0.96 > 0.05)
thrpt: [-2.2092% -0.0598% +1.3674%]
No change in performance detected.
Found 8 outliers among 100 measurements (8.00%)
5 (5.00%) low mild
2 (2.00%) high mild
1 (1.00%) high severe
bytes_in_range/4096 time: [3.3917 µs 3.4108 µs 3.4313 µs]
thrpt: [1.1117 GiB/s 1.1184 GiB/s 1.1247 GiB/s]
change:
time: [-5.3466% -4.7193% -4.1262%] (p = 0.00 < 0.05)
thrpt: [+4.3038% +4.9531% +5.6487%]
Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
1 (1.00%) low mild
5 (5.00%) high mild
bytes_in_range/65536 time: [88.175 µs 88.613 µs 89.111 µs]
thrpt: [701.37 MiB/s 705.31 MiB/s 708.82 MiB/s]
change:
time: [-0.6935% +0.3769% +1.4655%] (p = 0.50 > 0.05)
thrpt: [-1.4443% -0.3755% +0.6984%]
No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
2 (2.00%) high mild
chars/4096 time: [678.70 ns 680.38 ns 682.08 ns]
thrpt: [5.5927 GiB/s 5.6067 GiB/s 5.6206 GiB/s]
change:
time: [-0.6969% -0.2755% +0.1485%] (p = 0.20 > 0.05)
thrpt: [-0.1483% +0.2763% +0.7018%]
No change in performance detected.
Found 9 outliers among 100 measurements (9.00%)
5 (5.00%) low mild
4 (4.00%) high mild
chars/65536 time: [12.720 µs 12.775 µs 12.830 µs]
thrpt: [4.7573 GiB/s 4.7778 GiB/s 4.7983 GiB/s]
change:
time: [-0.6172% -0.1110% +0.4179%] (p = 0.68 > 0.05)
thrpt: [-0.4162% +0.1112% +0.6211%]
No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) low mild
1 (1.00%) high mild
clip_point/4096 time: [33.240 µs 33.310 µs 33.394 µs]
thrpt: [116.98 MiB/s 117.27 MiB/s 117.52 MiB/s]
change:
time: [-2.8892% -2.6305% -2.3438%] (p = 0.00 < 0.05)
thrpt: [+2.4000% +2.7015% +2.9751%]
Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
1 (1.00%) low mild
4 (4.00%) high mild
7 (7.00%) high severe
clip_point/65536 time: [1.6531 ms 1.6586 ms 1.6640 ms]
thrpt: [37.560 MiB/s 37.683 MiB/s 37.808 MiB/s]
change:
time: [-6.6381% -5.9395% -5.2680%] (p = 0.00 < 0.05)
thrpt: [+5.5610% +6.3146% +7.1100%]
Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
1 (1.00%) low mild
2 (2.00%) high mild
4 (4.00%) high severe
point_to_offset/4096 time: [11.586 µs 11.603 µs 11.621 µs]
thrpt: [336.15 MiB/s 336.67 MiB/s 337.16 MiB/s]
change:
time: [-14.289% -14.111% -13.939%] (p = 0.00 < 0.05)
thrpt: [+16.197% +16.429% +16.672%]
Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
3 (3.00%) low severe
5 (5.00%) low mild
4 (4.00%) high mild
point_to_offset/65536 time: [527.74 µs 532.08 µs 536.51 µs]
thrpt: [116.49 MiB/s 117.46 MiB/s 118.43 MiB/s]
change:
time: [-6.7825% -4.6235% -2.3533%] (p = 0.00 < 0.05)
thrpt: [+2.4100% +4.8477% +7.2760%]
Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
4 (4.00%) high mild
4 (4.00%) high severe
cursor/4096 time: [16.154 µs 16.192 µs 16.232 µs]
thrpt: [240.66 MiB/s 241.24 MiB/s 241.81 MiB/s]
change:
time: [-3.2536% -2.9145% -2.5526%] (p = 0.00 < 0.05)
thrpt: [+2.6194% +3.0019% +3.3630%]
Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) low mild
2 (2.00%) high mild
2 (2.00%) high severe
cursor/65536 time: [509.60 µs 511.24 µs 512.93 µs]
thrpt: [121.85 MiB/s 122.25 MiB/s 122.65 MiB/s]
change:
time: [-7.3677% -6.6017% -5.7840%] (p = 0.00 < 0.05)
thrpt: [+6.1391% +7.0683% +7.9537%]
Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
3 (3.00%) high mild
3 (3.00%) high severe
```
Release Notes:
- N/A *or* Added/Fixed/Improved ...
809 lines
25 KiB
Rust
809 lines
25 KiB
Rust
use collections::HashMap;
|
|
use git::{repository::RepoPath, status::GitSummary};
|
|
use std::{collections::BTreeMap, ops::Deref, path::Path};
|
|
use sum_tree::Cursor;
|
|
use text::Bias;
|
|
use worktree::{Entry, PathProgress, PathTarget, Traversal};
|
|
|
|
use super::{RepositoryId, RepositorySnapshot, StatusEntry};
|
|
|
|
/// Walks the worktree entries and their associated git statuses.
|
|
pub struct GitTraversal<'a> {
|
|
traversal: Traversal<'a>,
|
|
current_entry_summary: Option<GitSummary>,
|
|
repo_root_to_snapshot: BTreeMap<&'a Path, &'a RepositorySnapshot>,
|
|
repo_location: Option<(
|
|
RepositoryId,
|
|
Cursor<'a, 'static, StatusEntry, PathProgress<'a>>,
|
|
)>,
|
|
}
|
|
|
|
impl<'a> GitTraversal<'a> {
|
|
pub fn new(
|
|
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
|
|
traversal: Traversal<'a>,
|
|
) -> GitTraversal<'a> {
|
|
let repo_root_to_snapshot = repo_snapshots
|
|
.values()
|
|
.map(|snapshot| (&*snapshot.work_directory_abs_path, snapshot))
|
|
.collect();
|
|
let mut this = GitTraversal {
|
|
traversal,
|
|
current_entry_summary: None,
|
|
repo_location: None,
|
|
repo_root_to_snapshot,
|
|
};
|
|
this.synchronize_statuses(true);
|
|
this
|
|
}
|
|
|
|
fn repo_root_for_path(&self, path: &Path) -> Option<(&'a RepositorySnapshot, RepoPath)> {
|
|
// We might need to perform a range search multiple times, as there may be a nested repository inbetween
|
|
// the target and our path. E.g:
|
|
// /our_root_repo/
|
|
// .git/
|
|
// other_repo/
|
|
// .git/
|
|
// our_query.txt
|
|
let query = path.ancestors();
|
|
for query in query {
|
|
let (_, snapshot) = self
|
|
.repo_root_to_snapshot
|
|
.range(Path::new("")..=query)
|
|
.last()?;
|
|
|
|
let stripped = snapshot
|
|
.abs_path_to_repo_path(path)
|
|
.map(|repo_path| (*snapshot, repo_path));
|
|
if stripped.is_some() {
|
|
return stripped;
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn synchronize_statuses(&mut self, reset: bool) {
|
|
self.current_entry_summary = None;
|
|
|
|
let Some(entry) = self.entry() else {
|
|
return;
|
|
};
|
|
|
|
let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
|
|
self.repo_location = None;
|
|
return;
|
|
};
|
|
|
|
let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else {
|
|
self.repo_location = None;
|
|
return;
|
|
};
|
|
|
|
// Update our state if we changed repositories.
|
|
if reset
|
|
|| self
|
|
.repo_location
|
|
.as_ref()
|
|
.map(|(prev_repo_id, _)| *prev_repo_id)
|
|
!= Some(repo.id)
|
|
{
|
|
self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::<PathProgress>(())));
|
|
}
|
|
|
|
let Some((_, statuses)) = &mut self.repo_location else {
|
|
return;
|
|
};
|
|
|
|
if entry.is_dir() {
|
|
let mut statuses = statuses.clone();
|
|
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left);
|
|
let summary = statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left);
|
|
|
|
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) {
|
|
// TODO: Investigate statuses.item() being None here.
|
|
self.current_entry_summary = statuses.item().map(|item| item.status.into());
|
|
} else {
|
|
self.current_entry_summary = Some(GitSummary::UNCHANGED);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn advance(&mut self) -> bool {
|
|
let found = self.traversal.advance_by(1);
|
|
self.synchronize_statuses(false);
|
|
found
|
|
}
|
|
|
|
pub fn advance_to_sibling(&mut self) -> bool {
|
|
let found = self.traversal.advance_to_sibling();
|
|
self.synchronize_statuses(false);
|
|
found
|
|
}
|
|
|
|
pub fn back_to_parent(&mut self) -> bool {
|
|
let found = self.traversal.back_to_parent();
|
|
self.synchronize_statuses(true);
|
|
found
|
|
}
|
|
|
|
pub fn start_offset(&self) -> usize {
|
|
self.traversal.start_offset()
|
|
}
|
|
|
|
pub fn end_offset(&self) -> usize {
|
|
self.traversal.end_offset()
|
|
}
|
|
|
|
pub fn entry(&self) -> Option<GitEntryRef<'a>> {
|
|
let entry = self.traversal.entry()?;
|
|
let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED);
|
|
Some(GitEntryRef { entry, git_summary })
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for GitTraversal<'a> {
|
|
type Item = GitEntryRef<'a>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if let Some(item) = self.entry() {
|
|
self.advance();
|
|
Some(item)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ChildEntriesGitIter<'a> {
|
|
parent_path: &'a Path,
|
|
traversal: GitTraversal<'a>,
|
|
}
|
|
|
|
impl<'a> ChildEntriesGitIter<'a> {
|
|
pub fn new(
|
|
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
|
|
worktree_snapshot: &'a worktree::Snapshot,
|
|
parent_path: &'a Path,
|
|
) -> Self {
|
|
let mut traversal = GitTraversal::new(
|
|
repo_snapshots,
|
|
worktree_snapshot.traverse_from_path(true, true, true, parent_path),
|
|
);
|
|
traversal.advance();
|
|
ChildEntriesGitIter {
|
|
parent_path,
|
|
traversal,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for ChildEntriesGitIter<'a> {
|
|
type Item = GitEntryRef<'a>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if let Some(item) = self.traversal.entry()
|
|
&& item.path.starts_with(self.parent_path)
|
|
{
|
|
self.traversal.advance_to_sibling();
|
|
return Some(item);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct GitEntryRef<'a> {
|
|
pub entry: &'a Entry,
|
|
pub git_summary: GitSummary,
|
|
}
|
|
|
|
impl GitEntryRef<'_> {
|
|
pub fn to_owned(self) -> GitEntry {
|
|
GitEntry {
|
|
entry: self.entry.clone(),
|
|
git_summary: self.git_summary,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Deref for GitEntryRef<'_> {
|
|
type Target = Entry;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.entry
|
|
}
|
|
}
|
|
|
|
impl AsRef<Entry> for GitEntryRef<'_> {
|
|
fn as_ref(&self) -> &Entry {
|
|
self.entry
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct GitEntry {
|
|
pub entry: Entry,
|
|
pub git_summary: GitSummary,
|
|
}
|
|
|
|
impl GitEntry {
|
|
pub fn to_ref(&self) -> GitEntryRef<'_> {
|
|
GitEntryRef {
|
|
entry: &self.entry,
|
|
git_summary: self.git_summary,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Deref for GitEntry {
|
|
type Target = Entry;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.entry
|
|
}
|
|
}
|
|
|
|
impl AsRef<Entry> for GitEntry {
|
|
fn as_ref(&self) -> &Entry {
|
|
&self.entry
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::time::Duration;
|
|
|
|
use crate::Project;
|
|
|
|
use super::*;
|
|
use fs::FakeFs;
|
|
use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode};
|
|
use gpui::TestAppContext;
|
|
use serde_json::json;
|
|
use settings::SettingsStore;
|
|
use util::path;
|
|
|
|
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
|
first_head: UnmergedStatusCode::Updated,
|
|
second_head: UnmergedStatusCode::Updated,
|
|
});
|
|
const ADDED: GitSummary = GitSummary {
|
|
index: TrackedSummary::ADDED,
|
|
count: 1,
|
|
..GitSummary::UNCHANGED
|
|
};
|
|
const MODIFIED: GitSummary = GitSummary {
|
|
index: TrackedSummary::MODIFIED,
|
|
count: 1,
|
|
..GitSummary::UNCHANGED
|
|
};
|
|
|
|
#[gpui::test]
|
|
async fn test_git_traversal_with_one_repo(cx: &mut TestAppContext) {
|
|
init_test(cx);
|
|
let fs = FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"x": {
|
|
".git": {},
|
|
"x1.txt": "foo",
|
|
"x2.txt": "bar",
|
|
"y": {
|
|
".git": {},
|
|
"y1.txt": "baz",
|
|
"y2.txt": "qux"
|
|
},
|
|
"z.txt": "sneaky..."
|
|
},
|
|
"z": {
|
|
".git": {},
|
|
"z1.txt": "quux",
|
|
"z2.txt": "quuux"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/x/.git")),
|
|
&[
|
|
(Path::new("x2.txt"), StatusCode::Modified.index()),
|
|
(Path::new("z.txt"), StatusCode::Added.index()),
|
|
],
|
|
);
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/x/y/.git")),
|
|
&[(Path::new("y1.txt"), CONFLICT)],
|
|
);
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/z/.git")),
|
|
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
|
);
|
|
|
|
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
|
(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
|
)
|
|
});
|
|
|
|
let traversal = GitTraversal::new(
|
|
&repo_snapshots,
|
|
worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
|
|
);
|
|
let entries = traversal
|
|
.map(|entry| (entry.path.clone(), entry.git_summary))
|
|
.collect::<Vec<_>>();
|
|
pretty_assertions::assert_eq!(
|
|
entries,
|
|
[
|
|
(Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
|
|
(Path::new("x/x2.txt").into(), MODIFIED),
|
|
(Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
|
|
(Path::new("x/z.txt").into(), ADDED),
|
|
(Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
|
|
(Path::new("z/z2.txt").into(), ADDED),
|
|
]
|
|
)
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_git_traversal_with_nested_repos(cx: &mut TestAppContext) {
|
|
init_test(cx);
|
|
let fs = FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"x": {
|
|
".git": {},
|
|
"x1.txt": "foo",
|
|
"x2.txt": "bar",
|
|
"y": {
|
|
".git": {},
|
|
"y1.txt": "baz",
|
|
"y2.txt": "qux"
|
|
},
|
|
"z.txt": "sneaky..."
|
|
},
|
|
"z": {
|
|
".git": {},
|
|
"z1.txt": "quux",
|
|
"z2.txt": "quuux"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/x/.git")),
|
|
&[
|
|
(Path::new("x2.txt"), StatusCode::Modified.index()),
|
|
(Path::new("z.txt"), StatusCode::Added.index()),
|
|
],
|
|
);
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/x/y/.git")),
|
|
&[(Path::new("y1.txt"), CONFLICT)],
|
|
);
|
|
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/z/.git")),
|
|
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
|
);
|
|
|
|
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
|
(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
|
)
|
|
});
|
|
|
|
// Sanity check the propagation for x/y and z
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
|
],
|
|
);
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("z"), ADDED),
|
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("z/z2.txt"), ADDED),
|
|
],
|
|
);
|
|
|
|
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("x"), MODIFIED + ADDED),
|
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
|
],
|
|
);
|
|
|
|
// Sanity check everything around it
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("x"), MODIFIED + ADDED),
|
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("x/x2.txt"), MODIFIED),
|
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("x/z.txt"), ADDED),
|
|
],
|
|
);
|
|
|
|
// Test the other fundamental case, transitioning from git repository to non-git repository
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new(""), GitSummary::UNCHANGED),
|
|
(Path::new("x"), MODIFIED + ADDED),
|
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
|
],
|
|
);
|
|
|
|
// And all together now
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new(""), GitSummary::UNCHANGED),
|
|
(Path::new("x"), MODIFIED + ADDED),
|
|
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("x/x2.txt"), MODIFIED),
|
|
(Path::new("x/y"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
|
(Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("x/z.txt"), ADDED),
|
|
(Path::new("z"), ADDED),
|
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("z/z2.txt"), ADDED),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_git_traversal_simple(cx: &mut TestAppContext) {
|
|
init_test(cx);
|
|
let fs = FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
".git": {},
|
|
"a": {
|
|
"b": {
|
|
"c1.txt": "",
|
|
"c2.txt": "",
|
|
},
|
|
"d": {
|
|
"e1.txt": "",
|
|
"e2.txt": "",
|
|
"e3.txt": "",
|
|
}
|
|
},
|
|
"f": {
|
|
"no-status.txt": ""
|
|
},
|
|
"g": {
|
|
"h1.txt": "",
|
|
"h2.txt": ""
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/.git")),
|
|
&[
|
|
(Path::new("a/b/c1.txt"), StatusCode::Added.index()),
|
|
(Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
|
|
(Path::new("g/h2.txt"), CONFLICT),
|
|
],
|
|
);
|
|
|
|
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
|
(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
|
)
|
|
});
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
|
|
(Path::new("g"), GitSummary::CONFLICT),
|
|
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
|
],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new(""), GitSummary::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"), GitSummary::UNCHANGED),
|
|
(Path::new("a/d"), MODIFIED),
|
|
(Path::new("a/d/e2.txt"), MODIFIED),
|
|
(Path::new("f"), GitSummary::UNCHANGED),
|
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("g"), GitSummary::CONFLICT),
|
|
(Path::new("g/h2.txt"), GitSummary::CONFLICT),
|
|
],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("a/b"), ADDED),
|
|
(Path::new("a/b/c1.txt"), ADDED),
|
|
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("a/d"), MODIFIED),
|
|
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("a/d/e2.txt"), MODIFIED),
|
|
(Path::new("f"), GitSummary::UNCHANGED),
|
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("g"), GitSummary::CONFLICT),
|
|
],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("a/b/c1.txt"), ADDED),
|
|
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("a/d/e2.txt"), MODIFIED),
|
|
(Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_git_traversal_with_repos_under_project(cx: &mut TestAppContext) {
|
|
init_test(cx);
|
|
let fs = FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"x": {
|
|
".git": {},
|
|
"x1.txt": "foo",
|
|
"x2.txt": "bar"
|
|
},
|
|
"y": {
|
|
".git": {},
|
|
"y1.txt": "baz",
|
|
"y2.txt": "qux"
|
|
},
|
|
"z": {
|
|
".git": {},
|
|
"z1.txt": "quux",
|
|
"z2.txt": "quuux"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/x/.git")),
|
|
&[(Path::new("x1.txt"), StatusCode::Added.index())],
|
|
);
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/y/.git")),
|
|
&[
|
|
(Path::new("y1.txt"), CONFLICT),
|
|
(Path::new("y2.txt"), StatusCode::Modified.index()),
|
|
],
|
|
);
|
|
fs.set_status_for_repo(
|
|
Path::new(path!("/root/z/.git")),
|
|
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
|
|
);
|
|
|
|
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
|
(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
|
)
|
|
});
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
|
|
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
|
(Path::new("y/y2.txt"), MODIFIED),
|
|
],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("z"), MODIFIED),
|
|
(Path::new("z/z2.txt"), MODIFIED),
|
|
],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
|
);
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new("x"), ADDED),
|
|
(Path::new("x/x1.txt"), ADDED),
|
|
(Path::new("x/x2.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
|
|
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
|
(Path::new("y/y2.txt"), MODIFIED),
|
|
(Path::new("z"), MODIFIED),
|
|
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("z/z2.txt"), MODIFIED),
|
|
],
|
|
);
|
|
}
|
|
|
|
fn init_test(cx: &mut gpui::TestAppContext) {
|
|
zlog::init_test();
|
|
|
|
cx.update(|cx| {
|
|
let settings_store = SettingsStore::test(cx);
|
|
cx.set_global(settings_store);
|
|
Project::init_settings(cx);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
|
|
init_test(cx);
|
|
|
|
// Create a worktree with a git directory.
|
|
let fs = FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
".git": {},
|
|
"a.txt": "",
|
|
"b": {
|
|
"c.txt": "",
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
fs.set_head_and_index_for_repo(
|
|
path!("/root/.git").as_ref(),
|
|
&[("a.txt".into(), "".into()), ("b/c.txt".into(), "".into())],
|
|
);
|
|
cx.run_until_parked();
|
|
|
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| {
|
|
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
|
(
|
|
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
|
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
|
)
|
|
});
|
|
|
|
// Regression test: after the directory is scanned, touch the git repo's
|
|
// working directory, bumping its mtime. That directory keeps its project
|
|
// entry id after the directories are re-scanned.
|
|
fs.touch_path(path!("/root")).await;
|
|
cx.executor().run_until_parked();
|
|
|
|
let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| {
|
|
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
|
(
|
|
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
|
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
|
)
|
|
});
|
|
assert_eq!(new_entry_ids, old_entry_ids);
|
|
assert_ne!(new_mtimes, old_mtimes);
|
|
|
|
// Regression test: changes to the git repository should still be
|
|
// detected.
|
|
fs.set_head_for_repo(
|
|
path!("/root/.git").as_ref(),
|
|
&[
|
|
("a.txt".into(), "".into()),
|
|
("b/c.txt".into(), "something-else".into()),
|
|
],
|
|
"deadbeef",
|
|
);
|
|
cx.executor().run_until_parked();
|
|
cx.executor().advance_clock(Duration::from_secs(1));
|
|
|
|
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
|
(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
|
)
|
|
});
|
|
|
|
check_git_statuses(
|
|
&repo_snapshots,
|
|
&worktree_snapshot,
|
|
&[
|
|
(Path::new(""), MODIFIED),
|
|
(Path::new("a.txt"), GitSummary::UNCHANGED),
|
|
(Path::new("b/c.txt"), MODIFIED),
|
|
],
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn check_git_statuses(
|
|
repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
|
|
worktree_snapshot: &worktree::Snapshot,
|
|
expected_statuses: &[(&Path, GitSummary)],
|
|
) {
|
|
let mut traversal = GitTraversal::new(
|
|
repo_snapshots,
|
|
worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
|
|
);
|
|
let found_statuses = expected_statuses
|
|
.iter()
|
|
.map(|&(path, _)| {
|
|
let git_entry = traversal
|
|
.find(|git_entry| &*git_entry.path == path)
|
|
.unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
|
|
(path, git_entry.git_summary)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
pretty_assertions::assert_eq!(found_statuses, expected_statuses);
|
|
}
|
|
}
|