Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,44 @@ and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v
## [Unreleased]

### Added
- **Commit-level filters: `--author`, `--committer`, `--start-date`,
`--end-date`, `--text` (ADR-0035).** Review a *set of commits* rather than a
single diff. `git diff` has no author/date/message options — those are
`git log` options — so setting any of these switches diff acquisition to a
commit walk: pick the matching commits, then concatenate their patches.
Different filter kinds are AND'd, multiple values of one kind are OR'd, so
`--author alice --author bob --start-date 2026-06-01` means "(Alice or Bob)
and since June". Identity matching is a case-insensitive substring over both
the name and the email. `--end-date` is **inclusive** of the day named
(git's bare `--until` stops at that day's midnight and silently drops it).
`--text` matches the commit message *and* the name of a branch, in which
case the commits unique to that branch are pulled in — best-effort by
nature, since a squash- or rebase-merged branch no longer owns its commits.
With no explicit range the walk covers `HEAD`; a subcommand's range bounds
it (`commitbrief diff main..develop --author alice`).
Two modifiers shape a walk but never start one, and are rejected if used
alone: `--max-commits N` (default 200) caps the selection and always reports
truncation rather than silently reviewing a subset, and `--merges` keeps
merge commits, which are excluded by default.
Available on the default review, `diff`, `summary`, `dry-run`, the MCP
`review` tool and `guard`. Rejected by `commit` (it describes the staged
index, which has no commits) and by `remote pr` (its diff comes from
`gh pr diff`, not local git).
- **`--exclude-file` / `--exclude-dir` path denylists.** The inverse of
`--file` / `--dir`, sharing their exact matching rules (literal path or
gitignore-style glob) and applied after them, so an exclusion always wins:
`--dir internal --exclude-dir internal/cli`. An invalid glob errors before
any provider call, as it does for the allowlist.
- **Path and commit filters are now reachable over MCP.** The `review` tool
gained `file`, `dir`, `exclude_file`, `exclude_dir`, `author`, `committer`,
`start_date`, `end_date`, `text`, `max_commits` and `merges` arguments.
`--file` / `--dir` were previously CLI-only in practice: the MCP seam resets
the global flag state, so a host had no way to narrow a review by path.
`guard` forwards the same set from its inherited persistent flags.
- **`meta.filtered_commits` in the JSON output.** Optional, `omitempty`, so
schema stays `1` — the count of commits whose patches make up the reviewed
diff. `dry-run` gains matching `Commits (walked)` / `Commits (matched)` lines
and an `--exclude-file/--exclude-dir` row in its per-layer file accounting.
- **`commitbrief upgrade` — in-tool updates across every install method (ADR-0034).**
Detects whether the running binary came from Homebrew, Scoop, `go install`
or a GitHub Releases tarball. Package-managed installs are delegated to
Expand All @@ -24,6 +62,12 @@ and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v
The version check runs **only** when you invoke the command: there is no
automatic update check and no telemetry.

### Fixed
- `commitbrief remote pr` now applies `--file` / `--dir` on the **posting**
path too. Only the `--no-post` path honored them, so a narrowed run that
commented on GitHub reviewed a different file set than the same command with
`--no-post`.

## [1.14.0] - 2026-07-25

### Added
Expand Down
61 changes: 59 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ commitbrief --unstaged --dir database/seeder --dir app/Models
commitbrief diff HEAD~3 HEAD --dir docs
commitbrief --staged --file '*.go' # gitignore-style glob (any depth)
commitbrief --staged --file 'internal/**/*.ts' # anchored recursive glob
commitbrief --staged --exclude-file '*_test.go' # denylist; wins over the includes
commitbrief --staged --dir internal --exclude-dir internal/cli

# Select the commits themselves — author, date window, message or branch name.
# Any of these walks history instead of reading the index, so they replace
# --staged/--unstaged rather than combining with them.
commitbrief --author alice --author bob # either person's commits
commitbrief --author alice@example.com # name or email, case-insensitive
commitbrief --start-date 2026-01-01 # on or after (inclusive)
commitbrief --end-date 2026-03-31 # on or before (inclusive)
commitbrief --text payment # commit message OR branch name
commitbrief --committer carol --merges # committer identity; keep merges
commitbrief --author alice --start-date 2026-06-01 --dir internal # all combinable
commitbrief diff main..develop --author alice # bound the walk to a range

# Plain-language change digest (read-only; no findings)
commitbrief summary # what's staged, grouped by area
Expand Down Expand Up @@ -851,7 +865,44 @@ Review content lives in two files:

## Filtering

Three layers, applied in order. Later layers win, so a `!pattern` in
Two independent axes: **which commits** are reviewed, and **which files**
within them.

### Commit filters (ADR-0035)

`--author`, `--committer`, `--start-date`, `--end-date` and `--text` select a
set of commits. Setting any of them switches the scope from "the index" to a
history walk, so they cannot be combined with `--staged` / `--unstaged` — those
have no commits yet. `commitbrief commit` and `commitbrief remote pr` reject
them outright (the first describes the index; the second reads its diff from
`gh`, not local git).

| Flag | Matches |
|---|---|
| `--author` | author name **or** email, case-insensitive substring; repeatable, OR'd |
| `--committer` | committer name or email; repeatable, OR'd |
| `--start-date YYYY-MM-DD` | author date on or after this day (inclusive) |
| `--end-date YYYY-MM-DD` | author date on or before this day (**inclusive** — unlike git's bare `--until`) |
| `--text` | the commit message, **plus** commits unique to a branch whose name contains the text |
| `--max-commits N` | cap the selection (default 200); truncation is always reported |
| `--merges` | keep merge commits, which are excluded by default |

Different kinds are AND'd, multiple values of one kind are OR'd:
`--author alice --author bob --start-date 2026-06-01` means "(Alice or Bob)
**and** since June".

The revision range walked is `HEAD` by default, or the range you give a
subcommand: `commitbrief diff main..develop --author alice`. The resulting
diff is the **concatenation of the matching commits' patches**, not a
cumulative range diff — so a file changed in three of them appears three
times, and no unmatched commit's work leaks in.

Branch-name matching is best-effort by nature: a squash- or rebase-merged
branch no longer owns its commits, so nothing will be found for it.

### File filters

Three ignore layers, applied in order. Later layers win, so a `!pattern` in
`.commitbriefignore` can revert a built-in exclusion:

1. **Built-in defaults** — binaries, lock files, `vendor/**`,
Expand All @@ -861,7 +912,13 @@ Three layers, applied in order. Later layers win, so a `!pattern` in
3. **`COMMITBRIEF.md` semantic filter** — natural-language rules the LLM
applies to whatever survives the first two layers.

`commitbrief dry-run --staged` reports how many files each layer removed.
On top of those, `--file` / `--dir` narrow to a path allowlist and
`--exclude-file` / `--exclude-dir` remove from it. All four share the same
matching rules (exact path or gitignore-style glob), and exclusion is applied
last, so it always wins.

`commitbrief dry-run` reports how many commits matched and how many files each
layer removed.

## Building from source

Expand Down
11 changes: 9 additions & 2 deletions internal/cli/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,16 @@ func runCommit(cmd *cobra.Command) error {
if global.json || global.markdown || global.output != "" {
return errors.New(app.Catalog.T("commit.flag_conflict_output"))
}
if len(global.files) > 0 || len(global.dirs) > 0 {
if len(global.files) > 0 || len(global.dirs) > 0 ||
len(global.excludeFiles) > 0 || len(global.excludeDirs) > 0 {
return errors.New(app.Catalog.T("commit.flag_conflict_filter"))
}
// Commit filters select commits that already exist; `commit` describes the
// staged index, which by definition has none yet. Reject rather than
// silently ignore.
if commitFiltersRequested() {
return errors.New(app.Catalog.T("commit.flag_conflict_commit_filter", commitFilterFlags))
}

// Committing needs confirmation we can only get on a TTY. A non-TTY run
// must pass --yes to commit the top suggestion unattended; otherwise we
Expand All @@ -102,7 +109,7 @@ func runCommit(cmd *cobra.Command) error {
defer prog.Close()

prog.Start(app.Catalog.T("progress.searching"))
raw, err := fetchDiff(app.Repo, reviewScopeFlags{staged: true}, nil)
raw, _, err := fetchDiff(cmd.Context(), app.Repo, reviewScopeFlags{staged: true}, nil, git.CommitFilter{})
if err != nil {
prog.Fail(err)
return err
Expand Down
Loading
Loading