479 lines · 18.2 KB · Rust
RawDownload
use std::{    borrow::Cow,    path::{Path, PathBuf},}; use bstr::{BStr, BString, ByteSlice}; use crate::{    Entry, EntryRef, entry,    entry::PathspecMatch,    walk::{Context, Error, ForDeletionMode, Options},}; /// Classify the `worktree_relative_root` path and return the first `PathKind` that indicates that/// it isn't a directory, leaving `buf` with the path matching the returned `PathKind`,/// which is at most equal to `worktree_relative_root`.pub fn root(    worktree_root: &Path,    buf: &mut BString,    worktree_relative_root: &Path,    options: Options<'_>,    ctx: &mut Context<'_>,) -> Result<(Outcome, bool), Error> {    buf.clear();    let mut last_length = None;    let mut path_buf = worktree_root.to_owned();    // These initial values kick in if worktree_relative_root.is_empty();    let file_kind = path_buf.symlink_metadata().ok().map(|m| m.file_type().into());    let mut out = path(&mut path_buf, buf, 0, file_kind, || None, options, ctx)?;    let worktree_root_is_repository = out        .disk_kind        .is_some_and(|kind| matches!(kind, entry::Kind::Repository));     for component in worktree_relative_root.components() {        if last_length.is_some() {            buf.push(b'/');        }        path_buf.push(component);        buf.extend_from_slice(gix_path::os_str_into_bstr(component.as_os_str()).expect("no illformed UTF8"));        let file_kind = path_buf.symlink_metadata().ok().map(|m| m.file_type().into());         out = path(            &mut path_buf,            buf,            last_length.map(|l| l + 1 /* slash */).unwrap_or_default(),            file_kind,            || None,            options,            ctx,        )?;        if !out.status.can_recurse(            out.disk_kind,            out.pathspec_match,            options.for_deletion,            worktree_root_is_repository,        ) {            break;        }        last_length = Some(buf.len());    }    Ok((out, worktree_root_is_repository))}/// The product of [`path()`] calls.#[derive(Debug, Copy, Clone)]pub struct Outcome {    /// The computed status of an entry. It can be seen as aggregate of things we know about an entry.    pub status: entry::Status,    /// An additional property.    pub property: Option<entry::Property>,    /// What the entry is on disk, or `None` if we aborted the classification early or an IO-error occurred    /// when querying the disk.    ///    /// Note that the index is used to avoid disk access provided its entries are marked uptodate    /// (possibly by a prior call to update the status).    pub disk_kind: Option<entry::Kind>,    /// What the entry looks like in the index, or `None` if we aborted early.    pub index_kind: Option<entry::Kind>,    /// If a pathspec matched, this is how it matched. Maybe `None` if computation didn't see the need to evaluate it.    pub pathspec_match: Option<PathspecMatch>,} impl Outcome {    fn with_status(mut self, status: entry::Status) -> Self {        self.status = status;        self    }     fn with_kind(mut self, disk_kind: Option<entry::Kind>, index_kind: Option<entry::Kind>) -> Self {        self.disk_kind = disk_kind;        self.index_kind = index_kind;        self    }} impl From<&Entry> for Outcome {    fn from(e: &Entry) -> Self {        Outcome {            status: e.status,            property: e.property,            disk_kind: e.disk_kind,            index_kind: e.index_kind,            pathspec_match: e.pathspec_match,        }    }} impl<'a> EntryRef<'a> {    pub(super) fn from_outcome(rela_path: Cow<'a, BStr>, info: crate::walk::classify::Outcome) -> Self {        EntryRef {            rela_path,            property: info.property,            status: info.status,            disk_kind: info.disk_kind,            index_kind: info.index_kind,            pathspec_match: info.pathspec_match,        }    }} /// Figure out what to do with `rela_path`, provided as worktree-relative path, with `disk_file_type` if it is known already/// as it helps to match pathspecs correctly, which can be different for directories./// `path` is a disk-accessible variant of `rela_path` which is within the `worktree_root`, and will be modified temporarily but remain unchanged.////// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns./// `filename_start_idx` is the index at which the filename begins, i.e. `a/b` has `2` as index./// It may resemble a directory on the way to a leaf (like a file)////// Returns `(status, file_kind, pathspec_matches_how)` to identify the `status` on disk, along with a classification `file_kind`,/// and if `file_kind` is not a directory, the way the pathspec matched with `pathspec_matches_how`.////// Note that non-files are pruned by default.pub fn path(    path: &mut PathBuf,    rela_path: &mut BString,    filename_start_idx: usize,    disk_kind: Option<entry::Kind>,    on_demand_disk_kind: impl FnOnce() -> Option<entry::Kind>,    Options {        ignore_case,        recurse_repositories,        emit_ignored,        for_deletion,        classify_untracked_bare_repositories,        symlinks_to_directories_are_ignored_like_directories,        worktree_relative_worktree_dirs,        ..    }: Options<'_>,    ctx: &mut Context<'_>,) -> Result<Outcome, Error> {    let mut out = Outcome {        status: entry::Status::Pruned,        property: None,        disk_kind,        index_kind: None,        pathspec_match: None,    };    if is_eq(rela_path[filename_start_idx..].as_bstr(), ".git", ignore_case) {        out.pathspec_match = ctx            .pathspec            .pattern_matching_relative_path(                rela_path.as_bstr(),                disk_kind.map(|ft| ft.is_dir()),                ctx.pathspec_attributes,            )            .map(Into::into);        if for_deletion.is_some() {            if let Some(excluded) = ctx                .excludes                .as_mut()                .map_or(Ok(None), |stack| {                    stack                        .at_entry(                            rela_path.as_bstr(),                            disk_kind.map(|ft| is_dir_to_mode(ft.is_dir())),                            ctx.objects,                        )                        .map(|platform| platform.excluded_kind())                })                .map_err(Error::ExcludesAccess)?                .filter(|_| filename_start_idx > 0)            {                out.status = entry::Status::Ignored(excluded);            }        }        out.property = entry::Property::DotGit.into();        return Ok(out);    }    let pathspec_could_match = ctx        .pathspec        .can_match_relative_path(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()));    if !pathspec_could_match {        return Ok(out.with_status(entry::Status::Pruned));    }     let (uptodate_index_kind, index_kind, property) = resolve_file_type_with_index(        rela_path,        ctx.index,        ctx.ignore_case_index_lookup.filter(|_| ignore_case),    );    let mut kind = uptodate_index_kind.or(disk_kind).or_else(on_demand_disk_kind);     // We always check the pathspec to have the value filled in reliably.    out.pathspec_match = ctx        .pathspec        .pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)        .map(Into::into);     if worktree_relative_worktree_dirs.is_some_and(|worktrees| worktrees.contains(&*rela_path)) {        return Ok(out            .with_kind(Some(entry::Kind::Repository), None)            .with_status(entry::Status::Tracked));    }     let maybe_status = if property.is_none() {        (index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked)    } else {        out.property = property;        Some(entry::Status::Pruned)    };     let is_dir = if symlinks_to_directories_are_ignored_like_directories        && ctx.excludes.is_some()        && kind == Some(entry::Kind::Symlink)    {        path.metadata().ok().map(|md| is_dir_to_mode(md.is_dir()))    } else {        kind.map(|ft| is_dir_to_mode(ft.is_dir()))    };     let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {        maybe_upgrade_to_repository(            current_kind,            find_harder,            recurse_repositories,            path,            ctx.current_dir,            ctx.git_dir_realpath,        )    };    if let Some(status) = maybe_status {        if kind == Some(entry::Kind::Directory) && index_kind == Some(entry::Kind::Repository) {            kind = maybe_upgrade_to_repository(kind, false);        }        return Ok(out.with_status(status).with_kind(kind, index_kind));    }     debug_assert!(maybe_status.is_none(), "It only communicates a single state right now");    if let Some(excluded) = ctx        .excludes        .as_mut()        .filter(|_| !rela_path.is_empty())        .map_or(Ok(None), |stack| {            stack                .at_entry(rela_path.as_bstr(), is_dir, ctx.objects)                .map(|platform| platform.excluded_kind())        })        .map_err(Error::ExcludesAccess)?    {        if emit_ignored.is_some() {            if matches!(                for_deletion,                Some(                    ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories                        | ForDeletionMode::FindRepositoriesInIgnoredDirectories                )            ) {                kind = maybe_upgrade_to_repository(                    kind,                    matches!(                        for_deletion,                        Some(ForDeletionMode::FindRepositoriesInIgnoredDirectories)                    ),                );            }            if kind.is_some_and(|d| d.is_recursable_dir())                && (out.pathspec_match.is_none()                    || worktree_relative_worktree_dirs.is_some_and(|worktrees| {                        for_deletion.is_some()                            && worktrees                                .iter()                                .any(|dir| dir.starts_with_str(&*rela_path) && dir.get(rela_path.len()) == Some(&b'/'))                    }))            {                // We have patterns that didn't match at all, *yet*, or there are contained worktrees.                // We want to look inside.                out.pathspec_match = Some(PathspecMatch::Prefix);            }        }        return Ok(out            .with_status(entry::Status::Ignored(excluded))            .with_kind(kind, index_kind));    }     debug_assert!(maybe_status.is_none());    let mut status = entry::Status::Untracked;     if kind.is_some_and(|ft| ft.is_dir()) {        kind = maybe_upgrade_to_repository(kind, classify_untracked_bare_repositories);    } else if out.pathspec_match.is_none() {        status = entry::Status::Pruned;    }    Ok(out.with_status(status).with_kind(kind, index_kind))} pub fn maybe_upgrade_to_repository(    current_kind: Option<entry::Kind>,    find_harder: bool,    recurse_repositories: bool,    path: &mut PathBuf,    current_dir: &Path,    git_dir_realpath: &Path,) -> Option<entry::Kind> {    if recurse_repositories {        return current_kind;    }    if find_harder {        let mut is_nested_repo = gix_discover::is_git(path).is_ok();        if is_nested_repo {            let git_dir_is_our_own = gix_path::realpath_opts(path, current_dir, gix_path::realpath::MAX_SYMLINKS)                .ok()                .is_some_and(|realpath_candidate| realpath_candidate == git_dir_realpath);            is_nested_repo = !git_dir_is_our_own;        }        if is_nested_repo {            return Some(entry::Kind::Repository);        }    }    path.push(gix_discover::DOT_GIT_DIR);    let mut is_nested_nonbare_repo = gix_discover::is_git(path).is_ok();    if is_nested_nonbare_repo {        let git_dir_is_our_own = gix_path::realpath_opts(path, current_dir, gix_path::realpath::MAX_SYMLINKS)            .ok()            .is_some_and(|realpath_candidate| realpath_candidate == git_dir_realpath);        is_nested_nonbare_repo = !git_dir_is_our_own;    }    path.pop();     if is_nested_nonbare_repo {        Some(entry::Kind::Repository)    } else {        current_kind    }} /// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns./// Also note `maybe_file_type` will be `None` for entries that aren't up-to-date and files, for directories at least one entry must be uptodate./// Returns `(maybe_file_type, Option<index_file_type>, flags)`, with the last option being a flag set only for sparse directories in the index./// `index_file_type` is the type of `rela_path` as available in the index.////// ### Shortcoming////// In case-insensitive mode, if there is an entry `d` and a `D/a` both in the index, we will always find the file `d` first, and always consider/// the entry as not uptodate, while classifying it as file (the first one we found). As quite a huge exception, this isn't properly represented/// in the data model, and we emit a trace to make it more obvious when it happens, in case this leads to less expected results.fn resolve_file_type_with_index(    rela_path: &mut BString,    index: &gix_index::State,    ignore_case: Option<&gix_index::AccelerateLookup<'_>>,) -> (Option<entry::Kind>, Option<entry::Kind>, Option<entry::Property>) {    // TODO: either get this to work for icase as well, or remove the need for it. Logic is different in both branches.    let mut special_property = None;     fn entry_to_kinds(entry: &gix_index::Entry) -> (Option<entry::Kind>, Option<entry::Kind>) {        let kind = if entry.mode.is_submodule() {            entry::Kind::Repository.into()        } else if entry.mode.contains(gix_index::entry::Mode::FILE) {            entry::Kind::File.into()        } else if entry.mode.contains(gix_index::entry::Mode::SYMLINK) {            entry::Kind::Symlink.into()        } else {            None        };        (            kind.filter(|_| {                entry                    .flags                    .intersects(gix_index::entry::Flags::UPTODATE | gix_index::entry::Flags::ASSUME_VALID)            }),            kind,        )    }     fn icase_directory_to_kinds(dir: Option<&gix_index::Entry>) -> (Option<entry::Kind>, Option<entry::Kind>) {        let index_kind = dir.map(|_| entry::Kind::Directory);        let uptodate_kind = dir            .filter(|entry| {                entry                    .flags                    .intersects(gix_index::entry::Flags::UPTODATE | gix_index::entry::Flags::ASSUME_VALID)            })            .map(|_| entry::Kind::Directory);        (uptodate_kind, index_kind)    }     // TODO(perf): multi-threaded hash-table so it's always used, even for case-sensitive lookups, just like Git does it.    let (uptodate_kind, index_kind) = if let Some(accelerate) = ignore_case {        match index.entry_by_path_icase(rela_path.as_bstr(), true, accelerate) {            None => icase_directory_to_kinds(index.entry_closest_to_directory_or_directory_icase(                rela_path.as_bstr(),                true,                accelerate,            )),            Some(entry) => {                if entry.mode.is_submodule() || entry.mode.is_sparse() {                    entry_to_kinds(entry)                } else {                    let icase_dir =                        index.entry_closest_to_directory_or_directory_icase(rela_path.as_bstr(), true, accelerate);                    let directory_matches_exactly = icase_dir.is_some_and(|dir| {                        let path = dir.path(index);                        let slash_idx = path.rfind_byte(b'/').expect("dir");                        path[..slash_idx].as_bstr() == rela_path                    });                    if directory_matches_exactly {                        icase_directory_to_kinds(icase_dir)                    } else {                        entry_to_kinds(entry)                    }                }            }        }    } else {        match index.entry_by_path(rela_path.as_bstr()) {            None => {                rela_path.push(b'/');                let res = index.prefixed_entries_range(rela_path.as_bstr());                rela_path.pop();                 let mut one_index_signalling_with_cone = None;                let mut all_excluded_from_worktree_non_cone = false;                let is_tracked = res.is_some();                let kind = res                    .filter(|range| {                        if range.len() == 1 {                            one_index_signalling_with_cone = range.start.into();                        }                        let entries = &index.entries()[range.clone()];                        let any_up_to_date = entries                            .iter()                            .any(|e| e.flags.contains(gix_index::entry::Flags::UPTODATE));                        if !any_up_to_date && one_index_signalling_with_cone.is_none() {                            all_excluded_from_worktree_non_cone = entries                                .iter()                                .all(|e| e.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE));                        }                        any_up_to_date                    })                    .map(|_| entry::Kind::Directory);                 if all_excluded_from_worktree_non_cone                    || one_index_signalling_with_cone                        .filter(|_| kind.is_none())                        .is_some_and(|idx| index.entries()[idx].mode.is_sparse())                {                    special_property = Some(entry::Property::TrackedExcluded);                }                (kind, is_tracked.then_some(entry::Kind::Directory))            }            Some(entry) => entry_to_kinds(entry),        }    };    (uptodate_kind, index_kind, special_property)} fn is_eq(lhs: &BStr, rhs: impl AsRef<BStr>, ignore_case: bool) -> bool {    if ignore_case {        lhs.eq_ignore_ascii_case(rhs.as_ref().as_ref())    } else {        lhs == rhs.as_ref()    }} fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode {    if is_dir {        gix_index::entry::Mode::DIR    } else {        gix_index::entry::Mode::FILE    }}