f0b06dea Andy McCall 1 month ago
1
// Activation: sign in, find the Oxygen extension's API, register the
2
// remote provider, wire the views, the review controller and the commands.
3
4
import * as vscode from "vscode";
5
import { HubAuth } from "./auth";
6
import { ChecksBar } from "./checks";
bf73998c Andy McCall 1 month ago ↶
7
import { ProposalContentProvider, SCHEME, openAllChanges, openFileDiff, refOf } from "./diff";
263de209 Andy McCall 1 month ago ↶
8
import { FlowLogs, FlowProvider, JobNode, LOG_SCHEME, RunNode, openLog, useLogs } from "./flow";
f0b06dea Andy McCall 1 month ago
9
import { hubUrl } from "./hub";
b2dd082c Andy McCall 1 month ago ↶
10
import { InboxBar } from "./inbox";
991516a1 Andy McCall 6 days ago ↶
11
import { AssetNode, draftRelease, ReleaseNode, ReleasesProvider, saveAsset, showTrust } from "./releases";
12
import { DROP_SCHEME } from "./dropkit";
13
import { DropFileSystem, DropsProvider, registerDropCommands, shareFiles, shareSelection } from "./drops";
14
import { newRepository } from "./newrepo";
15
import { HubTests } from "./tests";
d21a8595 Andy McCall 22 days ago ↶
16
import { DiffusionPinNode, DiffusionProvider, diffusionTab, pinUrl } from "./diffusion";
f902d5b8 Andy McCall 1 month ago ↶
17
import { ReferenceCompletions, ReferenceHovers, ReferenceIndex, SELECTOR } from "./references";
95f69b4a Andy McCall 1 month ago ↶
18
import { annotateTodo, issueBody, todoTitle } from "./refs";
f0b06dea Andy McCall 1 month ago
19
import { IssueNode, IssuesProvider, showIssue } from "./issues";
20
import type { OxygenAPI } from "./oxygenapi";
bf73998c Andy McCall 1 month ago ↶
21
import { HubRepo, ProposalFileNode, ProposalNode, ProposalsProvider, showDetail } from "./proposals";
f0b06dea Andy McCall 1 month ago
22
import { makeProvider } from "./provider";
23
import { ReviewController } from "./review";
24
25
let auth: HubAuth;
26
let oxygen: OxygenAPI | undefined;
27
let proposals: ProposalsProvider;
28
let issues: IssuesProvider;
29
let review: ReviewController;
30
let bar: ChecksBar;
bf73998c Andy McCall 1 month ago ↶
31
let content: ProposalContentProvider;
b2dd082c Andy McCall 1 month ago ↶
32
let inbox: InboxBar;
f902d5b8 Andy McCall 1 month ago ↶
33
let references: ReferenceIndex;
1cc0ff44 Andy McCall 1 month ago ↶
34
let releases: ReleasesProvider;
d21a8595 Andy McCall 22 days ago ↶
35
let diffusion: DiffusionProvider;
263de209 Andy McCall 1 month ago ↶
36
let flow: FlowProvider;
991516a1 Andy McCall 6 days ago ↶
37
let drops: DropsProvider;
38
let tests: HubTests;
f0b06dea Andy McCall 1 month ago
39
40
export async function activate(context: vscode.ExtensionContext): Promise<void> {
41
auth = new HubAuth(context.secrets);
42
await auth.load();
43
context.subscriptions.push(auth);
44
85a87a2d Andy McCall 1 month ago ↶
45
// Oxygen VCS may be missing or fail to start (two copies of it installed
46
// at once): OxygenHub still starts, so signing in and the hub views work.
47
const ext = vscode.extensions.getExtension<OxygenAPI>("oxygenhub.oxygen-vscode");
48
try {
49
oxygen = ext ? ((await ext.activate()) as OxygenAPI | undefined) : undefined;
50
} catch {
51
oxygen = undefined;
52
}
f0b06dea Andy McCall 1 month ago
53
if (!oxygen) {
85a87a2d Andy McCall 1 month ago ↶
54
void vscode.window.showWarningMessage("OxygenHub: the Oxygen VCS extension is not available; install Oxygen VCS (oxygenhub.oxygen-vscode) 0.17.1 or later, and only one copy of it.");
f0b06dea Andy McCall 1 month ago
55
} else {
56
context.subscriptions.push(oxygen.registerRemoteProvider(makeProvider({ bases: () => auth.bases() })));
57
context.subscriptions.push(oxygen.onDidChangeRepositories(() => refreshAll()));
58
}
59
60
proposals = new ProposalsProvider(() => oxygen, auth);
61
issues = new IssuesProvider(() => proposals.repos(), auth);
62
review = new ReviewController(auth);
63
bar = new ChecksBar(auth, () => proposals.repos());
b2dd082c Andy McCall 1 month ago ↶
64
inbox = new InboxBar(auth, async (t) => {
65
const repo = (await proposals.repos()).find((r) => r.base === t.base && r.owner === t.owner && r.name === t.name);
66
if (!repo) {
67
return false;
68
}
69
const client = auth.clientFor(repo.base);
70
if (t.kind === "proposal") {
71
const [p, checks] = await Promise.all([client.proposal(repo.owner, repo.name, t.number), client.proposalChecks(repo.owner, repo.name, t.number).catch(() => [])]);
72
showDetail(repo, p, checks);
73
} else {
74
await showIssue(auth, repo, await client.issue(repo.owner, repo.name, t.number));
75
}
76
return true;
77
});
78
context.subscriptions.push(inbox);
1cc0ff44 Andy McCall 1 month ago ↶
79
releases = new ReleasesProvider(() => proposals.repos(), auth);
80
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygenhub.releases", releases));
d21a8595 Andy McCall 22 days ago ↶
81
diffusion = new DiffusionProvider(() => proposals.repos(), auth);
82
context.subscriptions.push(
83
vscode.window.registerTreeDataProvider("oxygenhub.diffusion", diffusion),
84
// A bump on this repository opens beside the code, as any proposal does;
85
// the rest open on the hub.
86
vscode.commands.registerCommand("oxygenhub.openDiffusionPin", async (node: DiffusionPinNode) => {
87
const bump = node.pin.bump;
88
if (node.side === "takes" && bump !== null && bump !== undefined) {
89
try {
90
const client = auth.clientFor(node.repo.base);
91
const [p, checks] = await Promise.all([client.proposal(node.repo.owner, node.repo.name, bump), client.proposalChecks(node.repo.owner, node.repo.name, bump).catch(() => [])]);
92
showDetail(node.repo, p, checks);
93
return;
94
} catch (e) {
95
void vscode.window.showWarningMessage(`OxygenHub: ${(e as Error).message}`);
96
}
97
}
98
void vscode.env.openExternal(vscode.Uri.parse(pinUrl(node)));
99
}),
100
vscode.commands.registerCommand("oxygenhub.openDiffusionOnHub", (node?: DiffusionPinNode | { kind: "repo"; repo: HubRepo }) => {
101
if (node) {
102
void vscode.env.openExternal(vscode.Uri.parse(node.kind === "pin" ? pinUrl(node) : diffusionTab(node.repo)));
103
}
104
}),
105
);
263de209 Andy McCall 1 month ago ↶
106
flow = new FlowProvider(() => proposals.repos(), auth);
107
const flowLogs = new FlowLogs(auth);
108
useLogs(flowLogs);
109
context.subscriptions.push(
110
flow,
111
flowLogs,
112
vscode.window.registerTreeDataProvider("oxygenhub.flow", flow),
113
vscode.workspace.registerTextDocumentContentProvider(LOG_SCHEME, flowLogs),
114
vscode.commands.registerCommand("oxygenhub.flowRefresh", () => flow.refresh()),
115
vscode.commands.registerCommand("oxygenhub.runPipeline", (node?: { repo?: HubRepo }) => flow.start(node?.repo)),
116
vscode.commands.registerCommand("oxygenhub.rerunFlow", (node: RunNode) => flow.rerun(node)),
117
vscode.commands.registerCommand("oxygenhub.flowLog", (node: JobNode) => openLog(node.repo, node.run, node.job)),
118
vscode.commands.registerCommand("oxygenhub.openFlowRun", (node: RunNode) => vscode.env.openExternal(vscode.Uri.parse(node.run.url))),
119
vscode.commands.registerCommand("oxygenhub.flowRecent", (repo?: HubRepo) => flow.recent(repo)),
991516a1 Andy McCall 6 days ago ↶
120
vscode.commands.registerCommand("oxygenhub.approveJob", (node: JobNode) => flow.decide(node, true)),
121
vscode.commands.registerCommand("oxygenhub.rejectJob", (node: JobNode) => flow.decide(node, false)),
122
vscode.commands.registerCommand("oxygenhub.expediteRun", (node: RunNode) => flow.expedite(node)),
123
vscode.commands.registerCommand("oxygenhub.cancelRun", (node: RunNode) => flow.cancel(node)),
124
vscode.commands.registerCommand("oxygenhub.jobReports", (node: JobNode) => flow.reports(node)),
263de209 Andy McCall 1 month ago ↶
125
);
991516a1 Andy McCall 6 days ago ↶
126
drops = new DropsProvider(auth);
127
context.subscriptions.push(
128
vscode.window.registerTreeDataProvider("oxygenhub.drops", drops),
129
vscode.workspace.registerFileSystemProvider(DROP_SCHEME, new DropFileSystem(auth, () => drops.refresh()), { isCaseSensitive: true }),
130
...registerDropCommands(auth, drops),
131
vscode.commands.registerCommand("oxygenhub.shareSelectionAsDrop", (uri?: vscode.Uri) => shareSelection(auth, drops, uri instanceof vscode.Uri ? uri : undefined)),
132
vscode.commands.registerCommand("oxygenhub.shareFilesAsDrop", (uri?: vscode.Uri, uris?: vscode.Uri[]) => shareFiles(auth, drops, uri instanceof vscode.Uri ? uri : undefined, uris)),
133
);
134
tests = new HubTests(auth, () => proposals.repos());
135
context.subscriptions.push(
136
tests,
137
vscode.commands.registerCommand("oxygenhub.releaseTrust", (node: ReleaseNode) => showTrust(auth, node).catch((e: Error) => vscode.window.showErrorMessage(`OxygenHub: ${e.message}`))),
138
vscode.commands.registerCommand("oxygenhub.saveAsset", (node: AssetNode) => saveAsset(auth, node).catch((e: Error) => vscode.window.showErrorMessage(`OxygenHub: ${e.message}`))),
139
vscode.commands.registerCommand("oxygenhub.newRepository", () => newRepository(auth)),
140
vscode.commands.registerCommand("oxygenhub.markReady", (...args: unknown[]) => withProposal(args, (repo, number) => setDraft(repo, number, false))),
141
vscode.commands.registerCommand("oxygenhub.convertToDraft", (...args: unknown[]) => withProposal(args, (repo, number) => setDraft(repo, number, true))),
142
vscode.commands.registerCommand("oxygenhub.landable", () => landable()),
143
);
f902d5b8 Andy McCall 1 month ago ↶
144
references = new ReferenceIndex(auth, () => proposals.repos());
145
context.subscriptions.push(
146
vscode.languages.registerCompletionItemProvider(SELECTOR, new ReferenceCompletions(references), "#", "@"),
147
vscode.languages.registerHoverProvider(SELECTOR, new ReferenceHovers(references)),
148
);
bf73998c Andy McCall 1 month ago ↶
149
content = new ProposalContentProvider(auth);
150
context.subscriptions.push(content, vscode.workspace.registerTextDocumentContentProvider(SCHEME, content));
f0b06dea Andy McCall 1 month ago
151
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygenhub.proposals", proposals), vscode.window.registerTreeDataProvider("oxygenhub.issues", issues), review, bar);
152
context.subscriptions.push(auth.onDidChange(() => refreshAll()));
153
154
context.subscriptions.push(
155
vscode.commands.registerCommand("oxygenhub.signIn", async () => {
156
const repos = oxygen ? await guessHubs() : [];
157
await auth.signIn(repos[0]);
158
refreshAll();
159
}),
160
vscode.commands.registerCommand("oxygenhub.signOut", async () => {
161
const pick = await vscode.window.showQuickPick(auth.all().map((s) => s.base), { placeHolder: "Sign out of" });
162
if (pick) {
163
await auth.removeSession(pick);
164
refreshAll();
165
}
166
}),
167
vscode.commands.registerCommand("oxygenhub.refresh", () => refreshAll()),
b2dd082c Andy McCall 1 month ago ↶
168
vscode.commands.registerCommand("oxygenhub.inbox", () => inbox.show()),
1cc0ff44 Andy McCall 1 month ago ↶
169
vscode.commands.registerCommand("oxygenhub.draftRelease", async (node?: { repo?: HubRepo }) => {
170
const repos = await proposals.repos();
171
const repo = node?.repo ?? (repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r);
172
if (!repo) {
173
return;
174
}
175
try {
176
const r = await draftRelease(auth, repo);
177
if (r) {
178
releases.refresh();
092b8bf9 Andy McCall 1 month ago ↶
179
const open = await vscode.window.showInformationMessage(`OxygenHub: released ${r.tag}.`, "Open on Hub");
1cc0ff44 Andy McCall 1 month ago ↶
180
if (open) {
181
void vscode.env.openExternal(vscode.Uri.parse(r.url));
182
}
183
}
184
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
185
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
1cc0ff44 Andy McCall 1 month ago ↶
186
}
187
}),
f0b06dea Andy McCall 1 month ago
188
vscode.commands.registerCommand("oxygenhub.openProposal", async (node?: ProposalNode) => {
189
const n = node ?? (await pickProposal());
190
if (!n) {
191
return;
192
}
193
const checks = await auth.clientFor(n.repo.base).proposalChecks(n.repo.owner, n.repo.name, n.p.number).catch(() => []);
194
showDetail(n.repo, n.p, checks);
195
}),
196
vscode.commands.registerCommand("oxygenhub.openOnHub", (node?: ProposalNode | IssueNode) => {
197
if (!node) {
198
return;
199
}
200
const target = node.kind === "proposal" ? { kind: "proposal", number: node.p.number } : { kind: "issue", number: node.i.number };
201
void vscode.env.openExternal(vscode.Uri.parse(hubUrl(node.repo.base, node.repo.owner, node.repo.name, target)));
202
}),
bf73998c Andy McCall 1 month ago ↶
203
vscode.commands.registerCommand("oxygenhub.openProposalFile", async (node: ProposalFileNode) => {
204
await openFileDiff(node.repo, node.p.number, node.f);
205
await review.loadDiff(node.repo, node.p.number).catch(() => undefined);
206
}),
207
vscode.commands.registerCommand("oxygenhub.openProposalChanges", (...args: unknown[]) =>
208
withProposal(args, async (repo, number) => {
209
try {
210
const client = auth.clientFor(repo.base);
211
const [p, files] = await Promise.all([client.proposal(repo.owner, repo.name, number), client.proposalFiles(repo.owner, repo.name, number)]);
212
await openAllChanges(repo, p, files);
213
await review.loadDiff(repo, number);
214
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
215
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
bf73998c Andy McCall 1 month ago ↶
216
}
217
}),
218
),
f0b06dea Andy McCall 1 month ago
219
vscode.commands.registerCommand("oxygenhub.checkoutProposal", (...args: unknown[]) => withProposal(args, checkout)),
220
vscode.commands.registerCommand("oxygenhub.review", (...args: unknown[]) => withProposal(args, reviewProposal)),
221
vscode.commands.registerCommand("oxygenhub.sealApproval", (...args: unknown[]) => withProposal(args, (repo, number) => runInTerminal(repo, `oxy seal --proposal ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`))),
222
vscode.commands.registerCommand("oxygenhub.land", (...args: unknown[]) => withProposal(args, (repo, number) => runInTerminal(repo, `oxy proposal land ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`))),
223
vscode.commands.registerCommand("oxygenhub.closeProposal", (...args: unknown[]) =>
224
withProposal(args, async (repo, number) => {
225
void vscode.env.openExternal(vscode.Uri.parse(hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })));
226
}),
227
),
228
vscode.commands.registerCommand("oxygenhub.createProposal", () => createProposal()),
229
vscode.commands.registerCommand("oxygenhub.newIssue", () => newIssue()),
95f69b4a Andy McCall 1 month ago ↶
230
vscode.commands.registerCommand("oxygenhub.issueFromSelection", () => issueFromSelection()),
f0b06dea Andy McCall 1 month ago
231
vscode.commands.registerCommand("oxygenhub.openIssue", async (node?: IssueNode) => node && showIssue(auth, node.repo, node.i)),
232
vscode.commands.registerCommand("oxygenhub.commentIssue", (...args: unknown[]) =>
233
withIssue(args, async (repo, number) => {
234
const body = await vscode.window.showInputBox({ prompt: `Comment on #${number}` });
235
if (body) {
236
await auth.clientFor(repo.base).commentIssue(repo.owner, repo.name, number, body);
237
issues.refresh();
238
}
239
}),
240
),
241
vscode.commands.registerCommand("oxygenhub.closeIssue", (...args: unknown[]) => withIssue(args, async (repo, number) => (await auth.clientFor(repo.base).setIssueState(repo.owner, repo.name, number, "closed"), issues.refresh()))),
242
vscode.commands.registerCommand("oxygenhub.reopenIssue", (...args: unknown[]) => withIssue(args, async (repo, number) => (await auth.clientFor(repo.base).setIssueState(repo.owner, repo.name, number, "open"), issues.refresh()))),
243
vscode.commands.registerCommand("oxygenhub.assignMe", (...args: unknown[]) =>
244
withIssue(args, async (repo, number) => {
092b8bf9 Andy McCall 1 month ago ↶
245
void vscode.window.showInformationMessage(`OxygenHub: assigning is on the hub for now: ${hubUrl(repo.base, repo.owner, repo.name, { kind: "issue", number })}`);
f0b06dea Andy McCall 1 month ago
246
}),
247
),
248
vscode.commands.registerCommand("oxygenhub.reply", (reply: vscode.CommentReply) => review.reply(reply)),
249
vscode.commands.registerCommand("oxygenhub.newThread", (reply: vscode.CommentReply) => review.reply(reply)),
250
vscode.commands.registerCommand("oxygenhub.resolveThread", (thread: vscode.CommentThread) => {
251
thread.state = thread.state === vscode.CommentThreadState.Resolved ? vscode.CommentThreadState.Unresolved : vscode.CommentThreadState.Resolved;
252
}),
253
);
254
bar.start();
b2dd082c Andy McCall 1 month ago ↶
255
inbox.start();
991516a1 Andy McCall 6 days ago ↶
256
void tests.load();
f0b06dea Andy McCall 1 month ago
257
}
258
259
function refreshAll(): void {
260
proposals.refresh();
f902d5b8 Andy McCall 1 month ago ↶
261
references.clear();
bf73998c Andy McCall 1 month ago ↶
262
refreshOpenDiffs();
f0b06dea Andy McCall 1 month ago
263
issues.refresh();
1cc0ff44 Andy McCall 1 month ago ↶
264
releases.refresh();
d21a8595 Andy McCall 22 days ago ↶
265
diffusion.refresh();
991516a1 Andy McCall 6 days ago ↶
266
drops.refresh();
267
void tests.load();
263de209 Andy McCall 1 month ago ↶
268
void flow.refresh();
f0b06dea Andy McCall 1 month ago
269
void bar.refresh();
b2dd082c Andy McCall 1 month ago ↶
270
void inbox.refresh();
f0b06dea Andy McCall 1 month ago
271
}
272
bf73998c Andy McCall 1 month ago ↶
273
/** Reload the sides of open proposal diffs, which may have gained changes. */
274
function refreshOpenDiffs(): void {
275
const seen = new Set<string>();
276
for (const doc of vscode.workspace.textDocuments) {
277
const r = refOf(doc.uri);
278
const key = r && `${r.base}/${r.owner}/${r.name}#${r.number}`;
279
if (r && key && !seen.has(key)) {
280
seen.add(key);
281
content.refresh(r.base, r.owner, r.name, r.number);
282
}
283
}
284
}
285
f0b06dea Andy McCall 1 month ago
286
async function guessHubs(): Promise<string[]> {
287
const out: string[] = [];
288
for (const r of oxygen?.repositories ?? []) {
289
for (const remote of await r.remotes().catch(() => [])) {
290
const m = /^(https?:\/\/[^/]+)\/[^/]+\/[^/]+\/?$/.exec(remote.url);
58889495 Andy McCall 1 month ago ↶
291
if (m && remote.ca) {
292
auth.setCa(m[1], remote.ca);
293
}
f0b06dea Andy McCall 1 month ago
294
if (m && !out.includes(m[1])) {
295
out.push(m[1]);
296
}
297
}
298
}
299
return out;
300
}
301
302
async function pickProposal(): Promise<ProposalNode | undefined> {
303
const items: { label: string; description: string; node: ProposalNode }[] = [];
304
for (const repo of await proposals.repos()) {
305
for (const p of await proposals.proposalsOf(repo).catch(() => [])) {
306
items.push({ label: `#${p.number} ${p.title}`, description: `${repo.owner}/${repo.name} · ${p.author} → ${p.target}`, node: { kind: "proposal", repo, p } });
307
}
308
}
309
return (await vscode.window.showQuickPick(items, { placeHolder: "Proposal" }))?.node;
310
}
311
312
/** Commands take a tree node, or the webview's `[base, owner, name, number]`. */
313
async function withProposal(args: unknown[], f: (repo: HubRepo, number: number) => Promise<void>): Promise<void> {
314
const a = args[0];
315
if (a && typeof a === "object" && (a as ProposalNode).kind === "proposal") {
316
const n = a as ProposalNode;
317
return f(n.repo, n.p.number);
318
}
319
if (Array.isArray(a) && a.length === 4) {
320
const [base, owner, name, number] = a as [string, string, string, number];
321
const repo = (await proposals.repos()).find((r) => r.base === base && r.owner === owner && r.name === name);
322
if (repo) {
323
return f(repo, number);
324
}
325
}
326
const n = await pickProposal();
327
if (n) {
328
return f(n.repo, n.p.number);
329
}
330
}
331
332
async function withIssue(args: unknown[], f: (repo: HubRepo, number: number) => Promise<void>): Promise<void> {
333
const a = args[0];
334
if (a && typeof a === "object" && (a as IssueNode).kind === "issue") {
335
const n = a as IssueNode;
336
return f(n.repo, n.i.number);
337
}
338
if (Array.isArray(a) && a.length === 4) {
339
const [base, owner, name, number] = a as [string, string, string, number];
340
const repo = (await proposals.repos()).find((r) => r.base === base && r.owner === owner && r.name === name);
341
if (repo) {
342
return f(repo, number);
343
}
344
}
345
}
346
347
function runInTerminal(repo: HubRepo, command: string): Promise<void> {
092b8bf9 Andy McCall 1 month ago ↶
348
const t = vscode.window.createTerminal({ name: "OxygenHub", cwd: repo.handle.root });
f0b06dea Andy McCall 1 month ago
349
t.show();
350
t.sendText(command);
351
return Promise.resolve();
352
}
353
354
/** Fetch the proposal's changes and switch to a local bookmark at its projected heads (spec §5). */
355
async function checkout(repo: HubRepo, number: number): Promise<void> {
356
const p = await auth.clientFor(repo.base).proposal(repo.owner, repo.name, number);
357
const prefix = vscode.workspace.getConfiguration("oxygenhub").get<string>("proposalBookmarkPrefix") ?? "proposal/";
358
const bookmark = `${prefix}${number}`;
359
try {
360
await repo.handle.json(["fetch", repo.remote, "--json"]);
361
const existing = repo.handle.bookmarks.some((b) => b.name === bookmark);
362
if (!existing) {
363
await repo.handle.json(["bookmark", "create", bookmark, ...p.projected_heads, "--json"]).catch(() => repo.handle.json(["bookmark", "create", bookmark, "--json"]));
364
}
365
await repo.handle.json(["bookmark", "set", bookmark, ...p.projected_heads, "--json"]).catch(() => undefined);
366
await repo.handle.json(["bookmark", "switch", bookmark, "--json"]);
367
await repo.handle.refresh();
368
await review.load(repo, number);
092b8bf9 Andy McCall 1 month ago ↶
369
void vscode.window.showInformationMessage(`OxygenHub: checked out #${number} as ${bookmark}; review threads are in place.`);
f0b06dea Andy McCall 1 month ago
370
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
371
void vscode.window.showErrorMessage(`OxygenHub: could not check out #${number}: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
372
}
373
}
374
375
async function reviewProposal(repo: HubRepo, number: number): Promise<void> {
376
const verdict = await vscode.window.showQuickPick(
377
[
378
{ label: "Comment", value: "comment" as const },
379
{ label: "Approve", value: "approve" as const, description: "then seal it with oxy so it counts" },
380
{ label: "Request changes", value: "changes" as const },
381
],
382
{ placeHolder: `Review #${number}` },
383
);
384
if (!verdict) {
385
return;
386
}
387
const body = (await vscode.window.showInputBox({ prompt: "Summary (optional)" })) ?? "";
388
try {
389
await auth.clientFor(repo.base).review(repo.owner, repo.name, number, verdict.value, body);
390
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
391
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
392
return;
393
}
394
proposals.refresh();
395
if (verdict.value === "approve") {
396
const pick = await vscode.window.showInformationMessage("Approval recorded. Seal it with your key so it counts?", "Seal now");
397
if (pick === "Seal now") {
398
await runInTerminal(repo, `oxy seal --proposal ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`);
399
}
400
}
401
}
402
403
async function createProposal(): Promise<void> {
404
const repos = await proposals.repos();
405
const repo = repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r;
406
if (!repo) {
407
return;
408
}
409
const source = repo.handle.currentBookmark;
410
if (!source) {
092b8bf9 Andy McCall 1 month ago ↶
411
void vscode.window.showWarningMessage("OxygenHub: no current bookmark.");
f0b06dea Andy McCall 1 month ago
412
return;
413
}
414
const remoteBookmarks = await auth.clientFor(repo.base).bookmarks(repo.owner, repo.name).catch(() => []);
415
const target = await vscode.window.showQuickPick(remoteBookmarks.filter((b) => b.name !== source).map((b) => b.name), { placeHolder: `Propose ${source} into…` });
416
if (!target) {
417
return;
418
}
419
const title = await vscode.window.showInputBox({ prompt: "Title", value: source });
420
if (!title) {
421
return;
422
}
423
try {
424
await repo.handle.json(["push", repo.remote, "--bookmark", source, "--json"]).catch(() => undefined);
425
const p = await auth.clientFor(repo.base).createProposal(repo.owner, repo.name, { title, body: "", target, source, draft: false });
426
proposals.refresh();
427
void vscode.env.openExternal(vscode.Uri.parse(p.url));
428
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
429
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
430
}
431
}
432
433
async function newIssue(): Promise<void> {
434
const repos = await proposals.repos();
435
const repo = repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r;
436
if (!repo) {
437
return;
438
}
439
const title = await vscode.window.showInputBox({ prompt: "Issue title" });
440
if (!title) {
441
return;
442
}
443
const body = (await vscode.window.showInputBox({ prompt: "Description (optional)" })) ?? "";
444
try {
445
const i = await auth.clientFor(repo.base).createIssue(repo.owner, repo.name, title, body);
446
issues.refresh();
092b8bf9 Andy McCall 1 month ago ↶
447
void vscode.window.showInformationMessage(`OxygenHub: opened #${i.number}.`);
f0b06dea Andy McCall 1 month ago
448
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
449
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
450
}
451
}
452
95f69b4a Andy McCall 1 month ago ↶
453
/**
454
* An issue from the editor (spec §8.1): the selection, or the current line,
455
* quoted under a permalink to it on the hub. A TODO line suggests the title
456
* and afterwards records the issue's number.
457
*/
458
async function issueFromSelection(): Promise<void> {
459
const editor = vscode.window.activeTextEditor;
460
if (!editor || editor.document.uri.scheme !== "file") {
092b8bf9 Andy McCall 1 month ago ↶
461
void vscode.window.showWarningMessage("OxygenHub: open a file of the repository first.");
95f69b4a Andy McCall 1 month ago ↶
462
return;
463
}
464
const doc = editor.document;
465
const file = doc.uri.fsPath;
466
const repo = (await proposals.repos()).filter((r) => file.startsWith(r.handle.root + "/") || file.startsWith(r.handle.root + "\\")).sort((a, b) => b.handle.root.length - a.handle.root.length)[0];
467
if (!repo) {
092b8bf9 Andy McCall 1 month ago ↶
468
void vscode.window.showWarningMessage("OxygenHub: this file is not in a repository with a hub remote.");
95f69b4a Andy McCall 1 month ago ↶
469
return;
470
}
471
const sel = editor.selection;
472
const startLine = sel.start.line;
473
const endLine = sel.isEmpty ? sel.start.line : sel.end.character === 0 && sel.end.line > sel.start.line ? sel.end.line - 1 : sel.end.line;
474
const code = doc.getText(new vscode.Range(startLine, 0, endLine, doc.lineAt(endLine).text.length));
475
const todoLine = [...Array(endLine - startLine + 1).keys()].map((i) => startLine + i).find((l) => todoTitle(doc.lineAt(l).text) !== undefined);
476
const title = await vscode.window.showInputBox({ prompt: "Issue title", value: todoLine !== undefined ? todoTitle(doc.lineAt(todoLine).text) : "" });
477
if (!title) {
478
return;
479
}
480
// Pin the link to the head when the bookmark has one; otherwise follow the bookmark.
481
const bookmark = repo.handle.bookmarks.find((b) => b.name === repo.handle.currentBookmark);
482
const pinned = bookmark?.heads.length === 1 && !doc.isDirty;
483
const reference = pinned ? bookmark!.heads[0] : (repo.handle.currentBookmark ?? "main");
484
const path = file.slice(repo.handle.root.length + 1).split("\\").join("/");
485
const permalink = hubUrl(repo.base, repo.owner, repo.name, { kind: "file", reference, path, line: startLine + 1, endLine: endLine + 1 });
486
const body = issueBody({ permalink, path, startLine: startLine + 1, endLine: endLine + 1, language: doc.languageId === "plaintext" ? "" : doc.languageId, code, pinned });
487
try {
488
const client = auth.clientFor(repo.base);
489
const issue = await client.createIssue(repo.owner, repo.name, title, body);
490
issues.refresh();
491
references.clear();
492
if (todoLine !== undefined) {
493
const line = doc.lineAt(todoLine);
494
const annotated = annotateTodo(line.text, issue.number);
495
if (annotated !== line.text) {
092b8bf9 Andy McCall 1 month ago ↶
496
const pick = await vscode.window.showInformationMessage(`OxygenHub: opened #${issue.number}. Record it in the TODO?`, "Add (#" + issue.number + ")");
95f69b4a Andy McCall 1 month ago ↶
497
if (pick) {
498
await editor.edit((e) => e.replace(line.range, annotated));
499
}
500
}
501
}
502
await showIssue(auth, repo, issue);
991516a1 Andy McCall 6 days ago ↶
503
} catch (e) {
504
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
505
}
506
}
507
508
/** Mark a draft ready for review, or turn an open proposal back into a draft. */
509
async function setDraft(repo: HubRepo, number: number, draft: boolean): Promise<void> {
510
try {
511
await auth.clientFor(repo.base).setProposalDraft(repo.owner, repo.name, number, draft);
512
proposals.refresh();
513
vscode.window.setStatusBarMessage(draft ? `$(circle-outline) #${number} is a draft again` : `$(git-pull-request) #${number} is ready for review`, 4000);
95f69b4a Andy McCall 1 month ago ↶
514
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
515
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
991516a1 Andy McCall 6 days ago ↶
516
}
517
}
518
519
/** What you can land now, across every repository you write to (#311). */
520
async function landable(): Promise<void> {
521
const repos = await proposals.repos();
522
type Item = vscode.QuickPickItem & { url: string; repo?: HubRepo; number: number };
523
const items: Item[] = [];
524
for (const base of auth.all().map((x) => x.base)) {
525
const list = await auth.clientFor(base).userProposals(true).catch(() => undefined);
526
for (const p of list?.landable ?? []) {
527
const [owner, name] = p.repository.split("/");
528
const repo = repos.find((r) => r.base === base && r.owner === owner && r.name === name);
529
items.push({ label: `$(pass) #${p.number} ${p.title}`, description: `${p.repository} · ${p.author}`, detail: repo ? undefined : "not in this workspace: opens on the hub", url: p.url, repo, number: p.number });
530
}
531
}
532
if (!items.length) {
533
void vscode.window.showInformationMessage("OxygenHub: nothing is ready for you to land.");
534
return;
535
}
536
const pick = await vscode.window.showQuickPick(items, { placeHolder: "Ready to land: approved, checks passed, no conflicts" });
537
if (!pick) {
538
return;
539
}
540
if (!pick.repo) {
541
void vscode.env.openExternal(vscode.Uri.parse(pick.url));
542
return;
543
}
544
const act = await vscode.window.showQuickPick(["Land", "Open"], { placeHolder: `#${pick.number}` });
545
if (act === "Land") {
546
await runInTerminal(pick.repo, `oxy proposal land ${pick.url}`);
547
} else if (act === "Open") {
548
await vscode.commands.executeCommand("oxygenhub.openProposal", { kind: "proposal", repo: pick.repo, p: await auth.clientFor(pick.repo.base).proposal(pick.repo.owner, pick.repo.name, pick.number) });
95f69b4a Andy McCall 1 month ago ↶
549
}
550
}
551
f0b06dea Andy McCall 1 month ago
552
export function deactivate(): void {}