Skip to content

Commit

Permalink
feat: basic gix clean
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Feb 8, 2024
1 parent 9c10795 commit cf311e3
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ target/

# repositories used for local testing
/tests/fixtures/repos
$/tests/fixtures/repos/

/tests/fixtures/commit-graphs/
$/tests/fixtures/commit-graphs/

**/generated-do-not-edit/

# Cargo lock files of fuzz targets - let's have the latest versions of everything under test
**/fuzz/Cargo.lock

# newer Git sees these as precious, older Git falls through to the pattern above
$**/fuzz/Cargo.lock
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", "
cache-efficiency-debug = ["gix-features/cache-efficiency-debug"]

## A way to enable most `gitoxide-core` tools found in `ein tools`, namely `organize` and `estimate hours`.
gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours", "gitoxide-core-tools-archive"]
gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours", "gitoxide-core-tools-archive", "gitoxide-core-tools-clean"]

## A program to perform analytics on a `git` repository, using an auto-maintained sqlite database
gitoxide-core-tools-query = ["gitoxide-core/query"]
Expand All @@ -140,6 +140,9 @@ gitoxide-core-tools-corpus = ["gitoxide-core/corpus"]
## A sub-command to generate archive from virtual worktree checkouts.
gitoxide-core-tools-archive = ["gitoxide-core/archive"]

## A sub-command to clean the worktree from untracked and ignored files.
gitoxide-core-tools-clean = ["gitoxide-core/clean"]

#! ### Building Blocks for mutually exclusive networking
#! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail.
#! Within each section, features can be combined.
Expand Down
4 changes: 4 additions & 0 deletions gitoxide-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "
## The ability to create archives from virtual worktrees, similar to `git archive`.
archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"]

## The ability to clean a repository, similar to `git clean`.
clean = [ "dep:gix-dir" ]

#! ### Mutually Exclusive Networking
#! If both are set, _blocking-client_ will take precedence, allowing `--all-features` to be used.

Expand All @@ -49,6 +52,7 @@ gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.48.0", p
gix-transport-configuration-only = { package = "gix-transport", version = "^0.41.0", path = "../gix-transport", default-features = false }
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.9.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }
gix-status = { version = "^0.6.0", path = "../gix-status" }
gix-dir = { version = "^0.1.0", path = "../gix-dir", optional = true }
gix-fsck = { version = "^0.3.0", path = "../gix-fsck" }
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
anyhow = "1.0.42"
Expand Down
138 changes: 138 additions & 0 deletions gitoxide-core/src/repository/clean.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::OutputFormat;

pub struct Options {
pub format: OutputFormat,
pub execute: bool,
pub ignored: bool,
pub precious: bool,
pub directories: bool,
}
pub(crate) mod function {
use crate::repository::clean::Options;
use crate::OutputFormat;
use anyhow::bail;
use gix::bstr::BString;
use gix_dir::entry::{Kind, Status};
use gix_dir::walk::EmissionMode::{CollapseDirectory, Matching};
use std::path::Path;

pub fn clean(
repo: gix::Repository,
out: &mut dyn std::io::Write,
patterns: Vec<BString>,
Options {
format,
execute,
ignored,
precious,
directories,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("JSON output isn't implemented yet");
}
let Some(workdir) = repo.work_dir() else {
bail!("Need a worktree to clean, this is a bare repository");
};

let index = repo.index()?;
let mut excludes = repo
.excludes(
&index,
None,
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
)?
.detach();
let (mut pathspec, mut maybe_attributes) = repo
.pathspec(
patterns.iter(),
repo.work_dir().is_some(),
&index,
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
)?
.into_parts();

let prefix = repo.prefix()?.unwrap_or(Path::new(""));
let git_dir_realpath =
gix::path::realpath_opts(repo.git_dir(), repo.current_dir(), gix::path::realpath::MAX_SYMLINKS)?;
let mut collect = gix_dir::walk::delegate::Collect::default();
let fs_caps = repo.filesystem_options()?;
let emission_mode = if directories { CollapseDirectory } else { Matching };
let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing());
gix_dir::walk(
&workdir.join(prefix),
workdir,
gix_dir::walk::Context {
git_dir_realpath: git_dir_realpath.as_ref(),
current_dir: repo.current_dir(),
index: &index,
ignore_case_index_lookup: accelerate_lookup.as_ref(),
pathspec: &mut pathspec,
pathspec_attributes: &mut |relative_path, case, is_dir, out| {
let stack = maybe_attributes
.as_mut()
.expect("can only be called if attributes are used in patterns");
stack
.set_case(case)
.at_entry(relative_path, Some(is_dir), &repo.objects)
.map_or(false, |platform| platform.matching_attributes(out))
},
excludes: Some(&mut excludes),
objects: &repo.objects,
},
gix_dir::walk::Options {
precompose_unicode: fs_caps.precompose_unicode,
ignore_case: fs_caps.ignore_case,
recurse_repositories: false,
emit_pruned: false,
emit_ignored: (ignored || precious).then_some(emission_mode),
for_deletion: true,
emit_tracked: false,
emit_untracked: emission_mode,
emit_empty_directories: directories,
},
&mut collect,
)?;

let entries = collect.into_entries_by_path();
for entry in entries
.into_iter()
.filter_map(|(entry, dir_status)| dir_status.is_none().then_some(entry))
.filter(|entry| match entry.disk_kind {
Kind::File | Kind::Symlink => true,
Kind::EmptyDirectory | Kind::Directory | Kind::Repository => directories,
})
.filter(|e| match e.status {
Status::DotGit | Status::Pruned | Status::TrackedExcluded => {
unreachable!("Pruned aren't emitted")
}
Status::Tracked => {
unreachable!("tracked aren't emitted")
}
Status::Ignored(gix::ignore::Kind::Expendable) => ignored,
Status::Ignored(gix::ignore::Kind::Precious) => precious,
Status::Untracked => true,
})
{
if entry.disk_kind == gix_dir::entry::Kind::Repository {
writeln!(out, "Would skip repository {}", entry.rela_path)?;
continue;
}
writeln!(
out,
"{maybe} {}{} ({:?})",
entry.rela_path,
entry.disk_kind.is_dir().then_some("/").unwrap_or_default(),
entry.status,
maybe = if execute { "removing" } else { "WOULD remove" },
)?;
}
if !execute {
writeln!(
out,
"\nWARNING: would removes repositories that are hidden inside of ignored directories"
)?;
}
Ok(())
}
}
4 changes: 4 additions & 0 deletions gitoxide-core/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub mod config;
mod credential;
pub use credential::function as credential;
pub mod attributes;
#[cfg(feature = "clean")]
pub mod clean;
#[cfg(feature = "clean")]
pub use clean::function::clean;
#[cfg(feature = "blocking-client")]
pub mod clone;
pub mod exclude;
Expand Down
29 changes: 29 additions & 0 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ pub fn main() -> Result<()> {
}

match cmd {
#[cfg(feature = "gitoxide-core-tools-clean")]
Subcommands::Clean(crate::plumbing::options::clean::Command {
execute,
ignored,
precious,
directories,
pathspec,
}) => prepare_and_run(
"clean",
trace,
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, _err| {
core::repository::clean(
repository(Mode::Lenient)?,
out,
pathspec,
core::repository::clean::Options {
format,
execute,
ignored,
precious,
directories,
},
)
},
),
Subcommands::Status(crate::plumbing::options::status::Platform {
statistics,
submodules,
Expand Down
26 changes: 26 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub enum Subcommands {
/// Subcommands for creating worktree archives
#[cfg(feature = "gitoxide-core-tools-archive")]
Archive(archive::Platform),
#[cfg(feature = "gitoxide-core-tools-clean")]
Clean(clean::Command),
/// Subcommands for interacting with commit-graphs
#[clap(subcommand)]
CommitGraph(commitgraph::Subcommands),
Expand Down Expand Up @@ -478,6 +480,30 @@ pub mod mailmap {
}
}

pub mod clean {
use gitoxide::shared::CheckPathSpec;
use gix::bstr::BString;

#[derive(Debug, clap::Parser)]
pub struct Command {
/// Actually perform the operation, which deletes files on disk without chance of recovery.
#[arg(long, short = 'e')]
pub execute: bool,
/// Remove ignored (and expendable) files.
#[arg(long, short = 'x')]
pub ignored: bool,
/// Remove precious files.
#[arg(long, short = 'p')]
pub precious: bool,
/// Remove whole directories.
#[arg(long, short = 'd')]
pub directories: bool,
/// The git path specifications to list attributes for, or unset to read from stdin one per line.
#[clap(value_parser = CheckPathSpec)]
pub pathspec: Vec<BString>,
}
}

pub mod odb {
#[derive(Debug, clap::Subcommand)]
pub enum Subcommands {
Expand Down

0 comments on commit cf311e3

Please sign in to comment.