Skip to content

Commit

Permalink
Merge branch 'status'
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Feb 25, 2024
2 parents 221bce4 + aa7c190 commit d53504a
Show file tree
Hide file tree
Showing 11 changed files with 597 additions and 166 deletions.
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

0 comments on commit d53504a

Please sign in to comment.