feat: a Diffusion view, what each repository takes from and diffuses to (closes #5) d21a85954a34
Each row names the repository at the other end and reads like the hub's
pill (untested │ passes), with how far behind the pin is and the open bump;
its icon is the more urgent half. Picking a row opens the bump, beside the
code when it is on this repository. From GET /repos/{o}/{n}/diffusion
(oxygenhub#153). 0.12.0.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 13:42 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
d21a85954a34fffebde6bd462770c8fd9a62276776972a41afc5a165160e0dd4- Depends on
- 092b8bf9f2ac 1cc0ff44e5ca 263de209e5ba 426ddc5039b5 486c15b00314 85a87a2d7d5b bf73998c5198 d8c9bdc51ee0 e04950748142 f0b06deac4b9 f902d5b8bfeb
- Depended on by
- 991516a107fa a6b460b1ec04
- In bookmarks
- main
- In tags
- v0.13.0
- Edits
- 37 atoms, 3 tree operations, 10 files
Browse the tree at this change
added src/diffusion.ts +108 −0
| 1 | + | // The Diffusion view (oxygenhub-vscode#5, oxygenhub docs/diffusion.md): for |
|
| 2 | + | // each repository, the subrepos it takes from and the repositories it |
|
| 3 | + | // diffuses to, each with the hub's pill in words and its open bump. |
|
| 4 | + | ||
| 5 | + | import * as vscode from "vscode"; |
|
| 6 | + | import type * as api from "./hubapi"; |
|
| 7 | + | import { HubAuth } from "./auth"; |
|
| 8 | + | import { hubUrl } from "./hub"; |
|
| 9 | + | import type { HubRepo } from "./proposals"; |
|
| 10 | + | import { look, pinDescription, pinLabel, pinTooltip, tone } from "./diffusionview"; |
|
| 11 | + | ||
| 12 | + | type Side = "takes" | "gives"; |
|
| 13 | + | ||
| 14 | + | type Node = |
|
| 15 | + | | { kind: "repo"; repo: HubRepo } |
|
| 16 | + | | { kind: "side"; repo: HubRepo; side: Side; pins: api.DiffusionPin[] } |
|
| 17 | + | | { kind: "pin"; repo: HubRepo; side: Side; pin: api.DiffusionPin } |
|
| 18 | + | | { kind: "none"; repo: HubRepo }; |
|
| 19 | + | ||
| 20 | + | export type DiffusionPinNode = Extract<Node, { kind: "pin" }>; |
|
| 21 | + | ||
| 22 | + | export class DiffusionProvider implements vscode.TreeDataProvider<Node> { |
|
| 23 | + | private readonly emitter = new vscode.EventEmitter<void>(); |
|
| 24 | + | readonly onDidChangeTreeData = this.emitter.event; |
|
| 25 | + | ||
| 26 | + | constructor( |
|
| 27 | + | private readonly repos: () => Promise<HubRepo[]>, |
|
| 28 | + | private readonly auth: HubAuth, |
|
| 29 | + | ) {} |
|
| 30 | + | ||
| 31 | + | refresh(): void { |
|
| 32 | + | this.emitter.fire(); |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | getTreeItem(node: Node): vscode.TreeItem { |
|
| 36 | + | switch (node.kind) { |
|
| 37 | + | case "repo": { |
|
| 38 | + | const item = new vscode.TreeItem(`${node.repo.owner}/${node.repo.name}`, vscode.TreeItemCollapsibleState.Expanded); |
|
| 39 | + | item.iconPath = new vscode.ThemeIcon("repo"); |
|
| 40 | + | item.contextValue = "diffusionRepo"; |
|
| 41 | + | return item; |
|
| 42 | + | } |
|
| 43 | + | case "side": { |
|
| 44 | + | const item = new vscode.TreeItem(node.side === "takes" ? "Takes from" : "Diffuses to", vscode.TreeItemCollapsibleState.Expanded); |
|
| 45 | + | item.description = String(node.pins.length); |
|
| 46 | + | item.iconPath = new vscode.ThemeIcon(node.side === "takes" ? "arrow-down" : "arrow-up"); |
|
| 47 | + | return item; |
|
| 48 | + | } |
|
| 49 | + | case "pin": { |
|
| 50 | + | const t = tone(node.pin); |
|
| 51 | + | const l = look(t); |
|
| 52 | + | const item = new vscode.TreeItem(pinLabel(node.pin, node.side), vscode.TreeItemCollapsibleState.None); |
|
| 53 | + | item.description = pinDescription(node.pin); |
|
| 54 | + | item.iconPath = new vscode.ThemeIcon(l.icon, new vscode.ThemeColor(l.color)); |
|
| 55 | + | item.tooltip = new vscode.MarkdownString(pinTooltip(node.pin)); |
|
| 56 | + | item.contextValue = node.pin.bump !== null && node.pin.bump !== undefined ? "diffusionPinBump" : "diffusionPin"; |
|
| 57 | + | item.command = { title: "Open", command: "oxygenhub.openDiffusionPin", arguments: [node] }; |
|
| 58 | + | return item; |
|
| 59 | + | } |
|
| 60 | + | case "none": { |
|
| 61 | + | const item = new vscode.TreeItem("Neither pins nor is pinned by a repository on the hub", vscode.TreeItemCollapsibleState.None); |
|
| 62 | + | item.iconPath = new vscode.ThemeIcon("info"); |
|
| 63 | + | return item; |
|
| 64 | + | } |
|
| 65 | + | } |
|
| 66 | + | } |
|
| 67 | + | ||
| 68 | + | async getChildren(node?: Node): Promise<Node[]> { |
|
| 69 | + | if (!node) { |
|
| 70 | + | return (await this.repos()).map((repo) => ({ kind: "repo", repo })); |
|
| 71 | + | } |
|
| 72 | + | if (node.kind === "repo") { |
|
| 73 | + | try { |
|
| 74 | + | const d = await this.auth.clientFor(node.repo.base).diffusion(node.repo.owner, node.repo.name); |
|
| 75 | + | const out: Node[] = []; |
|
| 76 | + | if (d.takes_from.length > 0) { |
|
| 77 | + | out.push({ kind: "side", repo: node.repo, side: "takes", pins: d.takes_from }); |
|
| 78 | + | } |
|
| 79 | + | if (d.diffuses_to.length > 0) { |
|
| 80 | + | out.push({ kind: "side", repo: node.repo, side: "gives", pins: d.diffuses_to }); |
|
| 81 | + | } |
|
| 82 | + | return out.length > 0 ? out : [{ kind: "none", repo: node.repo }]; |
|
| 83 | + | } catch (e) { |
|
| 84 | + | void vscode.window.showWarningMessage(`OxygenHub: ${(e as Error).message}`); |
|
| 85 | + | return []; |
|
| 86 | + | } |
|
| 87 | + | } |
|
| 88 | + | if (node.kind === "side") { |
|
| 89 | + | return node.pins.map((pin) => ({ kind: "pin", repo: node.repo, side: node.side, pin })); |
|
| 90 | + | } |
|
| 91 | + | return []; |
|
| 92 | + | } |
|
| 93 | + | } |
|
| 94 | + | ||
| 95 | + | /** Where a row leads on the hub: its open bump, else the other repository. */ |
|
| 96 | + | export function pinUrl(node: DiffusionPinNode): string { |
|
| 97 | + | const [owner, name] = (node.side === "takes" ? node.pin.upstream : node.pin.consumer).split("/"); |
|
| 98 | + | const [cOwner, cName] = node.pin.consumer.split("/"); |
|
| 99 | + | if (node.pin.bump !== null && node.pin.bump !== undefined) { |
|
| 100 | + | return hubUrl(node.repo.base, cOwner, cName, { kind: "proposal", number: node.pin.bump }); |
|
| 101 | + | } |
|
| 102 | + | return hubUrl(node.repo.base, owner, name, { kind: "repository" }); |
|
| 103 | + | } |
|
| 104 | + | ||
| 105 | + | /** The repository's Diffusion tab on the hub. */ |
|
| 106 | + | export function diffusionTab(repo: HubRepo): string { |
|
| 107 | + | return hubUrl(repo.base, repo.owner, repo.name, { kind: "diffusion" }); |
|
| 108 | + | } |
added src/diffusionview.ts +71 −0
| 1 | + | // Diffusion in the editor, without VS Code (oxygenhub-vscode#5): how a pin |
|
| 2 | + | // between an upstream and its consumer looks. The hub's pill has two halves, |
|
| 3 | + | // the upstream's state and the consumer's; a tree row has one icon, so it |
|
| 4 | + | // shows the more urgent of the two and says both in words. |
|
| 5 | + | ||
| 6 | + | import type { DiffusionHalf, DiffusionPin } from "./hubapi"; |
|
| 7 | + | ||
| 8 | + | /** A codicon and a theme colour id. */ |
|
| 9 | + | export interface Look { |
|
| 10 | + | icon: string; |
|
| 11 | + | color: string; |
|
| 12 | + | } |
|
| 13 | + | ||
| 14 | + | /** Most urgent first: a failure, then what nothing checked, then waiting. */ |
|
| 15 | + | const URGENCY = ["red", "amber", "grey", "green", "off"]; |
|
| 16 | + | ||
| 17 | + | /** The tone a row shows: the more urgent half, ignoring a consumer that does not follow. */ |
|
| 18 | + | export function tone(pin: DiffusionPin): string { |
|
| 19 | + | const tones = [pin.upstream_state.tone, pin.consumer_state.tone].filter((t) => t !== "off"); |
|
| 20 | + | return tones.sort((a, b) => rank(a) - rank(b))[0] ?? "off"; |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | function rank(t: string): number { |
|
| 24 | + | const i = URGENCY.indexOf(t); |
|
| 25 | + | return i < 0 ? URGENCY.length : i; |
|
| 26 | + | } |
|
| 27 | + | ||
| 28 | + | export function look(t: string): Look { |
|
| 29 | + | switch (t) { |
|
| 30 | + | case "green": |
|
| 31 | + | return { icon: "pass-filled", color: "testing.iconPassed" }; |
|
| 32 | + | case "red": |
|
| 33 | + | return { icon: "error", color: "testing.iconFailed" }; |
|
| 34 | + | case "amber": |
|
| 35 | + | return { icon: "warning", color: "list.warningForeground" }; |
|
| 36 | + | case "grey": |
|
| 37 | + | return { icon: "clock", color: "testing.iconQueued" }; |
|
| 38 | + | default: |
|
| 39 | + | return { icon: "circle-outline", color: "disabledForeground" }; |
|
| 40 | + | } |
|
| 41 | + | } |
|
| 42 | + | ||
| 43 | + | /** The row's name: the repository on the other end of the pin. */ |
|
| 44 | + | export function pinLabel(pin: DiffusionPin, side: "takes" | "gives"): string { |
|
| 45 | + | return side === "takes" ? pin.upstream : pin.consumer; |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | /** "untested │ passes · 2 behind · bump #304", as the hub's pill reads. */ |
|
| 49 | + | export function pinDescription(pin: DiffusionPin): string { |
|
| 50 | + | const parts = [`${pin.upstream_state.word} │ ${pin.consumer_state.word}`]; |
|
| 51 | + | if (pin.behind !== null && pin.behind !== undefined && pin.behind > 0) { |
|
| 52 | + | parts.push(`${pin.behind} behind`); |
|
| 53 | + | } |
|
| 54 | + | if (pin.bump !== null && pin.bump !== undefined) { |
|
| 55 | + | parts.push(`bump #${pin.bump}`); |
|
| 56 | + | } |
|
| 57 | + | return parts.join(" · "); |
|
| 58 | + | } |
|
| 59 | + | ||
| 60 | + | const FOLLOW: Record<string, string> = { off: "does not follow", green: "follows when the upstream is green", any: "follows every move" }; |
|
| 61 | + | ||
| 62 | + | /** The tooltip, in Markdown: both halves' sentences, then the pin. */ |
|
| 63 | + | export function pinTooltip(pin: DiffusionPin): string { |
|
| 64 | + | const half = (who: string, h: DiffusionHalf) => `**${who}: ${h.word}.** ${h.title}`; |
|
| 65 | + | const run = pin.run !== null && pin.run !== undefined ? ` · run #${pin.run}${pin.run_status ? ` ${pin.run_status}` : ""}` : ""; |
|
| 66 | + | return [ |
|
| 67 | + | half(pin.upstream, pin.upstream_state), |
|
| 68 | + | half(pin.consumer, pin.consumer_state), |
|
| 69 | + | `Pinned at \`${pin.path}\` on \`${pin.bookmark}\`; ${pin.consumer} ${FOLLOW[pin.follow] ?? pin.follow}.${pin.bump !== null && pin.bump !== undefined ? ` Open bump #${pin.bump}${run}.` : ""}`, |
|
| 70 | + | ].join("\n\n"); |
|
| 71 | + | } |
added test/diffusionview.test.mjs +40 −0
| 1 | + | import { test } from "node:test"; |
|
| 2 | + | import assert from "node:assert/strict"; |
|
| 3 | + | import { look, pinDescription, pinLabel, pinTooltip, tone } from "../out/diffusionview.mjs"; |
|
| 4 | + | ||
| 5 | + | const half = (tone, word) => ({ tone, word, title: `${word}.` }); |
|
| 6 | + | const pin = (over = {}) => ({ |
|
| 7 | + | upstream: "doomsdayone/rstar-api", |
|
| 8 | + | consumer: "doomsdayone/metalgear", |
|
| 9 | + | path: "rstar-api", |
|
| 10 | + | bookmark: "develop", |
|
| 11 | + | behind: 2, |
|
| 12 | + | follow: "green", |
|
| 13 | + | bump: 304, |
|
| 14 | + | run: 194, |
|
| 15 | + | run_status: "passed", |
|
| 16 | + | upstream_state: half("amber", "untested"), |
|
| 17 | + | consumer_state: half("green", "passes"), |
|
| 18 | + | ...over, |
|
| 19 | + | }); |
|
| 20 | + | ||
| 21 | + | test("a row shows the more urgent half", () => { |
|
| 22 | + | assert.equal(tone(pin()), "amber", "untested outranks passes"); |
|
| 23 | + | assert.equal(tone(pin({ consumer_state: half("red", "fails") })), "red"); |
|
| 24 | + | assert.equal(tone(pin({ upstream_state: half("green", "checked"), consumer_state: half("off", "not following") })), "green", "not following is not urgent"); |
|
| 25 | + | assert.equal(tone(pin({ upstream_state: half("off", "x"), consumer_state: half("off", "y") })), "off"); |
|
| 26 | + | assert.equal(look("red").icon, "error"); |
|
| 27 | + | assert.equal(look("amber").color, "list.warningForeground"); |
|
| 28 | + | assert.equal(look("whatever").icon, "circle-outline"); |
|
| 29 | + | }); |
|
| 30 | + | ||
| 31 | + | test("a row names the other end and reads like the pill", () => { |
|
| 32 | + | assert.equal(pinLabel(pin(), "takes"), "doomsdayone/rstar-api"); |
|
| 33 | + | assert.equal(pinLabel(pin(), "gives"), "doomsdayone/metalgear"); |
|
| 34 | + | assert.equal(pinDescription(pin()), "untested │ passes · 2 behind · bump #304"); |
|
| 35 | + | assert.equal(pinDescription(pin({ behind: 0, bump: null, consumer_state: half("green", "up to date") })), "untested │ up to date"); |
|
| 36 | + | assert.equal(pinDescription(pin({ behind: null, bump: null })), "untested │ passes"); |
|
| 37 | + | const tip = pinTooltip(pin()); |
|
| 38 | + | assert.match(tip, /\*\*doomsdayone\/rstar-api: untested\.\*\*/); |
|
| 39 | + | assert.match(tip, /Pinned at `rstar-api` on `develop`; doomsdayone\/metalgear follows when the upstream is green\. Open bump #304 · run #194 passed\./); |
|
| 40 | + | }); |
modified CHANGELOG.md +4 −0
| 1 | 1 | # Changelog |
|
| 2 | 2 | ||
| 3 | + | ## 0.12.0 (2026-10-13) |
|
| 4 | + | ||
| 5 | + | - **A Diffusion view (#5).** What each repository takes from and diffuses to, each row reading like the hub's pill (`untested │ passes`), how far behind it is and its open bump; picking a row opens the bump. Needs an OxygenHub with `GET /repos/{o}/{n}/diffusion` (oxygenhub#153). |
|
| 6 | + | ||
| 3 | 7 | ## 0.11.2 (2026-09-30) |
|
| 4 | 8 | ||
| 5 | 9 | - **Signing in works from the Marketplace.** It depends on Oxygen VCS as `oxygenhub.oxygen-vscode`, the id the Marketplace has (0.11.1 named one it does not). If Oxygen VCS fails to start, OxygenHub now starts anyway, so Sign In and the hub views still work. |
|
| … | 95 unchanged lines |
modified README.md +5 −1
| … | 10 unchanged lines |
||
| 11 | 11 | ## Getting started |
|
| 12 | 12 | ||
| 13 | 13 | 1. Run **OxygenHub: Sign in**, give the hub's URL (for example `https://earth.example.org:7343`) and paste the token. The token is kept in VS Code's secret storage, never in settings. |
|
| 14 | - | 2. Open a repository whose remote is on that hub. The **OxygenHub** activity bar icon shows Proposals, Issues and Releases for it. |
|
| 14 | + | 2. Open a repository whose remote is on that hub. The **OxygenHub** activity bar icon shows Proposals, Issues, Releases, Flow and Diffusion for it. |
|
| 15 | 15 | 3. **Open on Remote** and **Copy Permalink** in the Oxygen VCS extension now open pages on the hub. |
|
| 16 | 16 | ||
| 17 | 17 | ## Walkthroughs |
|
| … | 30 unchanged lines |
||
| 48 | 48 | ||
| 49 | 49 | In **Releases**, run **Draft Release…** and pick a tag. The notes are drafted from the changes since the previous release; edit them before publishing. Each release lists its assets. |
|
| 50 | 50 | ||
| 51 | + | ### See what Diffusion is doing |
|
| 52 | + | ||
| 53 | + | **Diffusion** lists what each repository takes from (the subrepos it pins) and who takes from it, each with the hub's status in words (`untested │ passes`), how far behind it is and its open bump proposal. Pick a row to open the bump. |
|
| 54 | + | ||
| 51 | 55 | ### Keep up with notifications |
|
| 52 | 56 | ||
| 53 | 57 | The status bar shows how many notifications are unread. **Open Inbox** lists them, and each opens its proposal or issue in the editor. New ones are announced unless `oxygenhub.announceNotifications` is off. |
|
| … | 27 unchanged lines |
modified docs/spec.md +4 −0
| … | 88 unchanged lines |
||
| 89 | 89 | ||
| 90 | 90 | **Draft Release…** (the view's title bar, or inline on a repository) releases a tag that has none yet: pick the tag (`GET /tags`, minus those already released), confirm a title (the tag's message by default), and choose release or pre-release (a tag such as `v2.0-rc1` or `1.0.0-beta.2` suggests pre-release). The notes are generated: the changes in the tag's history that the newest full release's tag does not have, newest first, one line each with the message's subject and a short hash. The release is created with `POST /releases`; assets are uploaded on the hub or with `oxy`. Editing notes after creation happens on the hub. |
|
| 91 | 91 | ||
| 92 | + | ### 8.3 Diffusion |
|
| 93 | + | ||
| 94 | + | The **Diffusion** view (oxygenhub docs/diffusion.md) shows, for each repository, what it **takes from** (the subrepos it pins that are on the hub) and what it **diffuses to** (the repositories on the hub that pin it), from `GET /repos/{o}/{n}/diffusion`. Each row names the repository at the other end and reads like the hub's pill: the upstream's half, then the consumer's (`untested │ passes`), how far behind the pin is and the open bump proposal. Its icon shows the more urgent half: failing red, untested amber, waiting grey, else green; a consumer that does not follow does not count. The tooltip gives both halves' sentences, where the pin is and how the consumer follows. Picking a row opens its bump: beside the code when the bump is on this repository (the proposal detail, as the Proposals view opens it), else on the hub; a row without a bump opens the other repository. **Open on OxygenHub** (inline) opens the row's bump or repository, or a repository's Diffusion tab. Bumps are ordinary proposals, so they are also in Proposals, where they are reviewed, checked out and landed. |
|
| 95 | + | ||
| 92 | 96 | ## 9. Checks and status bar |
|
| 93 | 97 | ||
| 94 | 98 | A status bar item shows, for the current repository's current bookmark, the open proposal whose source it is and the state of its checks (✔ n passed, ✘ failing, … running). It subscribes to the hub's proposal event stream and refreshes on `check` events; clicking opens the proposal. |
|
| … | 22 unchanged lines |
modified package.json +31 −3
| … | 1 unchanged lines |
||
| 2 | 2 | "name": "oxygenhub", |
|
| 3 | 3 | "displayName": "OxygenHub VCS", |
|
| 4 | 4 | "description": "Proposals, reviews, issues and checks from an OxygenHub, in the editor. Builds on the Oxygen extension.", |
|
| 5 | - | "version": "0.11.2", |
|
| 5 | + | "version": "0.12.0", |
|
| 6 | 6 | "publisher": "oxygenhub", |
|
| 7 | 7 | "author": { |
|
| 8 | 8 | "name": "OxygenHub.io", |
|
| … | 112 unchanged lines |
||
| 121 | 121 | { |
|
| 122 | 122 | "id": "oxygenhub.flow", |
|
| 123 | 123 | "name": "Flow" |
|
| 124 | + | }, |
|
| 125 | + | { |
|
| 126 | + | "id": "oxygenhub.diffusion", |
|
| 127 | + | "name": "Diffusion" |
|
| 124 | 128 | } |
|
| 125 | 129 | ] |
|
| 126 | 130 | }, |
|
| … | 174 unchanged lines |
||
| 301 | 305 | { |
|
| 302 | 306 | "command": "oxygenhub.flowRecent", |
|
| 303 | 307 | "title": "Recent Flow Runs\u2026", |
|
| 308 | + | "category": "OxygenHub" |
|
| 309 | + | }, |
|
| 310 | + | { |
|
| 311 | + | "command": "oxygenhub.openDiffusionPin", |
|
| 312 | + | "title": "Open Bump or Repository", |
|
| 304 | 313 | "category": "OxygenHub" |
|
| 314 | + | }, |
|
| 315 | + | { |
|
| 316 | + | "command": "oxygenhub.openDiffusionOnHub", |
|
| 317 | + | "title": "Open on OxygenHub", |
|
| 318 | + | "category": "OxygenHub", |
|
| 319 | + | "icon": "$(link-external)" |
|
| 305 | 320 | } |
|
| 306 | 321 | ], |
|
| 307 | 322 | "menus": { |
|
| 308 | 323 | "view/title": [ |
|
| 309 | 324 | { |
|
| 310 | 325 | "command": "oxygenhub.refresh", |
|
| 311 | - | "when": "view == oxygenhub.proposals || view == oxygenhub.issues || view == oxygenhub.releases", |
|
| 326 | + | "when": "view == oxygenhub.proposals || view == oxygenhub.issues || view == oxygenhub.releases || view == oxygenhub.diffusion", |
|
| 312 | 327 | "group": "navigation" |
|
| 313 | 328 | }, |
|
| 314 | 329 | { |
|
| … | 89 unchanged lines |
||
| 404 | 419 | "group": "inline" |
|
| 405 | 420 | }, |
|
| 406 | 421 | { |
|
| 422 | + | "command": "oxygenhub.openDiffusionOnHub", |
|
| 423 | + | "when": "view == oxygenhub.diffusion && viewItem =~ /^diffusion/", |
|
| 424 | + | "group": "inline" |
|
| 425 | + | }, |
|
| 426 | + | { |
|
| 407 | 427 | "command": "oxygenhub.runPipeline", |
|
| 408 | 428 | "when": "view == oxygenhub.flow && viewItem == flowRepo", |
|
| 409 | 429 | "group": "inline" |
|
| … | 34 unchanged lines |
||
| 444 | 464 | } |
|
| 445 | 465 | ], |
|
| 446 | 466 | "commandPalette": [ |
|
| 467 | + | { |
|
| 468 | + | "command": "oxygenhub.openDiffusionPin", |
|
| 469 | + | "when": "false" |
|
| 470 | + | }, |
|
| 471 | + | { |
|
| 472 | + | "command": "oxygenhub.openDiffusionOnHub", |
|
| 473 | + | "when": "false" |
|
| 474 | + | }, |
|
| 447 | 475 | { |
|
| 448 | 476 | "command": "oxygenhub.openProposalFile", |
|
| 449 | 477 | "when": "false" |
|
| … | 101 unchanged lines |
||
| 551 | 579 | "build": "esbuild src/extension.ts --bundle --platform=node --format=cjs --external:vscode --outfile=dist/extension.js --sourcemap", |
|
| 552 | 580 | "watch": "npm run build -- --watch", |
|
| 553 | 581 | "typecheck": "tsc --noEmit -p .", |
|
| 554 | - | "pretest": "esbuild src/hub.ts --bundle --platform=node --format=esm --outfile=out/hub.mjs && esbuild src/provider.ts --bundle --platform=node --format=esm --outfile=out/provider.mjs && esbuild src/refs.ts --bundle --platform=node --format=esm --outfile=out/refs.mjs && esbuild src/flowview.ts --bundle --platform=node --format=esm --outfile=out/flowview.mjs", |
|
| 582 | + | "pretest": "esbuild src/hub.ts --bundle --platform=node --format=esm --outfile=out/hub.mjs && esbuild src/provider.ts --bundle --platform=node --format=esm --outfile=out/provider.mjs && esbuild src/refs.ts --bundle --platform=node --format=esm --outfile=out/refs.mjs && esbuild src/flowview.ts --bundle --platform=node --format=esm --outfile=out/flowview.mjs && esbuild src/diffusionview.ts --bundle --platform=node --format=esm --outfile=out/diffusionview.mjs", |
|
| 555 | 583 | "test": "node --test \"test/**/*.test.mjs\"", |
|
| 556 | 584 | "check": "npm run typecheck && npm run build && npm test", |
|
| 557 | 585 | "package": "vsce package --no-dependencies" |
|
| … | 9 unchanged lines |
modified src/extension.ts +28 −0
| … | 8 unchanged lines |
||
| 9 | 9 | import { hubUrl } from "./hub"; |
|
| 10 | 10 | import { InboxBar } from "./inbox"; |
|
| 11 | 11 | import { draftRelease, ReleasesProvider } from "./releases"; |
|
| 12 | + | import { DiffusionPinNode, DiffusionProvider, diffusionTab, pinUrl } from "./diffusion"; |
|
| 12 | 13 | import { ReferenceCompletions, ReferenceHovers, ReferenceIndex, SELECTOR } from "./references"; |
|
| 13 | 14 | import { annotateTodo, issueBody, todoTitle } from "./refs"; |
|
| 14 | 15 | import { IssueNode, IssuesProvider, showIssue } from "./issues"; |
|
| … | 12 unchanged lines |
||
| 27 | 28 | let inbox: InboxBar; |
|
| 28 | 29 | let references: ReferenceIndex; |
|
| 29 | 30 | let releases: ReleasesProvider; |
|
| 31 | + | let diffusion: DiffusionProvider; |
|
| 30 | 32 | let flow: FlowProvider; |
|
| 31 | 33 | ||
| 32 | 34 | export async function activate(context: vscode.ExtensionContext): Promise<void> { |
|
| … | 37 unchanged lines |
||
| 70 | 72 | context.subscriptions.push(inbox); |
|
| 71 | 73 | releases = new ReleasesProvider(() => proposals.repos(), auth); |
|
| 72 | 74 | context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygenhub.releases", releases)); |
|
| 75 | + | diffusion = new DiffusionProvider(() => proposals.repos(), auth); |
|
| 76 | + | context.subscriptions.push( |
|
| 77 | + | vscode.window.registerTreeDataProvider("oxygenhub.diffusion", diffusion), |
|
| 78 | + | // A bump on this repository opens beside the code, as any proposal does; |
|
| 79 | + | // the rest open on the hub. |
|
| 80 | + | vscode.commands.registerCommand("oxygenhub.openDiffusionPin", async (node: DiffusionPinNode) => { |
|
| 81 | + | const bump = node.pin.bump; |
|
| 82 | + | if (node.side === "takes" && bump !== null && bump !== undefined) { |
|
| 83 | + | try { |
|
| 84 | + | const client = auth.clientFor(node.repo.base); |
|
| 85 | + | 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(() => [])]); |
|
| 86 | + | showDetail(node.repo, p, checks); |
|
| 87 | + | return; |
|
| 88 | + | } catch (e) { |
|
| 89 | + | void vscode.window.showWarningMessage(`OxygenHub: ${(e as Error).message}`); |
|
| 90 | + | } |
|
| 91 | + | } |
|
| 92 | + | void vscode.env.openExternal(vscode.Uri.parse(pinUrl(node))); |
|
| 93 | + | }), |
|
| 94 | + | vscode.commands.registerCommand("oxygenhub.openDiffusionOnHub", (node?: DiffusionPinNode | { kind: "repo"; repo: HubRepo }) => { |
|
| 95 | + | if (node) { |
|
| 96 | + | void vscode.env.openExternal(vscode.Uri.parse(node.kind === "pin" ? pinUrl(node) : diffusionTab(node.repo))); |
|
| 97 | + | } |
|
| 98 | + | }), |
|
| 99 | + | ); |
|
| 73 | 100 | flow = new FlowProvider(() => proposals.repos(), auth); |
|
| 74 | 101 | const flowLogs = new FlowLogs(auth); |
|
| 75 | 102 | useLogs(flowLogs); |
|
| … | 129 unchanged lines |
||
| 205 | 232 | refreshOpenDiffs(); |
|
| 206 | 233 | issues.refresh(); |
|
| 207 | 234 | releases.refresh(); |
|
| 235 | + | diffusion.refresh(); |
|
| 208 | 236 | void flow.refresh(); |
|
| 209 | 237 | void bar.refresh(); |
|
| 210 | 238 | void inbox.refresh(); |
|
| … | 238 unchanged lines |
modified src/hub.ts +7 −0
| … | 202 unchanged lines |
||
| 203 | 203 | return this.request("GET", `/repos/${owner}/${name}/bookmarks`); |
|
| 204 | 204 | } |
|
| 205 | 205 | ||
| 206 | + | /** What the repository takes from and diffuses to (oxygenhub#153). */ |
|
| 207 | + | diffusion(owner: string, name: string): Promise<api.Diffusion> { |
|
| 208 | + | return this.request("GET", `/repos/${owner}/${name}/diffusion`); |
|
| 209 | + | } |
|
| 210 | + | ||
| 206 | 211 | releases(owner: string, name: string): Promise<api.Release[]> { |
|
| 207 | 212 | return this.request("GET", `/repos/${owner}/${name}/releases`); |
|
| 208 | 213 | } |
|
| … | 35 unchanged lines |
||
| 244 | 249 | return `${repo}/proposal/${target.number}`; |
|
| 245 | 250 | case "issue": |
|
| 246 | 251 | return `${repo}/issue/${target.number}`; |
|
| 252 | + | case "diffusion": |
|
| 253 | + | return `${repo}/diffusion`; |
|
| 247 | 254 | default: |
|
| 248 | 255 | return repo; |
|
| 249 | 256 | } |
|
| … | 115 unchanged lines |
modified src/hubapi.d.ts +79 −6
| … | 13 unchanged lines |
||
| 14 | 14 | visibility: Visibility; |
|
| 15 | 15 | }; |
|
| 16 | 16 | ||
| 17 | + | export type DiffusionHalf = { |
|
| 18 | + | title: string; |
|
| 19 | + | tone: string; |
|
| 20 | + | word: string; |
|
| 21 | + | }; |
|
| 22 | + | ||
| 23 | + | export type DiffusionPin = { |
|
| 24 | + | behind?: number | null; |
|
| 25 | + | bookmark: string; |
|
| 26 | + | bump?: number | null; |
|
| 27 | + | consumer: string; |
|
| 28 | + | consumer_state: DiffusionHalf; |
|
| 29 | + | follow: string; |
|
| 30 | + | path: string; |
|
| 31 | + | run?: number | null; |
|
| 32 | + | run_status?: string | null; |
|
| 33 | + | upstream: string; |
|
| 34 | + | upstream_state: DiffusionHalf; |
|
| 35 | + | }; |
|
| 36 | + | ||
| 37 | + | export type Diffusion = { |
|
| 38 | + | diffuses_to: DiffusionPin[]; |
|
| 39 | + | takes_from: DiffusionPin[]; |
|
| 40 | + | }; |
|
| 41 | + | ||
| 17 | 42 | export type Bookmark = { |
|
| 18 | 43 | changes: number; |
|
| 19 | 44 | default: boolean; |
|
| … | 28 unchanged lines |
||
| 48 | 73 | status: "invalid"; |
|
| 49 | 74 | }; |
|
| 50 | 75 | ||
| 76 | + | export type Via = { |
|
| 77 | + | expires_ms?: number | null; |
|
| 78 | + | key: string; |
|
| 79 | + | name: string; |
|
| 80 | + | revoked: boolean; |
|
| 81 | + | scope: string; |
|
| 82 | + | }; |
|
| 83 | + | ||
| 51 | 84 | export type ChangeSummary = { |
|
| 52 | 85 | author: Author; |
|
| 53 | 86 | deps: string[]; |
|
| … | 1 unchanged lines |
||
| 55 | 88 | head: boolean; |
|
| 56 | 89 | message: string; |
|
| 57 | 90 | signature: Signature; |
|
| 91 | + | via?: Via | null; |
|
| 58 | 92 | when_ms: number; |
|
| 59 | 93 | }; |
|
| 60 | 94 | ||
| … | 7 unchanged lines |
||
| 68 | 102 | account: string; |
|
| 69 | 103 | key?: string | null; |
|
| 70 | 104 | sealed: boolean; |
|
| 105 | + | }; |
|
| 106 | + | ||
| 107 | + | export type Label = { |
|
| 108 | + | colour: string; |
|
| 109 | + | description: string; |
|
| 110 | + | name: string; |
|
| 111 | + | }; |
|
| 112 | + | ||
| 113 | + | export type Landed = { |
|
| 114 | + | by: string; |
|
| 115 | + | via: string; |
|
| 116 | + | when_ms: number; |
|
| 71 | 117 | }; |
|
| 72 | 118 | ||
| 73 | 119 | export type ProposalSource = { |
|
| … | 19 unchanged lines |
||
| 93 | 139 | changes: ChangeSummary[]; |
|
| 94 | 140 | conflicted: boolean; |
|
| 95 | 141 | created_ms: number; |
|
| 142 | + | labels?: Label[]; |
|
| 96 | 143 | landable: boolean; |
|
| 144 | + | landed?: Landed | null; |
|
| 97 | 145 | number: number; |
|
| 98 | 146 | projected_heads: string[]; |
|
| 99 | 147 | projected_state?: string | null; |
|
| 100 | 148 | repository_url: string; |
|
| 149 | + | requested_reviewers?: string[]; |
|
| 101 | 150 | source: ProposalSource; |
|
| 102 | 151 | source_url?: string | null; |
|
| 103 | 152 | state: ProposalState; |
|
| … | 3 unchanged lines |
||
| 107 | 156 | url: string; |
|
| 108 | 157 | }; |
|
| 109 | 158 | ||
| 110 | - | export type Label = { |
|
| 111 | - | colour: string; |
|
| 112 | - | description: string; |
|
| 113 | - | name: string; |
|
| 114 | - | }; |
|
| 115 | - | ||
| 116 | 159 | export type Issue = { |
|
| 117 | 160 | assignees: string[]; |
|
| 118 | 161 | author: string; |
|
| … | 22 unchanged lines |
||
| 141 | 184 | title: string; |
|
| 142 | 185 | }; |
|
| 143 | 186 | ||
| 187 | + | export type NewMilestone = { |
|
| 188 | + | description?: string; |
|
| 189 | + | due?: string | null; |
|
| 190 | + | title: string; |
|
| 191 | + | }; |
|
| 192 | + | ||
| 193 | + | export type MilestoneEdit = { |
|
| 194 | + | description?: string | null; |
|
| 195 | + | due?: string | null; |
|
| 196 | + | state?: string | null; |
|
| 197 | + | title?: string | null; |
|
| 198 | + | }; |
|
| 199 | + | ||
| 144 | 200 | export type Atom = { |
|
| 145 | 201 | change: string; |
|
| 146 | 202 | index: number; |
|
| … | 40 unchanged lines |
||
| 187 | 243 | id: number; |
|
| 188 | 244 | sealed: boolean; |
|
| 189 | 245 | verdict: string; |
|
| 246 | + | via?: string; |
|
| 190 | 247 | }; |
|
| 191 | 248 | ||
| 192 | 249 | export type TreeEntry = { |
|
| … | 155 unchanged lines |
||
| 348 | 405 | body?: string | null; |
|
| 349 | 406 | labels?: string[] | null; |
|
| 350 | 407 | milestone?: string | null; |
|
| 408 | + | title?: string | null; |
|
| 409 | + | }; |
|
| 410 | + | ||
| 411 | + | export type ReleaseEdit = { |
|
| 412 | + | notes?: string | null; |
|
| 413 | + | prerelease?: boolean | null; |
|
| 414 | + | title?: string | null; |
|
| 415 | + | }; |
|
| 416 | + | ||
| 417 | + | export type ProposalEdit = { |
|
| 418 | + | body?: string | null; |
|
| 419 | + | labels?: string[] | null; |
|
| 420 | + | state?: string | null; |
|
| 351 | 421 | title?: string | null; |
|
| 352 | 422 | }; |
|
| 353 | 423 | ||
| … | 46 unchanged lines |
||
| 400 | 470 | export type ProjectEdit = { |
|
| 401 | 471 | closed?: boolean | null; |
|
| 402 | 472 | description?: string | null; |
|
| 473 | + | owner?: string | null; |
|
| 403 | 474 | private?: boolean | null; |
|
| 404 | 475 | title?: string | null; |
|
| 405 | 476 | }; |
|
| … | 89 unchanged lines |
||
| 495 | 566 | finished_ms?: number | null; |
|
| 496 | 567 | log_lines: number; |
|
| 497 | 568 | name: string; |
|
| 569 | + | placed?: string; |
|
| 498 | 570 | runner: string; |
|
| 499 | 571 | started_ms?: number | null; |
|
| 500 | 572 | status: string; |
|
| 573 | + | waiting?: string; |
|
| 501 | 574 | }; |
|
| 502 | 575 | ||
| 503 | 576 | export type FlowRun = { |
|
| … | 30 unchanged lines |