793 lines · 24.0 KB · Rust
RawDownload
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 util::rel_path::RelPath;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 abs_path = self.traversal.snapshot().absolutize(&entry.path);         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), Bias::Left);            let summary = statuses.summary(&PathTarget::Successor(&repo_path), 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), 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 RelPath,    traversal: GitTraversal<'a>,} impl<'a> ChildEntriesGitIter<'a> {    pub fn new(        repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,        worktree_snapshot: &'a worktree::Snapshot,        parent_path: &'a RelPath,    ) -> 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, rel_path::rel_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")),            &[                ("x2.txt", StatusCode::Modified.index()),                ("z.txt", StatusCode::Added.index()),            ],        );        fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);        fs.set_status_for_repo(            Path::new(path!("/root/z/.git")),            &[("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, RelPath::unix("x").unwrap()),        );        let entries = traversal            .map(|entry| (entry.path.clone(), entry.git_summary))            .collect::<Vec<_>>();        pretty_assertions::assert_eq!(            entries,            [                (rel_path("x/x1.txt").into(), GitSummary::UNCHANGED),                (rel_path("x/x2.txt").into(), MODIFIED),                (rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT),                (rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED),                (rel_path("x/z.txt").into(), ADDED),                (rel_path("z/z1.txt").into(), GitSummary::UNCHANGED),                (rel_path("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")),            &[                ("x2.txt", StatusCode::Modified.index()),                ("z.txt", StatusCode::Added.index()),            ],        );        fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]);         fs.set_status_for_repo(            Path::new(path!("/root/z/.git")),            &[("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,            &[                ("x/y", GitSummary::CONFLICT),                ("x/y/y1.txt", GitSummary::CONFLICT),                ("x/y/y2.txt", GitSummary::UNCHANGED),            ],        );        check_git_statuses(            &repo_snapshots,            &worktree_snapshot,            &[                ("z", ADDED),                ("z/z1.txt", GitSummary::UNCHANGED),                ("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,            &[                ("x", MODIFIED + ADDED),                ("x/y", GitSummary::CONFLICT),                ("x/y/y1.txt", GitSummary::CONFLICT),            ],        );         // Sanity check everything around it        check_git_statuses(            &repo_snapshots,            &worktree_snapshot,            &[                ("x", MODIFIED + ADDED),                ("x/x1.txt", GitSummary::UNCHANGED),                ("x/x2.txt", MODIFIED),                ("x/y", GitSummary::CONFLICT),                ("x/y/y1.txt", GitSummary::CONFLICT),                ("x/y/y2.txt", GitSummary::UNCHANGED),                ("x/z.txt", ADDED),            ],        );         // Test the other fundamental case, transitioning from git repository to non-git repository        check_git_statuses(            &repo_snapshots,            &worktree_snapshot,            &[                ("", GitSummary::UNCHANGED),                ("x", MODIFIED + ADDED),                ("x/x1.txt", GitSummary::UNCHANGED),            ],        );         // And all together now        check_git_statuses(            &repo_snapshots,            &worktree_snapshot,            &[                ("", GitSummary::UNCHANGED),                ("x", MODIFIED + ADDED),                ("x/x1.txt", GitSummary::UNCHANGED),                ("x/x2.txt", MODIFIED),                ("x/y", GitSummary::CONFLICT),                ("x/y/y1.txt", GitSummary::CONFLICT),                ("x/y/y2.txt", GitSummary::UNCHANGED),                ("x/z.txt", ADDED),                ("z", ADDED),                ("z/z1.txt", GitSummary::UNCHANGED),                ("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": "",                    }                },

Lines 1–500 of 793.