| 1 |
// The Bisect and Trust views (spec §5.11, §5.12): small trees fed by |
| 2 |
// `oxy bisect status --json`, `oxy policy show --json` and `oxy verify --json`. |
| 3 |
|
| 4 |
import * as vscode from "vscode"; |
| 5 |
import { BisectStatus, PolicyInfo, VerifyInfo } from "./oxy"; |
| 6 |
import { Repository } from "./repository"; |
| 7 |
|
| 8 |
class Line extends vscode.TreeItem { |
| 9 |
constructor(label: string, opts: { description?: string; icon?: string; command?: vscode.Command; tooltip?: string; context?: string } = {}) { |
| 10 |
super(label, vscode.TreeItemCollapsibleState.None); |
| 11 |
this.description = opts.description; |
| 12 |
this.tooltip = opts.tooltip; |
| 13 |
this.command = opts.command; |
| 14 |
this.contextValue = opts.context; |
| 15 |
if (opts.icon) { |
| 16 |
this.iconPath = new vscode.ThemeIcon(opts.icon); |
| 17 |
} |
| 18 |
} |
| 19 |
} |
| 20 |
|
| 21 |
abstract class RepoTree implements vscode.TreeDataProvider<vscode.TreeItem> { |
| 22 |
private readonly emitter = new vscode.EventEmitter<void>(); |
| 23 |
readonly onDidChangeTreeData = this.emitter.event; |
| 24 |
|
| 25 |
constructor(protected readonly repositories: () => Repository[]) {} |
| 26 |
|
| 27 |
refresh(): void { |
| 28 |
this.emitter.fire(); |
| 29 |
} |
| 30 |
|
| 31 |
getTreeItem(element: vscode.TreeItem): vscode.TreeItem { |
| 32 |
return element; |
| 33 |
} |
| 34 |
|
| 35 |
async getChildren(element?: vscode.TreeItem): Promise<vscode.TreeItem[]> { |
| 36 |
const repos = this.repositories(); |
| 37 |
if (!element) { |
| 38 |
if (repos.length === 1) { |
| 39 |
return this.items(repos[0]); |
| 40 |
} |
| 41 |
return repos.map((r) => { |
| 42 |
const item = new vscode.TreeItem(r.root.split("/").pop() ?? r.root, vscode.TreeItemCollapsibleState.Expanded); |
| 43 |
item.iconPath = new vscode.ThemeIcon("repo"); |
| 44 |
(item as vscode.TreeItem & { repository?: Repository }).repository = r; |
| 45 |
return item; |
| 46 |
}); |
| 47 |
} |
| 48 |
const repo = (element as vscode.TreeItem & { repository?: Repository }).repository; |
| 49 |
return repo ? this.items(repo) : []; |
| 50 |
} |
| 51 |
|
| 52 |
protected abstract items(repo: Repository): Promise<vscode.TreeItem[]>; |
| 53 |
} |
| 54 |
|
| 55 |
export class BisectProvider extends RepoTree { |
| 56 |
protected async items(repo: Repository): Promise<vscode.TreeItem[]> { |
| 57 |
let status: BisectStatus | null; |
| 58 |
try { |
| 59 |
status = await repo.oxy.bisectStatus(repo.root); |
| 60 |
} catch (e) { |
| 61 |
return [new Line(String(e), { icon: "warning" })]; |
| 62 |
} |
| 63 |
const cmd = (title: string, command: string, args: unknown[] = []): vscode.Command => ({ title, command, arguments: [repo, ...args] }); |
| 64 |
if (!status) { |
| 65 |
return [new Line("No bisect in progress", { icon: "search", description: "start one to find a regression", command: cmd("Start", "oxygen.bisectStart") })]; |
| 66 |
} |
| 67 |
switch (status.state) { |
| 68 |
case "need-marks": |
| 69 |
return [ |
| 70 |
new Line(`Marked ${status.good} good, ${status.bad} bad`, { icon: "search" }), |
| 71 |
new Line(status.good === 0 ? "Mark a known-good change or bookmark…" : "Mark a known-bad change or bookmark…", { icon: status.good === 0 ? "pass" : "error", command: cmd("Mark", status.good === 0 ? "oxygen.bisectGood" : "oxygen.bisectBad") }), |
| 72 |
new Line("Reset", { icon: "discard", command: cmd("Reset", "oxygen.bisectReset") }), |
| 73 |
]; |
| 74 |
case "testing": |
| 75 |
return [ |
| 76 |
new Line(`Testing: ${status.remaining} candidates left`, { icon: "beaker", description: `about ${status.steps_left} more steps` }), |
| 77 |
new Line("This state is good", { icon: "pass", command: cmd("Good", "oxygen.bisectGood", [[]]) }), |
| 78 |
new Line("This state is bad", { icon: "error", command: cmd("Bad", "oxygen.bisectBad", [[]]) }), |
| 79 |
new Line("Skip: cannot test this state", { icon: "debug-step-over", command: cmd("Skip", "oxygen.bisectSkip") }), |
| 80 |
new Line("Run a command for every step…", { icon: "run-all", command: cmd("Run", "oxygen.bisectRun") }), |
| 81 |
new Line("Reset", { icon: "discard", command: cmd("Reset", "oxygen.bisectReset") }), |
| 82 |
]; |
| 83 |
case "found": |
| 84 |
return [ |
| 85 |
new Line(`Found: ${status.culprit.slice(0, 12)}`, { icon: "bug", description: "the first change that makes the state bad", command: { title: "Show", command: "oxygen.showChange", arguments: [repo.root, status.culprit] } }), |
| 86 |
new Line("Reset: back to where you were", { icon: "discard", command: cmd("Reset", "oxygen.bisectReset") }), |
| 87 |
]; |
| 88 |
case "inconclusive": |
| 89 |
return [ |
| 90 |
new Line(`Inconclusive: ${status.candidates.length} candidates could not be told apart`, { icon: "question" }), |
| 91 |
...status.candidates.map((c) => new Line(c.slice(0, 12), { icon: "circle-outline", command: { title: "Show", command: "oxygen.showChange", arguments: [repo.root, c] } })), |
| 92 |
new Line("Reset", { icon: "discard", command: cmd("Reset", "oxygen.bisectReset") }), |
| 93 |
]; |
| 94 |
} |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
export class TrustProvider extends RepoTree { |
| 99 |
protected async items(repo: Repository): Promise<vscode.TreeItem[]> { |
| 100 |
let policy: PolicyInfo | null; |
| 101 |
let verify: VerifyInfo[]; |
| 102 |
try { |
| 103 |
[policy, verify] = await Promise.all([repo.oxy.policyShow(repo.root), repo.oxy.verify(repo.root)]); |
| 104 |
} catch (e) { |
| 105 |
return [new Line(String(e), { icon: "warning" })]; |
| 106 |
} |
| 107 |
const items: vscode.TreeItem[] = []; |
| 108 |
if (!policy) { |
| 109 |
items.push(new Line("No trust policy: this repository is unverified", { icon: "unlock", description: "oxy policy init creates one", tooltip: "Without a policy every change and reference update is accepted." })); |
| 110 |
} else { |
| 111 |
const expires = new Date(policy.expires_ms).toLocaleDateString(); |
| 112 |
items.push(new Line(`Policy serial ${policy.serial}`, { icon: policy.expired ? "error" : "shield", description: policy.expired ? `EXPIRED ${expires}` : `expires ${expires}` })); |
| 113 |
for (const r of policy.roots) { |
| 114 |
items.push(new Line(`Root: ${r.name || r.key.slice(0, 12)}`, { icon: "key", description: r.email, tooltip: r.key })); |
| 115 |
} |
| 116 |
items.push(new Line(`Root threshold ${policy.root_threshold}`, { icon: "law" })); |
| 117 |
for (const rule of policy.rules) { |
| 118 |
items.push(new Line(`Rule ${rule.pattern}`, { icon: "list-tree", description: `${rule.keys.map((k) => k.name || k.key.slice(0, 8)).join(", ")} · threshold ${rule.threshold} · ${rule.require}${rule.allow_conflicts ? "" : " · no conflicts"}` })); |
| 119 |
} |
| 120 |
for (const r of policy.revoked) { |
| 121 |
items.push(new Line(`Revoked: ${r.name || r.key.slice(0, 12)}`, { icon: "circle-slash" })); |
| 122 |
} |
| 123 |
} |
| 124 |
const counts = { signed: 0, sealed: 0, unsigned: 0, invalid: 0, revoked: 0 }; |
| 125 |
for (const v of verify) { |
| 126 |
if (v.status in counts) { |
| 127 |
counts[v.status as keyof typeof counts] += 1; |
| 128 |
} |
| 129 |
if (v.revoked) { |
| 130 |
counts.revoked += 1; |
| 131 |
} |
| 132 |
} |
| 133 |
items.push( |
| 134 |
new Line(`${verify.length} changes verified`, { |
| 135 |
icon: counts.invalid > 0 ? "error" : "verified", |
| 136 |
description: `${counts.signed} signed, ${counts.sealed} sealed, ${counts.unsigned} unsigned${counts.invalid ? `, ${counts.invalid} INVALID` : ""}${counts.revoked ? `, ${counts.revoked} by revoked keys` : ""}`, |
| 137 |
command: { title: "Verify", command: "oxygen.verify", arguments: [repo] }, |
| 138 |
}), |
| 139 |
); |
| 140 |
return items; |
| 141 |
} |
| 142 |
} |