@simtlix/simfinity-js 2.4.4 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
- package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
- package/.claude/worktrees/agitated-kepler/README.md +3941 -0
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
- package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
- package/.claude/worktrees/agitated-kepler/package.json +41 -0
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
- package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
- package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
- package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
- package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
- package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
- package/.cursor/rules/simfinity-core-functions.mdc +3 -1
- package/README.md +202 -0
- package/git-report.js +224 -0
- package/package.json +1 -1
- package/src/index.js +237 -23
|
@@ -55,11 +55,13 @@ Handles: scalars, enums, relation fields (`IdInputType` for refs, nested input f
|
|
|
55
55
|
`buildQuery(args, gqltype)` translates GraphQL filter arguments into a MongoDB aggregation pipeline:
|
|
56
56
|
|
|
57
57
|
- `buildQueryTerms()` -- converts scalar/enum filters to `$match`, object/relation filters to `$lookup` + `$unwind` + `$match`
|
|
58
|
+
- `buildFilterGroupMatch()` -- recursively processes `QLFilterGroup` (AND/OR) into nested MongoDB `$and`/`$or` match expressions. Reuses `buildQueryTerms()` for each leaf condition. Deduplicates `$lookup` stages via shared `aggregationsIncluded` dict. Max depth: 5.
|
|
58
59
|
- `buildAggregationsForSort()` -- adds `$lookup`/`$unwind` for sort on related fields
|
|
59
60
|
- Pagination: `$skip` + `$limit` from `QLPagination` input
|
|
60
61
|
- Sort: `$sort` from `QLSortExpression` input
|
|
62
|
+
- AND/OR: optional `AND`/`OR` arguments containing `QLFilterGroup` trees. Flat field filters and scope-injected filters are always ANDed at the top level (cannot be bypassed by user OR).
|
|
61
63
|
|
|
62
|
-
`buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX).
|
|
64
|
+
`buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX). Supports the same AND/OR filter pattern.
|
|
63
65
|
|
|
64
66
|
## Mutation Pipeline
|
|
65
67
|
|
package/README.md
CHANGED
|
@@ -265,6 +265,208 @@ query {
|
|
|
265
265
|
- `NIN` - Not in array
|
|
266
266
|
- `BTW` - Between two values
|
|
267
267
|
|
|
268
|
+
### Logical Filters (AND / OR)
|
|
269
|
+
|
|
270
|
+
By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the `AND` and `OR` query arguments.
|
|
271
|
+
|
|
272
|
+
#### Simple OR
|
|
273
|
+
|
|
274
|
+
Return books in either the Sci-Fi or Fantasy category:
|
|
275
|
+
|
|
276
|
+
```graphql
|
|
277
|
+
query {
|
|
278
|
+
books(
|
|
279
|
+
OR: [
|
|
280
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
281
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
282
|
+
]
|
|
283
|
+
) {
|
|
284
|
+
id
|
|
285
|
+
title
|
|
286
|
+
category
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Flat Filters Combined with OR
|
|
292
|
+
|
|
293
|
+
Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:
|
|
294
|
+
|
|
295
|
+
```graphql
|
|
296
|
+
query {
|
|
297
|
+
books(
|
|
298
|
+
rating: { operator: GTE, value: 7.0 }
|
|
299
|
+
OR: [
|
|
300
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
301
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
302
|
+
]
|
|
303
|
+
) {
|
|
304
|
+
id
|
|
305
|
+
title
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This translates to: `rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy")`.
|
|
311
|
+
|
|
312
|
+
#### Nested AND inside OR
|
|
313
|
+
|
|
314
|
+
```graphql
|
|
315
|
+
query {
|
|
316
|
+
books(
|
|
317
|
+
OR: [
|
|
318
|
+
{ AND: [
|
|
319
|
+
{ conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
|
|
320
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
321
|
+
]}
|
|
322
|
+
{ AND: [
|
|
323
|
+
{ conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
|
|
324
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
325
|
+
]}
|
|
326
|
+
]
|
|
327
|
+
) {
|
|
328
|
+
id
|
|
329
|
+
title
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This translates to: `(rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy")`.
|
|
335
|
+
|
|
336
|
+
#### Filtering on Relationships within AND/OR
|
|
337
|
+
|
|
338
|
+
Use the `path` parameter to filter on related entity fields:
|
|
339
|
+
|
|
340
|
+
```graphql
|
|
341
|
+
query {
|
|
342
|
+
books(
|
|
343
|
+
OR: [
|
|
344
|
+
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
|
|
345
|
+
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
|
|
346
|
+
]
|
|
347
|
+
) {
|
|
348
|
+
id
|
|
349
|
+
title
|
|
350
|
+
author { name }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### Mixing Flat Filters with AND/OR
|
|
356
|
+
|
|
357
|
+
You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:
|
|
358
|
+
|
|
359
|
+
```graphql
|
|
360
|
+
query {
|
|
361
|
+
books(
|
|
362
|
+
rating: { operator: GTE, value: 7.0 }
|
|
363
|
+
author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
|
|
364
|
+
OR: [
|
|
365
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
366
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
367
|
+
]
|
|
368
|
+
) {
|
|
369
|
+
id
|
|
370
|
+
title
|
|
371
|
+
rating
|
|
372
|
+
category
|
|
373
|
+
author { name country }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
This translates to: `rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy")`. The flat field filters (`rating`, `author`) use the existing syntax while the OR group uses the new `QLFilterGroup` syntax.
|
|
379
|
+
|
|
380
|
+
You can also combine flat filters with explicit AND groups for more complex logic:
|
|
381
|
+
|
|
382
|
+
```graphql
|
|
383
|
+
query {
|
|
384
|
+
books(
|
|
385
|
+
rating: { operator: GTE, value: 5.0 }
|
|
386
|
+
AND: [
|
|
387
|
+
{
|
|
388
|
+
OR: [
|
|
389
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
390
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
{
|
|
394
|
+
OR: [
|
|
395
|
+
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
|
|
396
|
+
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
) {
|
|
401
|
+
id
|
|
402
|
+
title
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
This translates to: `rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US")`.
|
|
408
|
+
|
|
409
|
+
#### Collection Filtering with AND/OR
|
|
410
|
+
|
|
411
|
+
AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same `AND` and `OR` arguments:
|
|
412
|
+
|
|
413
|
+
```graphql
|
|
414
|
+
query {
|
|
415
|
+
series {
|
|
416
|
+
seasons(
|
|
417
|
+
OR: [
|
|
418
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
|
|
419
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2021 }] }
|
|
420
|
+
]
|
|
421
|
+
) {
|
|
422
|
+
number
|
|
423
|
+
year
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
You can mix flat collection filters with AND/OR:
|
|
430
|
+
|
|
431
|
+
```graphql
|
|
432
|
+
query {
|
|
433
|
+
series {
|
|
434
|
+
seasons(
|
|
435
|
+
number: { operator: GT, value: 1 }
|
|
436
|
+
OR: [
|
|
437
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
|
|
438
|
+
{
|
|
439
|
+
AND: [
|
|
440
|
+
{ conditions: [{ field: "year", operator: GTE, value: 2022 }] }
|
|
441
|
+
{ conditions: [
|
|
442
|
+
{ field: "episodes", path: "name", operator: LIKE, value: "Final" }
|
|
443
|
+
] }
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
) {
|
|
448
|
+
number
|
|
449
|
+
year
|
|
450
|
+
episodes { name }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final"))`.
|
|
457
|
+
|
|
458
|
+
#### Filter Types Reference
|
|
459
|
+
|
|
460
|
+
| Type | Fields | Description |
|
|
461
|
+
|------|--------|-------------|
|
|
462
|
+
| `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
|
|
463
|
+
| `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |
|
|
464
|
+
|
|
465
|
+
- `field` identifies the entity field by name (e.g., `"title"`, `"author"`)
|
|
466
|
+
- `path` is required for object/relationship fields (e.g., `"name"`, `"country.name"`)
|
|
467
|
+
- Multiple `conditions` in the same group are combined with AND
|
|
468
|
+
- Maximum nesting depth: 5 levels
|
|
469
|
+
|
|
268
470
|
### Collection Field Filtering
|
|
269
471
|
|
|
270
472
|
Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
|
package/git-report.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { writeFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const CHANGE_TYPE_LABELS = {
|
|
8
|
+
A: 'Added',
|
|
9
|
+
M: 'Modified',
|
|
10
|
+
D: 'Deleted',
|
|
11
|
+
R: 'Renamed',
|
|
12
|
+
C: 'Copied',
|
|
13
|
+
T: 'TypeChanged',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const COMMIT_DELIMITER = '---GIT_REPORT_COMMIT---';
|
|
17
|
+
const FIELD_DELIMITER = '---GIT_REPORT_FIELD---';
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
const help = `
|
|
21
|
+
Usage: node git-report.js --branch <branch> --author <author> --months <N> [--output <file.csv>]
|
|
22
|
+
|
|
23
|
+
Generate a CSV report of git commits for a given author on a branch within a time period.
|
|
24
|
+
|
|
25
|
+
Required arguments:
|
|
26
|
+
--branch <branch> Git branch name to analyze
|
|
27
|
+
--author <author> Git author name or email to filter by
|
|
28
|
+
--months <N> Number of months to look back from today
|
|
29
|
+
|
|
30
|
+
Optional arguments:
|
|
31
|
+
--output <file> Output CSV file path (default: git-report.csv)
|
|
32
|
+
--help, -h Show this help message and exit
|
|
33
|
+
|
|
34
|
+
Output:
|
|
35
|
+
A CSV file with one row per affected file per commit, containing:
|
|
36
|
+
commit_hash, author, date, message, change_type, file_path
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
node git-report.js --branch master --author "John Doe" --months 3
|
|
40
|
+
node git-report.js --branch main --author john@example.com --months 6 --output report.csv
|
|
41
|
+
`;
|
|
42
|
+
console.log(help.trim());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const args = argv.slice(2);
|
|
47
|
+
|
|
48
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
49
|
+
printHelp();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const opts = {};
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
switch (args[i]) {
|
|
56
|
+
case '--branch':
|
|
57
|
+
opts.branch = args[++i];
|
|
58
|
+
break;
|
|
59
|
+
case '--author':
|
|
60
|
+
opts.author = args[++i];
|
|
61
|
+
break;
|
|
62
|
+
case '--months':
|
|
63
|
+
opts.months = args[++i];
|
|
64
|
+
break;
|
|
65
|
+
case '--output':
|
|
66
|
+
opts.output = args[++i];
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
console.error(`Unknown argument: ${args[i]}`);
|
|
70
|
+
printHelp();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const missing = [];
|
|
76
|
+
if (!opts.branch) missing.push('--branch');
|
|
77
|
+
if (!opts.author) missing.push('--author');
|
|
78
|
+
if (!opts.months) missing.push('--months');
|
|
79
|
+
|
|
80
|
+
if (missing.length > 0) {
|
|
81
|
+
console.error(`Missing required arguments: ${missing.join(', ')}`);
|
|
82
|
+
printHelp();
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const months = parseInt(opts.months, 10);
|
|
87
|
+
if (Number.isNaN(months) || months <= 0) {
|
|
88
|
+
console.error('--months must be a positive integer');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
opts.months = months;
|
|
92
|
+
opts.output = opts.output || 'git-report.csv';
|
|
93
|
+
|
|
94
|
+
return opts;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateGitEnvironment() {
|
|
98
|
+
try {
|
|
99
|
+
execFileSync('git', ['--version'], { stdio: 'pipe' });
|
|
100
|
+
} catch {
|
|
101
|
+
console.error('Error: git is not installed or not found in PATH.');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'pipe' });
|
|
107
|
+
} catch {
|
|
108
|
+
console.error('Error: current directory is not inside a git repository.');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function computeSinceDate(months) {
|
|
114
|
+
const date = new Date();
|
|
115
|
+
date.setMonth(date.getMonth() - months);
|
|
116
|
+
return date.toISOString().slice(0, 10);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeCsvField(value) {
|
|
120
|
+
const str = String(value).replace(/\r?\n/g, ' ');
|
|
121
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
122
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
123
|
+
}
|
|
124
|
+
return str;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getGitLog(branch, author, sinceDate) {
|
|
128
|
+
const format = [
|
|
129
|
+
`${COMMIT_DELIMITER}`,
|
|
130
|
+
`%h${FIELD_DELIMITER}%an${FIELD_DELIMITER}%ai${FIELD_DELIMITER}%s`,
|
|
131
|
+
].join('');
|
|
132
|
+
|
|
133
|
+
const args = [
|
|
134
|
+
'log',
|
|
135
|
+
`--format=${format}`,
|
|
136
|
+
'--name-status',
|
|
137
|
+
`--author=${author}`,
|
|
138
|
+
`--since=${sinceDate}`,
|
|
139
|
+
branch,
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
return execFileSync('git', args, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Error running git log: ${err.message}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseGitLog(rawOutput) {
|
|
151
|
+
const rows = [];
|
|
152
|
+
const commits = rawOutput.split(COMMIT_DELIMITER).filter((s) => s.trim());
|
|
153
|
+
|
|
154
|
+
for (const block of commits) {
|
|
155
|
+
const lines = block.trim().split('\n');
|
|
156
|
+
if (lines.length === 0) continue;
|
|
157
|
+
|
|
158
|
+
const headerLine = lines[0];
|
|
159
|
+
const parts = headerLine.split(FIELD_DELIMITER);
|
|
160
|
+
if (parts.length < 4) continue;
|
|
161
|
+
|
|
162
|
+
const [hash, author, dateRaw, message] = parts;
|
|
163
|
+
const date = dateRaw.slice(0, 10);
|
|
164
|
+
|
|
165
|
+
for (let i = 1; i < lines.length; i++) {
|
|
166
|
+
const line = lines[i].trim();
|
|
167
|
+
if (!line) continue;
|
|
168
|
+
|
|
169
|
+
const match = line.match(/^([AMDRTC])\d*\t(.+)$/);
|
|
170
|
+
if (!match) continue;
|
|
171
|
+
|
|
172
|
+
const statusCode = match[1];
|
|
173
|
+
const filePath = match[2].includes('\t') ? match[2].split('\t').pop() : match[2];
|
|
174
|
+
const changeType = CHANGE_TYPE_LABELS[statusCode] || statusCode;
|
|
175
|
+
|
|
176
|
+
rows.push({
|
|
177
|
+
commit_hash: hash,
|
|
178
|
+
author,
|
|
179
|
+
date,
|
|
180
|
+
message,
|
|
181
|
+
change_type: changeType,
|
|
182
|
+
file_path: filePath,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function writeCsv(rows, outputPath) {
|
|
191
|
+
const columns = ['commit_hash', 'author', 'date', 'message', 'change_type', 'file_path'];
|
|
192
|
+
const header = columns.join(',');
|
|
193
|
+
const lines = rows.map((row) => columns.map((col) => escapeCsvField(row[col])).join(','));
|
|
194
|
+
|
|
195
|
+
const csv = [header, ...lines, ''].join('\n');
|
|
196
|
+
const absPath = resolve(outputPath);
|
|
197
|
+
writeFileSync(absPath, csv, 'utf-8');
|
|
198
|
+
return absPath;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function main() {
|
|
202
|
+
const opts = parseArgs(process.argv);
|
|
203
|
+
validateGitEnvironment();
|
|
204
|
+
|
|
205
|
+
const sinceDate = computeSinceDate(opts.months);
|
|
206
|
+
console.error(`Searching commits on branch "${opts.branch}" by "${opts.author}" since ${sinceDate}...`);
|
|
207
|
+
|
|
208
|
+
const rawLog = getGitLog(opts.branch, opts.author, sinceDate);
|
|
209
|
+
const rows = parseGitLog(rawLog);
|
|
210
|
+
|
|
211
|
+
const uniqueCommits = new Set(rows.map((r) => r.commit_hash)).size;
|
|
212
|
+
|
|
213
|
+
if (rows.length === 0) {
|
|
214
|
+
console.error('No commits found matching the criteria.');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const absPath = writeCsv(rows, opts.output);
|
|
219
|
+
|
|
220
|
+
console.error(`Done. ${uniqueCommits} commit(s), ${rows.length} file change(s).`);
|
|
221
|
+
console.error(`Report written to: ${absPath}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main();
|