@profullstack/g1tz 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/LICENSE +21 -0
- package/README.md +90 -0
- package/bin/g1tz.mjs +2 -0
- package/dist/diff.d.ts +44 -0
- package/dist/diff.js +112 -0
- package/dist/git.d.ts +61 -0
- package/dist/git.js +182 -0
- package/dist/github.d.ts +63 -0
- package/dist/github.js +154 -0
- package/dist/main.d.ts +71 -0
- package/dist/main.js +458 -0
- package/dist/pulse-view.d.ts +62 -0
- package/dist/pulse-view.js +244 -0
- package/dist/pulse.d.ts +120 -0
- package/dist/pulse.js +348 -0
- package/dist/traffic.d.ts +37 -0
- package/dist/traffic.js +68 -0
- package/package.json +53 -0
- package/src/diff.ts +147 -0
- package/src/git.ts +225 -0
- package/src/github.ts +213 -0
- package/src/main.ts +466 -0
- package/src/pulse-view.ts +294 -0
- package/src/pulse.ts +413 -0
- package/src/traffic.ts +100 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Pulse screen: what moved in this repository over a period.
|
|
3
|
+
*
|
|
4
|
+
* Git on the left (overview, commits over time, authors), the wider world on
|
|
5
|
+
* the right (the files that changed, GitHub's pull requests, issues, releases
|
|
6
|
+
* and stars, and the repository's views and clones from a gh-pulse report).
|
|
7
|
+
* Pure: everything drawn comes from PulseState, so the screen is asserted on
|
|
8
|
+
* as text in the tests and captured for the hqtui.com gallery the same way.
|
|
9
|
+
*/
|
|
10
|
+
import { type Container, type Theme } from "@profullstack/hqtui";
|
|
11
|
+
import type { GitHubPulse } from "./github.ts";
|
|
12
|
+
import { type Pulse, type RangeKey } from "./pulse.ts";
|
|
13
|
+
import type { Traffic } from "./traffic.ts";
|
|
14
|
+
export type GitHubStatus = "idle" | "loading" | "ready" | "unavailable";
|
|
15
|
+
export interface PulseState {
|
|
16
|
+
range: RangeKey;
|
|
17
|
+
data: Pulse | null;
|
|
18
|
+
github: GitHubPulse | null;
|
|
19
|
+
githubStatus: GitHubStatus;
|
|
20
|
+
/** Why GitHub is unavailable, when it is. */
|
|
21
|
+
githubNote: string;
|
|
22
|
+
/** Ticket of the latest GitHub request; a reply to an older one is dropped. */
|
|
23
|
+
githubRequest: number;
|
|
24
|
+
traffic: Traffic | null;
|
|
25
|
+
filesOffset: number;
|
|
26
|
+
note: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function createPulseState(range?: RangeKey): PulseState;
|
|
29
|
+
export interface PulseActions {
|
|
30
|
+
pickRange: (key: RangeKey) => void;
|
|
31
|
+
refresh: () => void;
|
|
32
|
+
/** Back to the repository screen. */
|
|
33
|
+
back: () => void;
|
|
34
|
+
/** Open the Pulse screen from the repository screen. */
|
|
35
|
+
pulse: () => void;
|
|
36
|
+
}
|
|
37
|
+
export declare const NO_ACTIONS: PulseActions;
|
|
38
|
+
export interface ViewArgs {
|
|
39
|
+
ui: Container;
|
|
40
|
+
theme: Theme;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
elapsed: number;
|
|
44
|
+
}
|
|
45
|
+
/** Keep the tail of a path, which is the part that tells files apart; the library's own truncation keeps the head. */
|
|
46
|
+
export declare function shortenPath(path: string, max: number): string;
|
|
47
|
+
export declare const clampOffset: (offset: number, total: number) => number;
|
|
48
|
+
/**
|
|
49
|
+
* Content columns of the two panes: the body splits 1fr : 1.2fr with a gap of
|
|
50
|
+
* one, and a panel's border and padding take two on each side. An estimate,
|
|
51
|
+
* since the layout is not known until it is solved; a column short is fine,
|
|
52
|
+
* a column over is a truncated axis label.
|
|
53
|
+
*/
|
|
54
|
+
export declare const leftPaneColumns: (width: number) => number;
|
|
55
|
+
export declare const rightPaneColumns: (width: number) => number;
|
|
56
|
+
export declare function commitTotals(pulse: Pulse): {
|
|
57
|
+
commits: number;
|
|
58
|
+
merges: number;
|
|
59
|
+
};
|
|
60
|
+
/** What the status bar says on the right: the GitHub read, or nothing. */
|
|
61
|
+
export declare function githubLine(state: PulseState, elapsed: number): string;
|
|
62
|
+
export declare function pulseView({ ui, theme, width, height, elapsed }: ViewArgs, state: PulseState, root: string, actions?: PulseActions): void;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Pulse screen: what moved in this repository over a period.
|
|
3
|
+
*
|
|
4
|
+
* Git on the left (overview, commits over time, authors), the wider world on
|
|
5
|
+
* the right (the files that changed, GitHub's pull requests, issues, releases
|
|
6
|
+
* and stars, and the repository's views and clones from a gh-pulse report).
|
|
7
|
+
* Pure: everything drawn comes from PulseState, so the screen is asserted on
|
|
8
|
+
* as text in the tests and captured for the hqtui.com gallery the same way.
|
|
9
|
+
*/
|
|
10
|
+
import { widgets } from "@profullstack/hqtui";
|
|
11
|
+
import { RANGE_KEYS, RANGE_LABEL, foldBuckets } from "./pulse.js";
|
|
12
|
+
export function createPulseState(range = "week") {
|
|
13
|
+
return {
|
|
14
|
+
range,
|
|
15
|
+
data: null,
|
|
16
|
+
github: null,
|
|
17
|
+
githubStatus: "idle",
|
|
18
|
+
githubNote: "",
|
|
19
|
+
githubRequest: 0,
|
|
20
|
+
traffic: null,
|
|
21
|
+
filesOffset: 0,
|
|
22
|
+
note: "",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const NO_ACTIONS = { pickRange: () => { }, refresh: () => { }, back: () => { }, pulse: () => { } };
|
|
26
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
27
|
+
const fmtStamp = (iso) => iso.replace("T", " ").slice(0, 16);
|
|
28
|
+
const names = (refs, max) => `${refs.slice(0, max).map((r) => r.name).join(", ")}${refs.length > max ? ", …" : ""}`;
|
|
29
|
+
/** Keep the tail of a path, which is the part that tells files apart; the library's own truncation keeps the head. */
|
|
30
|
+
export function shortenPath(path, max) {
|
|
31
|
+
if (max < 4 || path.length <= max)
|
|
32
|
+
return path;
|
|
33
|
+
return `…${path.slice(path.length - (max - 1))}`;
|
|
34
|
+
}
|
|
35
|
+
export const clampOffset = (offset, total) => Math.max(0, Math.min(Math.max(0, total - 1), offset));
|
|
36
|
+
/**
|
|
37
|
+
* Content columns of the two panes: the body splits 1fr : 1.2fr with a gap of
|
|
38
|
+
* one, and a panel's border and padding take two on each side. An estimate,
|
|
39
|
+
* since the layout is not known until it is solved; a column short is fine,
|
|
40
|
+
* a column over is a truncated axis label.
|
|
41
|
+
*/
|
|
42
|
+
export const leftPaneColumns = (width) => Math.max(10, Math.floor((width - 1) / 2.2) - 4);
|
|
43
|
+
export const rightPaneColumns = (width) => Math.max(10, Math.floor(((width - 1) * 1.2) / 2.2) - 4);
|
|
44
|
+
export function commitTotals(pulse) {
|
|
45
|
+
let merges = 0;
|
|
46
|
+
for (const c of pulse.commits)
|
|
47
|
+
if (c.merge)
|
|
48
|
+
merges++;
|
|
49
|
+
return { commits: pulse.commits.length - merges, merges };
|
|
50
|
+
}
|
|
51
|
+
/** What the status bar says on the right: the GitHub read, or nothing. */
|
|
52
|
+
export function githubLine(state, elapsed) {
|
|
53
|
+
switch (state.githubStatus) {
|
|
54
|
+
case "loading": return `${widgets.spinnerFrame(elapsed, widgets.SPINNER_FRAMES.dots)} GitHub`;
|
|
55
|
+
case "unavailable": return `GitHub: ${state.githubNote}`;
|
|
56
|
+
case "ready": return state.github?.partial ? "GitHub counts are floors (page budget)" : "";
|
|
57
|
+
default: return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function overviewPanel(col, theme, pulse) {
|
|
61
|
+
col.panel({ title: "Overview", size: 9 }, (p) => {
|
|
62
|
+
if (!pulse) {
|
|
63
|
+
p.label("Press r to read the repository.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { commits, merges } = commitTotals(pulse);
|
|
67
|
+
const authors = pulse.authors;
|
|
68
|
+
p.keyValues([
|
|
69
|
+
{
|
|
70
|
+
label: "Commits",
|
|
71
|
+
value: `${commits} on ${pulse.branch || "HEAD"}${merges ? `, ${plural(merges, "merge")}` : ""}${pulse.commitsTruncated ? " (capped)" : ""}`,
|
|
72
|
+
color: theme.accent,
|
|
73
|
+
},
|
|
74
|
+
{ label: "All branches", value: `${plural(pulse.allBranchCommits, "commit")}, merges excluded` },
|
|
75
|
+
{
|
|
76
|
+
label: "Authors",
|
|
77
|
+
value: authors.length ? `${authors.length}: ${authors.slice(0, 3).map((a) => a.name).join(", ")}${authors.length > 3 ? ", …" : ""}` : "none",
|
|
78
|
+
},
|
|
79
|
+
{ label: "Changed", value: `${plural(pulse.filesChanged, "file")}, +${pulse.additions} −${pulse.deletions}`, color: theme.success },
|
|
80
|
+
{ label: "Branches", value: pulse.branches.length ? `${pulse.branches.length} active: ${names(pulse.branches, 3)}` : "none active" },
|
|
81
|
+
{ label: "Tags", value: pulse.tags.length ? `${pulse.tags.length} new: ${names(pulse.tags, 4)}` : "none" },
|
|
82
|
+
{ label: "Period", value: `${pulse.since ? fmtStamp(pulse.since) : "the first commit"} to ${fmtStamp(pulse.until)}` },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function activityPanel(col, theme, pulse, label, cols) {
|
|
87
|
+
col.panel({ title: pulse ? `Commits per ${pulse.unit}` : "Commits", size: 8 }, (p) => {
|
|
88
|
+
if (!pulse)
|
|
89
|
+
return;
|
|
90
|
+
const buckets = foldBuckets(pulse.buckets, Math.max(1, Math.floor(cols / 2)));
|
|
91
|
+
const peak = buckets.reduce((m, b) => Math.max(m, b.count), 0);
|
|
92
|
+
if (peak === 0) {
|
|
93
|
+
p.label(`No commits, ${label}.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Each bucket is widened to fill the pane, as one column per bucket would leave most of it empty.
|
|
97
|
+
const per = Math.max(1, Math.floor(cols / Math.max(1, buckets.length)));
|
|
98
|
+
p.text(`${plural(pulse.commits.length, "commit")}, peak ${peak} in one ${pulse.unit}`, { fg: theme.muted, size: 1 });
|
|
99
|
+
p.histogram({ values: buckets.flatMap((b) => Array(per).fill(b.count)), color: theme.accent, size: 4 });
|
|
100
|
+
const first = buckets[0]?.label ?? "";
|
|
101
|
+
const last = buckets.at(-1)?.label ?? "";
|
|
102
|
+
p.text(`${first}${" ".repeat(Math.max(1, cols - first.length - last.length))}${last}`, { fg: theme.muted, size: 1 });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function authorsPanel(col, theme, pulse, label) {
|
|
106
|
+
col.panel({ title: `Authors (${pulse?.authors.length ?? 0})`, size: "1fr" }, (p) => {
|
|
107
|
+
if (!pulse || pulse.authors.length === 0) {
|
|
108
|
+
p.label(`No commits, ${label}.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const total = pulse.authors.reduce((t, a) => t + a.commits, 0);
|
|
112
|
+
p.table({
|
|
113
|
+
rows: pulse.authors,
|
|
114
|
+
header: false,
|
|
115
|
+
scrollbar: true,
|
|
116
|
+
columns: [
|
|
117
|
+
{ key: "name", title: "", min: 8, color: theme.foreground },
|
|
118
|
+
{ key: "commits", title: "", width: 6, align: "right", color: theme.accent, render: (a) => String(a.commits) },
|
|
119
|
+
{
|
|
120
|
+
key: "share",
|
|
121
|
+
title: "",
|
|
122
|
+
width: 16,
|
|
123
|
+
color: theme.muted,
|
|
124
|
+
render: (a) => {
|
|
125
|
+
const filled = Math.round((a.commits / total) * 10);
|
|
126
|
+
return `${"█".repeat(filled)}${"·".repeat(10 - filled)} ${Math.round((100 * a.commits) / total)}%`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function filesPanel(col, theme, state, label, cols) {
|
|
134
|
+
const pulse = state.data;
|
|
135
|
+
const title = pulse ? `Files changed (${pulse.filesChanged}) +${pulse.additions} −${pulse.deletions}` : "Files changed";
|
|
136
|
+
col.panel({ title, size: "1fr" }, (p) => {
|
|
137
|
+
if (!pulse || pulse.files.length === 0) {
|
|
138
|
+
p.label(`Nothing changed, ${label}.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const pathWidth = Math.max(8, cols - 16);
|
|
142
|
+
p.table({
|
|
143
|
+
rows: pulse.files,
|
|
144
|
+
offset: state.filesOffset,
|
|
145
|
+
header: false,
|
|
146
|
+
scrollbar: true,
|
|
147
|
+
onScroll: (delta) => { state.filesOffset = clampOffset(state.filesOffset + delta, pulse.files.length); },
|
|
148
|
+
columns: [
|
|
149
|
+
{ key: "path", title: "", min: 8, color: theme.foreground, render: (f) => shortenPath(f.from ? `${f.from} → ${f.path}` : f.path, pathWidth) },
|
|
150
|
+
{ key: "added", title: "", width: 7, align: "right", color: theme.success, render: (f) => (f.binary ? "bin" : `+${f.added}`) },
|
|
151
|
+
{ key: "deleted", title: "", width: 7, align: "right", color: theme.danger, render: (f) => (f.binary ? "" : `−${f.deleted}`) },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function githubPanel(col, theme, state, label, elapsed) {
|
|
157
|
+
const g = state.github;
|
|
158
|
+
col.panel({ title: g ? `GitHub ${g.repo}` : "GitHub", size: 10 }, (p) => {
|
|
159
|
+
if (state.githubStatus === "loading") {
|
|
160
|
+
p.spinner({ label: "asking GitHub", text: label, elapsed });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (state.githubStatus !== "ready" || !g) {
|
|
164
|
+
p.text(state.githubNote || "Pull requests, issues, releases and stars appear here when gh is logged in.", { fg: theme.muted, wrap: true });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
p.keyValues([
|
|
168
|
+
{
|
|
169
|
+
label: "Pull requests",
|
|
170
|
+
value: `${g.prsMerged.length} merged, ${g.prsOpened.length} opened, ${g.prsClosed.length} closed unmerged`,
|
|
171
|
+
color: theme.accent,
|
|
172
|
+
},
|
|
173
|
+
{ label: "Issues", value: `${g.issuesOpened.length} opened, ${g.issuesClosed.length} closed, ${g.openIssues} open now` },
|
|
174
|
+
{ label: "Releases", value: g.releases.length ? `${g.releases.length}: ${g.releases.slice(0, 4).map((r) => r.tag).join(", ")}` : "none" },
|
|
175
|
+
{ label: "Stars", value: `+${g.newStars}${g.partial ? " or more" : ""}, ${g.stars} total, ${plural(g.forks, "fork")}` },
|
|
176
|
+
]);
|
|
177
|
+
const lines = [];
|
|
178
|
+
for (const pr of g.prsMerged.slice(0, 2))
|
|
179
|
+
lines.push(`merged #${pr.number} ${pr.title} (${pr.user})`);
|
|
180
|
+
for (const pr of g.prsOpened.slice(0, 1))
|
|
181
|
+
lines.push(`opened #${pr.number} ${pr.title} (${pr.user})`);
|
|
182
|
+
for (const is of g.issuesOpened.slice(0, 1))
|
|
183
|
+
lines.push(`issue #${is.number} ${is.title} (${is.user})`);
|
|
184
|
+
if (lines.length)
|
|
185
|
+
p.text(lines.join("\n"), { fg: theme.muted });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function trafficPanel(col, theme, t) {
|
|
189
|
+
col.panel({ title: t ? `Traffic gh-pulse ${fmtStamp(t.reportAt)}` : "Traffic", size: 7 }, (p) => {
|
|
190
|
+
if (!t) {
|
|
191
|
+
p.text("Views and clones appear here when a gh-pulse report exists on this machine (~/.local/share/gh-pulse).", { fg: theme.muted, wrap: true });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const sum = (series, k) => series.reduce((n, d) => n + d[k], 0);
|
|
195
|
+
const series = (s) => (s.length ? s.map((d) => d.count) : [0]);
|
|
196
|
+
p.sparkline({ values: series(t.views14d), label: "Views 14d", text: `${sum(t.views14d, "count")} / ${sum(t.views14d, "uniques")} unique`, color: theme.info, size: 1 });
|
|
197
|
+
p.sparkline({ values: series(t.clones14d), label: "Clones 14d", text: `${sum(t.clones14d, "count")} / ${sum(t.clones14d, "uniques")} unique`, color: theme.warning, size: 1 });
|
|
198
|
+
p.text(t.referrers.length ? `Referrers: ${t.referrers.slice(0, 4).map((r) => `${r.referrer} ${r.count}`).join(" · ")}` : "No referrers in the report.", { fg: theme.muted, size: 1 });
|
|
199
|
+
p.text(t.paths.length ? `Popular: ${t.paths.slice(0, 3).map((q) => `${q.path.replace(`/${t.repo}`, "") || "/"} ${q.count}`).join(" · ")}` : "No popular paths in the report.", { fg: theme.muted, size: 1 });
|
|
200
|
+
p.text(`${t.url}/graphs/traffic`, { fg: theme.muted, size: 1 });
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
export function pulseView({ ui, theme, width, height, elapsed }, state, root, actions = NO_ACTIONS) {
|
|
204
|
+
const pulse = state.data;
|
|
205
|
+
const label = RANGE_LABEL[state.range];
|
|
206
|
+
ui.row({ size: 1 }, (header) => {
|
|
207
|
+
header.text(" g1tz", { fg: theme.title, bold: true, size: 7 });
|
|
208
|
+
header.text("pulse", { fg: theme.accent, size: 7 });
|
|
209
|
+
header.text(pulse ? pulse.branch || "(detached)" : "", { fg: theme.accent, size: 24 });
|
|
210
|
+
header.text(label, { fg: theme.muted });
|
|
211
|
+
header.text(`${root} d w m q y a range p repo ctrl+c quit `, { fg: theme.muted, align: "right" });
|
|
212
|
+
});
|
|
213
|
+
// One button per range, the current one lit, all clickable. `focused` is
|
|
214
|
+
// pinned off: the library would otherwise paint whichever button holds its
|
|
215
|
+
// focus index (the first) in the strong style, beside the real range.
|
|
216
|
+
ui.buttons(RANGE_KEYS.map((k) => ({
|
|
217
|
+
label: k,
|
|
218
|
+
variant: (state.range === k ? "primary" : "ghost"),
|
|
219
|
+
focused: false,
|
|
220
|
+
onPress: () => actions.pickRange(k),
|
|
221
|
+
})), { size: 1 });
|
|
222
|
+
ui.row({ size: height - 3, gap: 1 }, (row) => {
|
|
223
|
+
row.column({ width: "1fr", gap: 1 }, (left) => {
|
|
224
|
+
overviewPanel(left, theme, pulse);
|
|
225
|
+
activityPanel(left, theme, pulse, label, leftPaneColumns(width));
|
|
226
|
+
authorsPanel(left, theme, pulse, label);
|
|
227
|
+
});
|
|
228
|
+
row.column({ width: "1.2fr", gap: 1 }, (right) => {
|
|
229
|
+
filesPanel(right, theme, state, label, rightPaneColumns(width));
|
|
230
|
+
githubPanel(right, theme, state, label, elapsed);
|
|
231
|
+
trafficPanel(right, theme, state.traffic);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
ui.statusBar({
|
|
235
|
+
items: [
|
|
236
|
+
{ key: "d w m q y a", label, active: true },
|
|
237
|
+
{ key: "↑↓", label: "Files" },
|
|
238
|
+
{ key: "r", label: "Refresh", onPress: actions.refresh },
|
|
239
|
+
{ key: "p", label: "Repo", onPress: actions.back },
|
|
240
|
+
{ key: "ctrl+c", label: "Quit" },
|
|
241
|
+
],
|
|
242
|
+
right: [{ label: state.note || githubLine(state, elapsed) }],
|
|
243
|
+
});
|
|
244
|
+
}
|
package/dist/pulse.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse: what moved in this repository over a period, read from git.
|
|
3
|
+
*
|
|
4
|
+
* The same idea as GitHub's Insights > Pulse tab, worked out locally so it is
|
|
5
|
+
* there offline and for repositories that are not on GitHub at all: commits
|
|
6
|
+
* and authors on the current branch and across every branch, the net change
|
|
7
|
+
* to the tree since the period began, the branches that saw commits and the
|
|
8
|
+
* tags that were created. GitHub's own side (pull requests, issues, stars and
|
|
9
|
+
* releases) is an optional, separate read in github.ts; views and clones come
|
|
10
|
+
* from a gh-pulse report when one exists (traffic.ts).
|
|
11
|
+
*
|
|
12
|
+
* Everything here is read-only, which is why the file list can afford
|
|
13
|
+
* display-grade path handling: a munged path on this screen cannot touch the
|
|
14
|
+
* working tree, unlike one in the Files pane.
|
|
15
|
+
*/
|
|
16
|
+
import { type GitError } from "./git.ts";
|
|
17
|
+
export declare const RANGE_KEYS: readonly ["day", "week", "month", "quarter", "year", "all"];
|
|
18
|
+
export type RangeKey = (typeof RANGE_KEYS)[number];
|
|
19
|
+
export declare const RANGE_MS: Record<RangeKey, number>;
|
|
20
|
+
export declare const RANGE_LABEL: Record<RangeKey, string>;
|
|
21
|
+
export declare const RANGE_HOTKEY: Record<RangeKey, string>;
|
|
22
|
+
export declare function parseRangeKey(text: string): RangeKey | null;
|
|
23
|
+
export declare function rangeForHotkey(key: string): RangeKey | null;
|
|
24
|
+
/** The start of a range, or null for all time. */
|
|
25
|
+
export declare function sinceFor(range: RangeKey, now: Date): Date | null;
|
|
26
|
+
export interface PulseCommit {
|
|
27
|
+
hash: string;
|
|
28
|
+
short: string;
|
|
29
|
+
author: string;
|
|
30
|
+
email: string;
|
|
31
|
+
/** Committer date, ISO 8601. --since selects on it, so a bucket never falls outside the range. */
|
|
32
|
+
at: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
merge: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface PulseAuthor {
|
|
37
|
+
name: string;
|
|
38
|
+
commits: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PulseFile {
|
|
41
|
+
path: string;
|
|
42
|
+
/** Original path for a rename. */
|
|
43
|
+
from?: string;
|
|
44
|
+
added: number;
|
|
45
|
+
deleted: number;
|
|
46
|
+
binary: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PulseRef {
|
|
49
|
+
name: string;
|
|
50
|
+
at: string;
|
|
51
|
+
}
|
|
52
|
+
export type BucketUnit = "hour" | "day" | "week" | "month";
|
|
53
|
+
export interface Bucket {
|
|
54
|
+
label: string;
|
|
55
|
+
/** ISO start of the bucket. */
|
|
56
|
+
start: string;
|
|
57
|
+
count: number;
|
|
58
|
+
}
|
|
59
|
+
export interface Pulse {
|
|
60
|
+
range: RangeKey;
|
|
61
|
+
/** ISO start of the period, or null for all time. */
|
|
62
|
+
since: string | null;
|
|
63
|
+
until: string;
|
|
64
|
+
branch: string;
|
|
65
|
+
/** Every commit reachable from HEAD in the period, merges included, newest first. */
|
|
66
|
+
commits: PulseCommit[];
|
|
67
|
+
commitsTruncated: boolean;
|
|
68
|
+
/** Commits on every local branch in the period, merges excluded. */
|
|
69
|
+
allBranchCommits: number;
|
|
70
|
+
/** Merges excluded, busiest first. */
|
|
71
|
+
authors: PulseAuthor[];
|
|
72
|
+
filesChanged: number;
|
|
73
|
+
additions: number;
|
|
74
|
+
deletions: number;
|
|
75
|
+
/** The net change per file since the period began, biggest first. */
|
|
76
|
+
files: PulseFile[];
|
|
77
|
+
unit: BucketUnit;
|
|
78
|
+
buckets: Bucket[];
|
|
79
|
+
/** Local branches that received a commit in the period. */
|
|
80
|
+
branches: PulseRef[];
|
|
81
|
+
/** Tags created in the period. */
|
|
82
|
+
tags: PulseRef[];
|
|
83
|
+
errors: GitError[];
|
|
84
|
+
}
|
|
85
|
+
/** `git log --format=<fields>%x00`: one NUL-terminated record per commit, fields separated by 0x1f. */
|
|
86
|
+
export declare function parsePulseLog(out: string): PulseCommit[];
|
|
87
|
+
/**
|
|
88
|
+
* `git diff --numstat -z`: `added TAB deleted TAB path NUL`. A rename is
|
|
89
|
+
* `added TAB deleted TAB NUL old NUL new NUL`, and a binary file has `-` for
|
|
90
|
+
* both counts. Paths are raw, so one with a newline in it still parses.
|
|
91
|
+
*/
|
|
92
|
+
export declare function parseNumstat(out: string): PulseFile[];
|
|
93
|
+
/** `git diff --shortstat`: " 3 files changed, 12 insertions(+), 4 deletions(-)", any part of which may be missing. */
|
|
94
|
+
export declare function parseShortstat(out: string): {
|
|
95
|
+
filesChanged: number;
|
|
96
|
+
additions: number;
|
|
97
|
+
deletions: number;
|
|
98
|
+
};
|
|
99
|
+
/** `git for-each-ref --format=%(refname:short)%1f%(<date>:iso-strict)`, one ref per line. */
|
|
100
|
+
export declare function parseRefDates(out: string): PulseRef[];
|
|
101
|
+
export declare function refsSince(refs: PulseRef[], since: Date | null): PulseRef[];
|
|
102
|
+
/** Merges excluded, as GitHub counts them; busiest first, then by name. */
|
|
103
|
+
export declare function countAuthors(commits: readonly PulseCommit[]): PulseAuthor[];
|
|
104
|
+
export declare function unitFor(range: RangeKey): BucketUnit;
|
|
105
|
+
/** More buckets than this and the range is not one anybody can read; the newest ones win. */
|
|
106
|
+
export declare const BUCKET_CAP = 2000;
|
|
107
|
+
/**
|
|
108
|
+
* Contiguous buckets from the start of the period (or the first commit, for
|
|
109
|
+
* all time) to now, with the commits counted into them. Every bucket is
|
|
110
|
+
* present even when empty, so a quiet week shows as a gap rather than
|
|
111
|
+
* vanishing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function bucketCommits(commits: readonly PulseCommit[], since: Date | null, until: Date, unit: BucketUnit): Bucket[];
|
|
114
|
+
/** At most `max` buckets, merging neighbours; a group takes the label of its first. */
|
|
115
|
+
export declare function foldBuckets(buckets: readonly Bucket[], max: number): Bucket[];
|
|
116
|
+
/** The hash of the empty tree, so the diff for all time (or from a root commit) has something to diff against. */
|
|
117
|
+
export declare const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
118
|
+
export declare const COMMIT_CAP = 50000;
|
|
119
|
+
export declare const FILE_CAP = 500;
|
|
120
|
+
export declare function readPulse(root: string, range: RangeKey, now?: Date): Pulse;
|