Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrate gix-status #1301

Merged
merged 7 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 71 additions & 23 deletions gitoxide-core/src/repository/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct Options {
pub precious: bool,
pub directories: bool,
pub repositories: bool,
pub pathspec_matches_result: bool,
pub skip_hidden_repositories: Option<FindRepository>,
pub find_untracked_repositories: FindRepository,
}
Expand Down Expand Up @@ -46,6 +47,7 @@ pub(crate) mod function {
repositories,
skip_hidden_repositories,
find_untracked_repositories,
pathspec_matches_result,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
Expand All @@ -56,6 +58,7 @@ pub(crate) mod function {
};

let index = repo.index_or_empty()?;
let pathspec_for_dirwalk = !pathspec_matches_result;
let has_patterns = !patterns.is_empty();
let mut collect = InterruptableCollect::default();
let collapse_directories = CollapseDirectory;
Expand All @@ -66,19 +69,39 @@ pub(crate) mod function {
match skip_hidden_repositories {
Some(FindRepository::NonBare) => Some(FindNonBareRepositoriesInIgnoredDirectories),
Some(FindRepository::All) => Some(FindRepositoriesInIgnoredDirectories),
None => None,
None => Some(Default::default()),
}
} else {
Some(IgnoredDirectoriesCanHideNestedRepositories)
Some(Default::default())
})
.classify_untracked_bare_repositories(matches!(find_untracked_repositories, FindRepository::All))
.emit_untracked(collapse_directories)
.emit_ignored(Some(collapse_directories))
.empty_patterns_match_prefix(true)
.emit_empty_directories(true);
repo.dirwalk(&index, patterns, options, &mut collect)?;
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
repo.dirwalk(
&index,
if pathspec_for_dirwalk {
patterns.clone()
} else {
Vec::new()
},
options,
&mut collect,
)?;

let mut pathspec = pathspec_matches_result
.then(|| {
repo.pathspec(
true,
patterns,
true,
&index,
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
)
})
.transpose()?;
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
let entries = collect.inner.into_entries_by_path();
let mut entries_to_clean = 0;
let mut skipped_directories = 0;
Expand All @@ -88,7 +111,7 @@ pub(crate) mod function {
let mut pruned_entries = 0;
let mut saw_ignored_directory = false;
let mut saw_untracked_directory = false;
for (entry, dir_status) in entries.into_iter() {
for (mut entry, dir_status) in entries.into_iter() {
if dir_status.is_some() {
if debug {
writeln!(
Expand All @@ -101,21 +124,25 @@ pub(crate) mod function {
continue;
}

let pathspec_includes_entry = entry
.pathspec_match
.map_or(false, |m| m != gix::dir::entry::PathspecMatch::Excluded);
let pathspec_includes_entry = match pathspec.as_mut() {
None => entry
.pathspec_match
.map_or(false, |m| m != gix::dir::entry::PathspecMatch::Excluded),
Some(pathspec) => pathspec
.pattern_matching_relative_path(entry.rela_path.as_bstr(), entry.disk_kind.map(|k| k.is_dir()))
.map_or(false, |m| !m.is_excluded()),
};
pruned_entries += usize::from(!pathspec_includes_entry);
if !pathspec_includes_entry && debug {
writeln!(err, "DBG: prune '{}' as it is excluded by pathspec", entry.rela_path).ok();
writeln!(err, "DBG: prune '{}'", entry.rela_path).ok();
}
if entry.status.is_pruned() || !pathspec_includes_entry {
continue;
}

let mut disk_kind = entry.disk_kind.expect("present if not pruned");
let keep = match entry.status {
Status::DotGit | Status::Pruned | Status::TrackedExcluded => {
unreachable!("BUG: Pruned are skipped already as their pathspec is always None")
Status::Pruned => {
unreachable!("BUG: we skipped these above")
}
Status::Tracked => {
unreachable!("BUG: tracked aren't emitted")
Expand All @@ -130,6 +157,14 @@ pub(crate) mod function {
}
Status::Untracked => true,
};
if entry.disk_kind.is_none() {
entry.disk_kind = workdir
.join(gix::path::from_bstr(entry.rela_path.as_bstr()))
.metadata()
.ok()
.map(|e| e.file_type().into());
}
let mut disk_kind = entry.disk_kind.expect("present if not pruned");
if !keep {
if debug {
writeln!(err, "DBG: prune '{}' as -x or -p is missing", entry.rela_path).ok();
Expand All @@ -148,7 +183,7 @@ pub(crate) mod function {

match disk_kind {
Kind::File | Kind::Symlink => {}
Kind::EmptyDirectory | Kind::Directory => {
Kind::Directory => {
if !directories {
skipped_directories += 1;
if debug {
Expand All @@ -175,6 +210,11 @@ pub(crate) mod function {
saw_ignored_directory |= is_ignored;
saw_untracked_directory |= entry.status == gix::dir::entry::Status::Untracked;
}

if gix::interrupt::is_triggered() {
execute = false;
}
let mut may_remove_this_entry = execute;
writeln!(
out,
"{maybe}{suffix} {}{} {status}",
Expand All @@ -200,24 +240,32 @@ pub(crate) mod function {
"".into()
},
},
maybe = if execute { "removing" } else { "WOULD remove" },
suffix = match disk_kind {
Kind::File | Kind::Symlink | Kind::Directory => {
""
maybe = if entry.property == Some(gix::dir::entry::Property::EmptyDirectoryAndCWD) {
may_remove_this_entry = false;
if execute {
"Refusing to remove empty current working directory"
} else {
"Would refuse to remove empty current working directory"
}
Kind::EmptyDirectory => {
} else if execute {
"removing"
} else {
"WOULD remove"
},
suffix = match disk_kind {
Kind::Directory if entry.property == Some(gix::dir::entry::Property::EmptyDirectory) => {
" empty"
}
Kind::Repository => {
" repository"
}
Kind::File | Kind::Symlink | Kind::Directory => {
""
}
},
)?;

if gix::interrupt::is_triggered() {
execute = false;
}
if execute {
if may_remove_this_entry {
let path = workdir.join(entry_path);
if disk_kind.is_dir() {
std::fs::remove_dir_all(path)?;
Expand Down Expand Up @@ -256,7 +304,7 @@ pub(crate) mod function {
}));
messages.extend((pruned_entries > 0 && has_patterns).then(|| {
format!(
"try to adjust your pathspec to reveal some of the {pruned_entries} pruned {entries}",
"try to adjust your pathspec to reveal some of the {pruned_entries} pruned {entries} - show with --debug",
entries = plural("entry", "entries", pruned_entries)
)
}));
Expand Down
71 changes: 47 additions & 24 deletions gix-dir/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,51 @@ use crate::walk::ForDeletionMode;
use crate::{Entry, EntryRef};
use std::borrow::Cow;

/// The kind of the entry.
/// A way of attaching additional information to an [Entry] .
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Property {
/// The entry was named `.git`, matched according to the case-sensitivity rules of the repository.
DotGit,
/// The entry is a directory, and that directory is empty.
EmptyDirectory,
/// The entry is a directory, it is empty and the current working directory.
///
/// The caller should pay special attention to this very special case, as it is indeed only possible to run into it
/// while traversing the directory for deletion.
/// Non-empty directory will never be collapsed, hence if they are working directories, they naturally become unobservable.
EmptyDirectoryAndCWD,
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
///
/// Note that evne if the directory is empty, it will only have this state, not `EmptyDirectory`.
TrackedExcluded,
}

/// The kind of the entry, seated in their kinds available on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Kind {
/// The entry is a blob, executable or not.
File,
/// The entry is a symlink.
Symlink,
/// A directory that contains no file or directory.
EmptyDirectory,
/// The entry is an ordinary directory.
///
/// Note that since we don't check for bare repositories, this could in fact be a collapsed
/// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
Directory,
/// The entry is a directory which *contains* a `.git` folder.
/// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
Repository,
}

/// The kind of entry as obtained from a directory.
///
/// The order of variants roughly relates from cheap-to-compute to most expensive, as each level needs more tests to assert.
/// Thus, `DotGit` is the cheapest, while `Untracked` is among the most expensive and one of the major outcomes of any
/// [`walk`](crate::walk()) run.
/// For example, if an entry was `Pruned`, we effectively don't know if it would have been `Untracked` as well as we stopped looking.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Status {
/// The filename of an entry was `.git`, which is generally pruned.
DotGit,
/// The provided pathspec prevented further processing as the path didn't match.
/// If this happens, no further checks are done so we wouldn't know if the path is also ignored for example (by mention in `.gitignore`).
/// The entry was removed from the walk due to its other properties, like [Property] or [PathspecMatch]
///
/// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
/// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
/// while they will not be available for any interactions in read-only mode.
Pruned,
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker - i.e. a directory
/// that is excluded, so its whole content is excluded and not checked out nor is part of the index.
TrackedExcluded,
/// The entry is tracked in Git.
Tracked,
/// The entry is ignored as per `.gitignore` files and their rules.
Expand All @@ -52,7 +63,7 @@ pub enum Status {
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
pub enum PathspecMatch {
/// The match happened because there wasn't any pattern, which matches all, or because there was a nil pattern or one with an empty path.
/// Thus this is not a match by merit.
/// Thus, this is not a match by merit.
Always,
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
Excluded,
Expand Down Expand Up @@ -84,12 +95,24 @@ impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
}
}

impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
if m.is_excluded() {
PathspecMatch::Excluded
} else {
m.kind.into()
}
}
}

/// Conversion
impl EntryRef<'_> {
/// Strip the lifetime to obtain a fully owned copy.
pub fn to_owned(&self) -> Entry {
Entry {
rela_path: self.rela_path.clone().into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
Expand All @@ -101,19 +124,22 @@ impl EntryRef<'_> {
Entry {
rela_path: self.rela_path.into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
}

/// Conversion
impl Entry {
/// Obtain an [`EntryRef`] from this instance.
pub fn to_ref(&self) -> EntryRef<'_> {
EntryRef {
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
Expand All @@ -136,10 +162,7 @@ impl From<std::fs::FileType> for Kind {
impl Status {
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
pub fn is_pruned(&self) -> bool {
match self {
Status::DotGit | Status::TrackedExcluded | Status::Pruned => true,
Status::Ignored(_) | Status::Untracked | Status::Tracked => false,
}
matches!(&self, Status::Pruned)
}
/// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
/// This implements the default rules of `git status`, which is good for a minimal traversal through
Expand All @@ -158,7 +181,7 @@ impl Status {
return false;
}
match self {
Status::DotGit | Status::TrackedExcluded | Status::Pruned => false,
Status::Pruned => false,
Status::Ignored(_) => {
for_deletion.map_or(false, |fd| {
matches!(
Expand All @@ -174,12 +197,12 @@ impl Status {
}

impl Kind {
fn is_recursable_dir(&self) -> bool {
pub(super) fn is_recursable_dir(&self) -> bool {
matches!(self, Kind::Directory)
}

/// Return `true` if this is a directory on disk. Note that this is true for repositories as well.
pub fn is_dir(&self) -> bool {
matches!(self, Kind::EmptyDirectory | Kind::Directory | Kind::Repository)
matches!(self, Kind::Directory | Kind::Repository)
}
}
12 changes: 7 additions & 5 deletions gix-dir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ pub struct EntryRef<'a> {
/// Note that many entries with status `Pruned` will not show up as their kind hasn't yet been determined when they were
/// pruned very early on.
pub status: entry::Status,
/// Further specify the what the entry is on disk, similar to a file mode.
/// This is `None` if the entry was pruned by a pathspec that could not match, as we then won't invest the time to obtain
/// the kind of the entry on disk.
/// Additional properties of the entry.
pub property: Option<entry::Property>,
/// Further specify what the entry is on disk, similar to a file mode.
/// This is `None` if we decided it's not worth it to exit early and avoid trying to obtain this information.
pub disk_kind: Option<entry::Kind>,
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
pub index_kind: Option<entry::Kind>,
/// Determines how the pathspec matched.
/// Can also be `None` if no pathspec matched, or if the status check stopped prior to checking for pathspec matches which is the case for [`entry::Status::DotGit`].
/// Note that it can also be `Some(PathspecMatch::Excluded)` if a negative pathspec matched.
pub pathspec_match: Option<entry::PathspecMatch>,
}
Expand All @@ -48,7 +48,9 @@ pub struct Entry {
pub rela_path: BString,
/// The status of entry, most closely related to what we know from `git status`, but not the same.
pub status: entry::Status,
/// Further specify the what the entry is on disk, similar to a file mode.
/// Additional flags that further clarify properties of the entry.
pub property: Option<entry::Property>,
/// Further specify what the entry is on disk, similar to a file mode.
pub disk_kind: Option<entry::Kind>,
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
pub index_kind: Option<entry::Kind>,
Expand Down
Loading
Loading