Skip to content

Commit bdce541

Browse files
committed
chore: update CI workflow to use latest action versions and improve caching strategy; enhance fuzzy search functionality with validation and new options
1 parent 57e5e2b commit bdce541

11 files changed

Lines changed: 434 additions & 54 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,23 @@ jobs:
1616
matrix:
1717
node-version: [18.x, 20.x]
1818
steps:
19-
- uses: actions/checkout@v3
19+
- uses: actions/checkout@v4
2020
- name: Cache npm modules
21-
uses: actions/cache@v3
21+
uses: actions/cache@v4
2222
with:
2323
path: ~/.npm
2424
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
2525
restore-keys: |
2626
${{ runner.os }}-node-
2727
- name: Cache node_modules
28-
uses: actions/cache@v3
28+
uses: actions/cache@v4
2929
with:
3030
path: node_modules
3131
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
3232
restore-keys: |
3333
${{ runner.os }}-node-modules-
3434
- name: Use Node.js ${{ matrix.node-version }}
35-
uses: actions/setup-node@v3
35+
uses: actions/setup-node@v4
3636
with:
3737
node-version: ${{ matrix.node-version }}
3838
- run: npm ci
@@ -60,20 +60,22 @@ jobs:
6060
issues: write # for the "fail" plugin
6161
runs-on: ubuntu-latest
6262
steps:
63-
- uses: actions/checkout@v3
63+
- uses: actions/checkout@v4
6464
with:
6565
fetch-depth: 0 # full history needed for tagging
6666
- name: Configure npm for semantic-release
6767
run: |
6868
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
69+
env:
70+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
6971
- name: Use Node.js
70-
uses: actions/setup-node@v3
72+
uses: actions/setup-node@v4
7173
with:
7274
node-version: '20.x'
7375
registry-url: 'https://registry.npmjs.org'
7476
- run: npm ci
7577
- name: Run semantic-release
7678
env:
7779
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
80+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
7981
run: npm run release

.releaserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"@semantic-release/release-notes-generator",
66
["@semantic-release/changelog", {"changelogFile": "CHANGELOG.md"}],
77
["@semantic-release/npm", {"npmPublish": true}],
8-
["@semantic-release/github", {"assets": "dist/**", "labels": ["bug", "release"]}],
8+
["@semantic-release/github", {"labels": ["bug", "release"]}],
99
["@semantic-release/git", {
1010
"assets": ["package.json", "CHANGELOG.md"],
1111
"message": "chore(release): ${nextRelease.version} [skip ci]"

README.md

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
**Human-friendly, temporal & intent-driven wrapper around `git log`.**
99

10-
TGQ provides natural-language-style flags, fuzzy search, glob path filters, named presets, and structured exports (table, JSON, CSV, Markdown).
10+
Git-When provides natural-language-style flags, fuzzy search, glob path filters, named presets, and structured exports (table, JSON, CSV, Markdown).
1111

1212
---
1313

@@ -45,13 +45,19 @@ npm install -g .
4545
• Dates passed to `--when` are interpreted in UTC by default (e.g. `2023-05-07``2023-05-07T00:00:00Z`). If your local timezone is behind UTC, a shorter-than-expected time window may appear. Always use ISO (`YYYY-MM-DD..YYYY-MM-DD`) or natural ranges (`today`, `last-week`).
4646

4747
### Glob Pattern Gotchas
48-
• Your shell may expand globs before TGQ sees them. Quote patterns (e.g. `--path="src/**/*.js"`) to avoid unintended matches.
48+
• Your shell may expand globs before Git-When sees them. Quote patterns (e.g. `--path="src/**/*.js"`) to avoid unintended matches.
4949

5050
### Tips for Large Repositories
5151
- Use `--limit <n>` to stop after the first _n_ commits.
5252
- Push filtering into Git: `--author`, `--grep`, and `--path` reduce data before JS processing.
5353
- Combine flags: `git-when --who Alice --limit 50 --format=json` for a quick API export.
5454

55+
### Fuzzy Search Tips
56+
- **Default threshold**: 0.4 (balanced between strict and lenient)
57+
- **Stricter matching**: Use `--fuzzy-level=0.1` to `0.3` for more precise results
58+
- **Lenient matching**: Use `--fuzzy-level=0.6` to `0.8` to catch more variations
59+
- **Exact matching**: Use Git's native `--author` and `--grep` flags instead of `--who`/`--what`
60+
5561
### Exit Codes
5662
- **0**: Successful execution (even if no commits match).
5763
- **1**: An error occurred, such as invalid flags, invalid date ranges, malformed presets, or Git errors (e.g., not a Git repository).
@@ -62,11 +68,17 @@ npm install -g .
6268
"perf-q2": {
6369
"when": "2025-02-01..2025-02-28",
6470
"who": "Bob",
65-
"what": "perf"
71+
"what": "perf",
72+
"fuzzyThreshold": 0.3
6673
},
6774
"alice-week": {
6875
"when": "last-week",
6976
"who": "Alice"
77+
},
78+
"strict-search": {
79+
"who": "john",
80+
"fuzzyThreshold": 0.1,
81+
"format": "json"
7082
}
7183
}
7284
```
@@ -89,17 +101,18 @@ git-when [preset] [options]
89101

90102
- **Preset**: If the first argument matches a saved preset name, its filters load automatically.
91103
- **Options**:
92-
- `-w`, `--when <range>` Date range (e.g. `2023-01..2023-03`, `last-week`, `today`)
93-
- `-a`, `--who <author>` Fuzzy author name
94-
- `-k`, `--what <keyword>` Fuzzy commit message keyword
95-
- `-p`, `--path <glob>` Glob file path (e.g. `src/**/*.js`, `*.md`)
96-
- `-f`, `--format <type>` Output format: `table` (default), `json`, `csv`, `md`
97-
- `-s`, `--save <name>` Save current filters as preset `<name>`
98-
- `-n`, `--limit <number>` Limit the number of commits fetched
99-
- `--list-presets` List saved preset names
100-
- `--delete-preset <name>` Remove a saved preset
101-
- `--help` Show this help
102-
- `--version` Show version
104+
- `-w`, `--when <range>` Date range (e.g. `2023-01..2023-03`, `last-week`, `today`)
105+
- `-a`, `--who <author>` Fuzzy author name
106+
- `-k`, `--what <keyword>` Fuzzy commit message keyword
107+
- `-p`, `--path <glob>` Glob file path (e.g. `src/**/*.js`, `*.md`)
108+
- `-f`, `--format <type>` Output format: `table` (default), `json`, `csv`, `md`
109+
- `--fuzzy-level <0.0-1.0>` Fuzzy search threshold (default: 0.4, lower = more strict)
110+
- `-s`, `--save <name>` Save current filters as preset `<name>`
111+
- `-n`, `--limit <number>` Limit the number of commits fetched
112+
- `--list-presets` List saved preset names
113+
- `--delete-preset <name>` Remove a saved preset
114+
- `--help` Show this help
115+
- `--version` Show version
103116

104117
---
105118

@@ -128,7 +141,12 @@ git-when --what="fix" -w="today" --format=json > fixes-today.json
128141
# 5. Manage presets
129142
git-when --list-presets
130143
git-when --delete-preset weekly-bob
131-
# 6. Batch Markdown output
144+
145+
# 6. Fine-tune fuzzy search sensitivity
146+
git-when --who="john" --fuzzy-level=0.2 # Stricter matching
147+
git-when --who="john" --fuzzy-level=0.8 # More lenient matching
148+
149+
# 7. Batch Markdown output
132150
git-when --when="last-week" --format=markdown --batch-size=50
133151
```
134152

@@ -137,9 +155,9 @@ git-when --when="last-week" --format=markdown --batch-size=50
137155
## Features
138156

139157
- **Natural flags** (`--when`, `--who`, `--what`, `--path`)
140-
- **Fuzzy search** on authors and commit messages via [`fuse.js`](https://github.com/krisk/Fuse)
158+
- **Configurable fuzzy search** on authors and commit messages via [`fuse.js`](https://github.com/krisk/Fuse) with adjustable threshold
141159
- **Glob file filtering** via [`minimatch`](https://github.com/isaacs/minimatch)
142-
- **Named presets** for saving and reusing common filters
160+
- **Named presets** for saving and reusing common filters with validation
143161
- **Structured output**: ASCII table, JSON, CSV, or Markdown
144162
- **Cross-platform**: Node.js CLI (macOS, Linux, Windows)
145163

lib/filters.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ const minimatch = mm.minimatch || mm.default || mm;
1010
* @param {string} [opts.who]
1111
* @param {string} [opts.what]
1212
* @param {string} [opts.path]
13+
* @param {number} [opts.fuzzyThreshold] - Fuzzy search threshold (0.0-1.0, default: 0.4)
1314
*/
1415
function applyFilters(commits, opts = {}) {
1516
let result = commits;
17+
const fuzzyThreshold = opts.fuzzyThreshold !== undefined ? opts.fuzzyThreshold : 0.4;
1618

1719
// Fuzzy match on author
1820
if (opts.who) {
19-
const fuse = new Fuse(result, { keys: ['author'], threshold: 0.4 });
21+
const fuse = new Fuse(result, { keys: ['author'], threshold: fuzzyThreshold });
2022
result = fuse.search(opts.who).map(r => r.item);
2123
}
2224

2325
// Fuzzy match on commit message
2426
if (opts.what) {
25-
const fuse = new Fuse(result, { keys: ['message'], threshold: 0.4 });
27+
const fuse = new Fuse(result, { keys: ['message'], threshold: fuzzyThreshold });
2628
result = fuse.search(opts.what).map(r => r.item);
2729
}
2830

lib/index.js

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const path = require('path');
22
const pkg = require(path.join(__dirname, '..', 'package.json'));
3-
const { loadPresets, savePreset, deletePreset, listPresets } = require('./presets');
3+
const { loadPresets, savePreset, deletePreset, listPresets, validatePresetOptions } = require('./presets');
44
const { parseDateRange } = require('./utils');
55
const { fetchCommits, streamCommits } = require('./gitClient');
66
const { applyFilters } = require('./filters');
@@ -23,6 +23,7 @@ Options:
2323
-k, --what <keyword> Commit message keyword (fuzzy)
2424
-p, --path <glob> File path glob (e.g. 'src/**/*.js')
2525
-f, --format <type> Output format: table, json, csv, md
26+
--fuzzy-level <0.0-1.0> Fuzzy search threshold (default: 0.4)
2627
--batch-size <num> For pretty formats, render N commits per batch
2728
-s, --save <name> Save current flags as preset
2829
-o, --overwrite Overwrite existing preset without warning
@@ -97,6 +98,7 @@ Options:
9798
if (arg.startsWith('--path=')) { optsEdit.path = arg.slice('--path='.length); continue; }
9899
if (arg.startsWith('--format=')){ optsEdit.format= arg.slice('--format='.length);continue; }
99100
if (arg.startsWith('--limit=')){ optsEdit.limit = parseInt(arg.slice('--limit='.length), 10); continue; }
101+
if (arg.startsWith('--fuzzy-level=')){ optsEdit.fuzzyThreshold = parseFloat(arg.slice('--fuzzy-level='.length)); continue; }
100102
switch (arg) {
101103
case '-w': case '--when': optsEdit.when = args[++i]; break;
102104
case '-a': case '--who': optsEdit.who = args[++i]; break;
@@ -110,11 +112,25 @@ Options:
110112
process.exit(1);
111113
}
112114
break;
115+
case '--fuzzy-level':
116+
optsEdit.fuzzyThreshold = parseFloat(args[++i]);
117+
if (isNaN(optsEdit.fuzzyThreshold) || optsEdit.fuzzyThreshold < 0 || optsEdit.fuzzyThreshold > 1) {
118+
console.error('Error: --fuzzy-level must be a number between 0.0 and 1.0');
119+
process.exit(1);
120+
}
121+
break;
113122
default:
114123
console.error(`Unknown option during edit: ${arg}`);
115124
process.exit(1);
116125
}
117126
}
127+
// Validate the edited preset before saving
128+
try {
129+
validatePresetOptions(optsEdit);
130+
} catch (e) {
131+
console.error(`Error: ${e.message}`);
132+
process.exit(1);
133+
}
118134
savePreset(name, optsEdit);
119135
console.log(`Updated preset '${name}'.`);
120136
process.exit(0);
@@ -175,6 +191,14 @@ Options:
175191
}
176192
continue;
177193
}
194+
if (arg.startsWith('--fuzzy-level=')) {
195+
opts.fuzzyThreshold = parseFloat(arg.slice('--fuzzy-level='.length));
196+
if (isNaN(opts.fuzzyThreshold) || opts.fuzzyThreshold < 0 || opts.fuzzyThreshold > 1) {
197+
console.error('Error: --fuzzy-level must be a number between 0.0 and 1.0');
198+
process.exit(1);
199+
}
200+
continue;
201+
}
178202
switch (arg) {
179203
case '-w':
180204
case '--when':
@@ -215,6 +239,13 @@ Options:
215239
process.exit(1);
216240
}
217241
break;
242+
case '--fuzzy-level':
243+
opts.fuzzyThreshold = parseFloat(args[++i]);
244+
if (isNaN(opts.fuzzyThreshold) || opts.fuzzyThreshold < 0 || opts.fuzzyThreshold > 1) {
245+
console.error('Error: --fuzzy-level must be a number between 0.0 and 1.0');
246+
process.exit(1);
247+
}
248+
break;
218249
default:
219250
console.error(`Unknown option: ${arg}`);
220251
printUsage();
@@ -230,35 +261,54 @@ Options:
230261
process.exit(1);
231262
}
232263
const name = opts.save;
233-
delete opts.save;
234-
delete opts.overwrite;
235-
savePreset(name, opts);
264+
const presetOpts = { ...opts };
265+
delete presetOpts.save;
266+
delete presetOpts.overwrite;
267+
268+
// Validate the preset before saving
269+
try {
270+
validatePresetOptions(presetOpts);
271+
} catch (e) {
272+
console.error(`Error: ${e.message}`);
273+
process.exit(1);
274+
}
275+
276+
savePreset(name, presetOpts);
236277
console.log(`Saved preset '${name}'.`);
237278
process.exit(0);
238279
}
239280

240281
// Parse date range
241-
let since, until;
282+
let since, until, isShorthand;
242283
try {
243284
const range = opts.when || '';
244-
({ since, until } = parseDateRange(range));
285+
({ since, until, isShorthand } = parseDateRange(range));
245286
} catch (e) {
246287
console.error('Invalid date range:', e.message);
247288
process.exit(1);
248289
}
249290

250-
// Determine git log date filters: raw strings for explicit ranges, ISO for parsed dates
291+
// Determine git log date filters: optimize for Git when possible
251292
let gitSince, gitUntil;
252-
if (opts.when && opts.when.includes('..')) {
293+
let needsJSDateFiltering = false;
294+
295+
if (isShorthand) {
296+
// For shorthand dates, pass them directly to Git for optimal performance
297+
gitSince = since ? since.toISOString().replace(/\.\d+Z$/, 'Z') : undefined;
298+
gitUntil = until ? until.toISOString().replace(/\.\d+Z$/, 'Z') : undefined;
299+
needsJSDateFiltering = false; // Git handles the filtering
300+
} else if (opts.when && opts.when.includes('..')) {
253301
const parts = opts.when.split('..');
254302
// Use full UTC date-times to avoid local-timezone exclusion
255303
const start = parts[0];
256304
const end = parts[1];
257305
gitSince = `${start}T00:00:00+00:00`;
258306
gitUntil = `${end}T23:59:59+00:00`;
307+
needsJSDateFiltering = false; // Git handles explicit ranges
259308
} else {
260309
gitSince = since ? since.toISOString().replace(/\.\d+Z$/, 'Z') : undefined;
261310
gitUntil = until ? until.toISOString().replace(/\.\d+Z$/, 'Z') : undefined;
311+
needsJSDateFiltering = false; // Git can handle single dates too
262312
}
263313

264314
// Streaming NDJSON output: emit each commit as a JSON line and exit
@@ -285,10 +335,12 @@ Options:
285335
}
286336
process.exit(1);
287337
}
288-
// If we used an explicit '..' range, Git already filtered by date; otherwise, apply JS date filtering
338+
// Apply date filtering only if Git didn't handle it completely
289339
let dated = commits;
290-
if (since) dated = dated.filter(c => new Date(c.date) >= since);
291-
if (until) dated = dated.filter(c => new Date(c.date) <= until);
340+
if (needsJSDateFiltering) {
341+
if (since) dated = dated.filter(c => new Date(c.date) >= since);
342+
if (until) dated = dated.filter(c => new Date(c.date) <= until);
343+
}
292344
const filtered = applyFilters(dated, opts);
293345

294346
// Batch-size for pretty formats (table, CSV, Markdown)

0 commit comments

Comments
 (0)