OXYGENHUB

0.7.0: exported API with repository handles, change events and remote providers; Open on Remote commands f6bda77ecb9f

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-27 19:32 UTC
Signature
✔ signed by Andy McCall
Hash
f6bda77ecb9f3551f579d5594834d44936fbf0211fec60331da6f8449547c399
Depends on
25103dddd993 3006b700c514 35d56b5803d9 810a1c538e78 9020cf612749 ab81294a9ba9 bed495a96408 c8f955891224 d2c9a5843004
Depended on by
dac6876e4d08 013fefef11e3 0c26846a9a81 cdcffe39dd9f 1f7fc1e3ae4a 44bfd5adfc9a b4626290e660 9627171e23e2
In bookmarks
main
In tags
v0.16.0 v0.16.1 v0.17.0 v0.17.1 v0.18.0
Edits
26 atoms, 3 tree operations, 7 files

Browse the tree at this change

added docs/plan.md +2 −0
1 +
2 +
The cross-product order of work is in `oxygen/docs/roadmap.md`.
added src/api.ts +83 −0
1 +
// The API this extension exports to others (spec §9, "extension API"): the
2 +
// same idea as the built-in Git extension's `getAPI()`. A forge extension
3 +
// such as oxygenhub-vscode consumes it instead of running oxy itself, and
4 +
// registers a remote provider so this extension can offer "Open on …"
5 +
// links without knowing any hub.
6 +
//
7 +
// Pure functions live here so they can be tested with plain Node; the
8 +
// vscode-facing object is assembled in extension.ts.
9 +
10 +
/** What a remote-aware extension registers. */
11 +
export interface RemoteProvider {
12 +
  /** A stable id, e.g. `oxygenhub`. */
13 +
  readonly id: string;
14 +
  /** Shown in menus: "Open on Oxygenhub". */
15 +
  readonly label: string;
16 +
  /** Whether this provider understands a remote URL. */
17 +
  matches(remoteUrl: string): boolean;
18 +
  /** A web URL for a thing in that remote, or undefined when the provider has none. */
19 +
  urlFor(remoteUrl: string, target: RemoteTarget): string | undefined;
20 +
}
21 +
22 +
export type RemoteTarget =
23 +
  | { kind: "repository" }
24 +
  | { kind: "bookmark"; name: string }
25 +
  | { kind: "change"; hash: string }
26 +
  | { kind: "file"; reference: string; path: string; line?: number }
27 +
  | { kind: "proposals" }
28 +
  | { kind: "issues" };
29 +
30 +
/** A minimal view of one repository, stable across versions. */
31 +
export interface RepositoryHandle {
32 +
  readonly root: string;
33 +
  /** `oxy status --json` as last refreshed, or undefined before the first refresh. */
34 +
  readonly status: unknown;
35 +
  readonly bookmarks: { name: string; heads: string[]; current?: boolean }[];
36 +
  readonly currentBookmark: string | undefined;
37 +
  remotes(): Promise<{ name: string; url: string }[]>;
38 +
  /** Run an oxy command in this repository and parse its JSON output. */
39 +
  json<T>(args: string[]): Promise<T>;
40 +
  refresh(): Promise<void>;
41 +
}
42 +
43 +
export interface OxygenAPI {
44 +
  readonly version: 1;
45 +
  readonly repositories: readonly RepositoryHandle[];
46 +
  /** Fires with the repository whose status changed. */
47 +
  readonly onDidChangeStatus: (listener: (repo: RepositoryHandle) => void) => { dispose(): void };
48 +
  /** Fires when repositories are discovered or removed. */
49 +
  readonly onDidChangeRepositories: (listener: () => void) => { dispose(): void };
50 +
  getRepository(fsPath: string): RepositoryHandle | undefined;
51 +
  registerRemoteProvider(provider: RemoteProvider): { dispose(): void };
52 +
  /** The providers registered so far, for callers that build their own menus. */
53 +
  remoteProviders(): readonly RemoteProvider[];
54 +
}
55 +
56 +
/** The provider (and remote) to use for a repository's remotes, if any. */
57 +
export function pickProvider(providers: readonly RemoteProvider[], remotes: { name: string; url: string }[]): { provider: RemoteProvider; remote: { name: string; url: string } } | undefined {
58 +
  // Prefer a remote called origin or earth, then any.
59 +
  const ordered = [...remotes].sort((a, b) => rank(a.name) - rank(b.name));
60 +
  for (const remote of ordered) {
61 +
    for (const provider of providers) {
62 +
      if (provider.matches(remote.url)) {
63 +
        return { provider, remote };
64 +
      }
65 +
    }
66 +
  }
67 +
  return undefined;
68 +
}
69 +
70 +
function rank(name: string): number {
71 +
  return name === "origin" ? 0 : name === "hub" ? 1 : 2;
72 +
}
73 +
74 +
/** The repository handle a folder path belongs to: the longest root that prefixes it. */
75 +
export function repositoryFor(roots: string[], fsPath: string): string | undefined {
76 +
  let best: string | undefined;
77 +
  for (const r of roots) {
78 +
    if ((fsPath === r || fsPath.startsWith(r.endsWith("/") ? r : r + "/")) && (!best || r.length > best.length)) {
79 +
      best = r;
80 +
    }
81 +
  }
82 +
  return best;
83 +
}
added test/api.test.mjs +23 −0
1 +
import { test } from "node:test";
2 +
import assert from "node:assert/strict";
3 +
import { pickProvider, repositoryFor } from "../out/api.mjs";
4 +
5 +
test("picks the first provider that matches, preferring origin", () => {
6 +
  const hub = { id: "hub", label: "Hub", matches: (u) => u.includes("hub.example"), urlFor: () => undefined };
7 +
  const other = { id: "other", label: "Other", matches: (u) => u.includes("other.example"), urlFor: () => undefined };
8 +
  const remotes = [
9 +
    { name: "mirror", url: "https://other.example/a/b" },
10 +
    { name: "origin", url: "https://hub.example/a/b" },
11 +
  ];
12 +
  const pick = pickProvider([hub, other], remotes);
13 +
  assert.equal(pick.provider.id, "hub");
14 +
  assert.equal(pick.remote.name, "origin");
15 +
  assert.equal(pickProvider([hub], [{ name: "x", url: "https://nowhere" }]), undefined);
16 +
});
17 +
18 +
test("repositoryFor picks the longest matching root", () => {
19 +
  const roots = ["/w/a", "/w/a/sub", "/w/b"];
20 +
  assert.equal(repositoryFor(roots, "/w/a/sub/file.rs"), "/w/a/sub");
21 +
  assert.equal(repositoryFor(roots, "/w/a/x.rs"), "/w/a");
22 +
  assert.equal(repositoryFor(roots, "/w/ab/x"), undefined);
23 +
});
modified CHANGELOG.md +5 −0
1 1
# Changelog
2 2
3 +
## 0.7.0
4 +
5 +
- An exported API for other extensions (`vscode.extensions.getExtension("...").exports`): repositories with status, bookmarks, remotes and `json(args)`, change events, and `registerRemoteProvider` so a forge extension such as Oxygenhub can say which remotes it understands and where things live on the web.
6 +
- "Open on Remote" and "Open Change on Remote" commands, shown once a provider is registered.
7 +
3 8
## 0.6.0 (2026-09-27)
4 9
5 10
Milestone 0.6: a Subrepos group with update and open, Oxygen Bisect and Oxygen Trust views, Verify Signatures, tag picker and Create Tag, Add Workspace, and Clone Repository.
…
32 unchanged lines
modified docs/spec.md +4 −0
…
205 unchanged lines
206 206
- `oxy` gained `tag list --json` and `workspace list --json` for §4, and two bugs the extension's tests found were fixed in `oxy`: a chosen addition hunk now carries its content, and an unsigned repository keeps a tag's message.
207 207
- Extensions are per VS Code profile; the install command must name the profile a workspace uses.
208 208
209 +
## 9a. Exported API (0.7.0)
210 +
211 +
`activate` returns an `OxygenAPI` (`src/api.ts`): the repositories as handles (root, status, bookmarks, current bookmark, `remotes()`, `json(args)`, `refresh()`), `onDidChangeStatus`, `onDidChangeRepositories`, `getRepository(path)`, and `registerRemoteProvider`. A remote provider gives an id and label, `matches(remoteUrl)` and `urlFor(remoteUrl, target)` for repository, bookmark, change, file (with line), proposals and issues targets. This extension uses the providers for its **Open on Remote** commands and sets the `oxygen.hasRemoteProvider` context; oxygenhub-vscode registers one for hubs and consumes the handles instead of running `oxy` itself, mirroring how the GitHub Pull Requests extension builds on the built-in Git extension.
212 +
209 213
## 9. Open questions
210 214
211 215
1. Whether the extension should ship `oxy` binaries for the three platforms or only locate one; shipping simplifies onboarding but ties releases together.
…
2 unchanged lines
modified package.json +567 −109
…
1 unchanged lines
2 2
  "name": "oxygen-vscode",
3 3
  "displayName": "Oxygen",
4 4
  "description": "Oxygen version control: source control view, diffs, decorations, bookmarks and history for oxy repositories",
5 -
  "version": "0.6.0",
5 +
  "version": "0.7.0",
6 6
  "publisher": "oxygen",
7 7
  "license": "MIT OR Apache-2.0",
8 8
  "icon": "assets/images/oxygen.png",
…
17 unchanged lines
26 26
  "main": "./dist/extension.js",
27 27
  "contributes": {
28 28
    "commands": [
29 -
      { "command": "oxygen.refresh", "title": "Refresh", "category": "Oxygen", "icon": "$(refresh)" },
30 -
      { "command": "oxygen.commit", "title": "Commit", "category": "Oxygen", "icon": "$(check)" },
31 -
      { "command": "oxygen.openFile", "title": "Open File", "category": "Oxygen", "icon": "$(go-to-file)" },
32 -
      { "command": "oxygen.openChanges", "title": "Open Changes", "category": "Oxygen" },
33 -
      { "command": "oxygen.openBaseVersion", "title": "Open Base Version", "category": "Oxygen" },
34 -
      { "command": "oxygen.showOutput", "title": "Show Output", "category": "Oxygen" },
35 -
      { "command": "oxygen.showLog", "title": "Show Log", "category": "Oxygen" },
36 -
      { "command": "oxygen.init", "title": "Initialize Repository", "category": "Oxygen" },
37 -
      { "command": "oxygen.discard", "title": "Discard Changes", "category": "Oxygen", "icon": "$(discard)" },
38 -
      { "command": "oxygen.commitSelected", "title": "Commit Selected Paths", "category": "Oxygen" },
39 -
      { "command": "oxygen.commitHunks", "title": "Commit Hunks…", "category": "Oxygen" },
40 -
      { "command": "oxygen.amend", "title": "Amend", "category": "Oxygen" },
41 -
      { "command": "oxygen.undo", "title": "Undo Last Operation", "category": "Oxygen", "icon": "$(discard)" },
42 -
      { "command": "oxygen.undoTo", "title": "Undo to Here", "category": "Oxygen", "icon": "$(discard)" },
43 -
      { "command": "oxygen.refreshOperations", "title": "Refresh Operations", "category": "Oxygen", "icon": "$(refresh)" },
44 -
      { "command": "oxygen.switchBookmark", "title": "Switch Bookmark…", "category": "Oxygen", "icon": "$(bookmark)" },
45 -
      { "command": "oxygen.createBookmark", "title": "Create Bookmark…", "category": "Oxygen" },
46 -
      { "command": "oxygen.mergeBookmark", "title": "Merge Bookmark…", "category": "Oxygen" },
47 -
      { "command": "oxygen.deleteBookmark", "title": "Delete Bookmark…", "category": "Oxygen" },
48 -
      { "command": "oxygen.fetch", "title": "Fetch", "category": "Oxygen" },
49 -
      { "command": "oxygen.pull", "title": "Pull", "category": "Oxygen" },
50 -
      { "command": "oxygen.push", "title": "Push", "category": "Oxygen" },
51 -
      { "command": "oxygen.pushBookmark", "title": "Push Bookmark…", "category": "Oxygen" },
52 -
      { "command": "oxygen.sync", "title": "Sync", "category": "Oxygen", "icon": "$(sync)" },
53 -
      { "command": "oxygen.showHistory", "title": "Show History", "category": "Oxygen", "icon": "$(git-commit)" },
54 -
      { "command": "oxygen.fileHistory", "title": "File History…", "category": "Oxygen", "icon": "$(history)" },
55 -
      { "command": "oxygen.toggleBlame", "title": "Toggle Inline Blame", "category": "Oxygen" },
56 -
      { "command": "oxygen.showChange", "title": "Show Change", "category": "Oxygen" },
57 -
      { "command": "oxygen.acceptSides", "title": "Accept Conflict Sides", "category": "Oxygen" },
58 -
      { "command": "oxygen.keepCandidate", "title": "Keep This Candidate", "category": "Oxygen", "icon": "$(check)" },
59 -
      { "command": "oxygen.markResolved", "title": "Mark Resolved", "category": "Oxygen", "icon": "$(check-all)" },
60 -
      { "command": "oxygen.openConflict", "title": "Open Conflict", "category": "Oxygen" },
61 -
      { "command": "oxygen.openSubrepo", "title": "Open Subrepo", "category": "Oxygen" },
62 -
      { "command": "oxygen.updateSubrepo", "title": "Update to Pin", "category": "Oxygen", "icon": "$(cloud-download)" },
63 -
      { "command": "oxygen.updateSubrepos", "title": "Update Subrepos", "category": "Oxygen" },
64 -
      { "command": "oxygen.bisectStart", "title": "Bisect: Start", "category": "Oxygen" },
65 -
      { "command": "oxygen.bisectGood", "title": "Bisect: Good", "category": "Oxygen" },
66 -
      { "command": "oxygen.bisectBad", "title": "Bisect: Bad", "category": "Oxygen" },
67 -
      { "command": "oxygen.bisectSkip", "title": "Bisect: Skip", "category": "Oxygen" },
68 -
      { "command": "oxygen.bisectRun", "title": "Bisect: Run…", "category": "Oxygen" },
69 -
      { "command": "oxygen.bisectReset", "title": "Bisect: Reset", "category": "Oxygen" },
70 -
      { "command": "oxygen.refreshBisect", "title": "Refresh", "category": "Oxygen", "icon": "$(refresh)" },
71 -
      { "command": "oxygen.refreshTrust", "title": "Refresh", "category": "Oxygen", "icon": "$(refresh)" },
72 -
      { "command": "oxygen.verify", "title": "Verify Signatures", "category": "Oxygen" },
73 -
      { "command": "oxygen.tags", "title": "Tags…", "category": "Oxygen" },
74 -
      { "command": "oxygen.createTag", "title": "Create Tag…", "category": "Oxygen" },
75 -
      { "command": "oxygen.addWorkspace", "title": "Add Workspace…", "category": "Oxygen" },
76 -
      { "command": "oxygen.clone", "title": "Clone Repository…", "category": "Oxygen" }
29 +
      {
30 +
        "command": "oxygen.refresh",
31 +
        "title": "Refresh",
32 +
        "category": "Oxygen",
33 +
        "icon": "$(refresh)"
34 +
      },
35 +
      {
36 +
        "command": "oxygen.commit",
37 +
        "title": "Commit",
38 +
        "category": "Oxygen",
39 +
        "icon": "$(check)"
40 +
      },
41 +
      {
42 +
        "command": "oxygen.openFile",
43 +
        "title": "Open File",
44 +
        "category": "Oxygen",
45 +
        "icon": "$(go-to-file)"
46 +
      },
47 +
      {
48 +
        "command": "oxygen.openChanges",
49 +
        "title": "Open Changes",
50 +
        "category": "Oxygen"
51 +
      },
52 +
      {
53 +
        "command": "oxygen.openBaseVersion",
54 +
        "title": "Open Base Version",
55 +
        "category": "Oxygen"
56 +
      },
57 +
      {
58 +
        "command": "oxygen.showOutput",
59 +
        "title": "Show Output",
60 +
        "category": "Oxygen"
61 +
      },
62 +
      {
63 +
        "command": "oxygen.showLog",
64 +
        "title": "Show Log",
65 +
        "category": "Oxygen"
66 +
      },
67 +
      {
68 +
        "command": "oxygen.init",
69 +
        "title": "Initialize Repository",
70 +
        "category": "Oxygen"
71 +
      },
72 +
      {
73 +
        "command": "oxygen.discard",
74 +
        "title": "Discard Changes",
75 +
        "category": "Oxygen",
76 +
        "icon": "$(discard)"
77 +
      },
78 +
      {
79 +
        "command": "oxygen.commitSelected",
80 +
        "title": "Commit Selected Paths",
81 +
        "category": "Oxygen"
82 +
      },
83 +
      {
84 +
        "command": "oxygen.commitHunks",
85 +
        "title": "Commit Hunks\u2026",
86 +
        "category": "Oxygen"
87 +
      },
88 +
      {
89 +
        "command": "oxygen.amend",
90 +
        "title": "Amend",
91 +
        "category": "Oxygen"
92 +
      },
93 +
      {
94 +
        "command": "oxygen.undo",
95 +
        "title": "Undo Last Operation",
96 +
        "category": "Oxygen",
97 +
        "icon": "$(discard)"
98 +
      },
99 +
      {
100 +
        "command": "oxygen.undoTo",
101 +
        "title": "Undo to Here",
102 +
        "category": "Oxygen",
103 +
        "icon": "$(discard)"
104 +
      },
105 +
      {
106 +
        "command": "oxygen.refreshOperations",
107 +
        "title": "Refresh Operations",
108 +
        "category": "Oxygen",
109 +
        "icon": "$(refresh)"
110 +
      },
111 +
      {
112 +
        "command": "oxygen.switchBookmark",
113 +
        "title": "Switch Bookmark\u2026",
114 +
        "category": "Oxygen",
115 +
        "icon": "$(bookmark)"
116 +
      },
117 +
      {
118 +
        "command": "oxygen.createBookmark",
119 +
        "title": "Create Bookmark\u2026",
120 +
        "category": "Oxygen"
121 +
      },
122 +
      {
123 +
        "command": "oxygen.mergeBookmark",
124 +
        "title": "Merge Bookmark\u2026",
125 +
        "category": "Oxygen"
126 +
      },
127 +
      {
128 +
        "command": "oxygen.deleteBookmark",
129 +
        "title": "Delete Bookmark\u2026",
130 +
        "category": "Oxygen"
131 +
      },
132 +
      {
133 +
        "command": "oxygen.fetch",
134 +
        "title": "Fetch",
135 +
        "category": "Oxygen"
136 +
      },
137 +
      {
138 +
        "command": "oxygen.pull",
139 +
        "title": "Pull",
140 +
        "category": "Oxygen"
141 +
      },
142 +
      {
143 +
        "command": "oxygen.push",
144 +
        "title": "Push",
145 +
        "category": "Oxygen"
146 +
      },
147 +
      {
148 +
        "command": "oxygen.pushBookmark",
149 +
        "title": "Push Bookmark\u2026",
150 +
        "category": "Oxygen"
151 +
      },
152 +
      {
153 +
        "command": "oxygen.sync",
154 +
        "title": "Sync",
155 +
        "category": "Oxygen",
156 +
        "icon": "$(sync)"
157 +
      },
158 +
      {
159 +
        "command": "oxygen.showHistory",
160 +
        "title": "Show History",
161 +
        "category": "Oxygen",
162 +
        "icon": "$(git-commit)"
163 +
      },
164 +
      {
165 +
        "command": "oxygen.fileHistory",
166 +
        "title": "File History\u2026",
167 +
        "category": "Oxygen",
168 +
        "icon": "$(history)"
169 +
      },
170 +
      {
171 +
        "command": "oxygen.toggleBlame",
172 +
        "title": "Toggle Inline Blame",
173 +
        "category": "Oxygen"
174 +
      },
175 +
      {
176 +
        "command": "oxygen.showChange",
177 +
        "title": "Show Change",
178 +
        "category": "Oxygen"
179 +
      },
180 +
      {
181 +
        "command": "oxygen.acceptSides",
182 +
        "title": "Accept Conflict Sides",
183 +
        "category": "Oxygen"
184 +
      },
185 +
      {
186 +
        "command": "oxygen.keepCandidate",
187 +
        "title": "Keep This Candidate",
188 +
        "category": "Oxygen",
189 +
        "icon": "$(check)"
190 +
      },
191 +
      {
192 +
        "command": "oxygen.markResolved",
193 +
        "title": "Mark Resolved",
194 +
        "category": "Oxygen",
195 +
        "icon": "$(check-all)"
196 +
      },
197 +
      {
198 +
        "command": "oxygen.openConflict",
199 +
        "title": "Open Conflict",
200 +
        "category": "Oxygen"
201 +
      },
202 +
      {
203 +
        "command": "oxygen.openSubrepo",
204 +
        "title": "Open Subrepo",
205 +
        "category": "Oxygen"
206 +
      },
207 +
      {
208 +
        "command": "oxygen.updateSubrepo",
209 +
        "title": "Update to Pin",
210 +
        "category": "Oxygen",
211 +
        "icon": "$(cloud-download)"
212 +
      },
213 +
      {
214 +
        "command": "oxygen.updateSubrepos",
215 +
        "title": "Update Subrepos",
216 +
        "category": "Oxygen"
217 +
      },
218 +
      {
219 +
        "command": "oxygen.bisectStart",
220 +
        "title": "Bisect: Start",
221 +
        "category": "Oxygen"
222 +
      },
223 +
      {
224 +
        "command": "oxygen.bisectGood",
225 +
        "title": "Bisect: Good",
226 +
        "category": "Oxygen"
227 +
      },
228 +
      {
229 +
        "command": "oxygen.bisectBad",
230 +
        "title": "Bisect: Bad",
231 +
        "category": "Oxygen"
232 +
      },
233 +
      {
234 +
        "command": "oxygen.bisectSkip",
235 +
        "title": "Bisect: Skip",
236 +
        "category": "Oxygen"
237 +
      },
238 +
      {
239 +
        "command": "oxygen.bisectRun",
240 +
        "title": "Bisect: Run\u2026",
241 +
        "category": "Oxygen"
242 +
      },
243 +
      {
244 +
        "command": "oxygen.bisectReset",
245 +
        "title": "Bisect: Reset",
246 +
        "category": "Oxygen"
247 +
      },
248 +
      {
249 +
        "command": "oxygen.refreshBisect",
250 +
        "title": "Refresh",
251 +
        "category": "Oxygen",
252 +
        "icon": "$(refresh)"
253 +
      },
254 +
      {
255 +
        "command": "oxygen.refreshTrust",
256 +
        "title": "Refresh",
257 +
        "category": "Oxygen",
258 +
        "icon": "$(refresh)"
259 +
      },
260 +
      {
261 +
        "command": "oxygen.verify",
262 +
        "title": "Verify Signatures",
263 +
        "category": "Oxygen"
264 +
      },
265 +
      {
266 +
        "command": "oxygen.tags",
267 +
        "title": "Tags\u2026",
268 +
        "category": "Oxygen"
269 +
      },
270 +
      {
271 +
        "command": "oxygen.createTag",
272 +
        "title": "Create Tag\u2026",
273 +
        "category": "Oxygen"
274 +
      },
275 +
      {
276 +
        "command": "oxygen.addWorkspace",
277 +
        "title": "Add Workspace\u2026",
278 +
        "category": "Oxygen"
279 +
      },
280 +
      {
281 +
        "command": "oxygen.clone",
282 +
        "title": "Clone Repository\u2026",
283 +
        "category": "Oxygen"
284 +
      },
285 +
      {
286 +
        "command": "oxygen.openOnRemote",
287 +
        "title": "Open on Remote",
288 +
        "category": "Oxygen",
289 +
        "icon": "$(link-external)"
290 +
      },
291 +
      {
292 +
        "command": "oxygen.openChangeOnRemote",
293 +
        "title": "Open Change on Remote",
294 +
        "category": "Oxygen"
295 +
      }
77 296
    ],
78 297
    "views": {
79 298
      "scm": [
80 -
        { "id": "oxygen.operations", "name": "Oxygen Operations", "when": "oxygen.hasRepository", "icon": "assets/images/oxygen.svg", "visibility": "collapsed" },
81 -
        { "id": "oxygen.bisect", "name": "Oxygen Bisect", "when": "oxygen.hasRepository", "icon": "assets/images/oxygen.svg", "visibility": "collapsed" },
82 -
        { "id": "oxygen.trust", "name": "Oxygen Trust", "when": "oxygen.hasRepository", "icon": "assets/images/oxygen.svg", "visibility": "collapsed" }
299 +
        {
300 +
          "id": "oxygen.operations",
301 +
          "name": "Oxygen Operations",
302 +
          "when": "oxygen.hasRepository",
303 +
          "icon": "assets/images/oxygen.svg",
304 +
          "visibility": "collapsed"
305 +
        },
306 +
        {
307 +
          "id": "oxygen.bisect",
308 +
          "name": "Oxygen Bisect",
309 +
          "when": "oxygen.hasRepository",
310 +
          "icon": "assets/images/oxygen.svg",
311 +
          "visibility": "collapsed"
312 +
        },
313 +
        {
314 +
          "id": "oxygen.trust",
315 +
          "name": "Oxygen Trust",
316 +
          "when": "oxygen.hasRepository",
317 +
          "icon": "assets/images/oxygen.svg",
318 +
          "visibility": "collapsed"
319 +
        }
83 320
      ]
84 321
    },
85 322
    "viewsWelcome": [
…
5 unchanged lines
91 328
    ],
92 329
    "menus": {
93 330
      "scm/title": [
94 -
        { "command": "oxygen.commit", "group": "navigation@1", "when": "scmProvider == oxygen" },
95 -
        { "command": "oxygen.sync", "group": "navigation@2", "when": "scmProvider == oxygen" },
96 -
        { "command": "oxygen.refresh", "group": "navigation@3", "when": "scmProvider == oxygen" },
97 -
        { "command": "oxygen.pull", "group": "4_sync@1", "when": "scmProvider == oxygen" },
98 -
        { "command": "oxygen.push", "group": "4_sync@2", "when": "scmProvider == oxygen" },
99 -
        { "command": "oxygen.pushBookmark", "group": "4_sync@3", "when": "scmProvider == oxygen" },
100 -
        { "command": "oxygen.fetch", "group": "4_sync@4", "when": "scmProvider == oxygen" },
101 -
        { "command": "oxygen.switchBookmark", "group": "5_bookmarks@1", "when": "scmProvider == oxygen" },
102 -
        { "command": "oxygen.createBookmark", "group": "5_bookmarks@2", "when": "scmProvider == oxygen" },
103 -
        { "command": "oxygen.mergeBookmark", "group": "5_bookmarks@3", "when": "scmProvider == oxygen" },
104 -
        { "command": "oxygen.deleteBookmark", "group": "5_bookmarks@4", "when": "scmProvider == oxygen" },
105 -
        { "command": "oxygen.tags", "group": "6_more@1", "when": "scmProvider == oxygen" },
106 -
        { "command": "oxygen.createTag", "group": "6_more@2", "when": "scmProvider == oxygen" },
107 -
        { "command": "oxygen.addWorkspace", "group": "6_more@3", "when": "scmProvider == oxygen" },
108 -
        { "command": "oxygen.updateSubrepos", "group": "6_more@4", "when": "scmProvider == oxygen" },
109 -
        { "command": "oxygen.verify", "group": "6_more@5", "when": "scmProvider == oxygen" },
110 -
        { "command": "oxygen.commitHunks", "group": "2_commit@1", "when": "scmProvider == oxygen" },
111 -
        { "command": "oxygen.amend", "group": "2_commit@2", "when": "scmProvider == oxygen" },
112 -
        { "command": "oxygen.undo", "group": "3_undo", "when": "scmProvider == oxygen" },
113 -
        { "command": "oxygen.markResolved", "group": "2_commit@3", "when": "scmProvider == oxygen" },
114 -
        { "command": "oxygen.showHistory", "group": "1_view@1", "when": "scmProvider == oxygen" },
115 -
        { "command": "oxygen.showLog", "group": "1_view@2", "when": "scmProvider == oxygen" },
116 -
        { "command": "oxygen.showOutput", "group": "1_view@3", "when": "scmProvider == oxygen" }
331 +
        {
332 +
          "command": "oxygen.commit",
333 +
          "group": "navigation@1",
334 +
          "when": "scmProvider == oxygen"
335 +
        },
336 +
        {
337 +
          "command": "oxygen.sync",
338 +
          "group": "navigation@2",
339 +
          "when": "scmProvider == oxygen"
340 +
        },
341 +
        {
342 +
          "command": "oxygen.refresh",
343 +
          "group": "navigation@3",
344 +
          "when": "scmProvider == oxygen"
345 +
        },
346 +
        {
347 +
          "command": "oxygen.pull",
348 +
          "group": "4_sync@1",
349 +
          "when": "scmProvider == oxygen"
350 +
        },
351 +
        {
352 +
          "command": "oxygen.push",
353 +
          "group": "4_sync@2",
354 +
          "when": "scmProvider == oxygen"
355 +
        },
356 +
        {
357 +
          "command": "oxygen.pushBookmark",
358 +
          "group": "4_sync@3",
359 +
          "when": "scmProvider == oxygen"
360 +
        },
361 +
        {
362 +
          "command": "oxygen.fetch",
363 +
          "group": "4_sync@4",
364 +
          "when": "scmProvider == oxygen"
365 +
        },
366 +
        {
367 +
          "command": "oxygen.switchBookmark",
368 +
          "group": "5_bookmarks@1",
369 +
          "when": "scmProvider == oxygen"
370 +
        },
371 +
        {
372 +
          "command": "oxygen.createBookmark",
373 +
          "group": "5_bookmarks@2",
374 +
          "when": "scmProvider == oxygen"
375 +
        },
376 +
        {
377 +
          "command": "oxygen.mergeBookmark",
378 +
          "group": "5_bookmarks@3",
379 +
          "when": "scmProvider == oxygen"
380 +
        },
381 +
        {
382 +
          "command": "oxygen.deleteBookmark",
383 +
          "group": "5_bookmarks@4",
384 +
          "when": "scmProvider == oxygen"
385 +
        },
386 +
        {
387 +
          "command": "oxygen.tags",
388 +
          "group": "6_more@1",
389 +
          "when": "scmProvider == oxygen"
390 +
        },
391 +
        {
392 +
          "command": "oxygen.createTag",
393 +
          "group": "6_more@2",
394 +
          "when": "scmProvider == oxygen"
395 +
        },
396 +
        {
397 +
          "command": "oxygen.addWorkspace",
398 +
          "group": "6_more@3",
399 +
          "when": "scmProvider == oxygen"
400 +
        },
401 +
        {
402 +
          "command": "oxygen.updateSubrepos",
403 +
          "group": "6_more@4",
404 +
          "when": "scmProvider == oxygen"
405 +
        },
406 +
        {
407 +
          "command": "oxygen.verify",
408 +
          "group": "6_more@5",
409 +
          "when": "scmProvider == oxygen"
410 +
        },
411 +
        {
412 +
          "command": "oxygen.commitHunks",
413 +
          "group": "2_commit@1",
414 +
          "when": "scmProvider == oxygen"
415 +
        },
416 +
        {
417 +
          "command": "oxygen.amend",
418 +
          "group": "2_commit@2",
419 +
          "when": "scmProvider == oxygen"
420 +
        },
421 +
        {
422 +
          "command": "oxygen.undo",
423 +
          "group": "3_undo",
424 +
          "when": "scmProvider == oxygen"
425 +
        },
426 +
        {
427 +
          "command": "oxygen.markResolved",
428 +
          "group": "2_commit@3",
429 +
          "when": "scmProvider == oxygen"
430 +
        },
431 +
        {
432 +
          "command": "oxygen.showHistory",
433 +
          "group": "1_view@1",
434 +
          "when": "scmProvider == oxygen"
435 +
        },
436 +
        {
437 +
          "command": "oxygen.showLog",
438 +
          "group": "1_view@2",
439 +
          "when": "scmProvider == oxygen"
440 +
        },
441 +
        {
442 +
          "command": "oxygen.showOutput",
443 +
          "group": "1_view@3",
444 +
          "when": "scmProvider == oxygen"
445 +
        },
446 +
        {
447 +
          "command": "oxygen.openOnRemote",
448 +
          "when": "scmProvider == oxygen",
449 +
          "group": "navigation@9"
450 +
        }
117 451
      ],
118 452
      "editor/title": [
119 -
        { "command": "oxygen.fileHistory", "group": "navigation@9", "when": "oxygen.hasRepository && resourceScheme == file" }
453 +
        {
454 +
          "command": "oxygen.fileHistory",
455 +
          "group": "navigation@9",
456 +
          "when": "oxygen.hasRepository && resourceScheme == file"
457 +
        }
120 458
      ],
121 459
      "editor/context": [
122 -
        { "command": "oxygen.toggleBlame", "group": "oxygen@1", "when": "oxygen.hasRepository && resourceScheme == file" },
123 -
        { "command": "oxygen.fileHistory", "group": "oxygen@2", "when": "oxygen.hasRepository && resourceScheme == file" }
460 +
        {
461 +
          "command": "oxygen.toggleBlame",
462 +
          "group": "oxygen@1",
463 +
          "when": "oxygen.hasRepository && resourceScheme == file"
464 +
        },
465 +
        {
466 +
          "command": "oxygen.fileHistory",
467 +
          "group": "oxygen@2",
468 +
          "when": "oxygen.hasRepository && resourceScheme == file"
469 +
        }
124 470
      ],
125 471
      "explorer/context": [
126 -
        { "command": "oxygen.fileHistory", "group": "7_modification@9", "when": "oxygen.hasRepository && !explorerResourceIsFolder" }
472 +
        {
473 +
          "command": "oxygen.fileHistory",
474 +
          "group": "7_modification@9",
475 +
          "when": "oxygen.hasRepository && !explorerResourceIsFolder"
476 +
        }
127 477
      ],
128 478
      "scm/resourceState/context": [
129 -
        { "command": "oxygen.openFile", "group": "inline@1", "when": "scmProvider == oxygen" },
130 -
        { "command": "oxygen.discard", "group": "inline@2", "when": "scmProvider == oxygen" },
131 -
        { "command": "oxygen.openChanges", "group": "navigation", "when": "scmProvider == oxygen" },
132 -
        { "command": "oxygen.openBaseVersion", "group": "navigation", "when": "scmProvider == oxygen" },
133 -
        { "command": "oxygen.commitSelected", "group": "1_modification@1", "when": "scmProvider == oxygen" },
134 -
        { "command": "oxygen.discard", "group": "1_modification@2", "when": "scmProvider == oxygen" },
135 -
        { "command": "oxygen.keepCandidate", "group": "inline@0", "when": "scmProvider == oxygen && scmResourceState == candidate" },
136 -
        { "command": "oxygen.updateSubrepo", "group": "inline@0", "when": "scmProvider == oxygen && scmResourceState == subrepo" },
137 -
        { "command": "oxygen.keepCandidate", "group": "0_conflict", "when": "scmProvider == oxygen && scmResourceState == candidate" }
479 +
        {
480 +
          "command": "oxygen.openFile",
481 +
          "group": "inline@1",
482 +
          "when": "scmProvider == oxygen"
483 +
        },
484 +
        {
485 +
          "command": "oxygen.discard",
486 +
          "group": "inline@2",
487 +
          "when": "scmProvider == oxygen"
488 +
        },
489 +
        {
490 +
          "command": "oxygen.openChanges",
491 +
          "group": "navigation",
492 +
          "when": "scmProvider == oxygen"
493 +
        },
494 +
        {
495 +
          "command": "oxygen.openBaseVersion",
496 +
          "group": "navigation",
497 +
          "when": "scmProvider == oxygen"
498 +
        },
499 +
        {
500 +
          "command": "oxygen.commitSelected",
501 +
          "group": "1_modification@1",
502 +
          "when": "scmProvider == oxygen"
503 +
        },
504 +
        {
505 +
          "command": "oxygen.discard",
506 +
          "group": "1_modification@2",
507 +
          "when": "scmProvider == oxygen"
508 +
        },
509 +
        {
510 +
          "command": "oxygen.keepCandidate",
511 +
          "group": "inline@0",
512 +
          "when": "scmProvider == oxygen && scmResourceState == candidate"
513 +
        },
514 +
        {
515 +
          "command": "oxygen.updateSubrepo",
516 +
          "group": "inline@0",
517 +
          "when": "scmProvider == oxygen && scmResourceState == subrepo"
518 +
        },
519 +
        {
520 +
          "command": "oxygen.keepCandidate",
521 +
          "group": "0_conflict",
522 +
          "when": "scmProvider == oxygen && scmResourceState == candidate"
523 +
        }
138 524
      ],
139 525
      "scm/resourceGroup/context": [
140 -
        { "command": "oxygen.markResolved", "group": "inline", "when": "scmProvider == oxygen && scmResourceGroup == conflicts" }
526 +
        {
527 +
          "command": "oxygen.markResolved",
528 +
          "group": "inline",
529 +
          "when": "scmProvider == oxygen && scmResourceGroup == conflicts"
530 +
        }
141 531
      ],
142 532
      "view/title": [
143 -
        { "command": "oxygen.refreshOperations", "group": "navigation", "when": "view == oxygen.operations" },
144 -
        { "command": "oxygen.refreshBisect", "group": "navigation", "when": "view == oxygen.bisect" },
145 -
        { "command": "oxygen.refreshTrust", "group": "navigation", "when": "view == oxygen.trust" }
533 +
        {
534 +
          "command": "oxygen.refreshOperations",
535 +
          "group": "navigation",
536 +
          "when": "view == oxygen.operations"
537 +
        },
538 +
        {
539 +
          "command": "oxygen.refreshBisect",
540 +
          "group": "navigation",
541 +
          "when": "view == oxygen.bisect"
542 +
        },
543 +
        {
544 +
          "command": "oxygen.refreshTrust",
545 +
          "group": "navigation",
546 +
          "when": "view == oxygen.trust"
547 +
        }
146 548
      ],
147 549
      "view/item/context": [
148 -
        { "command": "oxygen.undoTo", "group": "inline", "when": "view == oxygen.operations && viewItem == operation" }
550 +
        {
551 +
          "command": "oxygen.undoTo",
552 +
          "group": "inline",
553 +
          "when": "view == oxygen.operations && viewItem == operation"
554 +
        }
149 555
      ],
150 556
      "commandPalette": [
151 -
        { "command": "oxygen.openFile", "when": "false" },
152 -
        { "command": "oxygen.openChanges", "when": "false" },
153 -
        { "command": "oxygen.openBaseVersion", "when": "false" },
154 -
        { "command": "oxygen.discard", "when": "false" },
155 -
        { "command": "oxygen.commitSelected", "when": "false" },
156 -
        { "command": "oxygen.undoTo", "when": "false" },
157 -
        { "command": "oxygen.refreshOperations", "when": "false" },
158 -
        { "command": "oxygen.showChange", "when": "false" },
159 -
        { "command": "oxygen.acceptSides", "when": "false" },
160 -
        { "command": "oxygen.keepCandidate", "when": "false" },
161 -
        { "command": "oxygen.openConflict", "when": "false" },
162 -
        { "command": "oxygen.openSubrepo", "when": "false" },
163 -
        { "command": "oxygen.updateSubrepo", "when": "false" },
164 -
        { "command": "oxygen.refreshBisect", "when": "false" },
165 -
        { "command": "oxygen.refreshTrust", "when": "false" }
557 +
        {
558 +
          "command": "oxygen.openFile",
559 +
          "when": "false"
560 +
        },
561 +
        {
562 +
          "command": "oxygen.openChanges",
563 +
          "when": "false"
564 +
        },
565 +
        {
566 +
          "command": "oxygen.openBaseVersion",
567 +
          "when": "false"
568 +
        },
569 +
        {
570 +
          "command": "oxygen.discard",
571 +
          "when": "false"
572 +
        },
573 +
        {
574 +
          "command": "oxygen.commitSelected",
575 +
          "when": "false"
576 +
        },
577 +
        {
578 +
          "command": "oxygen.undoTo",
579 +
          "when": "false"
580 +
        },
581 +
        {
582 +
          "command": "oxygen.refreshOperations",
583 +
          "when": "false"
584 +
        },
585 +
        {
586 +
          "command": "oxygen.showChange",
587 +
          "when": "false"
588 +
        },
589 +
        {
590 +
          "command": "oxygen.acceptSides",
591 +
          "when": "false"
592 +
        },
593 +
        {
594 +
          "command": "oxygen.keepCandidate",
595 +
          "when": "false"
596 +
        },
597 +
        {
598 +
          "command": "oxygen.openConflict",
599 +
          "when": "false"
600 +
        },
601 +
        {
602 +
          "command": "oxygen.openSubrepo",
603 +
          "when": "false"
604 +
        },
605 +
        {
606 +
          "command": "oxygen.updateSubrepo",
607 +
          "when": "false"
608 +
        },
609 +
        {
610 +
          "command": "oxygen.refreshBisect",
611 +
          "when": "false"
612 +
        },
613 +
        {
614 +
          "command": "oxygen.refreshTrust",
615 +
          "when": "false"
616 +
        }
617 +
      ],
618 +
      "editor/title/context": [
619 +
        {
620 +
          "command": "oxygen.openOnRemote",
621 +
          "when": "oxygen.hasRemoteProvider",
622 +
          "group": "oxygen"
623 +
        }
166 624
      ]
167 625
    },
168 626
    "configuration": {
…
41 unchanged lines
210 668
    "build": "esbuild src/extension.ts --bundle --platform=node --format=cjs --external:vscode --outfile=dist/extension.js --sourcemap",
211 669
    "watch": "npm run build -- --watch",
212 670
    "typecheck": "tsc --noEmit -p .",
213 -
    "pretest": "esbuild src/oxy.ts --bundle --platform=node --format=esm --outfile=out/oxy.mjs && esbuild src/diffutil.ts --bundle --platform=node --format=esm --outfile=out/diffutil.mjs && esbuild src/markers.ts --bundle --platform=node --format=esm --outfile=out/markers.mjs",
671 +
    "pretest": "esbuild src/oxy.ts --bundle --platform=node --format=esm --outfile=out/oxy.mjs && esbuild src/diffutil.ts --bundle --platform=node --format=esm --outfile=out/diffutil.mjs && esbuild src/markers.ts --bundle --platform=node --format=esm --outfile=out/markers.mjs && esbuild src/api.ts --bundle --platform=node --format=esm --outfile=out/api.mjs",
214 672
    "test": "node --test \"test/**/*.test.mjs\"",
215 673
    "check": "npm run typecheck && npm run build && npm test",
216 674
    "package": "vsce package --no-dependencies"
…
9 unchanged lines
modified src/extension.ts +95 −1
…
1 unchanged lines
2 2
// decorations, the content provider, and the commands (spec §3).
3 3
4 4
import * as vscode from "vscode";
5 +
import { OxygenAPI, RemoteProvider, RemoteTarget, RepositoryHandle, pickProvider, repositoryFor as rootFor } from "./api";
5 6
import { basename } from "node:path";
6 7
import { Oxy, OxyError, findBinary } from "./oxy";
7 8
import { Repository, OxyResourceState, SubrepoState } from "./repository";
…
18 unchanged lines
26 27
let trustView: TrustProvider;
27 28
const repositories = new Map<string, Repository>();
28 29
29 -
export async function activate(context: vscode.ExtensionContext): Promise<void> {
30 +
export async function activate(context: vscode.ExtensionContext): Promise<OxygenAPI | undefined> {
30 31
  output = vscode.window.createOutputChannel("Oxygen");
31 32
  context.subscriptions.push(output);
32 33
  output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`);
…
32 unchanged lines
65 66
      await Promise.all([...repositories.values()].map((r) => r.refresh()));
66 67
    }),
67 68
    vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
69 +
    vscode.commands.registerCommand("oxygen.openOnRemote", (arg?: Repository | vscode.SourceControl) => openOnRemote(undefined, arg instanceof Repository ? arg : undefined)),
70 +
    vscode.commands.registerCommand("oxygen.openChangeOnRemote", (root: string, hash: string) => openOnRemote({ kind: "change", hash }, repositories.get(root))),
68 71
    vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
69 72
    vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
70 73
    vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
…
67 unchanged lines
138 141
  context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
139 142
140 143
  await discover();
144 +
  return buildApi();
145 +
}
146 +
147 +
148 +
// ---------------------------------------------------------------- exported API
149 +
150 +
const remoteProviders: RemoteProvider[] = [];
151 +
const onDidChangeRepositoriesEmitter = new vscode.EventEmitter<void>();
152 +
const onDidChangeStatusEmitter = new vscode.EventEmitter<RepositoryHandle>();
153 +
154 +
function handleOf(repo: Repository): RepositoryHandle {
155 +
  return {
156 +
    root: repo.root,
157 +
    get status() {
158 +
      return repo.status;
159 +
    },
160 +
    get bookmarks() {
161 +
      return repo.bookmarks.map((b) => ({ name: b.name, heads: b.heads, current: b.current }));
162 +
    },
163 +
    get currentBookmark() {
164 +
      return repo.currentBookmark?.name;
165 +
    },
166 +
    remotes: async () => (await repo.oxy.remotes(repo.root)).map((r) => ({ name: r.name, url: r.url })),
167 +
    json: <T>(args: string[]) => repo.oxy.json<T>(repo.root, args),
168 +
    refresh: () => repo.refresh(),
169 +
  };
170 +
}
171 +
172 +
function buildApi(): OxygenAPI {
173 +
  return {
174 +
    version: 1,
175 +
    get repositories() {
176 +
      return [...repositories.values()].map(handleOf);
177 +
    },
178 +
    onDidChangeStatus: (listener) => onDidChangeStatusEmitter.event(listener),
179 +
    onDidChangeRepositories: (listener) => onDidChangeRepositoriesEmitter.event(listener),
180 +
    getRepository: (fsPath) => {
181 +
      const root = rootFor([...repositories.keys()], fsPath);
182 +
      const repo = root ? repositories.get(root) : undefined;
183 +
      return repo ? handleOf(repo) : undefined;
184 +
    },
185 +
    registerRemoteProvider: (provider) => {
186 +
      remoteProviders.push(provider);
187 +
      void vscode.commands.executeCommand("setContext", "oxygen.hasRemoteProvider", true);
188 +
      return {
189 +
        dispose: () => {
190 +
          const i = remoteProviders.indexOf(provider);
191 +
          if (i >= 0) {
192 +
            remoteProviders.splice(i, 1);
193 +
          }
194 +
          void vscode.commands.executeCommand("setContext", "oxygen.hasRemoteProvider", remoteProviders.length > 0);
195 +
        },
196 +
      };
197 +
    },
198 +
    remoteProviders: () => [...remoteProviders],
199 +
  };
200 +
}
201 +
202 +
/** Open the current repository, file or change on whichever provider knows its remote. */
203 +
async function openOnRemote(target?: RemoteTarget, repoArg?: Repository): Promise<void> {
204 +
  const editor = vscode.window.activeTextEditor;
205 +
  const repo = repoArg ?? (editor ? repositoryFor2(editor.document.uri) : undefined) ?? [...repositories.values()][0];
206 +
  if (!repo) {
207 +
    void vscode.window.showInformationMessage("Oxygen: no repository is open.");
208 +
    return;
209 +
  }
210 +
  const remotes = (await repo.oxy.remotes(repo.root)).map((r) => ({ name: r.name, url: r.url }));
211 +
  const pick = pickProvider(remoteProviders, remotes);
212 +
  if (!pick) {
213 +
    void vscode.window.showInformationMessage("Oxygen: no extension knows this repository's remotes. Install a forge extension such as Oxygenhub.");
214 +
    return;
215 +
  }
216 +
  let t: RemoteTarget = target ?? { kind: "repository" };
217 +
  if (!target && editor) {
218 +
    const rel = repo.relativePath(editor.document.uri);
219 +
    if (rel) {
220 +
      t = { kind: "file", reference: repo.currentBookmark?.name ?? "main", path: rel, line: editor.selection.active.line + 1 };
221 +
    }
222 +
  }
223 +
  const url = pick.provider.urlFor(pick.remote.url, t);
224 +
  if (url) {
225 +
    await vscode.env.openExternal(vscode.Uri.parse(url));
226 +
  }
227 +
}
228 +
229 +
function repositoryFor2(uri: vscode.Uri): Repository | undefined {
230 +
  const root = rootFor([...repositories.keys()], uri.fsPath);
231 +
  return root ? repositories.get(root) : undefined;
141 232
}
142 233
143 234
export function deactivate(): void {
…
16 unchanged lines
160 251
    if (!wanted.has(root)) {
161 252
      repo.dispose();
162 253
      repositories.delete(root);
254 +
      onDidChangeRepositoriesEmitter.fire();
163 255
    }
164 256
  }
165 257
  if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) {
…
17 unchanged lines
183 275
        repo.sourceControl.inputBox.placeholder = resolved ? "Describe the resolution (Ctrl+Enter to commit)" : "Message (Ctrl+Enter to commit)";
184 276
      });
185 277
      repositories.set(root, repo);
278 +
      repo.onDidChangeStatus(() => onDidChangeStatusEmitter.fire(handleOf(repo)));
279 +
      onDidChangeRepositoriesEmitter.fire();
186 280
      output.appendLine(`repository ${root}`);
187 281
      await repo.refresh();
188 282
    }
…
842 unchanged lines