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