Skip to content

Commit cf311e3

Browse files
committed
feat: basic gix clean
1 parent 9c10795 commit cf311e3

File tree

8 files changed

+212
-1
lines changed

8 files changed

+212
-1
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ target/
66

77
# repositories used for local testing
88
/tests/fixtures/repos
9+
$/tests/fixtures/repos/
10+
911
/tests/fixtures/commit-graphs/
12+
$/tests/fixtures/commit-graphs/
1013

1114
**/generated-do-not-edit/
1215

1316
# Cargo lock files of fuzz targets - let's have the latest versions of everything under test
1417
**/fuzz/Cargo.lock
18+
19+
# newer Git sees these as precious, older Git falls through to the pattern above
20+
$**/fuzz/Cargo.lock

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", "
129129
cache-efficiency-debug = ["gix-features/cache-efficiency-debug"]
130130

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

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

143+
## A sub-command to clean the worktree from untracked and ignored files.
144+
gitoxide-core-tools-clean = ["gitoxide-core/clean"]
145+
143146
#! ### Building Blocks for mutually exclusive networking
144147
#! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail.
145148
#! Within each section, features can be combined.

gitoxide-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "
2828
## The ability to create archives from virtual worktrees, similar to `git archive`.
2929
archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"]
3030

31+
## The ability to clean a repository, similar to `git clean`.
32+
clean = [ "dep:gix-dir" ]
33+
3134
#! ### Mutually Exclusive Networking
3235
#! If both are set, _blocking-client_ will take precedence, allowing `--all-features` to be used.
3336

@@ -49,6 +52,7 @@ gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.48.0", p
4952
gix-transport-configuration-only = { package = "gix-transport", version = "^0.41.0", path = "../gix-transport", default-features = false }
5053
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.9.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }
5154
gix-status = { version = "^0.6.0", path = "../gix-status" }
55+
gix-dir = { version = "^0.1.0", path = "../gix-dir", optional = true }
5256
gix-fsck = { version = "^0.3.0", path = "../gix-fsck" }
5357
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
5458
anyhow = "1.0.42"

gitoxide-core/src/repository/clean.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use crate::OutputFormat;
2+
3+
pub struct Options {
4+
pub format: OutputFormat,
5+
pub execute: bool,
6+
pub ignored: bool,
7+
pub precious: bool,
8+
pub directories: bool,
9+
}
10+
pub(crate) mod function {
11+
use crate::repository::clean::Options;
12+
use crate::OutputFormat;
13+
use anyhow::bail;
14+
use gix::bstr::BString;
15+
use gix_dir::entry::{Kind, Status};
16+
use gix_dir::walk::EmissionMode::{CollapseDirectory, Matching};
17+
use std::path::Path;
18+
19+
pub fn clean(
20+
repo: gix::Repository,
21+
out: &mut dyn std::io::Write,
22+
patterns: Vec<BString>,
23+
Options {
24+
format,
25+
execute,
26+
ignored,
27+
precious,
28+
directories,
29+
}: Options,
30+
) -> anyhow::Result<()> {
31+
if format != OutputFormat::Human {
32+
bail!("JSON output isn't implemented yet");
33+
}
34+
let Some(workdir) = repo.work_dir() else {
35+
bail!("Need a worktree to clean, this is a bare repository");
36+
};
37+
38+
let index = repo.index()?;
39+
let mut excludes = repo
40+
.excludes(
41+
&index,
42+
None,
43+
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
44+
)?
45+
.detach();
46+
let (mut pathspec, mut maybe_attributes) = repo
47+
.pathspec(
48+
patterns.iter(),
49+
repo.work_dir().is_some(),
50+
&index,
51+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
52+
)?
53+
.into_parts();
54+
55+
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
56+
let git_dir_realpath =
57+
gix::path::realpath_opts(repo.git_dir(), repo.current_dir(), gix::path::realpath::MAX_SYMLINKS)?;
58+
let mut collect = gix_dir::walk::delegate::Collect::default();
59+
let fs_caps = repo.filesystem_options()?;
60+
let emission_mode = if directories { CollapseDirectory } else { Matching };
61+
let accelerate_lookup = fs_caps.ignore_case.then(|| index.prepare_icase_backing());
62+
gix_dir::walk(
63+
&workdir.join(prefix),
64+
workdir,
65+
gix_dir::walk::Context {
66+
git_dir_realpath: git_dir_realpath.as_ref(),
67+
current_dir: repo.current_dir(),
68+
index: &index,
69+
ignore_case_index_lookup: accelerate_lookup.as_ref(),
70+
pathspec: &mut pathspec,
71+
pathspec_attributes: &mut |relative_path, case, is_dir, out| {
72+
let stack = maybe_attributes
73+
.as_mut()
74+
.expect("can only be called if attributes are used in patterns");
75+
stack
76+
.set_case(case)
77+
.at_entry(relative_path, Some(is_dir), &repo.objects)
78+
.map_or(false, |platform| platform.matching_attributes(out))
79+
},
80+
excludes: Some(&mut excludes),
81+
objects: &repo.objects,
82+
},
83+
gix_dir::walk::Options {
84+
precompose_unicode: fs_caps.precompose_unicode,
85+
ignore_case: fs_caps.ignore_case,
86+
recurse_repositories: false,
87+
emit_pruned: false,
88+
emit_ignored: (ignored || precious).then_some(emission_mode),
89+
for_deletion: true,
90+
emit_tracked: false,
91+
emit_untracked: emission_mode,
92+
emit_empty_directories: directories,
93+
},
94+
&mut collect,
95+
)?;
96+
97+
let entries = collect.into_entries_by_path();
98+
for entry in entries
99+
.into_iter()
100+
.filter_map(|(entry, dir_status)| dir_status.is_none().then_some(entry))
101+
.filter(|entry| match entry.disk_kind {
102+
Kind::File | Kind::Symlink => true,
103+
Kind::EmptyDirectory | Kind::Directory | Kind::Repository => directories,
104+
})
105+
.filter(|e| match e.status {
106+
Status::DotGit | Status::Pruned | Status::TrackedExcluded => {
107+
unreachable!("Pruned aren't emitted")
108+
}
109+
Status::Tracked => {
110+
unreachable!("tracked aren't emitted")
111+
}
112+
Status::Ignored(gix::ignore::Kind::Expendable) => ignored,
113+
Status::Ignored(gix::ignore::Kind::Precious) => precious,
114+
Status::Untracked => true,
115+
})
116+
{
117+
if entry.disk_kind == gix_dir::entry::Kind::Repository {
118+
writeln!(out, "Would skip repository {}", entry.rela_path)?;
119+
continue;
120+
}
121+
writeln!(
122+
out,
123+
"{maybe} {}{} ({:?})",
124+
entry.rela_path,
125+
entry.disk_kind.is_dir().then_some("/").unwrap_or_default(),
126+
entry.status,
127+
maybe = if execute { "removing" } else { "WOULD remove" },
128+
)?;
129+
}
130+
if !execute {
131+
writeln!(
132+
out,
133+
"\nWARNING: would removes repositories that are hidden inside of ignored directories"
134+
)?;
135+
}
136+
Ok(())
137+
}
138+
}

gitoxide-core/src/repository/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ pub mod config;
2424
mod credential;
2525
pub use credential::function as credential;
2626
pub mod attributes;
27+
#[cfg(feature = "clean")]
28+
pub mod clean;
29+
#[cfg(feature = "clean")]
30+
pub use clean::function::clean;
2731
#[cfg(feature = "blocking-client")]
2832
pub mod clone;
2933
pub mod exclude;

src/plumbing/main.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,35 @@ pub fn main() -> Result<()> {
146146
}
147147

148148
match cmd {
149+
#[cfg(feature = "gitoxide-core-tools-clean")]
150+
Subcommands::Clean(crate::plumbing::options::clean::Command {
151+
execute,
152+
ignored,
153+
precious,
154+
directories,
155+
pathspec,
156+
}) => prepare_and_run(
157+
"clean",
158+
trace,
159+
verbose,
160+
progress,
161+
progress_keep_open,
162+
None,
163+
move |_progress, out, _err| {
164+
core::repository::clean(
165+
repository(Mode::Lenient)?,
166+
out,
167+
pathspec,
168+
core::repository::clean::Options {
169+
format,
170+
execute,
171+
ignored,
172+
precious,
173+
directories,
174+
},
175+
)
176+
},
177+
),
149178
Subcommands::Status(crate::plumbing::options::status::Platform {
150179
statistics,
151180
submodules,

src/plumbing/options/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub enum Subcommands {
8181
/// Subcommands for creating worktree archives
8282
#[cfg(feature = "gitoxide-core-tools-archive")]
8383
Archive(archive::Platform),
84+
#[cfg(feature = "gitoxide-core-tools-clean")]
85+
Clean(clean::Command),
8486
/// Subcommands for interacting with commit-graphs
8587
#[clap(subcommand)]
8688
CommitGraph(commitgraph::Subcommands),
@@ -478,6 +480,30 @@ pub mod mailmap {
478480
}
479481
}
480482

483+
pub mod clean {
484+
use gitoxide::shared::CheckPathSpec;
485+
use gix::bstr::BString;
486+
487+
#[derive(Debug, clap::Parser)]
488+
pub struct Command {
489+
/// Actually perform the operation, which deletes files on disk without chance of recovery.
490+
#[arg(long, short = 'e')]
491+
pub execute: bool,
492+
/// Remove ignored (and expendable) files.
493+
#[arg(long, short = 'x')]
494+
pub ignored: bool,
495+
/// Remove precious files.
496+
#[arg(long, short = 'p')]
497+
pub precious: bool,
498+
/// Remove whole directories.
499+
#[arg(long, short = 'd')]
500+
pub directories: bool,
501+
/// The git path specifications to list attributes for, or unset to read from stdin one per line.
502+
#[clap(value_parser = CheckPathSpec)]
503+
pub pathspec: Vec<BString>,
504+
}
505+
}
506+
481507
pub mod odb {
482508
#[derive(Debug, clap::Subcommand)]
483509
pub enum Subcommands {

0 commit comments

Comments
 (0)