Summary 🗣️
Note
Concurrent Git change-summary engine. Discovers every repository under a root directory, sorts its tags chronologically, and emits clean, deduplicated diffs for each release window - consecutive tags, latest tag to HEAD, or the entire history when a repository has no tags. Parallel by design: hundreds of repositories summarised in seconds. One binary. Zero configuration. Read your whole fleet's history at a glance.
Terminal
cargo install psummaryThe crate is published as psummary and installs two binaries with
identical functionality:
Summary- the primary binaryPSummary- capitalized alias for case-insensitive filesystems
Important
The installed binaries are named Summary and PSummary - only the crate
itself is called psummary.
Terminal
git clone https://github.com/PlayForm/Summary.git
cd Summary
cargo build --releaseSummarising what changed across many repositories is a chore: open each repo,
list its tags, figure out which release pair matters, run git diff, and
repeat - then repeat again for the next tag, and again for the next repo.
git diff output itself is noisy: binary files, whitespace churn, lockfiles,
and changelogs drown out the changes that matter, and every repository repeats
the same manual ceremony.
Summary collapses the whole loop into one command. It walks a root directory,
finds every repository, builds the release timeline from tag timestamps, and
prints only the semantic changes - additions and deletions - grouped by release
window, deduplicated, and ordered longest-first.
Pipeline
ANY ROOT ───────► Summary ───────► stdout (grouped, deduplicated)
│
├─ discover → walkdir over --Root, skipping --Exclude
├─ identify → path whose last component matches --Pattern (.git)
├─ window → tags sorted by commit timestamp
│ • tag₁ → tag₂, tag₂ → tag₃, …
│ • latest tag → HEAD
│ • first commit → last commit (untagged repos)
├─ diff → git2 diff, 53 binary extensions + --Omit regex
└─ aggregate → DashMap + DefaultHasher, dedupe, sorted print
Six steps, all in Rust:
- Discover -
walkdirtraverses the filesystem from--Root, filtering entries against--Exclude; a segment whose name equals--Patternis never excluded, so.gitis always found - even insidenode_modules. - Identify - repositories are paths whose last component matches
--Pattern(default.git). - Window - each repository's tags are resolved to commit timestamps and sorted chronologically; diffs are generated between every consecutive tag pair, plus the latest tag to HEAD. Untagged repositories collapse to a single first-commit-to-last-commit window.
- Diff -
git2::DiffOptionswithindent_heuristic,minimal,force_text,ignore_filemode,ignore_case, and the fullignore_whitespace*family; aregex::RegexSetof 53 built-in binary extensions plus user--Omitpatterns drops unwanted files before output. - Deduplicate - each diff is hashed with
std::collections::hash_map::DefaultHasherinto aDashMap; identical diffs inside a window collapse to a single entry. - Aggregate - windows are grouped by their
🗣️ Summary from … in …header, sorted alphabetically, and printed with the longest diffs first.
Layout
Source/
├── Library.rs ← #[tokio::main] entry point
├── Struct/
│ ├── Binary/
│ │ └── Command.rs ← wiring: path separator, parallel/sequential dispatch
│ └── Summary/
│ └── Difference.rs ← --Omit pattern holder
└── Fn/
├── Binary/
│ ├── Command.rs ← clap CLI definition (builder API)
│ └── Command/
│ ├── Entry.rs ← walkdir discovery + --Exclude filtering
│ ├── Parallel.rs ← rayon scan + tokio tasks (FuturesUnordered)
│ └── Sequential.rs ← join_all fallback
└── Summary.rs ← per-repo tag chronology + diff windows
└── Summary/
├── Difference.rs ← git2 diff options + regex omit + binary filter
├── Insert.rs ← DashMap insertion, hashed key
│ └── Hash.rs ← std DefaultHasher
├── First.rs ← first-commit revwalk (topological, reversed)
└── Group.rs ← dedupe, sort, print
| Mode | Discovery | Per-repo work | Failure handling |
|---|---|---|---|
Parallel (-P) |
rayon into_par_iter() |
tokio spawn() + FuturesUnordered → mpsc |
logged to stderr, continues |
| Sequential (default) | plain iteration | futures::join_all of async per-repo tasks |
errors collected, repo skipped |
All diffing is done in-process via git2 - no shelling out to git.
graph LR
subgraph main
A[Start] --> B[Parse command-line arguments]
B --> C[Generate entry paths]
C --> D[Process entries]
D --> E[Generate summaries]
E --> F[Output results]
F --> G[End]
end
subgraph Process entries
subgraph Entry processing
H[Filter and process entries] --> I[Generate file paths]
end
subgraph Parallel processing
J[Spawn tasks] --> K[Generate summaries]
K --> L[Collect results]
end
subgraph Sequential processing
M[Process entries one by one] --> N[Generate summaries]
end
end
subgraph Generate summaries
O[Retrieve commits] --> P[Generate diffs]
P --> Q[Insert into DashMap]
end
Terminal
Summary 🗣️
Usage: Summary [OPTIONS]
Options:
-P, --Parallel Parallel ⏩
-R, --Root <ROOT> Root 📂 [default: .]
-E, --Exclude <EXCLUDE> Exclude 🚫 [default: node_modules]
--Pattern <PATTERN> Pattern 🔍 [default: .git]
-O, --Omit <OMIT> Omit 🚫 [default: (?i)documentation (?i)target (?i)changelog\.md$ (?i)summary\.md$]
-h, --help Print help
-V, --version Print version
1. Summarise every repository under the current directory
Terminal
Summary -P2. Scan a projects folder and save the report
Terminal
Summary -P -R ~/Developer > changes.diff3. Skip common build directories
Terminal
Summary -P -E "node_modules target dist"Run against this repository itself, Summary emits one block per release
window - each headed by 🗣️ Summary from <start> to <end> in <repo>, where
<repo> is the path relative to --Root:
Output
🗣️ Summary from Summary/v0.0.1 to Summary/v0.0.2 in .
diff --git a/Cargo.toml b/Cargo.toml
index 745ad03..c769c35 100644
--- a/Cargo.toml
+++ b/Cargo.toml
- version = "0.0.1"
+ version = "0.0.2"A single window can span many files at once - this one between v0.0.2 and v0.0.3
captures a build-script reformat, a dependency addition, and a version bump in
one pass (the full window also touches README.md and
Source/Fn/Binary/Command.rs):
Output
🗣️ Summary from Summary/v0.0.2 to Summary/v0.0.3 in .
diff --git a/build.rs b/build.rs
index 73ccc94..1f0de60 100644
--- a/build.rs
+++ b/build.rs
- use serde::Deserialize;
- use std::fs;
-
+
+ use serde::Deserialize;
+ use std::fs;
diff --git a/Cargo.toml b/Cargo.toml
index c769c35..c10016a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
+ regex = "1.10.5"
- version = "0.0.2"
+ version = "0.0.3"The newest window of a tagged repository reads
Summary from <latest> to last commit; an untagged repository collapses to
Summary from first commit to last commit.
Warning
The output is a lossy summary, not a patch: hunk headers, context lines, and
binary content are stripped, so it cannot be fed to git apply. It is a
reading aid, not a replay mechanism.
Everything lives on the command line - no config file, no environment setup.
| Option | Meaning | Default |
|---|---|---|
-P, --Parallel |
enable parallel mode | off (sequential) |
-R, --Root <ROOT> |
directory to start scanning from | . |
-E, --Exclude <EXCLUDE> |
space-separated directory names to skip | node_modules |
--Pattern <PATTERN> |
last path component that marks a repository | .git |
-O, --Omit <OMIT> |
repeatable regex; matching files are dropped from every diff | (?i)documentation (?i)target (?i)changelog\.md$ (?i)summary\.md$ |
Important
--Exclude splits on spaces and matches as a substring of any path segment
- but a segment whose name equals
--Patternis never excluded, so.gitis always discovered, even insidenode_modules.--Patternmatches the last path component only, which is what makes.gitwork.
Tip
Only local tags are analysed. Run git fetch --tags first to include
remote tags. --Omit patterns are case-sensitive by default; prefix with
(?i) for case-insensitive matching (the built-in defaults already do).
Terminal
# Skip lockfiles, markdown files, and dist folders entirely
Summary -P -O ".*\.lock$" -O "(?i)\.md$" -O "/dist/"Summary processes repositories concurrently, making it dramatically faster
than running sequential git commands manually. In typical scenarios scanning
100+ repositories:
| Operation | Parallel Time | Sequential Time | Speedup |
|---|---|---|---|
| Generate tag diffs | ~2-3 seconds | ~15-20 seconds | 6-8x |
| Diff all commits (no tags) | ~2-3 seconds | ~12-18 seconds | 6-8x |
(Actual performance depends on repository count, sizes, and I/O speed)
The parallelism splits naturally: rayon handles the CPU-bound path scan, tokio
spawns one async task per repository, and DashMap aggregates without lock
contention.
Summary is built with these excellent Rust crates:
clap- ergonomic command-line parsing (builder API)git2- libgit2 bindings: repositories, tags, trees, diffsrayon- data-parallel path scanningtokio- async runtime (full) for concurrent diff generationwalkdir- efficient cross-platform directory traversalregex-RegexSetfor omit patterns and binary extensionsdashmap- sharded concurrent hash map for lock-free aggregationfutures-FuturesUnorderedandjoin_alltask orchestrationchrono- tag timestamp resolution and chronologyitertools-sorted_by,sorted_by_keyresult ordering
Build-time: serde (derive) and
toml stamp the package version into the
binary via build.rs.
Note
num_cpus and unbug are declared in Cargo.toml but not referenced by the
source - they are listed for completeness only.
Summary automatically excludes these 53 binary file types from diffs using
case-insensitive patterns:
.7z .accdb .avi .bak .bin .bmp .class .dat .db .dll .dll.lib .dll.exp
.doc .docx .dylib .exe .flac .gif .gz .heic .ico .img .iso .jpeg .jpg
.m4a .mdb .mkv .mov .mp3 .mp4 .o .obj .ogg .pdb .pdf .png .ppt .pptx
.pyc .pyo .rar .so .sqlite .svg .tar .tiff .wav .webp .wmv .xls .xlsx .zip
The filter operates on file paths only - content is never inspected. The full
list lives in
Source/Fn/Summary/Difference.rs.
| Want to… | Start here |
|---|---|
| Report a bug | Open an issue |
| Suggest a feature | Start a discussion |
| Submit a PR | Fork & open a PR |
| Ask a question | Discussions Q&A |
Please read CONTRIBUTING.md and
CODE_OF_CONDUCT.md first. No contribution is too small -
first-time contributors are especially welcome.
Released under CC0-1.0 - public domain. Use, modify, distribute, and build upon it freely.
Built with ❤️ by PlayForm.