Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f4b973d
fix(ui): stable DJB2 directory hues (no abs-trap) + remove silent 200…
Ti-03 Jul 5, 2026
f36dd88
merge: stable directory hues + full child listing
Ti-03 Jul 5, 2026
8c3c1e4
fix(refresh): allocated sizes, recursive new-dir scan, symlink/hidden…
Ti-03 Jul 5, 2026
18a16cc
perf(duplicates): parallel bounded hashing, single-read small files, …
Ti-03 Jul 5, 2026
760068d
merge: FSEvents refresh path fixes
Ti-03 Jul 5, 2026
ba0d4e0
merge: parallel duplicate hashing
Ti-03 Jul 5, 2026
f2efab0
perf(scanner): hoist UserDefaults out of hot path + bound task fan-out
Ti-03 Jul 5, 2026
726fed1
merge: scanner hot-path and fan-out fixes
Ti-03 Jul 5, 2026
69933ec
fix(refresh): don't resurrect deduplicated hardlinks to full size on …
Ti-03 Jul 5, 2026
e95ee04
chore(diag): env-gated scan diagnostics (MDS_DEBUG_SKIPS, MDS_DEBUG_T…
Ti-03 Jul 6, 2026
319117c
build(release): drop App Sandbox so Full Disk Access works (Developer…
Ti-03 Jul 6, 2026
020a48b
merge: de-sandbox release build
Ti-03 Jul 6, 2026
876412a
feat(trust): surface access-denied folders + FDA banner + hidden-spac…
Ti-03 Jul 6, 2026
78d392e
merge: access-denied surfacing + hidden-space node (resolved onto fix…
Ti-03 Jul 6, 2026
e180618
perf(duplicates): honor cancellation inside chunked hash loops
Ti-03 Jul 6, 2026
87c1712
fix(refresh): inode-identity hardlink handling (no double-count on ne…
Ti-03 Jul 6, 2026
d04a2de
feat(fda): guided Full Disk Access onboarding — explainer sheet, live…
Ti-03 Jul 6, 2026
88757c5
merge: guided Full Disk Access onboarding
Ti-03 Jul 6, 2026
462717b
build(xcode): add FullDiskAccessSheet.swift to the app target
Ti-03 Jul 6, 2026
9418926
merge: scanner refresh + algo bug fixes (bounded fan-out, hardlink re…
Ti-03 Jul 24, 2026
27a334c
perf(scanner): getattrlistbulk enumeration + iterative bounded traversal
Ti-03 Jul 24, 2026
0fe30ae
perf(scanner): auto-summarize tiny-file directories (node_modules etc.)
Ti-03 Jul 24, 2026
d3f809d
perf(scanner): parallel auto-summary walk
Ti-03 Jul 24, 2026
f7de751
WIP(phase2): flat FileTree store — app target green, tests not yet mi…
Ti-03 Jul 24, 2026
7441b49
perf(model): flat FileTree store replacing FSNode class tree
Ti-03 Jul 24, 2026
32dcd70
build(xcode): add scanner files to app target
Ti-03 Jul 24, 2026
bf9bedc
feat(cleanup): prune trashed items from the tree instead of rescanning
Ti-03 Jul 24, 2026
4b44ccb
feat(archive): save and reopen scans as .mdscan snapshots
Ti-03 Jul 24, 2026
1aded6b
feat(compare): diff two scans (added/removed/grown/shrank)
Ti-03 Jul 24, 2026
3c9d29c
perf(refresh): incremental subtree splice instead of full rescan
Ti-03 Jul 24, 2026
1c3f6ba
fix(scanner): make summary walk async, removing thread-blocking bridge
Ti-03 Jul 24, 2026
14303ac
fix(compare): report file<->directory type changes instead of droppin…
Ti-03 Jul 24, 2026
a2783e2
fix(cleanup): promote surviving hardlink twin when trashing a size ca…
Ti-03 Jul 24, 2026
52a663b
fix(refresh): keep hardlink dedup across the splice boundary
Ti-03 Jul 24, 2026
1560485
test(scanner): cover summary-walk cancellation with a test that has t…
Ti-03 Jul 24, 2026
550ce90
fix(refresh): promote surviving hardlink twin when a carrier vanishes…
Ti-03 Jul 24, 2026
b97fbf8
fix(model): re-sort the removed node's ancestor chain after subtree r…
Ti-03 Jul 24, 2026
9b5d32c
fix(refresh): never restart the scan from a live filesystem change
Ti-03 Jul 27, 2026
7e429e3
fix(scanner): stop dropping every macOS firmlink
Ti-03 Jul 27, 2026
e5c77d4
fix(model): give the synthetic hidden-space node its own child span
Ti-03 Jul 27, 2026
89628e1
fix(fda): don't claim access is granted when folders are still blocked
Ti-03 Jul 27, 2026
7be7884
build: bump to 1.2 (build 3) for the notarized test build
Ti-03 Jul 27, 2026
ba53983
feat(fda): drag the app icon straight into the permission list
Ti-03 Jul 28, 2026
18f7433
feat(settings): add a Permissions section with live status and drag tile
Ti-03 Jul 28, 2026
c689df9
build: bump to build 4 for the notarized share build
Ti-03 Aug 2, 2026
ca19161
build: release a notarized DMG of the app, and bump to 1.3.0
Ti-03 Aug 5, 2026
4995361
docs: changelog, README refresh, and ADRs for the scanner and store r…
Ti-03 Aug 5, 2026
a2068b5
docs: drop em dashes from the lines this branch added to the README
Ti-03 Aug 5, 2026
7cb2fa2
ci(release): skip signing cleanly when no credentials are configured
Ti-03 Aug 5, 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
174 changes: 163 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,192 @@ permissions:
contents: write
id-token: write

# Ships the actual macOS app, Developer ID signed and notarized, as a DMG.
#
# This used to package `swift build -c release` output instead: a bare SwiftPM
# executable, unsigned and un-notarized, which is not something anyone can
# install by double-clicking. That is what v1.2.0 shipped (a 560 KB zip) and
# what issue #22 reported.
#
# A DMG rather than a zip is deliberate. The app embeds Sparkle.framework,
# which contains symlinks, and messaging apps, webmail and third-party
# unarchivers routinely flatten symlinks out of a zip. That breaks the
# framework, invalidates the signature, and makes macOS report the app as
# "damaged". A disk image carries the bundle byte for byte.
#
# Required repository secrets:
# MACOS_CERTIFICATE Developer ID Application cert, .p12, base64 encoded
# MACOS_CERTIFICATE_PWD password for that .p12
# MACOS_SIGNING_IDENTITY e.g. "Developer ID Application: Name (TEAMID)"
# KEYCHAIN_PASSWORD any throwaway string for the temporary keychain
# NOTARY_APPLE_ID Apple ID used for notarization
# NOTARY_TEAM_ID Apple Developer team ID
# NOTARY_PASSWORD app-specific password for that Apple ID
jobs:
build-sign-release:
runs-on: macos-26
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Build release binary
run: swift build -c release
- name: Run tests before shipping anything
run: swift test

- name: Package artifact
# Signing and notarization need credentials that intentionally may not
# be present: the Developer ID private key is kept on the maintainer's
# machine rather than uploaded here, and releases are then built and
# published by hand. Without this probe a tag would start the job and
# fail partway, which is the failure mode that produced the unusable
# v1.2.0 asset. When the secrets are absent the job runs the tests and
# stops cleanly instead.
- name: Can this runner sign?
id: caps
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
run: |
if [ -n "$MACOS_CERTIFICATE" ]; then
echo "can_sign=true" >> "$GITHUB_OUTPUT"
else
echo "can_sign=false" >> "$GITHUB_OUTPUT"
echo "::notice::No signing secrets configured. Tests ran; publish the notarized DMG manually. See the secret list at the top of this workflow to automate it."
fi

# The signing identity has to live in a keychain the build can see. A
# dedicated, throwaway keychain keeps it out of the default one and
# disappears with the runner.
- name: Import Developer ID certificate
if: steps.caps.outputs.can_sign == 'true'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
set -euo pipefail
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/cert.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 3600 build.keychain
security import /tmp/cert.p12 -k build.keychain \
-P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm -f /tmp/cert.p12

- name: Archive the app
if: steps.caps.outputs.can_sign == 'true'
run: |
set -euo pipefail
xcodebuild -project MacDirStat.xcodeproj \
-scheme MacDirStat \
-configuration Release \
-destination 'platform=macOS' \
-archivePath "$RUNNER_TEMP/DirStat.xcarchive" \
archive

- name: Export with Developer ID
if: steps.caps.outputs.can_sign == 'true'
env:
NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }}
run: |
BIN=$(swift build -c release --show-bin-path)
ditto -c -k --keepParent "$BIN/MacDirStat" "MacDirStat-${GITHUB_REF_NAME}.zip"
set -euo pipefail
cat > "$RUNNER_TEMP/export.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>${NOTARY_TEAM_ID}</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
EOF
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/DirStat.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/export.plist" \
-exportPath "$RUNNER_TEMP/export"

# The Applications symlink is what makes the familiar "drag me across"
# install window work.
- name: Build the DMG
if: steps.caps.outputs.can_sign == 'true'
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p "$RUNNER_TEMP/dmgroot"
ditto "$RUNNER_TEMP/export/DirStat.app" "$RUNNER_TEMP/dmgroot/DirStat.app"
ln -sf /Applications "$RUNNER_TEMP/dmgroot/Applications"
hdiutil create -volname "DirStat $VERSION" \
-srcfolder "$RUNNER_TEMP/dmgroot" \
-ov -format UDZO "DirStat-${VERSION}.dmg"
codesign --sign "$MACOS_SIGNING_IDENTITY" --timestamp "DirStat-${VERSION}.dmg"

- name: Notarize and staple
if: steps.caps.outputs.can_sign == 'true'
env:
NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }}
NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
xcrun notarytool submit "DirStat-${VERSION}.dmg" \
--apple-id "$NOTARY_APPLE_ID" \
--team-id "$NOTARY_TEAM_ID" \
--password "$NOTARY_PASSWORD" \
--wait
xcrun stapler staple "DirStat-${VERSION}.dmg"

# Proves the artifact is installable before it is published: a stapled
# ticket Gatekeeper accepts, and an app inside the image whose signature
# survives with its symlinks intact.
- name: Verify the DMG the way a user receives it
if: steps.caps.outputs.can_sign == 'true'
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
xcrun stapler validate "DirStat-${VERSION}.dmg"
spctl -a -vvv -t open --context context:primary-signature "DirStat-${VERSION}.dmg"
hdiutil attach "DirStat-${VERSION}.dmg" -nobrowse -mountpoint /tmp/dmgcheck
codesign --verify --deep --strict --verbose=2 /tmp/dmgcheck/DirStat.app
spctl -a -vvv -t exec /tmp/dmgcheck/DirStat.app
hdiutil detach /tmp/dmgcheck

- name: Install cosign
if: steps.caps.outputs.can_sign == 'true'
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

# Keyless signing: cosign exchanges the job's OIDC token for a
# short-lived certificate from Fulcio and logs the signature in the
# Rekor transparency log. --yes acknowledges that upload. cosign v3
# writes signature + certificate + tlog proof as one .bundle file
# (the old --output-signature/--output-certificate flags are gone).
- name: Sign artifact (Sigstore keyless)
- name: Sign the DMG (Sigstore keyless)
if: steps.caps.outputs.can_sign == 'true'
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
cosign sign-blob --yes \
--bundle "MacDirStat-${GITHUB_REF_NAME}.zip.cosign.bundle" \
"MacDirStat-${GITHUB_REF_NAME}.zip"
--bundle "DirStat-${VERSION}.dmg.cosign.bundle" \
"DirStat-${VERSION}.dmg"

- name: Create GitHub release with signed artifact
- name: Create GitHub release with the signed DMG
if: steps.caps.outputs.can_sign == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
gh release create "$GITHUB_REF_NAME" \
--title "MacDirStat $GITHUB_REF_NAME" \
--generate-notes \
"MacDirStat-${GITHUB_REF_NAME}.zip" \
"MacDirStat-${GITHUB_REF_NAME}.zip.cosign.bundle"
"DirStat-${VERSION}.dmg" \
"DirStat-${VERSION}.dmg.cosign.bundle"

- name: Remove the temporary keychain
if: always() && steps.caps.outputs.can_sign == 'true'
run: security delete-keychain build.keychain || true
115 changes: 115 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Changelog

All notable changes to MacDirStat are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.3.0] - 2026-08-04

A performance-focused release: the scanner, the in-memory tree, and live
refresh were all rebuilt for speed on very large volumes, plus new ways to
save, reopen, and compare scans.

### Added

- **Auto-summarization of dependency and cache folders**: directories like
`node_modules` (and anything else that looks like thousands of tiny
files, by heuristic) collapse into a single summary row carrying the
total size and file count, instead of building a chart node for every
file inside. On a real Projects folder this cut a scan from 16.0s /
910,550 nodes to 8.6s / 104,551 nodes, with byte-identical totals.
- **Save and reopen scans**: File > Save Scan... writes the current scan to
an `.mdscan` file; File > Open Scan... reopens it later as a read-only
snapshot (no delete actions, no live watching), with a banner showing
when it was captured. Opening a corrupted or tampered file surfaces an
error instead of crashing.
- **Compare two scans**: File > Compare With Saved Scan shows what changed
between now and a saved snapshot: added, removed, grown, shrunk, and
files that were replaced by a folder (or vice versa) at the same path. A
whole added or removed directory collapses into a single row instead of
listing every file inside it.
- **Drag-to-grant Full Disk Access**: the guided permission sheet now shows
the running app's own icon as a draggable tile, so granting access can't
accidentally target the wrong build sitting in a file picker (macOS ties
the grant to one exact copy). A "Show this app in Finder" fallback covers
drag-and-drop from a Finder window instead.
- **Permissions section in Settings**: check and fix Full Disk Access
directly from Settings, with a live coloured-dot status, the same
drag-to-grant tile, and a direct link to the right System Settings pane.

### Changed

- The scanner now enumerates directories with `getattrlistbulk(2)`,
reading names and metadata in one batched syscall per directory instead
of one `readdir` plus one `fstatat` per entry, with a `readdir` fallback
for filesystems that don't support it. Traversal itself moved from
unbounded recursive fan-out to a bounded worker pool, so a scan no longer
spawns more concurrent work than the machine can use.
- The in-memory scan result is now a flat, contiguous store instead of a
tree of individual objects per file and folder, which noticeably lowers
memory use and speeds up sorting and layout on very large scans.
- Live refresh (the automatic re-scan while a folder is open and being
watched) now patches only the part of the tree that actually changed
instead of rescanning the whole root, so background file activity no
longer causes a visible full reload.
- Move to Trash now removes the deleted item from the current scan
directly instead of triggering a full rescan, so deleting from the
Duplicates view no longer bounces you back to the Treemap tab. Fixes
[#5](https://github.com/Ti-03/MacDirStat/issues/5).

### Fixed

- Scanning `/` (or any volume root) could dramatically under-report disk
usage, in one case showing 11.8 GB instead of the real 479.3 GB, because
`/Users`, `/Applications`, `/Library`, `/opt`, `/private`, `/Volumes` and
`/cores` are macOS firmlinks and were being silently dropped as 0 bytes.
Directory identity, mount-boundary checks, and alias de-duplication are
now all decided from the opened directory instead of from what the
parent folder's listing claimed, which is the only place a firmlink
resolves correctly.
- A background summarization pass (used for `node_modules`-style folders)
could, under load, block every available concurrency thread at once and
wedge a scan so it never finished and could not be stopped. It is now
fully asynchronous end to end.
- Scanning a volume root and then letting a live file change happen could
crash the app outright ("Index out of range") because of a bookkeeping
gap in the synthetic "Hidden & Unreadable Space" entry.
- A live filesystem change that couldn't be folded into the current scan
incrementally could escalate into a full rescan that cleared the
Treemap mid-render and then repeated forever. Such changes are now
skipped with a "Rescan" button offered instead, rather than looping.
- Comparing two scans now reports a file replaced by a folder (or a folder
replaced by a file) at the same path as a change, instead of dropping it
from the results entirely.
- Fixed several cases where disk usage from hardlinked files could be
under- or double-counted after trashing a file, after a live refresh, or
after an external process (Finder, `rm`, a build tool) deleted the copy
that was carrying the reported size, including ordering glitches in
ancestor folders left over from the fix.
- The Full Disk Access sheet no longer claims "Access granted!" when the
most recent scan still hit denied folders; it only shows success for a
grant it actually watched happen while it was open.
- GitHub Releases now include a proper `.dmg` installer image alongside
the signed zip, instead of shipping only a bare executable archive.
Fixes [#22](https://github.com/Ti-03/MacDirStat/issues/22).

Test suite grew from 50 to 142 tests across this cycle, all passing.

## [1.2.0] - 2026-07-24

- MkDocs Material documentation site and the first Architecture Decision
Record.
- Unit tests and a GitHub Actions CI workflow.
- Dependabot, Actions pinned to commit SHAs, and a least-privilege CI
token (OpenSSF Scorecard fixes), plus a SECURITY.md.
- Sigstore-signed release artifacts (keyless cosign).
- CONTRIBUTING, CODE_OF_CONDUCT, GOVERNANCE, issue templates, and a
license notice for the community.

## [1.1] - 2026-05-10

- macOS 13 Ventura and later are now supported (previously macOS 26 only).
- The Liquid Glass UI gracefully falls back on older macOS versions.

## [1.0] - 2026-05-04

- First public release.
Loading
Loading