OXYGENHUB
# c8f955891224 oxygen-vscode / src / extension.ts
24.4 KiB · last changed Milestone 0.3: bookmark picker (switch, create, merge, delete), fetch, pull, push and push bookmark with results, sync counter and Sync, push confirmation 1 month ago source blame history raw
1
// Activation: find oxy, find repositories, wire the source control view,
2
// decorations, the content provider, and the commands (spec §3).
3
4
import * as vscode from "vscode";
5
import { basename } from "node:path";
6
import { Oxy, OxyError, findBinary } from "./oxy";
7
import { Repository, OxyResourceState } from "./repository";
8
import { OxyContentProvider, SCHEME, baseUri } from "./contentProvider";
9
import { OxyDecorationProvider } from "./decorations";
10
import { OperationItem, OperationsProvider } from "./operations";
11
12
const MIN_VERSION = { format: 2, protocol: 1 };
13
14
let output: vscode.OutputChannel;
15
let oxy: Oxy;
16
let contentProvider: OxyContentProvider;
17
let decorations: OxyDecorationProvider;
18
let operations: OperationsProvider;
19
const repositories = new Map<string, Repository>();
20
21
export async function activate(context: vscode.ExtensionContext): Promise<void> {
22
  output = vscode.window.createOutputChannel("Oxygen");
23
  context.subscriptions.push(output);
24
  output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`);
25
  oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get<string>("path")), (line) => output.appendLine(line));
26
27
  try {
28
    const v = await oxy.version();
29
    output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`);
30
    if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) {
31
      void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`);
32
    }
33
  } catch (e) {
34
    const pick = await vscode.window.showErrorMessage(`Oxygen: could not run oxy (${(e as Error).message}). Set "oxygen.path" or put oxy on your PATH.`, "Open Settings");
35
    if (pick === "Open Settings") {
36
      void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path");
37
    }
38
    return;
39
  }
40
41
  contentProvider = new OxyContentProvider(oxy);
42
  context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider));
43
  decorations = new OxyDecorationProvider(() => [...repositories.values()]);
44
  context.subscriptions.push(decorations);
45
  operations = new OperationsProvider(() => [...repositories.values()]);
46
  context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygen.operations", operations));
47
48
  context.subscriptions.push(
49
    vscode.commands.registerCommand("oxygen.refresh", async () => {
50
      await Promise.all([...repositories.values()].map((r) => r.refresh()));
51
    }),
52
    vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
53
    vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
54
    vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
55
    vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
56
    vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)),
57
    vscode.commands.registerCommand("oxygen.showLog", () => showLog()),
58
    vscode.commands.registerCommand("oxygen.init", () => initRepository()),
59
    vscode.commands.registerCommand("oxygen.discard", (state?: OxyResourceState, states?: OxyResourceState[]) => discard(states?.length ? states : state ? [state] : [])),
60
    vscode.commands.registerCommand("oxygen.commitSelected", (state?: OxyResourceState, states?: OxyResourceState[]) => commitSelected(states?.length ? states : state ? [state] : [])),
61
    vscode.commands.registerCommand("oxygen.commitHunks", (arg?: Repository | vscode.SourceControl) => commitHunks(arg)),
62
    vscode.commands.registerCommand("oxygen.amend", (arg?: Repository | vscode.SourceControl) => amend(arg)),
63
    vscode.commands.registerCommand("oxygen.undo", (arg?: Repository | vscode.SourceControl) => undo(arg, 1)),
64
    vscode.commands.registerCommand("oxygen.undoTo", (item?: OperationItem) => item && undo(item.repository, item.stepsBack)),
65
    vscode.commands.registerCommand("oxygen.refreshOperations", () => operations.refresh()),
66
    vscode.commands.registerCommand("oxygen.switchBookmark", (arg?: Repository | vscode.SourceControl) => switchBookmark(arg)),
67
    vscode.commands.registerCommand("oxygen.createBookmark", (arg?: Repository | vscode.SourceControl) => createBookmark(arg)),
68
    vscode.commands.registerCommand("oxygen.mergeBookmark", (arg?: Repository | vscode.SourceControl) => mergeBookmark(arg)),
69
    vscode.commands.registerCommand("oxygen.deleteBookmark", (arg?: Repository | vscode.SourceControl) => deleteBookmark(arg)),
70
    vscode.commands.registerCommand("oxygen.fetch", (arg?: Repository | vscode.SourceControl) => fetch(arg)),
71
    vscode.commands.registerCommand("oxygen.pull", (arg?: Repository | vscode.SourceControl) => pull(arg)),
72
    vscode.commands.registerCommand("oxygen.push", (arg?: Repository | vscode.SourceControl) => push(arg, false)),
73
    vscode.commands.registerCommand("oxygen.pushBookmark", (arg?: Repository | vscode.SourceControl) => push(arg, true)),
74
    vscode.commands.registerCommand("oxygen.sync", (arg?: Repository | vscode.SourceControl) => sync(arg)),
75
    vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
76
    vscode.workspace.onDidChangeConfiguration((e) => {
77
      if (e.affectsConfiguration("oxygen.path")) {
78
        void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path.");
79
      }
80
      if (e.affectsConfiguration("oxygen")) {
81
        decorations.refresh();
82
        for (const r of repositories.values()) {
83
          r.scheduleRefresh();
84
        }
85
      }
86
    }),
87
  );
88
  context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
89
90
  await discover();
91
}
92
93
export function deactivate(): void {
94
  for (const r of repositories.values()) {
95
    r.dispose();
96
  }
97
  repositories.clear();
98
}
99
100
/** One Repository per distinct oxy root among the workspace folders. */
101
async function discover(): Promise<void> {
102
  const wanted = new Map<string, vscode.WorkspaceFolder>();
103
  for (const folder of vscode.workspace.workspaceFolders ?? []) {
104
    const info = await oxy.root(folder.uri.fsPath);
105
    if (info) {
106
      wanted.set(info.root, folder);
107
    }
108
  }
109
  for (const [root, repo] of repositories) {
110
    if (!wanted.has(root)) {
111
      repo.dispose();
112
      repositories.delete(root);
113
    }
114
  }
115
  if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) {
116
    output.appendLine("no Oxygen repository in the open folders (oxy root found none)");
117
  }
118
  for (const [root, folder] of wanted) {
119
    if (!repositories.has(root)) {
120
      const repo = new Repository(oxy, root, folder);
121
      repo.onDidChangeStatus(() => {
122
        decorations.refresh();
123
        contentProvider.invalidate(root);
124
        operations.refresh();
125
      });
126
      repositories.set(root, repo);
127
      output.appendLine(`repository ${root}`);
128
      await repo.refresh();
129
    }
130
  }
131
  void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0);
132
}
133
134
function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined {
135
  if (arg instanceof Repository) {
136
    return arg;
137
  }
138
  if (arg && "rootUri" in arg && arg.rootUri) {
139
    return repositories.get(arg.rootUri.fsPath);
140
  }
141
  const active = vscode.window.activeTextEditor?.document.uri;
142
  for (const repo of repositories.values()) {
143
    if (active && repo.relativePath(active) !== undefined) {
144
      return repo;
145
    }
146
  }
147
  return repositories.size === 1 ? [...repositories.values()][0] : undefined;
148
}
149
150
async function openChanges(state: OxyResourceState): Promise<void> {
151
  const repo = state.repository;
152
  const title = `${basename(state.path)} (base ↔ working copy)`;
153
  if (state.status === "Added") {
154
    await vscode.commands.executeCommand("vscode.open", state.resourceUri);
155
    return;
156
  }
157
  if (state.status === "Removed") {
158
    await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path));
159
    return;
160
  }
161
  await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title);
162
}
163
164
async function commit(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
165
  const repo = repositoryFor(arg);
166
  if (!repo) {
167
    void vscode.window.showErrorMessage("Oxygen: no repository for the active file.");
168
    return;
169
  }
170
  let message = repo.sourceControl.inputBox.value.trim();
171
  if (!message) {
172
    message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? "";
173
  }
174
  if (!message) {
175
    return;
176
  }
177
  try {
178
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message));
179
    repo.sourceControl.inputBox.value = "";
180
    await repo.refresh();
181
  } catch (e) {
182
    showError(e);
183
  }
184
}
185
186
async function initRepository(): Promise<void> {
187
  const folders = vscode.workspace.workspaceFolders ?? [];
188
  if (folders.length === 0) {
189
    void vscode.window.showErrorMessage("Oxygen: open a folder first.");
190
    return;
191
  }
192
  const folder = folders.length === 1 ? folders[0] : await vscode.window.showWorkspaceFolderPick({ placeHolder: "Folder to initialize as an Oxygen repository" });
193
  if (!folder) {
194
    return;
195
  }
196
  try {
197
    await oxy.check(folder.uri.fsPath, ["init"]);
198
    await discover();
199
    void vscode.window.showInformationMessage(`Oxygen: initialized ${folder.name}.`);
200
  } catch (e) {
201
    showError(e);
202
  }
203
}
204
205
async function discard(states: OxyResourceState[]): Promise<void> {
206
  if (states.length === 0) {
207
    return;
208
  }
209
  const repo = states[0].repository;
210
  const names = states.length === 1 ? basename(states[0].path) : `${states.length} files`;
211
  const pick = await vscode.window.showWarningMessage(`Discard changes in ${names}? This cannot be undone with oxy undo.`, { modal: true }, "Discard");
212
  if (pick !== "Discard") {
213
    return;
214
  }
215
  try {
216
    await repo.oxy.discard(repo.root, states.map((s) => s.path));
217
    await repo.refresh();
218
  } catch (e) {
219
    showError(e);
220
  }
221
}
222
223
async function commitSelected(states: OxyResourceState[]): Promise<void> {
224
  if (states.length === 0) {
225
    return;
226
  }
227
  const repo = states[0].repository;
228
  const message = await askMessage(repo, `Commit ${states.length === 1 ? basename(states[0].path) : `${states.length} files`}`);
229
  if (!message) {
230
    return;
231
  }
232
  try {
233
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message, states.map((s) => s.path)));
234
    repo.sourceControl.inputBox.value = "";
235
    await repo.refresh();
236
  } catch (e) {
237
    showError(e);
238
  }
239
}
240
241
async function commitHunks(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
242
  const repo = repositoryFor(arg);
243
  if (!repo) {
244
    return;
245
  }
246
  let hunks;
247
  try {
248
    hunks = await repo.oxy.hunks(repo.root);
249
  } catch (e) {
250
    showError(e);
251
    return;
252
  }
253
  if (hunks.length === 0) {
254
    void vscode.window.showInformationMessage("Oxygen: nothing to commit.");
255
    return;
256
  }
257
  const picks = await vscode.window.showQuickPick(
258
    hunks.map((h) => ({
259
      label: `$(diff) ${h.path}`,
260
      description: `#${h.index}`,
261
      detail: h.text.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("A ") || l.startsWith("D ") || l.startsWith("R ")).slice(0, 3).join("  ").slice(0, 160),
262
      hunk: h,
263
      picked: false,
264
    })),
265
    { canPickMany: true, placeHolder: "Choose the hunks to commit; anything a chosen hunk needs comes along" },
266
  );
267
  if (!picks || picks.length === 0) {
268
    return;
269
  }
270
  const message = await askMessage(repo, `Commit ${picks.length} hunk${picks.length === 1 ? "" : "s"}`);
271
  if (!message) {
272
    return;
273
  }
274
  try {
275
    await repo.oxy.commitHunks(repo.root, message, picks.map((p) => p.hunk.index));
276
    repo.sourceControl.inputBox.value = "";
277
    await repo.refresh();
278
  } catch (e) {
279
    showError(e);
280
  }
281
}
282
283
async function amend(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
284
  const repo = repositoryFor(arg);
285
  if (!repo) {
286
    return;
287
  }
288
  const message = repo.sourceControl.inputBox.value.trim();
289
  try {
290
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Amending" }, () => repo.oxy.amend(repo.root, message || undefined));
291
    repo.sourceControl.inputBox.value = "";
292
    await repo.refresh();
293
  } catch (e) {
294
    showError(e);
295
  }
296
}
297
298
async function undo(arg: Repository | vscode.SourceControl | undefined, steps: number): Promise<void> {
299
  const repo = repositoryFor(arg);
300
  if (!repo || steps <= 0) {
301
    return;
302
  }
303
  if (steps > 1) {
304
    const pick = await vscode.window.showWarningMessage(`Undo the last ${steps} operations?`, { modal: true }, "Undo");
305
    if (pick !== "Undo") {
306
      return;
307
    }
308
  }
309
  try {
310
    let last = "";
311
    for (let i = 0; i < steps; i++) {
312
      last = await repo.oxy.undo(repo.root);
313
    }
314
    await repo.refresh();
315
    vscode.window.setStatusBarMessage(`$(discard) ${last || "Undone"}`, 4000);
316
  } catch (e) {
317
    showError(e);
318
  }
319
}
320
321
// ---------------------------------------------------------------- bookmarks
322
323
async function switchBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
324
  const repo = repositoryFor(arg);
325
  if (!repo) {
326
    return;
327
  }
328
  let bookmarks;
329
  try {
330
    bookmarks = await repo.oxy.bookmarks(repo.root);
331
  } catch (e) {
332
    showError(e);
333
    return;
334
  }
335
  type Item = vscode.QuickPickItem & { action: "switch" | "create" | "merge" | "delete"; name?: string };
336
  const items: Item[] = bookmarks.map((b) => ({
337
    label: `${b.current ? "$(circle-filled)" : "$(bookmark)"} ${b.name}`,
338
    description: `${b.heads.length} head${b.heads.length === 1 ? "" : "s"}${b.ahead ? ` $(arrow-up)${b.ahead}` : ""}${b.behind ? ` $(arrow-down)${b.behind}` : ""}`,
339
    detail: b.current ? "current bookmark" : undefined,
340
    action: "switch",
341
    name: b.name,
342
  }));
343
  items.push(
344
    { label: "$(add) Create bookmark…", action: "create", alwaysShow: true },
345
    { label: "$(git-merge) Merge a bookmark into the current one…", action: "merge", alwaysShow: true },
346
    { label: "$(trash) Delete a bookmark…", action: "delete", alwaysShow: true },
347
  );
348
  const pick = await vscode.window.showQuickPick(items, { placeHolder: "Switch to a bookmark" });
349
  if (!pick) {
350
    return;
351
  }
352
  switch (pick.action) {
353
    case "create":
354
      return createBookmark(repo);
355
    case "merge":
356
      return mergeBookmark(repo);
357
    case "delete":
358
      return deleteBookmark(repo);
359
    case "switch":
360
      if (pick.name && !bookmarks.find((b) => b.name === pick.name)?.current) {
361
        await doSwitch(repo, pick.name);
362
      }
363
  }
364
}
365
366
async function doSwitch(repo: Repository, name: string): Promise<void> {
367
  try {
368
    await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Switching to ${name}` }, () => repo.oxy.bookmarkSwitch(repo.root, name));
369
  } catch (e) {
370
    if (e instanceof OxyError && e.message.includes("has changes")) {
371
      const pick = await vscode.window.showWarningMessage(`The working copy has uncommitted changes. Switching to ${name} would discard them.`, { modal: true }, "Discard and switch");
372
      if (pick === "Discard and switch") {
373
        try {
374
          await repo.oxy.bookmarkSwitch(repo.root, name, true);
375
        } catch (e2) {
376
          showError(e2);
377
        }
378
      }
379
    } else {
380
      showError(e);
381
    }
382
  }
383
  await repo.refresh();
384
}
385
386
async function createBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
387
  const repo = repositoryFor(arg);
388
  if (!repo) {
389
    return;
390
  }
391
  const name = await vscode.window.showInputBox({
392
    prompt: "New bookmark name (created at the working copy's base)",
393
    validateInput: (v) => (!v.trim() || v.startsWith("-") || v.includes("..") || v.includes("//") || /[\s@{]/.test(v) ? "Not a valid bookmark name" : undefined),
394
  });
395
  if (!name) {
396
    return;
397
  }
398
  try {
399
    await repo.oxy.bookmarkCreate(repo.root, name.trim());
400
    const pick = await vscode.window.showInformationMessage(`Oxygen: created bookmark ${name}.`, "Switch to it");
401
    if (pick) {
402
      await doSwitch(repo, name.trim());
403
    }
404
    await repo.refresh();
405
  } catch (e) {
406
    showError(e);
407
  }
408
}
409
410
async function pickBookmark(repo: Repository, placeHolder: string, excludeCurrent: boolean): Promise<string | undefined> {
411
  const bookmarks = await repo.oxy.bookmarks(repo.root);
412
  const items = bookmarks.filter((b) => !excludeCurrent || !b.current).map((b) => ({ label: b.name, description: `${b.heads.length} head${b.heads.length === 1 ? "" : "s"}` }));
413
  if (items.length === 0) {
414
    void vscode.window.showInformationMessage("Oxygen: no other bookmarks.");
415
    return undefined;
416
  }
417
  return (await vscode.window.showQuickPick(items, { placeHolder }))?.label;
418
}
419
420
async function mergeBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
421
  const repo = repositoryFor(arg);
422
  if (!repo) {
423
    return;
424
  }
425
  try {
426
    const name = await pickBookmark(repo, `Merge into ${repo.currentBookmark?.name ?? "the current bookmark"}`, true);
427
    if (!name) {
428
      return;
429
    }
430
    const out = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Merging ${name}` }, () => repo.oxy.merge(repo.root, name));
431
    await repo.refresh();
432
    if (repo.status?.conflicted) {
433
      void vscode.window.showWarningMessage(`Oxygen: merged ${name}; the result has conflicts to resolve.`);
434
    } else {
435
      vscode.window.setStatusBarMessage(`$(git-merge) ${out.split("\n")[0]}`, 5000);
436
    }
437
  } catch (e) {
438
    showError(e);
439
  }
440
}
441
442
async function deleteBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
443
  const repo = repositoryFor(arg);
444
  if (!repo) {
445
    return;
446
  }
447
  try {
448
    const name = await pickBookmark(repo, "Delete which bookmark? Its changes stay.", true);
449
    if (!name) {
450
      return;
451
    }
452
    const pick = await vscode.window.showWarningMessage(`Delete bookmark ${name}?`, { modal: true }, "Delete");
453
    if (pick !== "Delete") {
454
      return;
455
    }
456
    await repo.oxy.bookmarkDelete(repo.root, name);
457
    await repo.refresh();
458
  } catch (e) {
459
    showError(e);
460
  }
461
}
462
463
// ---------------------------------------------------------------- sync
464
465
async function pickRemote(repo: Repository): Promise<string | undefined> {
466
  const remotes = await repo.oxy.remotes(repo.root);
467
  if (remotes.length === 0) {
468
    void vscode.window.showErrorMessage("Oxygen: this repository has no remotes. Add one with oxy remote add <name> <url>.");
469
    return undefined;
470
  }
471
  if (remotes.length === 1) {
472
    return remotes[0].name;
473
  }
474
  return (await vscode.window.showQuickPick(remotes.map((r) => ({ label: r.name, description: r.url })), { placeHolder: "Remote" }))?.label;
475
}
476
477
async function fetch(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
478
  const repo = repositoryFor(arg);
479
  if (!repo) {
480
    return;
481
  }
482
  try {
483
    const remote = await pickRemote(repo);
484
    if (!remote) {
485
      return;
486
    }
487
    const info = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Fetching from ${remote}` }, () => repo.oxy.fetch(repo.root, remote));
488
    await repo.refresh();
489
    const fresh = info.refs.filter((r) => r.new > 0);
490
    const summary = fresh.length ? fresh.map((r) => `${r.name}: ${r.new} new`).join(", ") : "nothing new";
491
    vscode.window.setStatusBarMessage(`$(cloud-download) ${remote}: ${summary}`, 5000);
492
    if (info.unverified) {
493
      void vscode.window.showWarningMessage(`Oxygen: ${remote} has no trust policy; nothing fetched from it was verified.`);
494
    }
495
  } catch (e) {
496
    showError(e);
497
  }
498
}
499
500
async function pull(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
501
  const repo = repositoryFor(arg);
502
  if (!repo) {
503
    return;
504
  }
505
  try {
506
    const remote = await pickRemote(repo);
507
    if (!remote) {
508
      return;
509
    }
510
    const info = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Pulling from ${remote}` }, () => repo.oxy.pull(repo.root, remote));
511
    await repo.refresh();
512
    if (!info.changed) {
513
      vscode.window.setStatusBarMessage("$(check) Already up to date", 4000);
514
    } else if (info.conflicted) {
515
      void vscode.window.showWarningMessage("Oxygen: pulled; the result has conflicts to resolve.");
516
    } else if (!info.checked_out) {
517
      void vscode.window.showInformationMessage("Oxygen: pulled; the working copy was not updated because it has changes. Check out when ready.");
518
    } else {
519
      vscode.window.setStatusBarMessage(`$(cloud-download) Pulled from ${remote}`, 4000);
520
    }
521
  } catch (e) {
522
    showError(e);
523
  }
524
}
525
526
async function push(arg: Repository | vscode.SourceControl | undefined, chooseBookmark: boolean): Promise<void> {
527
  const repo = repositoryFor(arg);
528
  if (!repo) {
529
    return;
530
  }
531
  try {
532
    const remote = await pickRemote(repo);
533
    if (!remote) {
534
      return;
535
    }
536
    let bookmark: string | undefined;
537
    if (chooseBookmark) {
538
      bookmark = await pickBookmark(repo, "Push which bookmark?", false);
539
      if (!bookmark) {
540
        return;
541
      }
542
    }
543
    if (vscode.workspace.getConfiguration("oxygen").get<boolean>("confirmSync", true)) {
544
      const pick = await vscode.window.showInformationMessage(`Push ${bookmark ?? repo.currentBookmark?.name ?? "the current bookmark"} to ${remote}?`, { modal: false }, "Push", "Always push without asking");
545
      if (!pick) {
546
        return;
547
      }
548
      if (pick === "Always push without asking") {
549
        await vscode.workspace.getConfiguration("oxygen").update("confirmSync", false, vscode.ConfigurationTarget.Global);
550
      }
551
    }
552
    const results = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Pushing to ${remote}` }, () => repo.oxy.push(repo.root, remote, bookmark));
553
    await repo.refresh();
554
    const rejected = results.filter((r) => !r.ok);
555
    if (rejected.length === 0) {
556
      vscode.window.setStatusBarMessage(`$(cloud-upload) Pushed to ${remote}`, 4000);
557
      return;
558
    }
559
    const reason = rejected.map((r) => `${r.name}: ${r.reason}`).join("; ");
560
    const pick = await vscode.window.showErrorMessage(`Oxygen: push rejected. ${reason}`, "Pull", "Show Output");
561
    if (pick === "Pull") {
562
      await pull(repo);
563
    } else if (pick === "Show Output") {
564
      output.show();
565
    }
566
  } catch (e) {
567
    if (e instanceof OxyError && e.message.includes("pull first")) {
568
      const pick = await vscode.window.showWarningMessage(`Oxygen: ${e.message}`, "Pull");
569
      if (pick === "Pull") {
570
        await pull(repo);
571
      }
572
      return;
573
    }
574
    showError(e);
575
  }
576
}
577
578
async function sync(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
579
  const repo = repositoryFor(arg);
580
  if (!repo) {
581
    return;
582
  }
583
  await pull(repo);
584
  if (repo.status?.conflicted) {
585
    return;
586
  }
587
  if ((repo.currentBookmark?.ahead ?? 0) > 0) {
588
    await push(repo, false);
589
  }
590
}
591
592
/** The commit message from the input box, else a prompt. */
593
async function askMessage(repo: Repository, prompt: string): Promise<string | undefined> {
594
  const fromBox = repo.sourceControl.inputBox.value.trim();
595
  if (fromBox) {
596
    return fromBox;
597
  }
598
  return (await vscode.window.showInputBox({ prompt, placeHolder: "Commit message" }))?.trim() || undefined;
599
}
600
601
async function showLog(): Promise<void> {
602
  const repo = repositoryFor(undefined);
603
  if (!repo) {
604
    return;
605
  }
606
  try {
607
    const log = await repo.oxy.log(repo.root, 50);
608
    const items = log.changes
609
      .filter((c) => !c.working_copy)
610
      .map((c) => ({
611
        label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
612
        description: `${c.change_id.slice(0, 8)} · ${c.author}`,
613
        detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`,
614
        change: c,
615
      }));
616
    const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true });
617
    if (pick) {
618
      const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" });
619
      await vscode.window.showTextDocument(doc, { preview: true });
620
    }
621
  } catch (e) {
622
    showError(e);
623
  }
624
}
625
626
function showError(e: unknown): void {
627
  const message = e instanceof OxyError ? e.message : String(e);
628
  void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show());
629
}