Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9600176
fix(api): skip mounted directories by comparing device IDs
claude Mar 22, 2026
02edc48
refactor(api): use pub(crate) visibility for device_id items
claude Mar 22, 2026
7b4889d
docs(readme): remove outdated mounted filesystems limitation
claude Mar 22, 2026
ab0aae8
feat(cli): add -x/--one-file-system flag to skip mounted directories
claude Mar 22, 2026
e7df5d8
chore(cli): regenerate completions, help text, and usage docs
claude Mar 22, 2026
ad79436
fix(one-file-system): return UnsupportedFeature error on non-unix pla…
claude Mar 22, 2026
3f7db4b
docs(cli): hide `-x` on windows
KSXGitHub Mar 22, 2026
184872b
fix(cli): remove redundant visible_alias for --one-file-system
claude Mar 22, 2026
81414f6
test(one-file-system): add unit and integration tests for -x flag
claude Mar 22, 2026
e7aed33
fix(test): gate platform-specific tests with target_os
claude Mar 22, 2026
33f096e
refactor(test): improve one_file_system test style
claude Mar 22, 2026
f7d1405
refactor(test): replace brittle section parsing with NUL-delimited ou…
claude Mar 22, 2026
1518b5e
Merge origin/master into claude/fix-mounted-directories-pEvuV
claude Mar 22, 2026
b21eea3
chore(git): merge from master
claude Mar 22, 2026
a168848
test(one-file-system): fail loudly when unshare is unavailable
claude Mar 22, 2026
e6702a9
fix(test): probe tmpfs mount in unshare availability check
claude Mar 22, 2026
51af07b
refactor(test): return Result<(), String> from unshare_available
claude Mar 22, 2026
d81a960
refactor(test): replace `unshare` with `fuse2fs` for cross-device test
claude Mar 23, 2026
2c0e4ba
chore(git): merge from master
claude Mar 23, 2026
a33c417
ci(test): install FUSE dependencies for cross-device test
claude Mar 23, 2026
6506aa3
ci(test): add fuse2fs package to FUSE dependency installation
claude Mar 23, 2026
02d93a5
refactor(test): return probe results from fuse_probe instead of disca…
claude Mar 23, 2026
18b0d06
fix(test): use fakeroot option for fuse2fs mount
claude Mar 23, 2026
a535b40
chore(git): merge from master
claude Mar 23, 2026
c931b42
refactor(test): replace fuse2fs with squashfuse for cross-device test
claude Mar 23, 2026
bbbef2a
ci: update FUSE dependencies from e2fsprogs to squashfs-tools
claude Mar 23, 2026
1af976d
fix(ci): add apt update and use generic error messages
claude Mar 23, 2026
f3a73e0
refactor(test): improve FUSE probe and cleanup in cross-device test
claude Mar 23, 2026
d0690c4
chore(git): merge from master
claude Mar 23, 2026
17a7c1d
fix(test): replace fixed sleep with exponential backoff for FUSE mount
claude Mar 23, 2026
43e80b9
refactor(test): use cfg_attr(ignore) for cross-device test skipping
claude Mar 23, 2026
2582245
test: remove the `cfg`
KSXGitHub Mar 23, 2026
b8fc756
refactor: unify `use`
KSXGitHub Mar 23, 2026
9dc7465
refactor: use `with_arg`
KSXGitHub Mar 23, 2026
e653d9b
docs: remove some useless comments
KSXGitHub Mar 23, 2026
884bca2
refactor(test): use long flags and full tree comparison in cross-devi…
claude Mar 23, 2026
368f1ff
fix(test): match root name between CLI output and expected tree
claude Mar 23, 2026
1d064d2
refactor: reduce verbose control flow into clever iterator chain
KSXGitHub Mar 23, 2026
0b57e7e
refactor: stop qualifying
KSXGitHub Mar 23, 2026
5cc4cf2
refactor: reduce allocation, reduce import
KSXGitHub Mar 23, 2026
175f73d
refactor: rename some variables
KSXGitHub Mar 23, 2026
c763557
test(one-file-system): add contains assertions and FuseMount document…
claude Mar 23, 2026
90f9e20
docs(test): fix inaccurate skip description in cross-device test
claude Mar 23, 2026
1a9b4b6
docs(ci): document external test dependencies and clean up CI steps
claude Mar 23, 2026
e4fe9f4
docs: generalize skip flag references and fix fuse_probe doc
claude Mar 23, 2026
bf6ee32
docs: convert external dependencies table to bullet list
claude Mar 23, 2026
9e1e135
refactor: trim docs and include retry count in poll message
claude Mar 24, 2026
4c5bad0
refactor(test): use pipe to inline conditional arg in method chain
claude Mar 24, 2026
ddd221f
refactor: simplify conditional argument addition
KSXGitHub Mar 24, 2026
d066ce2
ci(devcontainer): expose FUSE device and install test dependencies
claude Mar 24, 2026
ee76a99
ci(devcontainer): skip FUSE and fs-error tests in Codespaces
claude Mar 24, 2026
be7c805
ci(devcontainer): only skip cross-device test, not fs-errors
claude Mar 24, 2026
f422c66
fix: preserve directory size on read_dir failure and strengthen FUSE …
claude Mar 24, 2026
9d8c235
chore(git): merge from master
claude Mar 25, 2026
d516699
chore(git): merge from master
claude Mar 25, 2026
60de739
docs(contributing): fix stale test-skip references
claude Mar 25, 2026
b0abbaa
refactor(app): move `one_file_system` between `hardlinks_handler` and…
claude Mar 27, 2026
66dbe59
refactor(fs_tree_builder): simplify visibility of device_id items to …
claude Mar 27, 2026
1729ef4
refactor: replace `one_file_system: bool` with `DeviceBoundary` enum
claude Mar 27, 2026
4863a60
refactor: arguments should stay together
KSXGitHub Mar 27, 2026
6c6cfe4
refactor: stop taking `bool`
KSXGitHub Mar 27, 2026
b48921b
refactor: use `pipe`
KSXGitHub Mar 27, 2026
15768f6
refactor(args): move `one_file_system` between `deduplicate_hardlinks…
claude Mar 27, 2026
876739a
chore(git): merge from master
claude Mar 27, 2026
4744905
feat(cli): set conflict
KSXGitHub Mar 27, 2026
27c740c
fix: completions
KSXGitHub Mar 27, 2026
077cbf0
test: set `--min-ratio`
KSXGitHub Mar 27, 2026
4e6f084
refactor(device): rename `from_1fs` to `from_one_file_system`
claude Mar 27, 2026
8b06f9e
refactor: reduce verbosity
KSXGitHub Mar 27, 2026
88c7d8d
refactor: use pipe
KSXGitHub Mar 27, 2026
30f37bc
refactor: rename some variables
KSXGitHub Mar 27, 2026
52d95d7
docs: remove the obvious error message
KSXGitHub Mar 27, 2026
cfddd52
style: add an empty line
KSXGitHub Mar 27, 2026
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
3 changes: 3 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "parallel-disk-usage (Rust only)",
"image": "mcr.microsoft.com/devcontainers/rust:1",
"remoteEnv": {
"TEST_SKIP": "cross_device_excludes_mount"
},
"customizations": {
"vscode": {
"extensions": [
Expand Down
3 changes: 3 additions & 0 deletions .devcontainer/full/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "parallel-disk-usage (full)",
"image": "mcr.microsoft.com/devcontainers/rust:1",
"remoteEnv": {
"TEST_SKIP": "cross_device_excludes_mount"
},
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "lts",
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ jobs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > $installer
bash $installer --default-toolchain $(cat rust-toolchain) -y

- name: Install external test dependencies
if: runner.os == 'Linux'
run: |
sudo apt update
sudo apt install -y squashfs-tools squashfuse fuse3

- name: Test (dev)
shell: bash
env:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ jobs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > $installer
bash $installer --default-toolchain $(cat rust-toolchain) -y

- name: Install external test dependencies
if: runner.os == 'Linux'
run: |
sudo apt update
sudo apt install -y squashfs-tools squashfuse fuse3

- name: Test (dev)
shell: bash
env:
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,16 @@ rustup toolchain install "$(< rust-toolchain)"
rustup component add --toolchain "$(< rust-toolchain)" rustfmt clippy
```

## Optional External Dependencies

Some integration tests require external (non-Cargo) tools that are **not** managed by `Cargo.toml`. These tests panic when the tools are absent; CI installs them to get full coverage.

- `squashfs-tools` (provides `mksquashfs`) — cross-device (`--one-file-system`) FUSE test
- `squashfuse` (provides `squashfuse`) — cross-device (`--one-file-system`) FUSE test
- `fuse3` (provides `fusermount3`, `/dev/fuse`) — cross-device (`--one-file-system`) FUSE test

Tests that need these tools will panic with a diagnostic message if they are missing. The panic message includes the specific `TEST_SKIP` variable to skip the test via `./test.sh`.

## Automated Checks

Before submitting, ensure:
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,4 @@ maplit = "1.0.2"
normalize-path = "0.2.1"
pretty_assertions = "1.4.1"
rand = "0.10.0"
which = "8.0.2"
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ The benchmark was generated by [a GitHub Workflow](https://github.com/KSXGitHub/

* Ignorant of reflinks (from COW filesystems such as BTRFS and ZFS).
* Does not follow symbolic links.
* Does not differentiate filesystems: Mounted folders are counted as normal folders.
* The runtime is optimized at the expense of binary size.

## Usage
Expand Down
7 changes: 7 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ How to display the numbers of bytes.

Detect and subtract the sizes of hardlinks from their parent directory totals.

<a id="option-x" name="option-x"></a><a id="one-file-system" name="one-file-system"></a>
### `--one-file-system`

* _Aliases:_ `-x`.

Skip directories on different filesystems.

<a id="top-down" name="top-down"></a>
### `--top-down`

Expand Down
2 changes: 1 addition & 1 deletion exports/completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ _pdu() {

case "${cmd}" in
pdu)
opts="-b -H -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --help --version [FILES]..."
opts="-b -H -x -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --one-file-system --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --help --version [FILES]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down
2 changes: 2 additions & 0 deletions exports/completion.elv
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ set edit:completion:arg-completer[pdu] = {|@words|
cand --deduplicate-hardlinks 'Detect and subtract the sizes of hardlinks from their parent directory totals'
cand --detect-links 'Detect and subtract the sizes of hardlinks from their parent directory totals'
cand --dedupe-links 'Detect and subtract the sizes of hardlinks from their parent directory totals'
cand -x 'Skip directories on different filesystems'
cand --one-file-system 'Skip directories on different filesystems'
cand --top-down 'Print the tree top-down instead of bottom-up'
cand --align-right 'Set the root of the bars to the right'
cand --no-sort 'Do not sort the branches in the tree'
Expand Down
1 change: 1 addition & 0 deletions exports/completion.fish
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ complete -c pdu -l threads -d 'Set the maximum number of threads to spawn. Could
complete -c pdu -l json-input -d 'Read JSON data from stdin'
complete -c pdu -l json-output -d 'Print JSON data instead of an ASCII chart'
complete -c pdu -s H -l deduplicate-hardlinks -l detect-links -l dedupe-links -d 'Detect and subtract the sizes of hardlinks from their parent directory totals'
complete -c pdu -s x -l one-file-system -d 'Skip directories on different filesystems'
complete -c pdu -l top-down -d 'Print the tree top-down instead of bottom-up'
complete -c pdu -l align-right -d 'Set the root of the bars to the right'
complete -c pdu -l no-sort -d 'Do not sort the branches in the tree'
Expand Down
2 changes: 2 additions & 0 deletions exports/completion.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Register-ArgumentCompleter -Native -CommandName 'pdu' -ScriptBlock {
[CompletionResult]::new('--deduplicate-hardlinks', '--deduplicate-hardlinks', [CompletionResultType]::ParameterName, 'Detect and subtract the sizes of hardlinks from their parent directory totals')
[CompletionResult]::new('--detect-links', '--detect-links', [CompletionResultType]::ParameterName, 'Detect and subtract the sizes of hardlinks from their parent directory totals')
[CompletionResult]::new('--dedupe-links', '--dedupe-links', [CompletionResultType]::ParameterName, 'Detect and subtract the sizes of hardlinks from their parent directory totals')
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'Skip directories on different filesystems')
[CompletionResult]::new('--one-file-system', '--one-file-system', [CompletionResultType]::ParameterName, 'Skip directories on different filesystems')
[CompletionResult]::new('--top-down', '--top-down', [CompletionResultType]::ParameterName, 'Print the tree top-down instead of bottom-up')
[CompletionResult]::new('--align-right', '--align-right', [CompletionResultType]::ParameterName, 'Set the root of the bars to the right')
[CompletionResult]::new('--no-sort', '--no-sort', [CompletionResultType]::ParameterName, 'Do not sort the branches in the tree')
Expand Down
4 changes: 3 additions & 1 deletion exports/completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ block-count\:"Count numbers of blocks"))' \
'-m+[Minimal size proportion required to appear]:MIN_RATIO:_default' \
'--min-ratio=[Minimal size proportion required to appear]:MIN_RATIO:_default' \
'--threads=[Set the maximum number of threads to spawn. Could be either "auto", "max", or a positive integer]:THREADS:_default' \
'(-q --quantity -H --deduplicate-hardlinks)--json-input[Read JSON data from stdin]' \
'(-q --quantity -H --deduplicate-hardlinks -x --one-file-system)--json-input[Read JSON data from stdin]' \
'--json-output[Print JSON data instead of an ASCII chart]' \
'-H[Detect and subtract the sizes of hardlinks from their parent directory totals]' \
'--deduplicate-hardlinks[Detect and subtract the sizes of hardlinks from their parent directory totals]' \
'--detect-links[Detect and subtract the sizes of hardlinks from their parent directory totals]' \
'--dedupe-links[Detect and subtract the sizes of hardlinks from their parent directory totals]' \
'-x[Skip directories on different filesystems]' \
'--one-file-system[Skip directories on different filesystems]' \
'--top-down[Print the tree top-down instead of bottom-up]' \
'--align-right[Set the root of the bars to the right]' \
'--no-sort[Do not sort the branches in the tree]' \
Expand Down
3 changes: 3 additions & 0 deletions exports/long.help
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Options:

[aliases: --detect-links, --dedupe-links]

-x, --one-file-system
Skip directories on different filesystems

--top-down
Print the tree top-down instead of bottom-up

Expand Down
2 changes: 2 additions & 0 deletions exports/short.help
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Options:
How to display the numbers of bytes [default: metric] [possible values: plain, metric, binary]
-H, --deduplicate-hardlinks
Detect and subtract the sizes of hardlinks from their parent directory totals [aliases: --detect-links, --dedupe-links]
-x, --one-file-system
Skip directories on different filesystems
--top-down
Print the tree top-down instead of bottom-up
--align-right
Expand Down
10 changes: 10 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub use sub::Sub;
use crate::{
args::{Args, Quantity, Threads},
bytes_format::BytesFormat,
device::DeviceBoundary,
get_size::{GetApparentSize, GetSize},
hardlink,
json_data::{JsonData, JsonDataBody, JsonShared, JsonTree},
Expand Down Expand Up @@ -133,6 +134,13 @@ impl App {
.pipe(Err);
}

#[cfg(not(unix))]
if self.args.one_file_system {
return crate::runtime_error::UnsupportedFeature::OneFileSystem
.pipe(RuntimeError::UnsupportedFeature)
.pipe(Err);
}

let threads = match self.args.threads {
Threads::Auto => {
let disks = Disks::new_with_refreshed_list();
Expand Down Expand Up @@ -283,6 +291,7 @@ impl App {
progress: $progress,
#[cfg(unix)] deduplicate_hardlinks: $hardlinks,
#[cfg(not(unix))] deduplicate_hardlinks: _,
one_file_system,
files,
json_output,
bytes_format,
Expand All @@ -299,6 +308,7 @@ impl App {
bar_alignment: BarAlignment::from_align_right(align_right),
size_getter: <$size_getter as GetSizeUtils>::INSTANCE,
hardlinks_handler: <$size_getter as CreateHardlinksHandler<{ cfg!(unix) && $hardlinks }, $progress>>::create_hardlinks_handler(),
device_boundary: DeviceBoundary::from_one_file_system(one_file_system),
reporter: <$size_getter as CreateReporter<$progress>>::create_reporter(report_error),
bytes_format: <$size_getter as GetSizeUtils>::formatter(bytes_format),
files,
Expand Down
5 changes: 5 additions & 0 deletions src/app/sub.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
args::{Depth, Fraction},
data_tree::DataTree,
device::DeviceBoundary,
fs_tree_builder::FsTreeBuilder,
get_size::GetSize,
hardlink::{DeduplicateSharedSize, HardlinkIgnorant, RecordHardlinks},
Expand Down Expand Up @@ -43,6 +44,8 @@ where
pub size_getter: SizeGetter,
/// Handle to detect, record, and deduplicate hardlinks.
pub hardlinks_handler: HardlinksHandler,
/// Whether to cross device boundary into a different filesystem.
pub device_boundary: DeviceBoundary,
/// Reports measurement progress.
pub reporter: Report,
/// Minimal size proportion required to appear.
Expand Down Expand Up @@ -71,6 +74,7 @@ where
max_depth,
size_getter,
hardlinks_handler,
device_boundary,
reporter,
min_ratio,
no_sort,
Expand All @@ -86,6 +90,7 @@ where
root,
size_getter,
hardlinks_recorder: &hardlinks_handler,
device_boundary,
max_depth,
}
.into()
Expand Down
7 changes: 6 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub struct Args {
/// Read JSON data from stdin.
#[clap(
long,
conflicts_with_all = ["quantity", "deduplicate_hardlinks"]
conflicts_with_all = ["quantity", "deduplicate_hardlinks", "one_file_system"]
)]
pub json_input: bool,

Expand All @@ -112,6 +112,11 @@ pub struct Args {
#[cfg_attr(not(unix), clap(hide = true))]
pub deduplicate_hardlinks: bool,

/// Skip directories on different filesystems.
#[clap(long, short = 'x')]
Comment thread
KSXGitHub marked this conversation as resolved.
#[cfg_attr(not(unix), clap(hide = true))]
pub one_file_system: bool,

/// Print the tree top-down instead of bottom-up.
#[clap(long)]
pub top_down: bool,
Expand Down
17 changes: 17 additions & 0 deletions src/device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// Whether to cross device boundary into a different filesystem.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceBoundary {
Cross,
Stay,
}

impl DeviceBoundary {
/// Derive device boundary from `--one-file-system`.
#[cfg(feature = "cli")]
pub(crate) fn from_one_file_system(one_file_system: bool) -> Self {
match one_file_system {
false => DeviceBoundary::Cross,
true => DeviceBoundary::Stay,
}
}
Comment thread
KSXGitHub marked this conversation as resolved.
}
Loading
Loading