reslop 0.2.6 → 0.3.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/CHANGELOG.md +27 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +504 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +16 -0
- package/lib/agents/models.js +460 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +61 -66
- package/lib/{files.js → common/files.js} +51 -52
- package/lib/common/find.js +61 -0
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +117 -51
- package/lib/dashboard/npm.js +133 -32
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +41 -31
- package/lib/dashboard/workspace.js +151 -0
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +123 -95
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +475 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +240 -71
- package/lib/{editor.js → input/editor.js} +7 -8
- package/lib/{keys.js → input/keys.js} +36 -39
- package/lib/input/line-edit.js +77 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +640 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/dashboard/blocks.js +490 -0
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +22 -8
- package/lib/render/footer.js +383 -0
- package/lib/render/frame.js +127 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +319 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +29 -13
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +89 -0
- package/lib/render/primitives.js +109 -16
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +214 -0
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +19 -61
- package/lib/render/tiles.js +203 -47
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +46 -14
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +9 -0
- package/lib/review/format.js +426 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +958 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +36 -44
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +122 -56
- package/lib/session/compose.js +197 -146
- package/lib/session/dashboard.js +183 -37
- package/lib/session/dispatch.js +120 -22
- package/lib/session/files-pane.js +60 -24
- package/lib/session/find.js +81 -0
- package/lib/session/import.js +82 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +96 -66
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +8 -3
- package/lib/session/npm/history.js +249 -0
- package/lib/session/npm/npm.js +815 -0
- package/lib/session/npm/packages.js +150 -0
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +216 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +166 -59
- package/lib/session/tasks.js +321 -66
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +72 -59
- package/lib/session/workspace.js +416 -0
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +2 -2
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -505
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -437
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -628
- package/lib/render/dash-blocks.js +0 -245
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/review.js +0 -744
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { REVIEW_DIR } = require('../common/files.js');
|
|
5
|
+
const { FileIndex, HEAT_MS } = require('../dashboard/tree.js');
|
|
6
|
+
const { UNKNOWN_PATH } = require('../dashboard/watch.js');
|
|
7
|
+
const workspace = require('../dashboard/workspace.js');
|
|
8
|
+
const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
|
|
9
|
+
const { readNpmSummary } = require('../dashboard/npm.js');
|
|
10
|
+
const { collectReport } = require('../git/deps.js');
|
|
11
|
+
|
|
12
|
+
const TICK_ID = 'repos';
|
|
13
|
+
const HEAT_TIMER = 'repos-heat';
|
|
14
|
+
|
|
15
|
+
const freshBag = () => ({
|
|
16
|
+
files: [],
|
|
17
|
+
git: false,
|
|
18
|
+
runs: false,
|
|
19
|
+
tasks: false,
|
|
20
|
+
npm: false,
|
|
21
|
+
activity: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const fullBag = () => ({
|
|
25
|
+
files: [UNKNOWN_PATH],
|
|
26
|
+
git: true,
|
|
27
|
+
runs: true,
|
|
28
|
+
tasks: true,
|
|
29
|
+
npm: true,
|
|
30
|
+
activity: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const mergeBag = (left, right) => {
|
|
34
|
+
const base = left ?? freshBag();
|
|
35
|
+
return {
|
|
36
|
+
files: [...base.files, ...right.files],
|
|
37
|
+
git: base.git || right.git,
|
|
38
|
+
runs: base.runs || right.runs,
|
|
39
|
+
tasks: base.tasks || right.tasks,
|
|
40
|
+
npm: base.npm || right.npm,
|
|
41
|
+
activity: base.activity || right.activity,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const blankRow = (repo) => ({
|
|
46
|
+
name: repo.name,
|
|
47
|
+
dir: repo.dir,
|
|
48
|
+
index: new FileIndex(repo.dir),
|
|
49
|
+
bytes: 0,
|
|
50
|
+
exts: [],
|
|
51
|
+
tasks: 0,
|
|
52
|
+
tasksDone: 0,
|
|
53
|
+
commits: 0,
|
|
54
|
+
diff: emptyDiff(),
|
|
55
|
+
run: null,
|
|
56
|
+
activityAt: 0,
|
|
57
|
+
filesReady: false,
|
|
58
|
+
commitsReady: false,
|
|
59
|
+
diffReady: false,
|
|
60
|
+
tasksReady: false,
|
|
61
|
+
runsReady: false,
|
|
62
|
+
npm: null,
|
|
63
|
+
npmReady: false,
|
|
64
|
+
pending: null,
|
|
65
|
+
running: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const readFiles = async (row, bag) => {
|
|
69
|
+
if (!bag.files.length) return;
|
|
70
|
+
const unknown = bag.files.includes(UNKNOWN_PATH);
|
|
71
|
+
if (unknown || !row.index.ready) await row.index.rescan();
|
|
72
|
+
else await row.index.touch(bag.files);
|
|
73
|
+
if (!row.index.ready) return;
|
|
74
|
+
const summary = row.index.summary();
|
|
75
|
+
row.bytes = summary.total.bytes;
|
|
76
|
+
row.exts = summary.exts.slice(0, 3);
|
|
77
|
+
row.filesReady = true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const readCommits = async (row) => {
|
|
81
|
+
row.commits = await countCommits(row.dir);
|
|
82
|
+
row.commitsReady = true;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const readDiff = async (row) => {
|
|
86
|
+
row.diff = await readDiffStat(row.dir);
|
|
87
|
+
row.diffReady = true;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const readTasks = (row) => {
|
|
91
|
+
const stats = taskStats(row.dir);
|
|
92
|
+
row.tasksDone = stats.done;
|
|
93
|
+
row.tasks = stats.total;
|
|
94
|
+
row.tasksReady = true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const readRuns = (row) => {
|
|
98
|
+
row.run = pickRun(row.dir);
|
|
99
|
+
row.runsReady = true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const reportCount = async (dir, kind) => {
|
|
103
|
+
const report = await collectReport(dir, kind);
|
|
104
|
+
return report ? report.size : 0;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const readNpm = async (row) => {
|
|
108
|
+
const summary = await readNpmSummary(row.dir, row.npm);
|
|
109
|
+
const [audit, outdated] = await Promise.all([
|
|
110
|
+
reportCount(row.dir, 'audit'),
|
|
111
|
+
reportCount(row.dir, 'outdated'),
|
|
112
|
+
]);
|
|
113
|
+
row.npm = { ...summary, audit, outdated };
|
|
114
|
+
row.npmReady = true;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const toTile = (row) => ({
|
|
118
|
+
id: row.name,
|
|
119
|
+
name: row.name,
|
|
120
|
+
bytes: row.bytes,
|
|
121
|
+
exts: row.exts,
|
|
122
|
+
tasks: row.tasks,
|
|
123
|
+
tasksDone: row.tasksDone,
|
|
124
|
+
commits: row.commits,
|
|
125
|
+
diff: row.diff,
|
|
126
|
+
run: row.run,
|
|
127
|
+
activityAt: row.activityAt,
|
|
128
|
+
filesReady: row.filesReady,
|
|
129
|
+
commitsReady: row.commitsReady,
|
|
130
|
+
diffReady: row.diffReady,
|
|
131
|
+
tasksReady: row.tasksReady,
|
|
132
|
+
runsReady: row.runsReady,
|
|
133
|
+
npm: row.npm,
|
|
134
|
+
npmReady: row.npmReady,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const rowBusy = (row) => {
|
|
138
|
+
if (!row.filesReady || !row.commitsReady) return true;
|
|
139
|
+
if (!row.diffReady || !row.tasksReady) return true;
|
|
140
|
+
return !row.runsReady || !row.npmReady;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const classifyRest = (rest) => {
|
|
144
|
+
const bag = freshBag();
|
|
145
|
+
if (!rest) {
|
|
146
|
+
bag.files.push(UNKNOWN_PATH);
|
|
147
|
+
bag.git = true;
|
|
148
|
+
bag.runs = true;
|
|
149
|
+
bag.tasks = true;
|
|
150
|
+
bag.npm = true;
|
|
151
|
+
bag.activity = true;
|
|
152
|
+
return bag;
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
rest === 'package.json' ||
|
|
156
|
+
rest === 'package-lock.json' ||
|
|
157
|
+
rest === 'node_modules/.package-lock.json'
|
|
158
|
+
) {
|
|
159
|
+
bag.npm = true;
|
|
160
|
+
}
|
|
161
|
+
if (rest === '.git' || rest.startsWith('.git/')) {
|
|
162
|
+
bag.git = true;
|
|
163
|
+
return bag;
|
|
164
|
+
}
|
|
165
|
+
if (rest === '.log' || rest.startsWith('.log/')) {
|
|
166
|
+
bag.runs = true;
|
|
167
|
+
return bag;
|
|
168
|
+
}
|
|
169
|
+
if (rest === REVIEW_DIR || rest.startsWith(`${REVIEW_DIR}/`)) {
|
|
170
|
+
bag.tasks = true;
|
|
171
|
+
return bag;
|
|
172
|
+
}
|
|
173
|
+
bag.files.push(rest);
|
|
174
|
+
bag.activity = true;
|
|
175
|
+
return bag;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
class Workspace {
|
|
179
|
+
constructor(ui) {
|
|
180
|
+
this.ui = ui;
|
|
181
|
+
this.epoch = 0;
|
|
182
|
+
this.reset();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
reset() {
|
|
186
|
+
this.stop();
|
|
187
|
+
this.rows = [];
|
|
188
|
+
this.byName = new Map();
|
|
189
|
+
this.cursor = 0;
|
|
190
|
+
this.cols = 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
seed() {
|
|
194
|
+
const info = this.ui.workspaceInfo;
|
|
195
|
+
const repos = info ? info.repos : [];
|
|
196
|
+
this.rows = repos.map(blankRow);
|
|
197
|
+
this.byName = new Map(this.rows.map((row) => [row.name, row]));
|
|
198
|
+
if (this.cursor >= this.rows.length) this.cursor = 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
stale(epoch) {
|
|
202
|
+
return epoch !== this.epoch || !this.active;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
publish(epoch) {
|
|
206
|
+
if (this.stale(epoch)) return;
|
|
207
|
+
this.afterDraw();
|
|
208
|
+
if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async settle(epoch, read) {
|
|
212
|
+
await read();
|
|
213
|
+
this.publish(epoch);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async apply(row, bag, epoch) {
|
|
217
|
+
if (bag.activity) row.activityAt = Date.now();
|
|
218
|
+
const jobs = [];
|
|
219
|
+
if (bag.files.length) {
|
|
220
|
+
jobs.push(this.settle(epoch, () => readFiles(row, bag)));
|
|
221
|
+
}
|
|
222
|
+
if (bag.git) {
|
|
223
|
+
jobs.push(this.settle(epoch, () => readCommits(row)));
|
|
224
|
+
jobs.push(this.settle(epoch, () => readDiff(row)));
|
|
225
|
+
}
|
|
226
|
+
if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
|
|
227
|
+
if (bag.runs) jobs.push(this.settle(epoch, () => readRuns(row)));
|
|
228
|
+
if (bag.npm) jobs.push(this.settle(epoch, () => readNpm(row)));
|
|
229
|
+
await Promise.all(jobs);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async pump(row) {
|
|
233
|
+
row.running = true;
|
|
234
|
+
while (row.pending && this.active) {
|
|
235
|
+
const bag = row.pending;
|
|
236
|
+
const epoch = this.epoch;
|
|
237
|
+
row.pending = null;
|
|
238
|
+
try {
|
|
239
|
+
await this.apply(row, bag, epoch);
|
|
240
|
+
} catch {
|
|
241
|
+
// keep the previous snapshot until the next event
|
|
242
|
+
}
|
|
243
|
+
if (this.stale(epoch)) break;
|
|
244
|
+
this.afterDraw();
|
|
245
|
+
if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
|
|
246
|
+
}
|
|
247
|
+
row.running = false;
|
|
248
|
+
if (row.pending && this.active) {
|
|
249
|
+
void this.pump(row);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.afterDraw();
|
|
253
|
+
if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
kick(row, bag) {
|
|
257
|
+
if (!row || !this.active) return;
|
|
258
|
+
if (bag.activity) row.activityAt = Date.now();
|
|
259
|
+
row.pending = mergeBag(row.pending, bag);
|
|
260
|
+
if (row.running) return;
|
|
261
|
+
void this.pump(row);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
requestAll() {
|
|
265
|
+
for (const row of this.rows) this.kick(row, fullBag());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
notePaths(paths) {
|
|
269
|
+
if (!this.active) return;
|
|
270
|
+
for (const rel of paths) {
|
|
271
|
+
if (rel === UNKNOWN_PATH) return void this.requestAll();
|
|
272
|
+
}
|
|
273
|
+
const found = new Map();
|
|
274
|
+
for (const rel of paths) {
|
|
275
|
+
const slash = rel.indexOf('/');
|
|
276
|
+
const name = slash < 0 ? rel : rel.slice(0, slash);
|
|
277
|
+
const rest = slash < 0 ? '' : rel.slice(slash + 1);
|
|
278
|
+
const row = this.byName.get(name);
|
|
279
|
+
if (!row) continue;
|
|
280
|
+
found.set(name, mergeBag(found.get(name), classifyRest(rest)));
|
|
281
|
+
}
|
|
282
|
+
for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
|
|
283
|
+
if (found.size) {
|
|
284
|
+
this.afterDraw();
|
|
285
|
+
if (this.ui.uiOpen) this.ui.paint();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
start() {
|
|
290
|
+
if (this.active) return;
|
|
291
|
+
this.active = true;
|
|
292
|
+
if (!this.rows.length) this.seed();
|
|
293
|
+
this.requestAll();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
stop() {
|
|
297
|
+
this.epoch += 1;
|
|
298
|
+
this.active = false;
|
|
299
|
+
this.cancelTimers();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
cancelTimers() {
|
|
303
|
+
const { progress, term } = this.ui;
|
|
304
|
+
if (!progress || !term) return;
|
|
305
|
+
progress.stop(TICK_ID);
|
|
306
|
+
term.cancel(HEAT_TIMER);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
hotUntil() {
|
|
310
|
+
let until = 0;
|
|
311
|
+
for (const row of this.rows) {
|
|
312
|
+
until = Math.max(until, row.activityAt + HEAT_MS);
|
|
313
|
+
}
|
|
314
|
+
return until;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
afterDraw() {
|
|
318
|
+
if (!this.active) return;
|
|
319
|
+
const visible = this.ui.uiOpen && this.ui.nav.pane === 'repos';
|
|
320
|
+
if (!visible) return void this.cancelTimers();
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
const until = this.hotUntil();
|
|
323
|
+
const hot = now < until;
|
|
324
|
+
const running = this.rows.some((row) => row.run?.status === 'running');
|
|
325
|
+
const loading = this.rows.some((row) => rowBusy(row));
|
|
326
|
+
const { progress, term } = this.ui;
|
|
327
|
+
if (running || hot || loading) progress.start(TICK_ID);
|
|
328
|
+
else progress.stop(TICK_ID);
|
|
329
|
+
if (!hot) return;
|
|
330
|
+
term.later(
|
|
331
|
+
HEAT_TIMER,
|
|
332
|
+
() => {
|
|
333
|
+
if (this.ui.nav.pane === 'repos') this.ui.paint();
|
|
334
|
+
},
|
|
335
|
+
until - now + 20,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
view() {
|
|
340
|
+
return {
|
|
341
|
+
now: Date.now(),
|
|
342
|
+
frame: this.ui.progressFrame,
|
|
343
|
+
cursor: this.cursor,
|
|
344
|
+
tiles: this.rows.map(toTile),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
move(dx, dy) {
|
|
349
|
+
const count = this.rows.length;
|
|
350
|
+
if (!count) return;
|
|
351
|
+
const cols = Math.max(1, this.cols || 1);
|
|
352
|
+
const next = this.cursor + dx + dy * cols;
|
|
353
|
+
if (next < 0 || next >= count) return;
|
|
354
|
+
this.cursor = next;
|
|
355
|
+
if (this.ui.uiOpen) this.ui.paint();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async switchTo(row) {
|
|
359
|
+
const ui = this.ui;
|
|
360
|
+
ui.flushReview();
|
|
361
|
+
this.stop();
|
|
362
|
+
ui.retarget({ cwd: row.dir, repoName: row.name, pane: 'dashboard' });
|
|
363
|
+
try {
|
|
364
|
+
await ui.ensureRepo();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
ui.status = error instanceof Error ? error.message : `${error}`;
|
|
367
|
+
return { loading: this.show() };
|
|
368
|
+
}
|
|
369
|
+
const loading = ui.openLoad();
|
|
370
|
+
if (ui.uiOpen) {
|
|
371
|
+
ui.lifecycle.startWatch();
|
|
372
|
+
ui.dashboard.start();
|
|
373
|
+
ui.paint();
|
|
374
|
+
}
|
|
375
|
+
return { loading };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async enter(name) {
|
|
379
|
+
const row = this.byName.get(name);
|
|
380
|
+
const ui = this.ui;
|
|
381
|
+
if (!row || ui.mode !== 'review') return;
|
|
382
|
+
const opened = await ui.ops.hold(this.switchTo(row));
|
|
383
|
+
if (opened) await opened.loading;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
openSelected() {
|
|
387
|
+
const row = this.rows[this.cursor];
|
|
388
|
+
if (!row) return Promise.resolve();
|
|
389
|
+
return this.enter(row.name);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
openAt(id) {
|
|
393
|
+
const index = this.rows.findIndex((row) => row.name === id);
|
|
394
|
+
if (index < 0) return Promise.resolve();
|
|
395
|
+
this.cursor = index;
|
|
396
|
+
return this.openSelected();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
show() {
|
|
400
|
+
const ui = this.ui;
|
|
401
|
+
if (!ui.workspaceRoot) return Promise.resolve();
|
|
402
|
+
ui.flushReview();
|
|
403
|
+
const root = ui.workspaceRoot;
|
|
404
|
+
const repoName = path.basename(root);
|
|
405
|
+
ui.retarget({ cwd: root, top: root, repoName, pane: 'repos' });
|
|
406
|
+
ui.loader.didLoad = true;
|
|
407
|
+
this.start();
|
|
408
|
+
if (ui.uiOpen) {
|
|
409
|
+
ui.lifecycle.startWatch();
|
|
410
|
+
ui.paint();
|
|
411
|
+
}
|
|
412
|
+
return Promise.resolve();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
module.exports = { Workspace };
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const READ_METHODS = [
|
|
4
4
|
'load',
|
|
5
|
-
'loadAsync',
|
|
6
5
|
'loadExtras',
|
|
7
6
|
'toplevel',
|
|
8
7
|
'resolveRev',
|
|
@@ -12,7 +11,6 @@ const READ_METHODS = [
|
|
|
12
11
|
|
|
13
12
|
const CHANGE_METHODS = [
|
|
14
13
|
'add',
|
|
15
|
-
'addAsync',
|
|
16
14
|
'unstage',
|
|
17
15
|
'revert',
|
|
18
16
|
'revertFile',
|
|
@@ -23,7 +21,7 @@ const CHANGE_METHODS = [
|
|
|
23
21
|
|
|
24
22
|
const BRANCH_METHODS = [
|
|
25
23
|
'commit',
|
|
26
|
-
'
|
|
24
|
+
'updateCommit',
|
|
27
25
|
'hasStaged',
|
|
28
26
|
'lastMessage',
|
|
29
27
|
'commitMessage',
|
|
@@ -31,23 +29,14 @@ const BRANCH_METHODS = [
|
|
|
31
29
|
'listCommits',
|
|
32
30
|
'currentBranch',
|
|
33
31
|
'checkout',
|
|
34
|
-
'checkoutAsync',
|
|
35
32
|
'createBranch',
|
|
36
|
-
'createBranchAsync',
|
|
37
33
|
'rebase',
|
|
38
|
-
'rebaseAsync',
|
|
39
34
|
'drop',
|
|
40
|
-
'dropAsync',
|
|
41
35
|
'dropCommit',
|
|
42
|
-
'dropCommitAsync',
|
|
43
36
|
'applyFixup',
|
|
44
|
-
'applyFixupAsync',
|
|
45
37
|
'reword',
|
|
46
|
-
'rewordAsync',
|
|
47
38
|
'pull',
|
|
48
|
-
'pullAsync',
|
|
49
39
|
'push',
|
|
50
|
-
'pushAsync',
|
|
51
40
|
];
|
|
52
41
|
|
|
53
42
|
const VIEW_CAPS = { read: true, changes: false, branches: false, review: true };
|
|
@@ -57,6 +46,8 @@ const SOURCE_CAPS = {
|
|
|
57
46
|
commit: VIEW_CAPS,
|
|
58
47
|
pr: VIEW_CAPS,
|
|
59
48
|
mr: VIEW_CAPS,
|
|
49
|
+
issue: VIEW_CAPS,
|
|
50
|
+
'gl-issue': VIEW_CAPS,
|
|
60
51
|
readonly: VIEW_CAPS,
|
|
61
52
|
};
|
|
62
53
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
const
|
|
6
|
-
const { isPathInScope } = files;
|
|
3
|
+
const { isHashObject } = require('metautil');
|
|
4
|
+
const { isPathInScope } = require('../common/files.js');
|
|
5
|
+
const { resource } = require('../common/utilities.js');
|
|
7
6
|
const remote = require('./remote.js');
|
|
8
7
|
const { RemoteClient, isAbortError, listUrl, parseLinkNext } = remote;
|
|
9
8
|
const { commentBody, importedTodo, noteFromLocation } = remote;
|
|
10
|
-
const { splitNotes, loadChange } = remote;
|
|
9
|
+
const { splitNotes, loadChange, stateIs, loadIssue } = remote;
|
|
11
10
|
|
|
12
11
|
const GITHUB_API = 'https://api.github.com';
|
|
13
12
|
const API_VERSION = '2022-11-28';
|
|
@@ -17,14 +16,9 @@ const DIFF_ACCEPT = 'application/vnd.github.diff';
|
|
|
17
16
|
const PR_TODO_FILE = 'pull request';
|
|
18
17
|
const IMPORT_HOST = 'github';
|
|
19
18
|
const GRAPHQL_URL = `${GITHUB_API}/graphql`;
|
|
20
|
-
const REVIEW_THREADS_QUERY =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
' pullRequest(number: $number) {' +
|
|
24
|
-
' reviewThreads(first: 100, after: $after) {' +
|
|
25
|
-
' pageInfo { hasNextPage endCursor }' +
|
|
26
|
-
' nodes { isResolved comments(first: 100) { nodes { databaseId } } }' +
|
|
27
|
-
' } } } }';
|
|
19
|
+
const REVIEW_THREADS_QUERY = resource(__dirname, 'review-threads.graphql')
|
|
20
|
+
.replace(/\s+/g, ' ')
|
|
21
|
+
.trim();
|
|
28
22
|
|
|
29
23
|
const GITHUB_PR_RE = new RegExp(
|
|
30
24
|
String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
|
|
@@ -35,18 +29,41 @@ const GITHUB_PR_RE = new RegExp(
|
|
|
35
29
|
'i',
|
|
36
30
|
);
|
|
37
31
|
|
|
38
|
-
const
|
|
32
|
+
const GITHUB_ISSUE_RE = new RegExp(
|
|
33
|
+
String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
|
|
34
|
+
String.raw`([^/?#]+)/([^/?#]+)/issues/(\d+)` +
|
|
35
|
+
String.raw`/?(?:[?#].*)?$`,
|
|
36
|
+
'i',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const GITHUB_ISSUE_LIST_RE = new RegExp(
|
|
40
|
+
String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
|
|
41
|
+
String.raw`([^/?#]+)/([^/?#]+?)(?:\.git)?` +
|
|
42
|
+
String.raw`(?:/issues)?` +
|
|
43
|
+
String.raw`/?(?:[?#].*)?$`,
|
|
44
|
+
'i',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const parseGithubUrl = (text, pattern) => {
|
|
39
48
|
if (typeof text !== 'string' || !text) return null;
|
|
40
|
-
const match =
|
|
49
|
+
const match = pattern.exec(text.trim());
|
|
41
50
|
if (!match) return null;
|
|
42
51
|
const owner = match[1];
|
|
43
52
|
const repo = match[2].replace(/\.git$/i, '');
|
|
53
|
+
if (!owner || !repo) return null;
|
|
54
|
+
if (!match[3]) return { owner, repo, list: true };
|
|
44
55
|
const number = parseInt(match[3], 10);
|
|
45
|
-
|
|
46
|
-
if (!owner || !repo || !hasNumber) return null;
|
|
56
|
+
if (!Number.isFinite(number) || number < 1) return null;
|
|
47
57
|
return { owner, repo, number };
|
|
48
58
|
};
|
|
49
59
|
|
|
60
|
+
const parseGithubPrUrl = (text) => parseGithubUrl(text, GITHUB_PR_RE);
|
|
61
|
+
|
|
62
|
+
const parseGithubIssueUrl = (text) => parseGithubUrl(text, GITHUB_ISSUE_RE);
|
|
63
|
+
|
|
64
|
+
const parseGithubIssueListUrl = (text) =>
|
|
65
|
+
parseGithubUrl(text, GITHUB_ISSUE_LIST_RE);
|
|
66
|
+
|
|
50
67
|
const githubToken = (env) => {
|
|
51
68
|
const from = env ?? process.env;
|
|
52
69
|
return from.GITHUB_TOKEN || from.GH_TOKEN || '';
|
|
@@ -69,12 +86,13 @@ const client = new RemoteClient({
|
|
|
69
86
|
nextPage: parseLinkNext,
|
|
70
87
|
});
|
|
71
88
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return `${GITHUB_API}/repos/${owner}/${repo}/pulls/${pr.number}`;
|
|
89
|
+
const repositoryApiUrl = ({ owner, repo }) => {
|
|
90
|
+
const name = `${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
91
|
+
return `${GITHUB_API}/repos/${name}`;
|
|
76
92
|
};
|
|
77
93
|
|
|
94
|
+
const prApiUrl = (pr) => `${repositoryApiUrl(pr)}/pulls/${pr.number}`;
|
|
95
|
+
|
|
78
96
|
const loginOf = (user) => user?.login || '';
|
|
79
97
|
|
|
80
98
|
const toChange = (pr, data) => {
|
|
@@ -131,12 +149,15 @@ const discussionToNotes = (discussion, items, paths = []) => {
|
|
|
131
149
|
const comments = discussion.reviewComments ?? [];
|
|
132
150
|
const reviews = discussion.reviews ?? [];
|
|
133
151
|
const issueComments = discussion.issueComments ?? [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
]
|
|
152
|
+
const reviewNote = (comment) => {
|
|
153
|
+
const note = reviewCommentToNote(comment, items, resolvedById, paths);
|
|
154
|
+
return note;
|
|
155
|
+
};
|
|
156
|
+
const reviewNotes = comments.map(reviewNote);
|
|
157
|
+
const summaries = [...reviews, ...issueComments];
|
|
158
|
+
const summaryNotes = summaries.map(summaryToTodo);
|
|
159
|
+
const notes = [...reviewNotes, ...summaryNotes];
|
|
160
|
+
return splitNotes(notes);
|
|
140
161
|
};
|
|
141
162
|
|
|
142
163
|
const requestThreads = async (variables, options) => {
|
|
@@ -184,12 +205,15 @@ const loadResolvedById = async (pr, options) => {
|
|
|
184
205
|
|
|
185
206
|
const loadDiscussion = async (pr, options) => {
|
|
186
207
|
const pull = prApiUrl(pr);
|
|
187
|
-
const issue =
|
|
208
|
+
const issue = `${repositoryApiUrl(pr)}/issues/${pr.number}`;
|
|
188
209
|
const list = (url) => client.list(listUrl(url), options);
|
|
189
|
-
const reviewComments
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
210
|
+
const [reviewComments, reviews, issueComments, resolvedById] =
|
|
211
|
+
await Promise.all([
|
|
212
|
+
list(`${pull}/comments`),
|
|
213
|
+
list(`${pull}/reviews`),
|
|
214
|
+
list(`${issue}/comments`),
|
|
215
|
+
loadResolvedById(pr, options),
|
|
216
|
+
]);
|
|
193
217
|
return { reviewComments, reviews, issueComments, resolvedById };
|
|
194
218
|
};
|
|
195
219
|
|
|
@@ -204,10 +228,56 @@ const loadPullRequest = (pr, options = {}) =>
|
|
|
204
228
|
discussionToNotes,
|
|
205
229
|
});
|
|
206
230
|
|
|
231
|
+
const issueSite = (issue) => {
|
|
232
|
+
const repository = `${issue.owner}/${issue.repo}`;
|
|
233
|
+
return {
|
|
234
|
+
repository,
|
|
235
|
+
id: `github.com/${repository}/issues`,
|
|
236
|
+
page: `https://github.com/${repository}/issues`,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const readIssue = (data) => ({
|
|
241
|
+
number: data.number,
|
|
242
|
+
title: data.title,
|
|
243
|
+
body: data.body,
|
|
244
|
+
author: loginOf(data.user),
|
|
245
|
+
state: data.state,
|
|
246
|
+
url: data['html_url'],
|
|
247
|
+
comments: data.comments,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const readIssueComment = (comment) => ({
|
|
251
|
+
reviewer: loginOf(comment.user),
|
|
252
|
+
body: comment.body,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const isOpenIssue = (data) => {
|
|
256
|
+
if (!isHashObject(data)) return false;
|
|
257
|
+
if (isHashObject(data['pull_request'])) return false;
|
|
258
|
+
return stateIs(data.state, 'open');
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const loadGithubIssue = async (issue, options = {}) =>
|
|
262
|
+
loadIssue(issue, options, {
|
|
263
|
+
client,
|
|
264
|
+
host: IMPORT_HOST,
|
|
265
|
+
site: issueSite(issue),
|
|
266
|
+
issuesUrl: `${repositoryApiUrl(issue)}/issues`,
|
|
267
|
+
openState: 'open',
|
|
268
|
+
comments: 'comments',
|
|
269
|
+
isOpen: isOpenIssue,
|
|
270
|
+
readIssue,
|
|
271
|
+
readComment: readIssueComment,
|
|
272
|
+
});
|
|
273
|
+
|
|
207
274
|
module.exports = {
|
|
208
275
|
parseGithubPrUrl,
|
|
276
|
+
parseGithubIssueUrl,
|
|
277
|
+
parseGithubIssueListUrl,
|
|
209
278
|
githubToken,
|
|
210
279
|
prApiUrl,
|
|
211
280
|
discussionToNotes,
|
|
212
281
|
loadPullRequest,
|
|
282
|
+
loadGithubIssue,
|
|
213
283
|
};
|