@young1lin/dsh-ui-gitworkbench 0.1.3 → 0.1.5

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The history filter, compiled into `git log` arguments.
3
+ *
4
+ * This is the pushdown half of the IDEA-style filter: matching runs inside
5
+ * `git log` over ALL history, not client-side over the loaded pages — the
6
+ * loaded-pages blind spot is the reason this module exists. Everything here is
7
+ * pure so the argument matrix is testable without spawning git.
8
+ *
9
+ * DIALECT RULE: whenever any pattern is emitted, the flags are `-i -E` and
10
+ * every literal input is escaped for POSIX ERE. Mixing `--fixed-strings` with
11
+ * `-E` is not possible (one overrides the other for EVERY pattern on the
12
+ * command line), so a single dialect keeps users literal no matter what the
13
+ * text criterion's regex toggle says; regex text alone is passed raw.
14
+ *
15
+ * Dates pass through verbatim: approxidate ("2 weeks ago") is git's language,
16
+ * and reimplementing even a slice of it client-side is how the dual-semantics
17
+ * bug farm starts. An invalid date is git's error to raise, surfaced by the
18
+ * host's usual exit-code + stderr format.
19
+ *
20
+ * @module @young1lin/dsh-ui-gitworkbench/log-filter
21
+ */
22
+ /** The filter that filters nothing — also the default when a client sends none. */
23
+ export function emptyLogFilter() {
24
+ return { users: [], text: '', textRegex: false, paths: [], after: '', before: '' };
25
+ }
26
+ /** POSIX ERE metacharacters, escaped so each matches itself. */
27
+ function escapeEre(literal) {
28
+ return literal.replace(/[\\.[\]*+?(){}|^$-]/g, '\\$&');
29
+ }
30
+ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
31
+ /**
32
+ * Expand a bare calendar day into an explicit moment. Git's approxidate
33
+ * parses `--since=2026-08-18` against an implementation-defined timezone,
34
+ * which on Windows silently excludes that very day's commits; `T00:00:00`
35
+ * pins it to local midnight (a picker day means the whole day, so `before`
36
+ * gets the day's last second instead).
37
+ */
38
+ function expandDay(bound, endOfDay) {
39
+ if (!DAY_RE.test(bound))
40
+ return bound;
41
+ return endOfDay ? `${bound}T23:59:59` : `${bound}T00:00:00`;
42
+ }
43
+ /**
44
+ * Compile a filter into the argument segment inserted after the log command's
45
+ * own flags. Pathspecs come last behind a bare `--`, so the CALLER must place
46
+ * this segment at the end of the argument list.
47
+ * @param filter - the query; blank entries are dropped, not turned into empty
48
+ * patterns (an empty `--author=` would match nothing).
49
+ */
50
+ export function logFilterArgs(filter) {
51
+ const users = filter.users.map(user => user.trim()).filter(user => user.length > 0);
52
+ const paths = filter.paths.map(path => path.trim()).filter(path => path.length > 0);
53
+ const text = filter.text.trim();
54
+ const after = filter.after.trim();
55
+ const before = filter.before.trim();
56
+ const args = [];
57
+ if (users.length > 0 || text.length > 0) {
58
+ args.push('-i', '-E');
59
+ for (const user of users)
60
+ args.push(`--author=${escapeEre(user)}`);
61
+ if (text.length > 0)
62
+ args.push(`--grep=${filter.textRegex ? text : escapeEre(text)}`);
63
+ }
64
+ if (after.length > 0)
65
+ args.push(`--since=${expandDay(after, false)}`);
66
+ if (before.length > 0)
67
+ args.push(`--until=${expandDay(before, true)}`);
68
+ if (paths.length > 0)
69
+ args.push('--', ...paths);
70
+ return args;
71
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Author roster from `git shortlog -sne`, for the filter popup's user picker.
3
+ *
4
+ * The REF the roster walks is the caller's choice (the host passes the same
5
+ * ref the history list walks, or `--all` in All-branches mode); shortlog
6
+ * already did the aggregation, and this module only parses its fixed shape
7
+ * (`<count> <name> <<email>>`), sorts by activity and applies the cap — with
8
+ * the cap VISIBLE, because a filter popup that silently lost the long tail of
9
+ * occasional committers would be quietly wrong about who exists.
10
+ *
11
+ * @module @young1lin/dsh-ui-gitworkbench/shortlog
12
+ */
13
+ /**
14
+ * Parse shortlog output into busiest-first authors, capped.
15
+ * @param stdout - `git shortlog -sne <ref>` output (the ref is the caller's choice).
16
+ * @param limit - how many authors to keep; the busier half survives.
17
+ * @returns the roster and whether it was cut short.
18
+ */
19
+ export function parseShortlog(stdout, limit) {
20
+ const authors = [];
21
+ for (const line of stdout.split('\n')) {
22
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
23
+ if (match === null)
24
+ continue;
25
+ const count = Number(match[1]);
26
+ const who = match[2];
27
+ // The email is the LAST <...> on the line; a name may legally contain
28
+ // anything else.
29
+ const emailMatch = /<([^>]*)>\s*$/.exec(who);
30
+ if (emailMatch === null)
31
+ continue;
32
+ const name = who.slice(0, emailMatch.index).trim();
33
+ if (name.length === 0)
34
+ continue;
35
+ authors.push({ name, email: emailMatch[1], count });
36
+ }
37
+ authors.sort((a, b) => b.count - a.count);
38
+ const truncated = authors.length > limit;
39
+ return { authors: truncated ? authors.slice(0, limit) : authors, truncated };
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@young1lin/dsh-ui-gitworkbench",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Out-of-tree dsh web UI plugin: a session-header git workbench chip opening a drawer with the file tree, per-file diff, history, compare, staging, commit, and sync (fetch/pull/push).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",