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