OXYGENHUB
# d2c9a5843004 oxygen-vscode / src / panels.ts
6.9 KiB · last changed Milestone 0.6: Subrepos group, Bisect and Trust views, verify, tags, add workspace, clone 1 month ago source blame history raw
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
}