@drazenbebic/wdid 0.1.3 → 0.2.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/README.md +46 -20
- package/dist/index.js +271 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> What did I do? — a small CLI that summarizes your git activity as a tidy table, so you can fill in your timesheet without trying to remember Tuesday.
|
|
4
4
|
|
|
5
|
-
`wdid` reads `git log` for your author across one or more repos and renders the output as a colorized table with **Date
|
|
5
|
+
`wdid` reads `git log` for your author across one or more repos and renders the output as a colorized table with **Date** (with commit time, shown in your local timezone), **Ticket** (JIRA-style `ABC-123` by default, parsed from the commit subject), and **Description**.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -28,17 +28,36 @@ By default `wdid` uses `git config user.name` as the author and the current work
|
|
|
28
28
|
### Example output
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
|
|
32
|
-
│ Date
|
|
33
|
-
|
|
34
|
-
│ 2026-05-27 │ ABC-123 │ feat(ABC-123): add login flow │
|
|
35
|
-
│ 2026-05-27 │ — │ chore: bump deps │
|
|
36
|
-
│ 2026-05-26 │ ABC-119 │ fix(ABC-119): handle empty payload │
|
|
37
|
-
|
|
31
|
+
┌──────────────────┬──────────────┬──────────────────────────────────────────────────┐
|
|
32
|
+
│ Date │ Ticket │ Description │
|
|
33
|
+
├──────────────────┼──────────────┼──────────────────────────────────────────────────┤
|
|
34
|
+
│ 2026-05-27 16:42 │ ABC-123 │ feat(ABC-123): add login flow │
|
|
35
|
+
│ 2026-05-27 11:08 │ — │ chore: bump deps │
|
|
36
|
+
│ 2026-05-26 17:53 │ ABC-119 │ fix(ABC-119): handle empty payload │
|
|
37
|
+
└──────────────────┴──────────────┴──────────────────────────────────────────────────┘
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
The time is rendered dimmed and shown in your local timezone (parsed from the committer's full ISO timestamp).
|
|
41
|
+
|
|
40
42
|
If a commit doesn't reference a ticket, the Ticket column is left blank (rendered as `—`).
|
|
41
43
|
|
|
44
|
+
With `--group-by-day`, the date moves into a section heading instead of repeating per row, making longer outputs feel more like a journal:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌───────┬──────────────┬─────────────────────────────────────┐
|
|
48
|
+
│ Time │ Ticket │ Description │
|
|
49
|
+
├───────┴──────────────┴─────────────────────────────────────┤
|
|
50
|
+
│ 2026-05-27 │
|
|
51
|
+
├───────┬──────────────┬─────────────────────────────────────┤
|
|
52
|
+
│ 16:42 │ ABC-123 │ feat(ABC-123): add login flow │
|
|
53
|
+
│ 11:08 │ — │ chore: bump deps │
|
|
54
|
+
├───────┴──────────────┴─────────────────────────────────────┤
|
|
55
|
+
│ 2026-05-26 │
|
|
56
|
+
├───────┬──────────────┬─────────────────────────────────────┤
|
|
57
|
+
│ 17:53 │ ABC-119 │ fix(ABC-119): handle empty payload │
|
|
58
|
+
└───────┴──────────────┴─────────────────────────────────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
42
61
|
## Options
|
|
43
62
|
|
|
44
63
|
| Option | Description |
|
|
@@ -50,6 +69,10 @@ If a commit doesn't reference a ticket, the Ticket column is left blank (rendere
|
|
|
50
69
|
| `--repo <path...>` | One or more repo paths to query. Defaults to `defaultRepos` in config, then the current dir. |
|
|
51
70
|
| `--format <preset>` | Ticket format: `jira`, `github`, `conventional`, or `custom`. Defaults to `jira`. |
|
|
52
71
|
| `--ticket-pattern <regex>` | Custom regex for ticket extraction. Implies `--format custom`; overrides `--format`. |
|
|
72
|
+
| `--no-color` | Disable colored output. Also honored via the `NO_COLOR` env var. |
|
|
73
|
+
| `--limit <N>` | Cap the table to the most recent `N` rows. Positive integer. |
|
|
74
|
+
| `--group-by-day` | Group rows under a bold date heading per day; the row only shows the time. |
|
|
75
|
+
| `--json` | Emit a JSON array of commit entries to stdout instead of the table. Empty result is `[]`. |
|
|
53
76
|
|
|
54
77
|
## Configuration
|
|
55
78
|
|
|
@@ -71,21 +94,24 @@ CLI flags always win. The first match in this list is used in full (configs do n
|
|
|
71
94
|
}
|
|
72
95
|
```
|
|
73
96
|
|
|
74
|
-
| Field
|
|
75
|
-
|
|
|
76
|
-
| `format`
|
|
77
|
-
| `customPattern`
|
|
78
|
-
| `defaultAuthor`
|
|
79
|
-
| `defaultRepos`
|
|
97
|
+
| Field | Type | Description |
|
|
98
|
+
| ------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
99
|
+
| `format` | `"jira" \| "github" \| "conventional" \| "custom"` | Ticket extraction preset. Default `jira`. |
|
|
100
|
+
| `customPattern` | `string` | Regex used when `format` is `"custom"`. First capture group wins, else full match. |
|
|
101
|
+
| `defaultAuthor` | `string` | Used when `--author` is not passed and you want to skip the `git config` lookup. |
|
|
102
|
+
| `defaultRepos` | `string[]` | Paths to query when no `--repo` is given. `~` is expanded. |
|
|
103
|
+
| `ticketColumnLabel` | `string` | Override the auto-picked column header (see below). |
|
|
80
104
|
|
|
81
105
|
### Format presets
|
|
82
106
|
|
|
83
|
-
| Preset | Matches | Example commit →
|
|
84
|
-
| -------------- | ------------------------------------------------ | -------------------------------------- |
|
|
85
|
-
| `jira` | `ABC-123` style (uppercase project key + digits) | `feat(ABC-123): add login` → `ABC-123` |
|
|
86
|
-
| `github` | `#123` style | `Closes #42` → `42` |
|
|
87
|
-
| `conventional` | Conventional Commit `type(scope)!` | `feat(auth)!: ...` → `feat(auth)!` |
|
|
88
|
-
| `custom` | Your `customPattern` regex | depends on the regex |
|
|
107
|
+
| Preset | Matches | Example commit → match | Column header |
|
|
108
|
+
| -------------- | ------------------------------------------------ | -------------------------------------- | ------------- |
|
|
109
|
+
| `jira` | `ABC-123` style (uppercase project key + digits) | `feat(ABC-123): add login` → `ABC-123` | `Ticket` |
|
|
110
|
+
| `github` | `#123` style | `Closes #42` → `42` | `Issue` |
|
|
111
|
+
| `conventional` | Conventional Commit `type(scope)!` | `feat(auth)!: ...` → `feat(auth)!` | `Type` |
|
|
112
|
+
| `custom` | Your `customPattern` regex | depends on the regex | `Match` |
|
|
113
|
+
|
|
114
|
+
The column header is picked automatically based on the active format. To override it for a specific preset (e.g. call them "Tasks" instead of "Ticket"), set `ticketColumnLabel` in your config.
|
|
89
115
|
|
|
90
116
|
## Development
|
|
91
117
|
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import chalk2 from "chalk";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/git.ts
|
|
7
|
-
import { execFile } from "child_process";
|
|
8
|
+
import { execFile, spawn } from "child_process";
|
|
8
9
|
import { promisify } from "util";
|
|
9
10
|
var execFileAsync = promisify(execFile);
|
|
10
11
|
var FIELD_SEP = "";
|
|
11
12
|
var RECORD_SEP = "";
|
|
13
|
+
var TRUNK_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
|
|
14
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
15
|
+
function formatLocalDateTime(iso) {
|
|
16
|
+
const d = new Date(iso);
|
|
17
|
+
if (Number.isNaN(d.getTime())) {
|
|
18
|
+
return { date: "", time: "" };
|
|
19
|
+
}
|
|
20
|
+
const date = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
21
|
+
const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
22
|
+
return { date, time };
|
|
23
|
+
}
|
|
12
24
|
function extractTicket(message, pattern) {
|
|
13
25
|
const match = message.match(pattern);
|
|
14
26
|
if (!match) {
|
|
@@ -16,6 +28,80 @@ function extractTicket(message, pattern) {
|
|
|
16
28
|
}
|
|
17
29
|
return match[1] ?? match[0];
|
|
18
30
|
}
|
|
31
|
+
function normalizeBranchName(rawName) {
|
|
32
|
+
const clean = rawName.replace(/[~^].*$/, "").trim();
|
|
33
|
+
if (!clean || clean === "undefined" || TRUNK_BRANCHES.has(clean)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return clean;
|
|
37
|
+
}
|
|
38
|
+
async function runGitWithStdin(cwd, args, input) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const proc = spawn("git", args, { cwd });
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
proc.stdout.on("data", (chunk) => {
|
|
44
|
+
stdout += chunk.toString();
|
|
45
|
+
});
|
|
46
|
+
proc.stderr.on("data", (chunk) => {
|
|
47
|
+
stderr += chunk.toString();
|
|
48
|
+
});
|
|
49
|
+
proc.on("error", reject);
|
|
50
|
+
proc.on("close", (code) => {
|
|
51
|
+
if (code !== 0) {
|
|
52
|
+
reject(new Error(`git ${args.join(" ")} failed: ${stderr.trim()}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
resolve(stdout);
|
|
56
|
+
});
|
|
57
|
+
proc.stdin.write(input);
|
|
58
|
+
proc.stdin.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function getBranchMap(cwd, shas) {
|
|
62
|
+
const map = /* @__PURE__ */ new Map();
|
|
63
|
+
if (shas.length === 0) {
|
|
64
|
+
return map;
|
|
65
|
+
}
|
|
66
|
+
let stdout;
|
|
67
|
+
try {
|
|
68
|
+
stdout = await runGitWithStdin(
|
|
69
|
+
cwd,
|
|
70
|
+
["name-rev", "--stdin", "--refs=refs/heads/*"],
|
|
71
|
+
shas.join("\n") + "\n"
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
for (const line of stdout.split("\n")) {
|
|
77
|
+
const m = line.match(/^([0-9a-fA-F]+)\s+\((.+)\)\s*$/);
|
|
78
|
+
if (!m) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const sha = m[1];
|
|
82
|
+
const rawName = m[2];
|
|
83
|
+
if (!sha || !rawName) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
map.set(sha, normalizeBranchName(rawName));
|
|
87
|
+
}
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
async function assertGitRepo(cwd) {
|
|
91
|
+
try {
|
|
92
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
93
|
+
cwd
|
|
94
|
+
});
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.code === "ENOENT") {
|
|
97
|
+
throw new Error("git is not installed or not on PATH", { cause: err });
|
|
98
|
+
}
|
|
99
|
+
throw new Error(
|
|
100
|
+
`not inside a git repository: ${cwd} \u2014 cd into a repo or pass --repo <path>`,
|
|
101
|
+
{ cause: err }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
19
105
|
async function getGitUserName(cwd) {
|
|
20
106
|
try {
|
|
21
107
|
const { stdout } = await execFileAsync("git", ["config", "user.name"], {
|
|
@@ -37,13 +123,14 @@ async function getGitUserName(cwd) {
|
|
|
37
123
|
}
|
|
38
124
|
}
|
|
39
125
|
async function getCommits(opts) {
|
|
126
|
+
await assertGitRepo(opts.cwd);
|
|
40
127
|
const args = [
|
|
41
128
|
"log",
|
|
42
129
|
"--all",
|
|
43
130
|
"--author-date-order",
|
|
44
131
|
"--regexp-ignore-case",
|
|
45
132
|
`--author=${opts.author}`,
|
|
46
|
-
`--pretty=format:%
|
|
133
|
+
`--pretty=format:%H${FIELD_SEP}%cI${FIELD_SEP}%s${RECORD_SEP}`
|
|
47
134
|
];
|
|
48
135
|
if (opts.from) {
|
|
49
136
|
args.push(`--after=${opts.from} 00:00`);
|
|
@@ -51,43 +138,127 @@ async function getCommits(opts) {
|
|
|
51
138
|
if (opts.to) {
|
|
52
139
|
args.push(`--before=${opts.to} 23:59`);
|
|
53
140
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
141
|
+
if (opts.limit !== void 0) {
|
|
142
|
+
args.push(`--max-count=${opts.limit}`);
|
|
143
|
+
}
|
|
144
|
+
let stdout;
|
|
145
|
+
try {
|
|
146
|
+
const result = await execFileAsync("git", args, {
|
|
147
|
+
cwd: opts.cwd,
|
|
148
|
+
maxBuffer: 32 * 1024 * 1024
|
|
149
|
+
});
|
|
150
|
+
stdout = result.stdout;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
const stderr = String(err.stderr ?? "");
|
|
153
|
+
if (stderr.includes("does not have any commits") || stderr.includes("bad default revision") || stderr.includes("unknown revision")) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
const parsed = stdout.split(RECORD_SEP).map((r) => r.trim()).filter((r) => r.length > 0).map((record) => {
|
|
159
|
+
const [sha = "", iso = "", subject = ""] = record.split(FIELD_SEP);
|
|
160
|
+
const { date, time } = formatLocalDateTime(iso);
|
|
60
161
|
return {
|
|
162
|
+
sha,
|
|
61
163
|
date,
|
|
164
|
+
time,
|
|
62
165
|
ticket: extractTicket(subject, opts.pattern),
|
|
63
166
|
description: subject
|
|
64
167
|
};
|
|
65
168
|
});
|
|
169
|
+
const branchMap = await getBranchMap(
|
|
170
|
+
opts.cwd,
|
|
171
|
+
parsed.map((p) => p.sha).filter((s) => s.length > 0)
|
|
172
|
+
);
|
|
173
|
+
return parsed.map((p) => ({
|
|
174
|
+
date: p.date,
|
|
175
|
+
time: p.time,
|
|
176
|
+
ticket: p.ticket,
|
|
177
|
+
description: p.description,
|
|
178
|
+
branch: branchMap.get(p.sha) ?? null
|
|
179
|
+
}));
|
|
66
180
|
}
|
|
67
181
|
|
|
68
182
|
// src/format.ts
|
|
69
183
|
import Table from "cli-table3";
|
|
70
184
|
import chalk from "chalk";
|
|
71
|
-
function
|
|
185
|
+
function renderDateCell(entry) {
|
|
186
|
+
if (!entry.time) {
|
|
187
|
+
return entry.date;
|
|
188
|
+
}
|
|
189
|
+
return `${entry.date} ${chalk.dim(entry.time)}`;
|
|
190
|
+
}
|
|
191
|
+
function renderDescriptionCell(entry) {
|
|
192
|
+
if (!entry.branch) {
|
|
193
|
+
return entry.description;
|
|
194
|
+
}
|
|
195
|
+
return `${entry.description} ${chalk.magenta(`[${entry.branch}]`)}`;
|
|
196
|
+
}
|
|
197
|
+
function renderTable(entries, ticketColumnLabel = "Ticket") {
|
|
72
198
|
const table = new Table({
|
|
73
199
|
head: [
|
|
74
200
|
chalk.bold.cyan("Date"),
|
|
75
|
-
chalk.bold.cyan(
|
|
201
|
+
chalk.bold.cyan(ticketColumnLabel),
|
|
202
|
+
chalk.bold.cyan("Description")
|
|
203
|
+
],
|
|
204
|
+
style: { head: [], border: [] },
|
|
205
|
+
wordWrap: true,
|
|
206
|
+
colWidths: [18, 14, 80]
|
|
207
|
+
});
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
table.push([
|
|
210
|
+
renderDateCell(entry),
|
|
211
|
+
entry.ticket ? chalk.yellow(entry.ticket) : chalk.gray("\u2014"),
|
|
212
|
+
renderDescriptionCell(entry)
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
return table.toString();
|
|
216
|
+
}
|
|
217
|
+
function renderTableGroupedByDay(entries, ticketColumnLabel = "Ticket") {
|
|
218
|
+
const table = new Table({
|
|
219
|
+
head: [
|
|
220
|
+
chalk.bold.cyan("Time"),
|
|
221
|
+
chalk.bold.cyan(ticketColumnLabel),
|
|
76
222
|
chalk.bold.cyan("Description")
|
|
77
223
|
],
|
|
78
224
|
style: { head: [], border: [] },
|
|
79
225
|
wordWrap: true,
|
|
80
|
-
colWidths: [
|
|
226
|
+
colWidths: [8, 14, 90]
|
|
81
227
|
});
|
|
228
|
+
let currentDate = "";
|
|
82
229
|
for (const entry of entries) {
|
|
230
|
+
if (entry.date && entry.date !== currentDate) {
|
|
231
|
+
table.push([
|
|
232
|
+
{
|
|
233
|
+
content: chalk.bold(entry.date),
|
|
234
|
+
colSpan: 3,
|
|
235
|
+
hAlign: "left"
|
|
236
|
+
}
|
|
237
|
+
]);
|
|
238
|
+
currentDate = entry.date;
|
|
239
|
+
}
|
|
83
240
|
table.push([
|
|
84
|
-
chalk.dim(entry.
|
|
241
|
+
entry.time ? chalk.dim(entry.time) : chalk.gray("\u2014"),
|
|
85
242
|
entry.ticket ? chalk.yellow(entry.ticket) : chalk.gray("\u2014"),
|
|
86
|
-
entry
|
|
243
|
+
renderDescriptionCell(entry)
|
|
87
244
|
]);
|
|
88
245
|
}
|
|
89
246
|
return table.toString();
|
|
90
247
|
}
|
|
248
|
+
function toJsonKey(label) {
|
|
249
|
+
return label.toLowerCase().replace(/\s+/g, "_");
|
|
250
|
+
}
|
|
251
|
+
function renderJson(entries, ticketColumnLabel = "Ticket") {
|
|
252
|
+
const key = toJsonKey(ticketColumnLabel);
|
|
253
|
+
const transformed = entries.map((e) => ({
|
|
254
|
+
date: e.date,
|
|
255
|
+
time: e.time,
|
|
256
|
+
[key]: e.ticket,
|
|
257
|
+
description: e.description,
|
|
258
|
+
branch: e.branch
|
|
259
|
+
}));
|
|
260
|
+
return JSON.stringify(transformed, null, 2);
|
|
261
|
+
}
|
|
91
262
|
function renderEmpty() {
|
|
92
263
|
return chalk.gray("No commits found for the given filters.");
|
|
93
264
|
}
|
|
@@ -100,6 +271,15 @@ import { homedir } from "os";
|
|
|
100
271
|
import { readFile } from "fs/promises";
|
|
101
272
|
import { join } from "path";
|
|
102
273
|
import { cosmiconfig } from "cosmiconfig";
|
|
274
|
+
var DEFAULT_COLUMN_LABELS = {
|
|
275
|
+
jira: "Ticket",
|
|
276
|
+
github: "Issue",
|
|
277
|
+
conventional: "Type",
|
|
278
|
+
custom: "Match"
|
|
279
|
+
};
|
|
280
|
+
function getColumnLabel(format, override) {
|
|
281
|
+
return override ?? DEFAULT_COLUMN_LABELS[format];
|
|
282
|
+
}
|
|
103
283
|
var PRESET_PATTERNS = {
|
|
104
284
|
jira: /\b([A-Z][A-Z0-9]+-\d+)\b/,
|
|
105
285
|
github: /#(\d+)/,
|
|
@@ -111,12 +291,28 @@ var VALID_FORMATS = [
|
|
|
111
291
|
"conventional",
|
|
112
292
|
"custom"
|
|
113
293
|
];
|
|
294
|
+
var MAX_CUSTOM_PATTERN_LENGTH = 500;
|
|
295
|
+
function compileUserRegex(pattern) {
|
|
296
|
+
if (pattern.length > MAX_CUSTOM_PATTERN_LENGTH) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`customPattern is ${pattern.length} characters; limit is ${MAX_CUSTOM_PATTERN_LENGTH}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return new RegExp(pattern);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`customPattern is not a valid regex: ${err.message}`,
|
|
306
|
+
{ cause: err }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
114
310
|
function getTicketPattern(format, customPattern) {
|
|
115
311
|
if (format === "custom") {
|
|
116
312
|
if (!customPattern) {
|
|
117
313
|
throw new Error('format "custom" requires customPattern to be set');
|
|
118
314
|
}
|
|
119
|
-
return
|
|
315
|
+
return compileUserRegex(customPattern);
|
|
120
316
|
}
|
|
121
317
|
return PRESET_PATTERNS[format];
|
|
122
318
|
}
|
|
@@ -147,6 +343,11 @@ function validateConfig(raw) {
|
|
|
147
343
|
if (typeof obj.customPattern !== "string") {
|
|
148
344
|
throw new Error("customPattern must be a string");
|
|
149
345
|
}
|
|
346
|
+
if (obj.customPattern.length > MAX_CUSTOM_PATTERN_LENGTH) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`customPattern is ${obj.customPattern.length} characters; limit is ${MAX_CUSTOM_PATTERN_LENGTH}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
150
351
|
cfg.customPattern = obj.customPattern;
|
|
151
352
|
}
|
|
152
353
|
if (cfg.format === "custom" && !cfg.customPattern) {
|
|
@@ -164,6 +365,12 @@ function validateConfig(raw) {
|
|
|
164
365
|
}
|
|
165
366
|
cfg.defaultRepos = obj.defaultRepos;
|
|
166
367
|
}
|
|
368
|
+
if ("ticketColumnLabel" in obj) {
|
|
369
|
+
if (typeof obj.ticketColumnLabel !== "string") {
|
|
370
|
+
throw new Error("ticketColumnLabel must be a string");
|
|
371
|
+
}
|
|
372
|
+
cfg.ticketColumnLabel = obj.ticketColumnLabel;
|
|
373
|
+
}
|
|
167
374
|
return cfg;
|
|
168
375
|
}
|
|
169
376
|
function globalConfigPath() {
|
|
@@ -195,6 +402,23 @@ async function loadConfig(cwd) {
|
|
|
195
402
|
}
|
|
196
403
|
|
|
197
404
|
// src/index.ts
|
|
405
|
+
function parseLimit(raw) {
|
|
406
|
+
if (raw === void 0) {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
const n = Number.parseInt(raw, 10);
|
|
410
|
+
if (!Number.isInteger(n) || n < 1 || String(n) !== raw.trim()) {
|
|
411
|
+
throw new Error(`invalid --limit "${raw}" \u2014 must be a positive integer`);
|
|
412
|
+
}
|
|
413
|
+
return n;
|
|
414
|
+
}
|
|
415
|
+
function shouldDisableColor(options) {
|
|
416
|
+
if (options.color === false) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
const noColor = process.env.NO_COLOR ?? "";
|
|
420
|
+
return noColor.length > 0;
|
|
421
|
+
}
|
|
198
422
|
var VALID_PRESETS = [
|
|
199
423
|
"jira",
|
|
200
424
|
"github",
|
|
@@ -242,21 +466,32 @@ async function run(dateArg, options) {
|
|
|
242
466
|
from = day;
|
|
243
467
|
to = day;
|
|
244
468
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
469
|
+
const limit = parseLimit(options.limit);
|
|
470
|
+
const perRepoEntries = await Promise.all(
|
|
471
|
+
repos.map(async (cwd) => {
|
|
472
|
+
const author = options.author ?? config.defaultAuthor ?? await getGitUserName(cwd);
|
|
473
|
+
return getCommits({ author, from, to, cwd, pattern, limit });
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
const allEntries = perRepoEntries.flat();
|
|
477
|
+
allEntries.sort(
|
|
478
|
+
(a, b) => `${b.date} ${b.time}`.localeCompare(`${a.date} ${a.time}`)
|
|
479
|
+
);
|
|
480
|
+
const display = limit !== void 0 ? allEntries.slice(0, limit) : allEntries;
|
|
481
|
+
const ticketColumnLabel = getColumnLabel(format, config.ticketColumnLabel);
|
|
482
|
+
if (options.json) {
|
|
483
|
+
process.stdout.write(renderJson(display, ticketColumnLabel) + "\n");
|
|
484
|
+
return;
|
|
250
485
|
}
|
|
251
|
-
|
|
252
|
-
if (allEntries.length === 0) {
|
|
486
|
+
if (display.length === 0) {
|
|
253
487
|
process.stdout.write(renderEmpty() + "\n");
|
|
254
488
|
return;
|
|
255
489
|
}
|
|
256
|
-
|
|
490
|
+
const rendered = options.groupByDay ? renderTableGroupedByDay(display, ticketColumnLabel) : renderTable(display, ticketColumnLabel);
|
|
491
|
+
process.stdout.write(rendered + "\n");
|
|
257
492
|
}
|
|
258
493
|
var program = new Command();
|
|
259
|
-
program.name("wdid").description("What did I do? \u2014 summarize your git commits as a table").version("0.
|
|
494
|
+
program.name("wdid").description("What did I do? \u2014 summarize your git commits as a table").version("0.2.0", "-V, --version", "output the version number").argument("[date]", 'a YYYY-MM-DD date or "today"; omit to show all history').option("--from <date>", 'start date (YYYY-MM-DD or "today")').option("--to <date>", 'end date (YYYY-MM-DD or "today")').option(
|
|
260
495
|
"--author <name>",
|
|
261
496
|
"override the git author (defaults to git config user.name, then defaultAuthor in config)"
|
|
262
497
|
).option(
|
|
@@ -268,7 +503,19 @@ program.name("wdid").description("What did I do? \u2014 summarize your git commi
|
|
|
268
503
|
).option(
|
|
269
504
|
"--ticket-pattern <regex>",
|
|
270
505
|
"custom regex for ticket extraction (implies --format custom; overrides --format)"
|
|
271
|
-
).
|
|
506
|
+
).option(
|
|
507
|
+
"--no-color",
|
|
508
|
+
"disable colored output (also honored via the NO_COLOR env var)"
|
|
509
|
+
).option(
|
|
510
|
+
"--limit <N>",
|
|
511
|
+
"cap the table to the most recent N rows (positive integer)"
|
|
512
|
+
).option(
|
|
513
|
+
"--group-by-day",
|
|
514
|
+
"group rows under a bold date heading per day (time-only in row)"
|
|
515
|
+
).option("--json", "emit a JSON array of commit entries instead of the table").action(async (dateArg, options) => {
|
|
516
|
+
if (shouldDisableColor(options)) {
|
|
517
|
+
chalk2.level = 0;
|
|
518
|
+
}
|
|
272
519
|
try {
|
|
273
520
|
await run(dateArg, options);
|
|
274
521
|
} catch (err) {
|