OXYGENHUB

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