bed495a9 Andy McCall 1 month ago ↶
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";
35d56b58 Andy McCall 1 month ago ↶
10
import { OperationItem, OperationsProvider } from "./operations";
bed495a9 Andy McCall 1 month ago ↶
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;
35d56b58 Andy McCall 1 month ago ↶
18
let operations: OperationsProvider;
bed495a9 Andy McCall 1 month ago ↶
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);
810a1c53 Andy McCall 1 month ago ↶
24
output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`);
bed495a9 Andy McCall 1 month ago ↶
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);
35d56b58 Andy McCall 1 month ago ↶
45
operations = new OperationsProvider(() => [...repositories.values()]);
46
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygen.operations", operations));
bed495a9 Andy McCall 1 month ago ↶
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()),
810a1c53 Andy McCall 1 month ago ↶
58
vscode.commands.registerCommand("oxygen.init", () => initRepository()),
35d56b58 Andy McCall 1 month ago ↶
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()),
c8f95589 Andy McCall 1 month ago ↶
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)),
bed495a9 Andy McCall 1 month ago ↶
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
}
810a1c53 Andy McCall 1 month ago ↶
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
}
bed495a9 Andy McCall 1 month ago ↶
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);
35d56b58 Andy McCall 1 month ago ↶
124
operations.refresh();
bed495a9 Andy McCall 1 month ago ↶
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
810a1c53 Andy McCall 1 month ago ↶
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
35d56b58 Andy McCall 1 month ago ↶
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
c8f95589 Andy McCall 1 month ago ↶
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
35d56b58 Andy McCall 1 month ago ↶
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
bed495a9 Andy McCall 1 month ago ↶
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
}