guito 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,38 @@
1
- import { existsSync } from "node:fs";
2
- import { execFile, spawn } from "node:child_process";
3
- import { readFile, rm, writeFile } from "node:fs/promises";
4
- import { randomBytes } from "node:crypto";
5
- import { userInfo } from "node:os";
6
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
7
- import fastify from "fastify";
8
- import fastifyStatic from "@fastify/static";
9
- import fastifyCors from "@fastify/cors";
10
- import fastifyCompress from "@fastify/compress";
11
- import { simpleGit } from "simple-git";
12
- import { createTwoFilesPatch } from "diff";
13
- import { promisify } from "node:util";
14
- import { workingTree } from "./working-tree.js";
15
- import { avatarCache } from "./avatars.js";
16
- import { repositoryState } from "./repository-state.js";
1
+ import { existsSync } from 'node:fs';
2
+ import { execFile, spawn } from 'node:child_process';
3
+ import { readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { userInfo } from 'node:os';
6
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import fastify from 'fastify';
8
+ import fastifyStatic from '@fastify/static';
9
+ import fastifyCors from '@fastify/cors';
10
+ import fastifyCompress from '@fastify/compress';
11
+ import { simpleGit } from 'simple-git';
12
+ import { createTwoFilesPatch } from 'diff';
13
+ import { promisify } from 'node:util';
14
+ import { workingTree } from './working-tree.js';
15
+ import { avatarCache } from './avatars.js';
16
+ import { repositoryState } from './repository-state.js';
17
17
  const execFileAsync = promisify(execFile);
18
- const AZURE_API_VERSION = "5.0-preview";
19
- export const DEFAULT_PR_BRANCH_NAME_TEMPLATE = "pr/${randomstring}";
18
+ const AZURE_API_VERSION = '5.0-preview';
19
+ export const DEFAULT_PR_BRANCH_NAME_TEMPLATE = 'pr/${randomstring}';
20
+ /** Matches the UI's insensitive-search behavior for casing and accents. */
21
+ function normalizeSearchText(value, sensitive) {
22
+ if (sensitive)
23
+ return value;
24
+ return value.normalize('NFD').replace(/\p{M}/gu, '').toLocaleLowerCase();
25
+ }
20
26
  const PR_BRANCH_TEMPLATE_VARIABLES = [
21
- "username",
22
- "randomstring",
23
- "branch",
24
- "targetbranch",
25
- "title",
26
- "repository",
27
- "date",
28
- "time",
29
- "timestamp",
27
+ 'username',
28
+ 'randomstring',
29
+ 'branch',
30
+ 'targetbranch',
31
+ 'title',
32
+ 'repository',
33
+ 'date',
34
+ 'time',
35
+ 'timestamp',
30
36
  ];
31
37
  /** Azure DevOps comment thread statuses, by their numeric REST value. */
32
38
  const AZURE_THREAD_STATUSES = {
@@ -38,7 +44,7 @@ const AZURE_THREAD_STATUSES = {
38
44
  byDesign: 6,
39
45
  pending: 7,
40
46
  };
41
- const azureThreadStatusName = (value) => Object.entries(AZURE_THREAD_STATUSES).find(([, numeric]) => numeric === value)?.[0] ?? "unknown";
47
+ const azureThreadStatusName = (value) => Object.entries(AZURE_THREAD_STATUSES).find(([, numeric]) => numeric === value)?.[0] ?? 'unknown';
42
48
  /** Pull request reviewer votes, by their numeric REST value. */
43
49
  const AZURE_VOTES = {
44
50
  reset: 0,
@@ -53,7 +59,7 @@ const AZURE_VOTES = {
53
59
  * forms, a trailing .git, and URL-encoded segments such as %20.
54
60
  */
55
61
  export function parseAzureRemoteUrl(remoteUrl) {
56
- const value = (remoteUrl ?? "").trim();
62
+ const value = (remoteUrl ?? '').trim();
57
63
  if (!value) {
58
64
  return null;
59
65
  }
@@ -70,19 +76,19 @@ export function parseAzureRemoteUrl(remoteUrl) {
70
76
  }
71
77
  path = scpMatch[1];
72
78
  }
73
- path = path.replace(/\.git\/?$/i, "");
79
+ path = path.replace(/\.git\/?$/i, '');
74
80
  try {
75
81
  path = decodeURIComponent(path);
76
82
  }
77
83
  catch {
78
84
  // Keep the raw path when it contains invalid percent-escapes.
79
85
  }
80
- const gitIndex = path.toLowerCase().lastIndexOf("/_git/");
86
+ const gitIndex = path.toLowerCase().lastIndexOf('/_git/');
81
87
  if (gitIndex < 0) {
82
88
  return null;
83
89
  }
84
- const projectPath = path.slice(0, gitIndex).replace(/^\/+|\/+$/g, "");
85
- const repo = path.slice(gitIndex + "/_git/".length).replace(/^\/+|\/+$/g, "");
90
+ const projectPath = path.slice(0, gitIndex).replace(/^\/+|\/+$/g, '');
91
+ const repo = path.slice(gitIndex + '/_git/'.length).replace(/^\/+|\/+$/g, '');
86
92
  if (!projectPath || !repo) {
87
93
  return null;
88
94
  }
@@ -94,7 +100,7 @@ export function parseAzureRemoteUrl(remoteUrl) {
94
100
  * prefix of the remote path it is not duplicated.
95
101
  */
96
102
  export function buildAzureProjectPrefix(baseUrl, remoteUrl) {
97
- const base = (baseUrl ?? "").trim().replace(/\/+$/, "");
103
+ const base = (baseUrl ?? '').trim().replace(/\/+$/, '');
98
104
  if (!base) {
99
105
  return null;
100
106
  }
@@ -103,25 +109,25 @@ export function buildAzureProjectPrefix(baseUrl, remoteUrl) {
103
109
  return null;
104
110
  }
105
111
  const encodePath = (value) => value
106
- .split("/")
112
+ .split('/')
107
113
  .filter(Boolean)
108
114
  .map((segment) => encodeURIComponent(segment))
109
- .join("/");
115
+ .join('/');
110
116
  try {
111
117
  const parsed = new URL(base);
112
118
  const decode = (path) => path
113
- .split("/")
119
+ .split('/')
114
120
  .filter(Boolean)
115
121
  .map((segment) => decodeURIComponent(segment));
116
122
  const baseParts = decode(parsed.pathname);
117
- const remoteParts = info.projectPath.split("/").filter(Boolean);
123
+ const remoteParts = info.projectPath.split('/').filter(Boolean);
118
124
  let overlap = Math.min(baseParts.length, remoteParts.length);
119
125
  while (overlap &&
120
126
  !baseParts
121
127
  .slice(-overlap)
122
128
  .every((part, index) => part.toLowerCase() === remoteParts[index].toLowerCase()))
123
129
  overlap--;
124
- return `${parsed.origin}/${[...baseParts, ...remoteParts.slice(overlap)].map(encodeURIComponent).join("/")}`;
130
+ return `${parsed.origin}/${[...baseParts, ...remoteParts.slice(overlap)].map(encodeURIComponent).join('/')}`;
125
131
  }
126
132
  catch {
127
133
  return `${base}/${encodePath(info.projectPath)}`;
@@ -145,60 +151,60 @@ export function buildAzurePullRequestUrl(baseUrl, remoteUrl) {
145
151
  * the current Windows session (SSPI single sign-on). Do not also request
146
152
  * --ntlm: some curl builds omit that option while still supporting Negotiate.
147
153
  */
148
- function defaultAzureRequest(method, url, body, contentType = "application/json") {
154
+ function defaultAzureRequest(method, url, body, contentType = 'application/json') {
149
155
  return new Promise((resolveRequest, rejectRequest) => {
150
156
  const args = [
151
- "-sS",
152
- "--negotiate",
153
- "-u",
154
- ":",
155
- "--max-time",
156
- "60",
157
- "-w",
158
- "\n%{http_code}",
159
- "-X",
157
+ '-sS',
158
+ '--negotiate',
159
+ '-u',
160
+ ':',
161
+ '--max-time',
162
+ '60',
163
+ '-w',
164
+ '\n%{http_code}',
165
+ '-X',
160
166
  method,
161
167
  ];
162
- if (method === "POST" || method === "PUT" || method === "PATCH") {
163
- args.push("-H", `Content-Type: ${contentType}`, "--data-binary", "@-");
168
+ if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
169
+ args.push('-H', `Content-Type: ${contentType}`, '--data-binary', '@-');
164
170
  }
165
171
  args.push(url);
166
- const child = spawn("curl.exe", args, { windowsHide: true });
167
- let stdout = "";
168
- let stderr = "";
169
- child.stdout.on("data", (chunk) => {
172
+ const child = spawn('curl.exe', args, { windowsHide: true });
173
+ let stdout = '';
174
+ let stderr = '';
175
+ child.stdout.on('data', (chunk) => {
170
176
  stdout += chunk;
171
177
  });
172
- child.stderr.on("data", (chunk) => {
178
+ child.stderr.on('data', (chunk) => {
173
179
  stderr += chunk;
174
180
  });
175
181
  // EPIPE when curl exits before reading the whole body; the close handler
176
182
  // reports the real failure.
177
- child.stdin.on("error", () => { });
178
- child.on("error", (err) => {
183
+ child.stdin.on('error', () => { });
184
+ child.on('error', (err) => {
179
185
  rejectRequest(new Error(`Failed to run curl.exe (Windows integrated auth requires Windows): ${err.message}`));
180
186
  });
181
- child.on("close", (code) => {
187
+ child.on('close', (code) => {
182
188
  if (code !== 0) {
183
189
  rejectRequest(new Error(stderr.trim() || `curl.exe exited with code ${code}.`));
184
190
  return;
185
191
  }
186
192
  // -w appends "\n<http_code>" after the response body.
187
- const separator = stdout.lastIndexOf("\n");
193
+ const separator = stdout.lastIndexOf('\n');
188
194
  const status = Number.parseInt(stdout.slice(separator + 1), 10);
189
195
  if (separator < 0 || !Number.isFinite(status)) {
190
- rejectRequest(new Error("Unexpected curl.exe output (missing HTTP status)."));
196
+ rejectRequest(new Error('Unexpected curl.exe output (missing HTTP status).'));
191
197
  return;
192
198
  }
193
199
  resolveRequest({ status, body: stdout.slice(0, separator) });
194
200
  });
195
- child.stdin.end(body, "utf8");
201
+ child.stdin.end(body, 'utf8');
196
202
  });
197
203
  }
198
204
  const randomBranchSuffix = () => {
199
- const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
205
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
200
206
  const bytes = randomBytes(6);
201
- let suffix = "";
207
+ let suffix = '';
202
208
  for (const byte of bytes) {
203
209
  suffix += alphabet[byte % alphabet.length];
204
210
  }
@@ -206,16 +212,16 @@ const randomBranchSuffix = () => {
206
212
  };
207
213
  const safeBranchTemplateValue = (value) => value
208
214
  .trim()
209
- .replace(/[\x00-\x20\x7f~^:?*\[\\]+/g, "-")
210
- .replace(/\.{2,}/g, "-")
211
- .replace(/-+/g, "-")
212
- .replace(/^[./-]+|[./-]+$/g, "") || "unknown";
215
+ .replace(/[\x00-\x20\x7f~^:?*\[\\]+/g, '-')
216
+ .replace(/\.{2,}/g, '-')
217
+ .replace(/-+/g, '-')
218
+ .replace(/^[./-]+|[./-]+$/g, '') || 'unknown';
213
219
  /** Expands a configured automatic PR branch name and rejects unknown variables. */
214
220
  export const formatPrBranchName = (template, values) => {
215
221
  const source = template.trim() || DEFAULT_PR_BRANCH_NAME_TEMPLATE;
216
222
  return source.replace(/\$\{([^}]+)\}/g, (_match, variable) => {
217
223
  if (!PR_BRANCH_TEMPLATE_VARIABLES.includes(variable)) {
218
- throw new Error(`Unknown pull request branch variable \${${variable}}. Supported variables: ${PR_BRANCH_TEMPLATE_VARIABLES.map((name) => `\${${name}}`).join(", ")}.`);
224
+ throw new Error(`Unknown pull request branch variable \${${variable}}. Supported variables: ${PR_BRANCH_TEMPLATE_VARIABLES.map((name) => `\${${name}}`).join(', ')}.`);
219
225
  }
220
226
  return safeBranchTemplateValue(values[variable]);
221
227
  });
@@ -223,23 +229,23 @@ export const formatPrBranchName = (template, values) => {
223
229
  /** Extracts Azure DevOps' human-readable error message from a REST response. */
224
230
  const azureErrorMessage = (result) => {
225
231
  try {
226
- return String(JSON.parse(result.body)?.message ?? "");
232
+ return String(JSON.parse(result.body)?.message ?? '');
227
233
  }
228
234
  catch {
229
235
  return result.body;
230
236
  }
231
237
  };
232
- export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, azureDevOpsUrl, prBranchNameTemplate, autoReload, diffViewer, showGraph, showStashes, showTags, showRemoteBranches, issueRegex, issueUrl, allowMerge, fileListView, searchMode, searchCaseSensitive, azureRequestImpl, avatarFetchImpl, onLog, }) {
238
+ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 8080, apiToken, azureDevOpsUrl, prBranchNameTemplate, autoReload, diffViewer, showGraph, showStashes, showTags, showRemoteBranches, issueRegex, issueUrl, allowMerge, fileListView, refListView, sidePanelSectionsExpanded, searchMode, searchCaseSensitive, azureRequestImpl, avatarFetchImpl, onLog, }) {
233
239
  // Initialize server
234
240
  const app = fastify({
235
241
  logger: false,
236
242
  });
237
243
  if (onLog) {
238
244
  // Report error responses to the host (e.g. the VS Code output channel).
239
- app.addHook("onSend", async (request, reply, payload) => {
245
+ app.addHook('onSend', async (request, reply, payload) => {
240
246
  if (reply.statusCode >= 400) {
241
- const body = typeof payload === "string" ? payload.slice(0, 500) : "";
242
- onLog(`${request.method} ${request.url} -> ${reply.statusCode}${body ? ` ${body}` : ""}`);
247
+ const body = typeof payload === 'string' ? payload.slice(0, 500) : '';
248
+ onLog(`${request.method} ${request.url} -> ${reply.statusCode}${body ? ` ${body}` : ''}`);
243
249
  }
244
250
  return payload;
245
251
  });
@@ -249,50 +255,44 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
249
255
  // Compress JSON responses (commit history payloads are highly repetitive)
250
256
  await app.register(fastifyCompress);
251
257
  if (apiToken) {
252
- app.addHook("onRequest", async (request, reply) => {
253
- const pathname = request.url.split("?", 1)[0];
254
- if (!pathname.startsWith("/api/")) {
258
+ app.addHook('onRequest', async (request, reply) => {
259
+ const pathname = request.url.split('?', 1)[0];
260
+ if (!pathname.startsWith('/api/')) {
255
261
  return;
256
262
  }
257
- const headerToken = request.headers["x-guito-token"];
258
- const queryToken = request.query
259
- ?.guitoToken;
260
- const archiveToken = pathname === "/api/archive" ? queryToken : undefined;
263
+ const headerToken = request.headers['x-guito-token'];
264
+ const queryToken = request.query?.guitoToken;
265
+ const archiveToken = pathname === '/api/archive' ? queryToken : undefined;
261
266
  if (headerToken !== apiToken && archiveToken !== apiToken) {
262
- await reply
263
- .status(401)
264
- .type("application/json")
265
- .send({ error: "unauthorized" });
267
+ await reply.status(401).type('application/json').send({ error: 'unauthorized' });
266
268
  }
267
269
  });
268
270
  }
269
271
  // Register static file provider (supports both `ui/` and `ui/browser/` layouts)
270
- const staticRoot = existsSync(join(uiRoot, "browser", "index.html"))
271
- ? join(uiRoot, "browser")
272
+ const staticRoot = existsSync(join(uiRoot, 'browser', 'index.html'))
273
+ ? join(uiRoot, 'browser')
272
274
  : uiRoot;
273
275
  if (existsSync(staticRoot)) {
274
276
  await app.register(fastifyStatic, { root: staticRoot });
275
277
  // Serve UI
276
- app.get("/", (_req, resp) => resp.sendFile("index.html"));
278
+ app.get('/', (_req, resp) => resp.sendFile('index.html'));
277
279
  }
278
280
  else {
279
- app.get("/", (_req, resp) => resp.send("Guito API is running. Build the UI with `npm run build:ui`."));
281
+ app.get('/', (_req, resp) => resp.send('Guito API is running. Build the UI with `npm run build:ui`.'));
280
282
  }
281
283
  // Initialize git lib
282
284
  const git = simpleGit(repositoryPath);
283
285
  const avatars = avatarCache(avatarFetchImpl);
284
- app.get("/api/avatar", async (req, reply) => {
285
- const email = String(req.query?.email ?? "").trim();
286
+ app.get('/api/avatar', async (req, reply) => {
287
+ const email = String(req.query?.email ?? '').trim();
286
288
  if (!email || email.length > 320)
287
- return reply.code(400).send({ error: "Valid email required" });
289
+ return reply.code(400).send({ error: 'Valid email required' });
288
290
  const image = await avatars(email);
289
- reply.header("Cache-Control", "private, max-age=3600");
290
- return image
291
- ? reply.type(image.contentType).send(image.data)
292
- : reply.code(204).send();
291
+ reply.header('Cache-Control', 'private, max-age=3600');
292
+ return image ? reply.type(image.contentType).send(image.data) : reply.code(204).send();
293
293
  });
294
294
  let stateRequest;
295
- app.get("/api/repository-state", async (_req, reply) => {
295
+ app.get('/api/repository-state', async (_req, reply) => {
296
296
  try {
297
297
  stateRequest ?? (stateRequest = repositoryState(git, repositoryPath).finally(() => {
298
298
  stateRequest = undefined;
@@ -306,16 +306,16 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
306
306
  // ==================== Diff parsing ====================
307
307
  function parseUnifiedDiff(rawDiff) {
308
308
  const files = [];
309
- const lines = rawDiff.split("\n");
309
+ const lines = rawDiff.split('\n');
310
310
  let current = null;
311
311
  let inHunk = false;
312
312
  let oldLine = 0;
313
313
  let newLine = 0;
314
314
  const startFile = (line) => {
315
315
  current = {
316
- path: "",
317
- oldPath: "",
318
- status: "modified",
316
+ path: '',
317
+ oldPath: '',
318
+ status: 'modified',
319
319
  lines: [],
320
320
  additions: 0,
321
321
  deletions: 0,
@@ -328,7 +328,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
328
328
  inHunk = false;
329
329
  };
330
330
  for (const line of lines) {
331
- if (line.startsWith("diff --git ")) {
331
+ if (line.startsWith('diff --git ')) {
332
332
  if (current)
333
333
  files.push(current);
334
334
  startFile(line);
@@ -336,46 +336,45 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
336
336
  }
337
337
  if (!current)
338
338
  continue;
339
- if (line.startsWith("@@")) {
339
+ if (line.startsWith('@@')) {
340
340
  inHunk = true;
341
341
  const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
342
342
  if (match) {
343
343
  oldLine = parseInt(match[1], 10);
344
344
  newLine = parseInt(match[2], 10);
345
345
  }
346
- current.lines.push({ type: "hunk", text: line });
346
+ current.lines.push({ type: 'hunk', text: line });
347
347
  continue;
348
348
  }
349
349
  if (!inHunk) {
350
350
  // File meta headers (mode, index, rename, binary...)
351
- if (line.startsWith("new file mode"))
352
- current.status = "added";
353
- else if (line.startsWith("deleted file mode"))
354
- current.status = "deleted";
355
- else if (line.startsWith("rename from "))
356
- current.status = "renamed";
357
- else if (line.startsWith("rename to "))
358
- current.path = line.slice("rename to ".length);
359
- else if (line.startsWith("Binary files") ||
360
- line.startsWith("GIT binary patch"))
361
- current.status = "binary";
351
+ if (line.startsWith('new file mode'))
352
+ current.status = 'added';
353
+ else if (line.startsWith('deleted file mode'))
354
+ current.status = 'deleted';
355
+ else if (line.startsWith('rename from '))
356
+ current.status = 'renamed';
357
+ else if (line.startsWith('rename to '))
358
+ current.path = line.slice('rename to '.length);
359
+ else if (line.startsWith('Binary files') || line.startsWith('GIT binary patch'))
360
+ current.status = 'binary';
362
361
  continue;
363
362
  }
364
- if (line.startsWith("\\")) {
363
+ if (line.startsWith('\\')) {
365
364
  // ""
366
365
  continue;
367
366
  }
368
- if (line.startsWith("+")) {
367
+ if (line.startsWith('+')) {
369
368
  current.lines.push({
370
- type: "add",
369
+ type: 'add',
371
370
  newLine: newLine++,
372
371
  text: line.slice(1),
373
372
  });
374
373
  current.additions++;
375
374
  }
376
- else if (line.startsWith("-")) {
375
+ else if (line.startsWith('-')) {
377
376
  current.lines.push({
378
- type: "del",
377
+ type: 'del',
379
378
  oldLine: oldLine++,
380
379
  text: line.slice(1),
381
380
  });
@@ -383,7 +382,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
383
382
  }
384
383
  else {
385
384
  current.lines.push({
386
- type: "context",
385
+ type: 'context',
387
386
  oldLine: oldLine++,
388
387
  newLine: newLine++,
389
388
  text: line.slice(1),
@@ -402,51 +401,49 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
402
401
  mutationQueue = result.catch(() => { });
403
402
  return result;
404
403
  };
405
- const repoRoot = async () => (await git.revparse(["--show-toplevel"])).trim();
404
+ const repoRoot = async () => (await git.revparse(['--show-toplevel'])).trim();
406
405
  // ==================== Azure DevOps settings ====================
407
406
  // Settings live in the repository's git directory so they are per-repo and
408
407
  // never committed. The VS Code extension passes its own setting via
409
408
  // azureDevOpsUrl, which wins over the file.
410
- let settingsPath = "";
409
+ let settingsPath = '';
411
410
  try {
412
- const gitDir = (await git.revparse(["--absolute-git-dir"])).trim();
413
- settingsPath = join(gitDir, "guito-settings.json");
411
+ const gitDir = (await git.revparse(['--absolute-git-dir'])).trim();
412
+ settingsPath = join(gitDir, 'guito-settings.json');
414
413
  }
415
414
  catch {
416
- settingsPath = join(repositoryPath, ".git", "guito-settings.json");
415
+ settingsPath = join(repositoryPath, '.git', 'guito-settings.json');
417
416
  }
418
417
  const readSettings = async () => {
419
418
  try {
420
- return JSON.parse(await readFile(settingsPath, "utf8"));
419
+ return JSON.parse(await readFile(settingsPath, 'utf8'));
421
420
  }
422
421
  catch {
423
422
  return {};
424
423
  }
425
424
  };
426
425
  const writeSettings = async (settings) => {
427
- await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
426
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
428
427
  };
429
428
  const azureRequest = azureRequestImpl ?? defaultAzureRequest;
430
429
  const effectiveAzureUrl = async () => {
431
- const configuredAzureUrl = azureDevOpsUrl?.trim() ?? "";
430
+ const configuredAzureUrl = azureDevOpsUrl?.trim() ?? '';
432
431
  if (configuredAzureUrl) {
433
- return { url: configuredAzureUrl, source: "vscode" };
432
+ return { url: configuredAzureUrl, source: 'vscode' };
434
433
  }
435
434
  const settings = await readSettings();
436
- const url = typeof settings.azureDevOpsUrl === "string"
437
- ? settings.azureDevOpsUrl.trim()
438
- : "";
439
- return { url, source: url ? "file" : "" };
435
+ const url = typeof settings.azureDevOpsUrl === 'string' ? settings.azureDevOpsUrl.trim() : '';
436
+ return { url, source: url ? 'file' : '' };
440
437
  };
441
438
  /** Resolves the Azure DevOps project prefix or throws a user-facing error. */
442
439
  const azureProjectContext = async () => {
443
440
  const effective = await effectiveAzureUrl();
444
441
  if (!effective.url) {
445
- throw new Error("Azure DevOps URL is not configured. Set it in Settings (gear icon) or the guito.azureDevOpsUrl VS Code setting.");
442
+ throw new Error('Azure DevOps URL is not configured. Set it in Settings (gear icon) or the guito.azureDevOpsUrl VS Code setting.');
446
443
  }
447
- let remoteUrl = "";
444
+ let remoteUrl = '';
448
445
  try {
449
- remoteUrl = (await git.remote(["get-url", "origin"])).trim();
446
+ remoteUrl = (await git.remote(['get-url', 'origin'])).trim();
450
447
  }
451
448
  catch {
452
449
  throw new Error('No "origin" remote is configured for this repository.');
@@ -466,71 +463,60 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
466
463
  const { prefix } = await azureProjectContext();
467
464
  // The prefix is "{collection-or-org}/{project}"; connectionData is
468
465
  // collection scoped.
469
- const collection = prefix.slice(0, prefix.lastIndexOf("/"));
470
- const result = await azureRequest("GET", `${collection}/_apis/connectionData?q=1&api-version=${AZURE_API_VERSION}`, "");
466
+ const collection = prefix.slice(0, prefix.lastIndexOf('/'));
467
+ const result = await azureRequest('GET', `${collection}/_apis/connectionData?q=1&api-version=${AZURE_API_VERSION}`, '');
471
468
  if (result.status < 200 || result.status >= 300) {
472
469
  throw new Error(azureErrorMessage(result) ||
473
470
  `Azure DevOps returned HTTP ${result.status} while resolving the current user.`);
474
471
  }
475
472
  const user = (JSON.parse(result.body) ?? {}).authenticatedUser;
476
- const id = String(user?.id ?? "");
473
+ const id = String(user?.id ?? '');
477
474
  if (!id) {
478
- throw new Error("Azure DevOps did not report an authenticated user.");
475
+ throw new Error('Azure DevOps did not report an authenticated user.');
479
476
  }
480
477
  cachedAzureMe = {
481
478
  id,
482
- name: String(user?.providerDisplayName ?? ""),
483
- email: String(user?.properties?.Mail?.$value ??
484
- user?.properties?.Account?.$value ??
485
- ""),
479
+ name: String(user?.providerDisplayName ?? ''),
480
+ email: String(user?.properties?.Mail?.$value ?? user?.properties?.Account?.$value ?? ''),
486
481
  };
487
482
  return cachedAzureMe;
488
483
  };
489
484
  /** Web UI URL of a pull request, used by the "Open in Azure DevOps" links. */
490
485
  const azurePrWebUrl = (prefix, remoteUrl, id) => {
491
- const repo = parseAzureRemoteUrl(remoteUrl)?.repo ?? "";
486
+ const repo = parseAzureRemoteUrl(remoteUrl)?.repo ?? '';
492
487
  return `${prefix}/_git/${encodeURIComponent(repo)}/pullrequest/${id}`;
493
488
  };
494
489
  /** Normalizes the Azure pull request merge state for the dialog. */
495
490
  const azureMergeStatus = (pr) => {
496
- const raw = String(pr?.mergeStatus ?? "notSet");
497
- return [
498
- "notSet",
499
- "queued",
500
- "conflicts",
501
- "succeeded",
502
- "rejectedByPolicy",
503
- "failure",
504
- ].includes(raw)
491
+ const raw = String(pr?.mergeStatus ?? 'notSet');
492
+ return ['notSet', 'queued', 'conflicts', 'succeeded', 'rejectedByPolicy', 'failure'].includes(raw)
505
493
  ? raw
506
- : "notSet";
494
+ : 'notSet';
507
495
  };
508
496
  /** Maps one Azure DevOps pull request record to the shape the UI consumes. */
509
497
  const mapAzurePullRequest = (pr, prefix, remoteUrl, me) => {
510
498
  const reviewers = (Array.isArray(pr?.reviewers) ? pr.reviewers : []).map((entry) => ({
511
- id: String(entry?.id ?? ""),
512
- name: String(entry?.displayName ?? ""),
513
- email: String(entry?.uniqueName ?? ""),
499
+ id: String(entry?.id ?? ''),
500
+ name: String(entry?.displayName ?? ''),
501
+ email: String(entry?.uniqueName ?? ''),
514
502
  vote: Number(entry?.vote ?? 0),
515
503
  isRequired: entry?.isRequired === true,
516
504
  }));
517
- const mine = me
518
- ? reviewers.find((reviewer) => reviewer.id === me.id)
519
- : undefined;
505
+ const mine = me ? reviewers.find((reviewer) => reviewer.id === me.id) : undefined;
520
506
  return {
521
507
  id: Number(pr?.pullRequestId ?? 0),
522
- title: String(pr?.title ?? ""),
508
+ title: String(pr?.title ?? ''),
523
509
  isDraft: pr?.isDraft === true,
524
510
  author: {
525
- id: String(pr?.createdBy?.id ?? ""),
526
- name: String(pr?.createdBy?.displayName ?? ""),
527
- email: String(pr?.createdBy?.uniqueName ?? ""),
511
+ id: String(pr?.createdBy?.id ?? ''),
512
+ name: String(pr?.createdBy?.displayName ?? ''),
513
+ email: String(pr?.createdBy?.uniqueName ?? ''),
528
514
  },
529
- createdAt: String(pr?.creationDate ?? ""),
530
- sourceBranch: String(pr?.sourceRefName ?? "").replace(/^refs\/heads\//, ""),
531
- targetBranch: String(pr?.targetRefName ?? "").replace(/^refs\/heads\//, ""),
532
- status: String(pr?.status ?? "active"),
533
- webUrl: String(pr?._links?.web?.href || "") ||
515
+ createdAt: String(pr?.creationDate ?? ''),
516
+ sourceBranch: String(pr?.sourceRefName ?? '').replace(/^refs\/heads\//, ''),
517
+ targetBranch: String(pr?.targetRefName ?? '').replace(/^refs\/heads\//, ''),
518
+ status: String(pr?.status ?? 'active'),
519
+ webUrl: String(pr?._links?.web?.href || '') ||
534
520
  azurePrWebUrl(prefix, remoteUrl, Number(pr?.pullRequestId ?? 0)),
535
521
  reviewers,
536
522
  myVote: mine ? Number(mine.vote ?? 0) : 0,
@@ -540,7 +526,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
540
526
  /** REST base of one pull request (".../pullrequests/{id}") plus repo context. */
541
527
  const azurePrApiBase = async (id) => {
542
528
  if (!Number.isInteger(id) || id <= 0) {
543
- throw new Error("A pull request id is required.");
529
+ throw new Error('A pull request id is required.');
544
530
  }
545
531
  const { prefix, remoteUrl } = await azureProjectContext();
546
532
  const repo = parseAzureRemoteUrl(remoteUrl)?.repo;
@@ -555,11 +541,10 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
555
541
  };
556
542
  };
557
543
  /** Runs one Azure request and turns non-2xx replies into user-facing errors. */
558
- const azureJson = async (method, url, body = "", contentType = "application/json") => {
544
+ const azureJson = async (method, url, body = '', contentType = 'application/json') => {
559
545
  const result = await azureRequest(method, url, body, contentType);
560
546
  if (result.status < 200 || result.status >= 300) {
561
- throw new Error(azureErrorMessage(result) ||
562
- `Azure DevOps returned HTTP ${result.status}.`);
547
+ throw new Error(azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`);
563
548
  }
564
549
  return (result.body ? JSON.parse(result.body) : {});
565
550
  };
@@ -567,19 +552,19 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
567
552
  const azurePrArtifactUrl = (projectId, repositoryId, id) => `vstfs:///Git/PullRequestId/${encodeURIComponent(projectId)}%2F${encodeURIComponent(repositoryId)}%2F${id}`;
568
553
  /** Reads one work item's relations from the WIT API. */
569
554
  const azureWorkItemRelations = async (prefix, workItemId) => {
570
- const item = await azureJson("GET", `${prefix}/_apis/wit/workitems/${workItemId}?$expand=relations&api-version=${AZURE_API_VERSION}`);
555
+ const item = await azureJson('GET', `${prefix}/_apis/wit/workitems/${workItemId}?$expand=relations&api-version=${AZURE_API_VERSION}`);
571
556
  return Array.isArray(item?.relations) ? item.relations : [];
572
557
  };
573
558
  /** Patches a work item's relations (the WIT API wants the patch content type). */
574
559
  const azurePatchWorkItemRelations = async (prefix, workItemId, patch) => {
575
- await azureJson("PATCH", `${prefix}/_apis/wit/workitems/${workItemId}?api-version=${AZURE_API_VERSION}`, JSON.stringify(patch), "application/json-patch+json");
560
+ await azureJson('PATCH', `${prefix}/_apis/wit/workitems/${workItemId}?api-version=${AZURE_API_VERSION}`, JSON.stringify(patch), 'application/json-patch+json');
576
561
  };
577
562
  /** Project prefix plus artifact URI of one pull request (work item links). */
578
563
  const azurePrArtifactOf = async (id) => {
579
564
  const { base, prefix } = await azurePrApiBase(id);
580
- const pr = await azureJson("GET", `${base}?api-version=${AZURE_API_VERSION}`);
581
- const projectId = String(pr?.repository?.project?.id ?? "");
582
- const repositoryId = String(pr?.repository?.id ?? "");
565
+ const pr = await azureJson('GET', `${base}?api-version=${AZURE_API_VERSION}`);
566
+ const projectId = String(pr?.repository?.project?.id ?? '');
567
+ const repositoryId = String(pr?.repository?.id ?? '');
583
568
  if (!projectId || !repositoryId) {
584
569
  throw new Error("Azure DevOps did not report the pull request's project and repository.");
585
570
  }
@@ -590,29 +575,21 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
590
575
  };
591
576
  /** Merges the VS Code settings with the server-side settings file. */
592
577
  const effectiveSettings = async () => {
593
- const [file, azure] = await Promise.all([
594
- readSettings(),
595
- effectiveAzureUrl(),
596
- ]);
597
- const fileAutoReload = typeof file.autoReload === "boolean" ? file.autoReload : undefined;
598
- const fileShowGraph = typeof file.showGraph === "boolean" ? file.showGraph : undefined;
599
- const fileShowStashes = typeof file.showStashes === "boolean" ? file.showStashes : undefined;
600
- const fileShowTags = typeof file.showTags === "boolean" ? file.showTags : undefined;
601
- const fileShowRemoteBranches = typeof file.showRemoteBranches === "boolean"
602
- ? file.showRemoteBranches
603
- : undefined;
604
- const fileFileListView = file.fileListView === "tree" || file.fileListView === "flat"
605
- ? file.fileListView
606
- : undefined;
607
- const fileSearchMode = file.searchMode === "filter" || file.searchMode === "navigate"
608
- ? file.searchMode
578
+ const [file, azure] = await Promise.all([readSettings(), effectiveAzureUrl()]);
579
+ const fileAutoReload = typeof file.autoReload === 'boolean' ? file.autoReload : undefined;
580
+ const fileShowGraph = typeof file.showGraph === 'boolean' ? file.showGraph : undefined;
581
+ const fileShowStashes = typeof file.showStashes === 'boolean' ? file.showStashes : undefined;
582
+ const fileShowTags = typeof file.showTags === 'boolean' ? file.showTags : undefined;
583
+ const fileShowRemoteBranches = typeof file.showRemoteBranches === 'boolean' ? file.showRemoteBranches : undefined;
584
+ const fileFileListView = file.fileListView === 'tree' || file.fileListView === 'flat' ? file.fileListView : undefined;
585
+ const fileRefListView = file.refListView === 'tree' || file.refListView === 'flat' ? file.refListView : undefined;
586
+ const fileSidePanelSectionsExpanded = typeof file.sidePanelSectionsExpanded === 'boolean'
587
+ ? file.sidePanelSectionsExpanded
609
588
  : undefined;
610
- const fileAllowMerge = typeof file.allowMerge === "boolean" ? file.allowMerge : undefined;
611
- const fileSearchCaseSensitive = typeof file.searchCaseSensitive === "boolean"
612
- ? file.searchCaseSensitive
613
- : undefined;
614
- const filePrBranchNameTemplate = typeof file.prBranchNameTemplate === "string" &&
615
- file.prBranchNameTemplate.trim()
589
+ const fileSearchMode = file.searchMode === 'filter' || file.searchMode === 'navigate' ? file.searchMode : undefined;
590
+ const fileAllowMerge = typeof file.allowMerge === 'boolean' ? file.allowMerge : undefined;
591
+ const fileSearchCaseSensitive = typeof file.searchCaseSensitive === 'boolean' ? file.searchCaseSensitive : undefined;
592
+ const filePrBranchNameTemplate = typeof file.prBranchNameTemplate === 'string' && file.prBranchNameTemplate.trim()
616
593
  ? file.prBranchNameTemplate.trim()
617
594
  : undefined;
618
595
  const localIssue = file.issueLinking;
@@ -624,8 +601,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
624
601
  useGlobally: true,
625
602
  };
626
603
  }
627
- else if (typeof localIssue?.regex === "string" &&
628
- typeof localIssue.url === "string") {
604
+ else if (typeof localIssue?.regex === 'string' && typeof localIssue.url === 'string') {
629
605
  issueLinking = {
630
606
  regex: localIssue.regex,
631
607
  url: localIssue.url,
@@ -634,12 +610,8 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
634
610
  }
635
611
  else {
636
612
  const [regex, url] = await Promise.all([
637
- git
638
- .raw(["config", "--global", "--get", "guito.issueRegex"])
639
- .catch(() => ""),
640
- git
641
- .raw(["config", "--global", "--get", "guito.issueUrl"])
642
- .catch(() => ""),
613
+ git.raw(['config', '--global', '--get', 'guito.issueRegex']).catch(() => ''),
614
+ git.raw(['config', '--global', '--get', 'guito.issueUrl']).catch(() => ''),
643
615
  ]);
644
616
  if (regex.trim() && url.trim()) {
645
617
  issueLinking = {
@@ -651,67 +623,62 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
651
623
  }
652
624
  return {
653
625
  azureDevOpsUrl: azure.url,
654
- prBranchNameTemplate: prBranchNameTemplate?.trim() ||
655
- filePrBranchNameTemplate ||
656
- DEFAULT_PR_BRANCH_NAME_TEMPLATE,
626
+ prBranchNameTemplate: prBranchNameTemplate?.trim() || filePrBranchNameTemplate || DEFAULT_PR_BRANCH_NAME_TEMPLATE,
657
627
  source: azure.source,
658
- autoReload: typeof autoReload === "boolean" ? autoReload : (fileAutoReload ?? true),
659
- diffViewer: diffViewer === "vscode" ? "vscode" : "guito",
660
- showGraph: typeof showGraph === "boolean" ? showGraph : (fileShowGraph ?? true),
661
- showStashes: typeof showStashes === "boolean"
662
- ? showStashes
663
- : (fileShowStashes ?? false),
664
- showTags: typeof showTags === "boolean" ? showTags : (fileShowTags ?? true),
665
- showRemoteBranches: typeof showRemoteBranches === "boolean"
628
+ autoReload: typeof autoReload === 'boolean' ? autoReload : (fileAutoReload ?? true),
629
+ diffViewer: diffViewer === 'vscode' ? 'vscode' : 'guito',
630
+ showGraph: typeof showGraph === 'boolean' ? showGraph : (fileShowGraph ?? true),
631
+ showStashes: typeof showStashes === 'boolean' ? showStashes : (fileShowStashes ?? false),
632
+ showTags: typeof showTags === 'boolean' ? showTags : (fileShowTags ?? true),
633
+ showRemoteBranches: typeof showRemoteBranches === 'boolean'
666
634
  ? showRemoteBranches
667
635
  : (fileShowRemoteBranches ?? true),
668
- fileListView: fileListView ?? fileFileListView ?? "flat",
669
- searchMode: searchMode ?? fileSearchMode ?? "navigate",
670
- searchCaseSensitive: typeof searchCaseSensitive === "boolean"
636
+ fileListView: fileListView ?? fileFileListView ?? 'flat',
637
+ refListView: refListView ?? fileRefListView ?? 'flat',
638
+ sidePanelSectionsExpanded: typeof sidePanelSectionsExpanded === 'boolean'
639
+ ? sidePanelSectionsExpanded
640
+ : (fileSidePanelSectionsExpanded ?? true),
641
+ searchMode: searchMode ?? fileSearchMode ?? 'navigate',
642
+ searchCaseSensitive: typeof searchCaseSensitive === 'boolean'
671
643
  ? searchCaseSensitive
672
644
  : (fileSearchCaseSensitive ?? false),
673
- allowMerge: typeof allowMerge === "boolean" ? allowMerge : (fileAllowMerge ?? true),
645
+ allowMerge: typeof allowMerge === 'boolean' ? allowMerge : (fileAllowMerge ?? true),
674
646
  issueLinking,
675
647
  };
676
648
  };
677
649
  const configuredIdentity = async () => {
678
650
  const config = await git.listConfig();
679
651
  return {
680
- name: String(config.all["user.name"] ?? "").trim(),
681
- email: String(config.all["user.email"] ?? "").trim(),
652
+ name: String(config.all['user.name'] ?? '').trim(),
653
+ email: String(config.all['user.email'] ?? '').trim(),
682
654
  };
683
655
  };
684
656
  const authorIdentity = (name, email, identity) => ({
685
- author_name: identity.name &&
686
- identity.email &&
687
- email.trim().toLowerCase() === identity.email.toLowerCase()
657
+ author_name: identity.name && identity.email && email.trim().toLowerCase() === identity.email.toLowerCase()
688
658
  ? identity.name
689
659
  : name,
690
660
  author_email: email,
691
661
  });
692
662
  // ==================== Repository ====================
693
- app.get("/api/repo", async (_req, resp) => {
663
+ app.get('/api/repo', async (_req, resp) => {
694
664
  try {
695
- const root = (await git.revparse(["--show-toplevel"])).trim();
696
- return resp.type("application/json").send({
665
+ const root = (await git.revparse(['--show-toplevel'])).trim();
666
+ return resp.type('application/json').send({
697
667
  root,
698
668
  name: basename(root),
699
669
  identity: await configuredIdentity(),
700
670
  });
701
671
  }
702
672
  catch (err) {
703
- return resp
704
- .status(400)
705
- .type("application/json")
706
- .send({ error: err.message });
673
+ return resp.status(400).type('application/json').send({ error: err.message });
707
674
  }
708
675
  });
709
676
  // ==================== Commits ====================
710
- app.get("/api/commits", async (req, resp) => {
677
+ app.get('/api/commits', async (req, resp) => {
711
678
  try {
712
679
  const query = (req.query ?? {});
713
- const limit = Number.parseInt(query.limit ?? "", 10);
714
- const skip = Number.parseInt(query.skip ?? "", 10);
680
+ const limit = Number.parseInt(query.limit ?? '', 10);
681
+ const skip = Number.parseInt(query.skip ?? '', 10);
715
682
  // Build simple-git options conditionally: an explicit key with an
716
683
  // undefined value would still be emitted as a bare command flag.
717
684
  // The commit body is intentionally omitted: it roughly doubles the
@@ -719,329 +686,270 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
719
686
  // on demand by /api/commit/detail instead.
720
687
  const options = {
721
688
  format: {
722
- hash: "%H",
723
- date: "%aI",
724
- message: "%s",
725
- refs: "%D",
726
- author_name: "%aN",
727
- author_email: "%aE",
728
- parents: "%P",
689
+ hash: '%H',
690
+ date: '%aI',
691
+ message: '%s',
692
+ refs: '%D',
693
+ author_name: '%aN',
694
+ author_email: '%aE',
695
+ parents: '%P',
729
696
  },
730
- "--all": null,
697
+ '--all': null,
731
698
  };
732
699
  if (Number.isFinite(limit) && limit > 0) {
733
700
  options.maxCount = limit;
734
701
  }
735
702
  if (Number.isFinite(skip) && skip > 0) {
736
- options["--skip"] = String(skip);
703
+ options['--skip'] = String(skip);
737
704
  }
738
705
  const [log, count, identity] = await Promise.all([
739
706
  git.log(options),
740
- git.raw(["rev-list", "--count", "--all"]),
707
+ git.raw(['rev-list', '--count', '--all']),
741
708
  configuredIdentity(),
742
709
  ]);
743
710
  const commits = log.all.map((commit) => ({
744
711
  ...commit,
745
712
  ...authorIdentity(commit.author_name, commit.author_email, identity),
746
- parents: String(commit.parents ?? "")
747
- .split(" ")
713
+ parents: String(commit.parents ?? '')
714
+ .split(' ')
748
715
  .filter(Boolean),
749
716
  }));
750
717
  return resp
751
- .type("application/json")
718
+ .type('application/json')
752
719
  .send({ commits, total: Number.parseInt(count, 10) || 0 });
753
720
  }
754
721
  catch (err) {
755
722
  // A repository without any commits has no history to list.
756
- if (/does not have any commits yet|unknown revision|bad default revision/i.test(err.message ?? "")) {
757
- return resp.type("application/json").send({ commits: [], total: 0 });
723
+ if (/does not have any commits yet|unknown revision|bad default revision/i.test(err.message ?? '')) {
724
+ return resp.type('application/json').send({ commits: [], total: 0 });
758
725
  }
759
- return resp
760
- .status(400)
761
- .type("application/json")
762
- .send({ error: err.message });
726
+ return resp.status(400).type('application/json').send({ error: err.message });
763
727
  }
764
728
  });
765
729
  // Full-text commit search. The list payload omits commit bodies, so body
766
- // matching runs here (git --grep covers subject and body together) and the
730
+ // matching runs here (the complete commit message is included) and the
767
731
  // client unions the resulting hashes with its local subject/author/hash
768
732
  // matches over the loaded history.
769
- app.get("/api/commits/search", async (req, resp) => {
770
- const query = String((req.query ?? {}).query ?? "").trim();
771
- const caseSensitive = String((req.query ?? {}).caseSensitive ?? "") === "1";
733
+ app.get('/api/commits/search', async (req, resp) => {
734
+ const query = String((req.query ?? {}).query ?? '').trim();
735
+ const caseSensitive = String((req.query ?? {}).caseSensitive ?? '') === '1';
772
736
  if (!query) {
773
- return resp.type("application/json").send({ hashes: [], indices: {} });
737
+ return resp.type('application/json').send({ hashes: [], indices: {} });
774
738
  }
775
739
  try {
776
- const grepArgs = ["log", "--all"];
777
- if (!caseSensitive)
778
- grepArgs.push("-i");
779
- grepArgs.push("--fixed-strings", `--grep=${query}`, "--format=%H");
780
- const [raw, history] = await Promise.all([
781
- git.raw(grepArgs),
782
- git.raw(["log", "--all", "--format=%H"]),
783
- ]);
784
- const hashes = raw
785
- .split("\n")
786
- .map((hash) => hash.trim())
787
- .filter(Boolean);
788
- const matches = new Set(hashes);
789
- // Use the same unfiltered Git log order as the paged history endpoint.
740
+ // NUL separators are safe because Git commit messages cannot contain
741
+ // NUL bytes. Searching in JS lets insensitive mode fold accents too,
742
+ // which Git's --regexp-ignore-case does not do consistently.
743
+ const raw = await git.raw(['log', '--all', '--format=%H%x00%B%x00']);
744
+ const fields = raw.split('\0');
745
+ const normalizedQuery = normalizeSearchText(query, caseSensitive);
746
+ const hashes = [];
790
747
  const indices = {};
791
- history
792
- .split("\n")
793
- .map((hash) => hash.trim())
794
- .filter(Boolean)
795
- .forEach((hash, index) => {
796
- if (matches.has(hash))
797
- indices[hash] = index;
798
- });
799
- return resp.type("application/json").send({
748
+ let historyIndex = 0;
749
+ for (let index = 0; index + 1 < fields.length; index += 2) {
750
+ const hash = fields[index].trim();
751
+ const message = fields[index + 1];
752
+ if (!hash)
753
+ continue;
754
+ if (normalizeSearchText(message, caseSensitive).includes(normalizedQuery)) {
755
+ hashes.push(hash);
756
+ indices[hash] = historyIndex;
757
+ }
758
+ historyIndex += 1;
759
+ }
760
+ return resp.type('application/json').send({
800
761
  hashes,
801
762
  indices,
802
763
  });
803
764
  }
804
765
  catch (err) {
805
- if (/does not have any commits yet|unknown revision|bad default revision/i.test(err.message ?? "")) {
806
- return resp.type("application/json").send({ hashes: [], indices: {} });
766
+ if (/does not have any commits yet|unknown revision|bad default revision/i.test(err.message ?? '')) {
767
+ return resp.type('application/json').send({ hashes: [], indices: {} });
807
768
  }
808
- return resp
809
- .status(400)
810
- .type("application/json")
811
- .send({ error: err.message });
769
+ return resp.status(400).type('application/json').send({ error: err.message });
812
770
  }
813
771
  });
814
- app.post("/api/commit", async (req, resp) => {
772
+ app.post('/api/commit', async (req, resp) => {
815
773
  try {
816
774
  const { message, description } = req.body ?? {};
817
775
  await mutate(() => working.commit(message, description));
818
- return resp.type("application/json").send({ success: true });
776
+ return resp.type('application/json').send({ success: true });
819
777
  }
820
778
  catch (err) {
821
- return resp
822
- .status(400)
823
- .type("application/json")
824
- .send({ error: err.message });
779
+ return resp.status(400).type('application/json').send({ error: err.message });
825
780
  }
826
781
  });
827
- app.post("/api/amend", async (req, resp) => {
782
+ app.post('/api/amend', async (req, resp) => {
828
783
  try {
829
784
  const { message, description } = req.body;
830
785
  await git.commit([message, ...(description ? [description] : [])], {
831
- "--amend": null,
832
- "--no-edit": null,
786
+ '--amend': null,
787
+ '--no-edit': null,
833
788
  });
834
- return resp.type("application/json").send({ success: true });
789
+ return resp.type('application/json').send({ success: true });
835
790
  }
836
791
  catch (err) {
837
- return resp
838
- .status(400)
839
- .type("application/json")
840
- .send({ error: err.message });
792
+ return resp.status(400).type('application/json').send({ error: err.message });
841
793
  }
842
794
  });
843
- app.post("/api/revert", async (req, resp) => {
795
+ app.post('/api/revert', async (req, resp) => {
844
796
  try {
845
797
  const { commit } = req.body;
846
798
  await git.revert(commit);
847
- return resp.type("application/json").send({ success: true });
799
+ return resp.type('application/json').send({ success: true });
848
800
  }
849
801
  catch (err) {
850
- return resp
851
- .status(400)
852
- .type("application/json")
853
- .send({ error: err.message });
802
+ return resp.status(400).type('application/json').send({ error: err.message });
854
803
  }
855
804
  });
856
- app.post("/api/cherry-pick", async (req, resp) => {
805
+ app.post('/api/cherry-pick', async (req, resp) => {
857
806
  try {
858
- await git.raw(["cherry-pick", req.body.commit]);
859
- return resp.type("application/json").send({ success: true });
807
+ await git.raw(['cherry-pick', req.body.commit]);
808
+ return resp.type('application/json').send({ success: true });
860
809
  }
861
810
  catch (err) {
862
- return resp
863
- .status(400)
864
- .type("application/json")
865
- .send({ error: err.message });
811
+ return resp.status(400).type('application/json').send({ error: err.message });
866
812
  }
867
813
  });
868
- app.post("/api/commit/drop", async (req, resp) => {
814
+ app.post('/api/commit/drop', async (req, resp) => {
869
815
  try {
870
- await git.raw(["reset", "--hard", `${req.body.commit}^`]);
871
- return resp.type("application/json").send({ success: true });
816
+ await git.raw(['reset', '--hard', `${req.body.commit}^`]);
817
+ return resp.type('application/json').send({ success: true });
872
818
  }
873
819
  catch (err) {
874
- return resp
875
- .status(400)
876
- .type("application/json")
877
- .send({ error: err.message });
820
+ return resp.status(400).type('application/json').send({ error: err.message });
878
821
  }
879
822
  });
880
- app.post("/api/reset-commit", async (req, resp) => {
823
+ app.post('/api/reset-commit', async (req, resp) => {
881
824
  try {
882
- const mode = ["soft", "mixed", "hard"].includes(req.body?.mode)
883
- ? req.body.mode
884
- : "hard";
885
- await git.raw(["reset", `--${mode}`, req.body.commit]);
886
- return resp.type("application/json").send({ success: true });
825
+ const mode = ['soft', 'mixed', 'hard'].includes(req.body?.mode) ? req.body.mode : 'hard';
826
+ await git.raw(['reset', `--${mode}`, req.body.commit]);
827
+ return resp.type('application/json').send({ success: true });
887
828
  }
888
829
  catch (err) {
889
- return resp
890
- .status(400)
891
- .type("application/json")
892
- .send({ error: err.message });
830
+ return resp.status(400).type('application/json').send({ error: err.message });
893
831
  }
894
832
  });
895
833
  // ==================== Staging ====================
896
- app.get("/api/status", async (_req, resp) => {
834
+ app.get('/api/status', async (_req, resp) => {
897
835
  try {
898
836
  const status = await git.status();
899
- return resp.type("application/json").send(status);
837
+ return resp.type('application/json').send(status);
900
838
  }
901
839
  catch (err) {
902
- return resp
903
- .status(400)
904
- .type("application/json")
905
- .send({ error: err.message });
840
+ return resp.status(400).type('application/json').send({ error: err.message });
906
841
  }
907
842
  });
908
- app.post("/api/stage", async (req, resp) => {
843
+ app.post('/api/stage', async (req, resp) => {
909
844
  try {
910
845
  await mutate(() => working.stage(req.body?.files));
911
- return resp.type("application/json").send({ success: true });
846
+ return resp.type('application/json').send({ success: true });
912
847
  }
913
848
  catch (err) {
914
- return resp
915
- .status(400)
916
- .type("application/json")
917
- .send({ error: err.message });
849
+ return resp.status(400).type('application/json').send({ error: err.message });
918
850
  }
919
851
  });
920
- app.post("/api/unstage", async (req, resp) => {
852
+ app.post('/api/unstage', async (req, resp) => {
921
853
  try {
922
854
  await mutate(() => working.unstage(req.body?.files));
923
- return resp.type("application/json").send({ success: true });
855
+ return resp.type('application/json').send({ success: true });
924
856
  }
925
857
  catch (err) {
926
- return resp
927
- .status(400)
928
- .type("application/json")
929
- .send({ error: err.message });
858
+ return resp.status(400).type('application/json').send({ error: err.message });
930
859
  }
931
860
  });
932
- app.post("/api/stage-lines", async (req, resp) => {
861
+ app.post('/api/stage-lines', async (req, resp) => {
933
862
  try {
934
863
  const { file, patch } = req.body;
935
864
  // Stage specific lines using git add -p equivalent (patch mode)
936
- await git.raw("add --patch", { input: patch });
937
- return resp.type("application/json").send({ success: true });
865
+ await git.raw('add --patch', { input: patch });
866
+ return resp.type('application/json').send({ success: true });
938
867
  }
939
868
  catch (err) {
940
- return resp
941
- .status(400)
942
- .type("application/json")
943
- .send({ error: err.message });
869
+ return resp.status(400).type('application/json').send({ error: err.message });
944
870
  }
945
871
  });
946
872
  // ==================== Branches ====================
947
- app.get("/api/branches/all", async (_req, resp) => {
873
+ app.get('/api/branches/all', async (_req, resp) => {
948
874
  try {
949
875
  const raw = await git.raw([
950
- "for-each-ref",
951
- "--format=%(refname)%09%(objectname)%09%(HEAD)",
952
- "refs/heads",
953
- "refs/remotes",
876
+ 'for-each-ref',
877
+ '--format=%(refname)%09%(objectname)%09%(HEAD)',
878
+ 'refs/heads',
879
+ 'refs/remotes',
954
880
  ]);
955
881
  const branches = raw
956
- .split("\n")
882
+ .split('\n')
957
883
  .map((line) => line.trim())
958
884
  .filter(Boolean)
959
885
  .map((line) => {
960
- const [refname, commit, headMarker] = line.split("\t");
961
- const remote = refname.startsWith("refs/remotes/");
962
- const name = refname.replace(/^refs\/(heads|remotes)\//, "");
963
- return { name, commit, current: headMarker === "*", remote };
886
+ const [refname, commit, headMarker] = line.split('\t');
887
+ const remote = refname.startsWith('refs/remotes/');
888
+ const name = refname.replace(/^refs\/(heads|remotes)\//, '');
889
+ return { name, commit, current: headMarker === '*', remote };
964
890
  });
965
- return resp.type("application/json").send(branches);
891
+ return resp.type('application/json').send(branches);
966
892
  }
967
893
  catch (err) {
968
- return resp
969
- .status(400)
970
- .type("application/json")
971
- .send({ error: err.message });
894
+ return resp.status(400).type('application/json').send({ error: err.message });
972
895
  }
973
896
  });
974
- app.get("/api/branches", async (_req, resp) => {
897
+ app.get('/api/branches', async (_req, resp) => {
975
898
  try {
976
899
  const branchSummary = await git.branchLocal();
977
- return resp.type("application/json").send(branchSummary);
900
+ return resp.type('application/json').send(branchSummary);
978
901
  }
979
902
  catch (err) {
980
- return resp
981
- .status(400)
982
- .type("application/json")
983
- .send({ error: err.message });
903
+ return resp.status(400).type('application/json').send({ error: err.message });
984
904
  }
985
905
  });
986
- app.post("/api/branch/create", async (req, resp) => {
906
+ app.post('/api/branch/create', async (req, resp) => {
987
907
  try {
988
908
  const { name, startPoint } = req.body;
989
909
  await git.branch([...(startPoint ? [name, startPoint] : [name])]);
990
- return resp.type("application/json").send({ success: true });
910
+ return resp.type('application/json').send({ success: true });
991
911
  }
992
912
  catch (err) {
993
- return resp
994
- .status(400)
995
- .type("application/json")
996
- .send({ error: err.message });
913
+ return resp.status(400).type('application/json').send({ error: err.message });
997
914
  }
998
915
  });
999
- app.post("/api/branch/delete", async (req, resp) => {
916
+ app.post('/api/branch/delete', async (req, resp) => {
1000
917
  try {
1001
918
  const { name, force } = req.body;
1002
919
  await git.deleteLocalBranch(name, force);
1003
- return resp.type("application/json").send({ success: true });
920
+ return resp.type('application/json').send({ success: true });
1004
921
  }
1005
922
  catch (err) {
1006
- return resp
1007
- .status(400)
1008
- .type("application/json")
1009
- .send({ error: err.message });
923
+ return resp.status(400).type('application/json').send({ error: err.message });
1010
924
  }
1011
925
  });
1012
- app.post("/api/branch/delete-remote", async (req, resp) => {
926
+ app.post('/api/branch/delete-remote', async (req, resp) => {
1013
927
  try {
1014
928
  const { remote, branch } = req.body;
1015
- await git.push([remote, "--delete", branch]);
1016
- return resp.type("application/json").send({ success: true });
929
+ await git.push([remote, '--delete', branch]);
930
+ return resp.type('application/json').send({ success: true });
1017
931
  }
1018
932
  catch (err) {
1019
- return resp
1020
- .status(400)
1021
- .type("application/json")
1022
- .send({ error: err.message });
933
+ return resp.status(400).type('application/json').send({ error: err.message });
1023
934
  }
1024
935
  });
1025
- app.post("/api/branch/rename", async (req, resp) => {
936
+ app.post('/api/branch/rename', async (req, resp) => {
1026
937
  try {
1027
938
  const { oldName, newName } = req.body;
1028
- await git.branch(["-m", oldName, newName]);
1029
- return resp.type("application/json").send({ success: true });
939
+ await git.branch(['-m', oldName, newName]);
940
+ return resp.type('application/json').send({ success: true });
1030
941
  }
1031
942
  catch (err) {
1032
- return resp
1033
- .status(400)
1034
- .type("application/json")
1035
- .send({ error: err.message });
943
+ return resp.status(400).type('application/json').send({ error: err.message });
1036
944
  }
1037
945
  });
1038
946
  // ==================== Worktrees ====================
1039
- app.get("/api/worktrees", async (_req, resp) => {
947
+ app.get('/api/worktrees', async (_req, resp) => {
1040
948
  try {
1041
- const raw = await git.raw(["worktree", "list", "--porcelain"]);
949
+ const raw = await git.raw(['worktree', 'list', '--porcelain']);
1042
950
  // Path comparison is case-insensitive on Windows so the served worktree
1043
951
  // is detected regardless of drive-letter casing.
1044
- const samePath = (a, b) => process.platform === "win32"
952
+ const samePath = (a, b) => process.platform === 'win32'
1045
953
  ? resolve(a).toLowerCase() === resolve(b).toLowerCase()
1046
954
  : resolve(a) === resolve(b);
1047
955
  const worktrees = raw
@@ -1050,201 +958,159 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1050
958
  .filter(Boolean)
1051
959
  .map((block) => {
1052
960
  const entry = {
1053
- path: "",
1054
- head: "",
1055
- branch: "",
961
+ path: '',
962
+ head: '',
963
+ branch: '',
1056
964
  bare: false,
1057
965
  detached: false,
1058
966
  current: false,
1059
967
  };
1060
- for (const line of block.split("\n").map((l) => l.trim())) {
1061
- if (line.startsWith("worktree "))
1062
- entry.path = line.slice("worktree ".length);
1063
- else if (line.startsWith("HEAD "))
1064
- entry.head = line.slice("HEAD ".length);
1065
- else if (line.startsWith("branch "))
1066
- entry.branch = line
1067
- .slice("branch ".length)
1068
- .replace(/^refs\/heads\//, "");
1069
- else if (line === "bare")
968
+ for (const line of block.split('\n').map((l) => l.trim())) {
969
+ if (line.startsWith('worktree '))
970
+ entry.path = line.slice('worktree '.length);
971
+ else if (line.startsWith('HEAD '))
972
+ entry.head = line.slice('HEAD '.length);
973
+ else if (line.startsWith('branch '))
974
+ entry.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
975
+ else if (line === 'bare')
1070
976
  entry.bare = true;
1071
- else if (line === "detached")
977
+ else if (line === 'detached')
1072
978
  entry.detached = true;
1073
979
  }
1074
980
  entry.current = !!entry.path && samePath(entry.path, repositoryPath);
1075
981
  return entry;
1076
982
  });
1077
- return resp.type("application/json").send(worktrees);
983
+ return resp.type('application/json').send(worktrees);
1078
984
  }
1079
985
  catch (err) {
1080
- return resp
1081
- .status(400)
1082
- .type("application/json")
1083
- .send({ error: err.message });
986
+ return resp.status(400).type('application/json').send({ error: err.message });
1084
987
  }
1085
988
  });
1086
- app.post("/api/worktrees", async (req, resp) => {
989
+ app.post('/api/worktrees', async (req, resp) => {
1087
990
  try {
1088
- const path = typeof req.body?.path === "string" ? req.body.path.trim() : "";
1089
- const branch = typeof req.body?.branch === "string" ? req.body.branch.trim() : "";
991
+ const path = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
992
+ const branch = typeof req.body?.branch === 'string' ? req.body.branch.trim() : '';
1090
993
  if (!path || !isAbsolute(path)) {
1091
- throw new Error("Choose an absolute folder path for the worktree.");
994
+ throw new Error('Choose an absolute folder path for the worktree.');
1092
995
  }
1093
996
  if (!branch) {
1094
- throw new Error("Choose a local branch for the worktree.");
997
+ throw new Error('Choose a local branch for the worktree.');
1095
998
  }
1096
999
  // Resolve the branch as a local ref before passing it to Git. Besides a
1097
1000
  // clearer error, this prevents a request from treating an option as a ref.
1098
- await git.raw([
1099
- "show-ref",
1100
- "--verify",
1101
- "--quiet",
1102
- `refs/heads/${branch}`,
1103
- ]);
1104
- await mutate(() => git
1105
- .raw(["worktree", "add", "--", resolve(path), branch])
1106
- .then(() => { }));
1107
- return resp.type("application/json").send({ success: true });
1001
+ await git.raw(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
1002
+ await mutate(() => git.raw(['worktree', 'add', '--', resolve(path), branch]).then(() => { }));
1003
+ return resp.type('application/json').send({ success: true });
1108
1004
  }
1109
1005
  catch (err) {
1110
- return resp
1111
- .status(400)
1112
- .type("application/json")
1113
- .send({ error: err.message });
1006
+ return resp.status(400).type('application/json').send({ error: err.message });
1114
1007
  }
1115
1008
  });
1116
- app.post("/api/worktrees/remove", async (req, resp) => {
1009
+ app.post('/api/worktrees/remove', async (req, resp) => {
1117
1010
  try {
1118
- const path = typeof req.body?.path === "string" ? req.body.path.trim() : "";
1011
+ const path = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
1119
1012
  if (!path || !isAbsolute(path)) {
1120
- throw new Error("A valid worktree path is required.");
1013
+ throw new Error('A valid worktree path is required.');
1121
1014
  }
1122
1015
  const target = resolve(path);
1123
- const samePath = (a, b) => process.platform === "win32"
1016
+ const samePath = (a, b) => process.platform === 'win32'
1124
1017
  ? resolve(a).toLowerCase() === resolve(b).toLowerCase()
1125
1018
  : resolve(a) === resolve(b);
1126
1019
  if (samePath(target, repositoryPath)) {
1127
- throw new Error("The worktree currently open in Guito cannot be removed.");
1020
+ throw new Error('The worktree currently open in Guito cannot be removed.');
1128
1021
  }
1129
1022
  // Only registered worktrees may be removed. Do not use --force: Git must
1130
1023
  // protect uncommitted or untracked files in the selected worktree.
1131
- const registered = (await git.raw(["worktree", "list", "--porcelain"]))
1024
+ const registered = (await git.raw(['worktree', 'list', '--porcelain']))
1132
1025
  .split(/\n\s*\n/)
1133
1026
  .some((block) => {
1134
- const line = block
1135
- .split("\n")
1136
- .find((entry) => entry.startsWith("worktree "));
1137
- return (!!line && samePath(line.slice("worktree ".length).trim(), target));
1027
+ const line = block.split('\n').find((entry) => entry.startsWith('worktree '));
1028
+ return !!line && samePath(line.slice('worktree '.length).trim(), target);
1138
1029
  });
1139
1030
  if (!registered) {
1140
- throw new Error("The selected folder is not a registered worktree.");
1031
+ throw new Error('The selected folder is not a registered worktree.');
1141
1032
  }
1142
- await mutate(() => git.raw(["worktree", "remove", "--", target]).then(() => { }));
1143
- return resp.type("application/json").send({ success: true });
1033
+ await mutate(() => git.raw(['worktree', 'remove', '--', target]).then(() => { }));
1034
+ return resp.type('application/json').send({ success: true });
1144
1035
  }
1145
1036
  catch (err) {
1146
- return resp
1147
- .status(400)
1148
- .type("application/json")
1149
- .send({ error: err.message });
1037
+ return resp.status(400).type('application/json').send({ error: err.message });
1150
1038
  }
1151
1039
  });
1152
- app.post("/api/checkout", async (req, resp) => {
1040
+ app.post('/api/checkout', async (req, resp) => {
1153
1041
  try {
1154
1042
  const { ref } = req.body;
1155
1043
  await git.checkout(ref);
1156
- return resp.type("application/json").send({ success: true });
1044
+ return resp.type('application/json').send({ success: true });
1157
1045
  }
1158
1046
  catch (err) {
1159
- return resp
1160
- .status(400)
1161
- .type("application/json")
1162
- .send({ error: err.message });
1047
+ return resp.status(400).type('application/json').send({ error: err.message });
1163
1048
  }
1164
1049
  });
1165
1050
  // ==================== Merge & Rebase ====================
1166
- app.post("/api/merge", async (req, resp) => {
1051
+ app.post('/api/merge', async (req, resp) => {
1167
1052
  try {
1168
1053
  const { branch } = req.body;
1169
1054
  await git.merge([branch]);
1170
- return resp.type("application/json").send({ success: true });
1055
+ return resp.type('application/json').send({ success: true });
1171
1056
  }
1172
1057
  catch (err) {
1173
- return resp
1174
- .status(400)
1175
- .type("application/json")
1176
- .send({ error: err.message });
1058
+ return resp.status(400).type('application/json').send({ error: err.message });
1177
1059
  }
1178
1060
  });
1179
- app.get("/api/archive", async (req, resp) => {
1061
+ app.get('/api/archive', async (req, resp) => {
1180
1062
  try {
1181
- const ref = String(req.query?.ref ?? "HEAD");
1063
+ const ref = String(req.query?.ref ?? 'HEAD');
1182
1064
  if (!/^[0-9a-fA-F]{7,40}$/.test(ref) && !/^[A-Za-z0-9._/-]+$/.test(ref)) {
1183
- return resp
1184
- .status(400)
1185
- .type("application/json")
1186
- .send({ error: "invalid ref" });
1065
+ return resp.status(400).type('application/json').send({ error: 'invalid ref' });
1187
1066
  }
1188
- const result = await execFileAsync("git", ["archive", "--format=zip", ref], {
1067
+ const result = await execFileAsync('git', ['archive', '--format=zip', ref], {
1189
1068
  cwd: repositoryPath,
1190
- encoding: "buffer",
1069
+ encoding: 'buffer',
1191
1070
  maxBuffer: 100 * 1024 * 1024,
1192
1071
  });
1193
1072
  return resp
1194
- .header("Content-Disposition", `attachment; filename="guito-${ref.slice(0, 8)}.zip"`)
1195
- .type("application/zip")
1073
+ .header('Content-Disposition', `attachment; filename="guito-${ref.slice(0, 8)}.zip"`)
1074
+ .type('application/zip')
1196
1075
  .send(result.stdout);
1197
1076
  }
1198
1077
  catch (err) {
1199
- return resp
1200
- .status(400)
1201
- .type("application/json")
1202
- .send({ error: err.message });
1078
+ return resp.status(400).type('application/json').send({ error: err.message });
1203
1079
  }
1204
1080
  });
1205
- app.post("/api/rebase", async (req, resp) => {
1081
+ app.post('/api/rebase', async (req, resp) => {
1206
1082
  try {
1207
1083
  const { branch } = req.body;
1208
1084
  await git.rebase([branch]);
1209
- return resp.type("application/json").send({ success: true });
1085
+ return resp.type('application/json').send({ success: true });
1210
1086
  }
1211
1087
  catch (err) {
1212
- return resp
1213
- .status(400)
1214
- .type("application/json")
1215
- .send({ error: err.message });
1088
+ return resp.status(400).type('application/json').send({ error: err.message });
1216
1089
  }
1217
1090
  });
1218
- app.post("/api/squash", async (req, resp) => {
1091
+ app.post('/api/squash', async (req, resp) => {
1219
1092
  try {
1220
1093
  const { commits } = req.body;
1221
1094
  // Squash: rebase -i with squash operation
1222
- await git.rebase(["-i", "HEAD~" + commits]);
1223
- return resp.type("application/json").send({ success: true });
1095
+ await git.rebase(['-i', 'HEAD~' + commits]);
1096
+ return resp.type('application/json').send({ success: true });
1224
1097
  }
1225
1098
  catch (err) {
1226
- return resp
1227
- .status(400)
1228
- .type("application/json")
1229
- .send({ error: err.message });
1099
+ return resp.status(400).type('application/json').send({ error: err.message });
1230
1100
  }
1231
1101
  });
1232
1102
  // ==================== Stash ====================
1233
- app.get("/api/stash/list", async (_req, resp) => {
1103
+ app.get('/api/stash/list', async (_req, resp) => {
1234
1104
  try {
1235
1105
  // Extra fields (date, author) let the commit table render stash rows;
1236
1106
  // they are NUL-separated because messages cannot contain NUL.
1237
- const raw = await git.raw([
1238
- "stash",
1239
- "list",
1240
- "--format=%H%x00%aI%x00%aN%x00%aE%x00%s",
1241
- ]);
1107
+ const raw = await git.raw(['stash', 'list', '--format=%H%x00%aI%x00%aN%x00%aE%x00%s']);
1242
1108
  const all = raw
1243
1109
  .trim()
1244
- .split("\n")
1110
+ .split('\n')
1245
1111
  .filter(Boolean)
1246
1112
  .map((line) => {
1247
- const [hash, date, authorName, authorEmail, message] = line.split("\u0000");
1113
+ const [hash, date, authorName, authorEmail, message] = line.split('\u0000');
1248
1114
  return {
1249
1115
  hash,
1250
1116
  date,
@@ -1253,324 +1119,266 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1253
1119
  message,
1254
1120
  };
1255
1121
  });
1256
- return resp.type("application/json").send({
1122
+ return resp.type('application/json').send({
1257
1123
  all,
1258
1124
  latest: all[0] ?? null,
1259
1125
  total: all.length,
1260
1126
  });
1261
1127
  }
1262
1128
  catch (err) {
1263
- return resp
1264
- .status(400)
1265
- .type("application/json")
1266
- .send({ error: err.message });
1129
+ return resp.status(400).type('application/json').send({ error: err.message });
1267
1130
  }
1268
1131
  });
1269
- app.post("/api/stash/save", async (req, resp) => {
1132
+ app.post('/api/stash/save', async (req, resp) => {
1270
1133
  try {
1271
1134
  const { message, scope } = req.body ?? {};
1272
1135
  // 'staged' → index only (--staged, git ≥ 2.35); 'unstaged' → working tree only
1273
1136
  // (--keep-index leaves staged changes staged); 'all' → index + working tree.
1274
1137
  // Untracked files ride along for 'all'/'unstaged' since the panel lists them as unstaged.
1275
- const args = ["push"];
1276
- if (scope === "staged") {
1277
- args.push("--staged");
1138
+ const args = ['push'];
1139
+ if (scope === 'staged') {
1140
+ args.push('--staged');
1278
1141
  }
1279
1142
  else {
1280
- if (scope === "unstaged")
1281
- args.push("--keep-index");
1282
- args.push("-u");
1143
+ if (scope === 'unstaged')
1144
+ args.push('--keep-index');
1145
+ args.push('-u');
1283
1146
  }
1284
1147
  if (message)
1285
- args.push("-m", message);
1148
+ args.push('-m', message);
1286
1149
  await git.stash(args);
1287
- return resp.type("application/json").send({ success: true });
1150
+ return resp.type('application/json').send({ success: true });
1288
1151
  }
1289
1152
  catch (err) {
1290
- return resp
1291
- .status(400)
1292
- .type("application/json")
1293
- .send({ error: err.message });
1153
+ return resp.status(400).type('application/json').send({ error: err.message });
1294
1154
  }
1295
1155
  });
1296
- app.post("/api/stash/apply", async (req, resp) => {
1156
+ app.post('/api/stash/apply', async (req, resp) => {
1297
1157
  try {
1298
1158
  const { index } = req.body;
1299
- await git.stash(["apply", `stash@{${index}}`]);
1300
- return resp.type("application/json").send({ success: true });
1159
+ await git.stash(['apply', `stash@{${index}}`]);
1160
+ return resp.type('application/json').send({ success: true });
1301
1161
  }
1302
1162
  catch (err) {
1303
- return resp
1304
- .status(400)
1305
- .type("application/json")
1306
- .send({ error: err.message });
1163
+ return resp.status(400).type('application/json').send({ error: err.message });
1307
1164
  }
1308
1165
  });
1309
- app.post("/api/stash/pop", async (req, resp) => {
1166
+ app.post('/api/stash/pop', async (req, resp) => {
1310
1167
  try {
1311
1168
  const { index } = req.body;
1312
- await git.stash(["pop", `stash@{${index}}`]);
1313
- return resp.type("application/json").send({ success: true });
1169
+ await git.stash(['pop', `stash@{${index}}`]);
1170
+ return resp.type('application/json').send({ success: true });
1314
1171
  }
1315
1172
  catch (err) {
1316
- return resp
1317
- .status(400)
1318
- .type("application/json")
1319
- .send({ error: err.message });
1173
+ return resp.status(400).type('application/json').send({ error: err.message });
1320
1174
  }
1321
1175
  });
1322
- app.post("/api/stash/drop", async (req, resp) => {
1176
+ app.post('/api/stash/drop', async (req, resp) => {
1323
1177
  try {
1324
1178
  const { index } = req.body;
1325
- await git.stash(["drop", `stash@{${index}}`]);
1326
- return resp.type("application/json").send({ success: true });
1179
+ await git.stash(['drop', `stash@{${index}}`]);
1180
+ return resp.type('application/json').send({ success: true });
1327
1181
  }
1328
1182
  catch (err) {
1329
- return resp
1330
- .status(400)
1331
- .type("application/json")
1332
- .send({ error: err.message });
1183
+ return resp.status(400).type('application/json').send({ error: err.message });
1333
1184
  }
1334
1185
  });
1335
- app.post("/api/stash/show", async (req, resp) => {
1186
+ app.post('/api/stash/show', async (req, resp) => {
1336
1187
  try {
1337
1188
  const { index } = req.body;
1338
- const show = await git.stash(["show", "-p", `stash@{${index}}`]);
1339
- return resp.type("application/json").send({ preview: show });
1189
+ const show = await git.stash(['show', '-p', `stash@{${index}}`]);
1190
+ return resp.type('application/json').send({ preview: show });
1340
1191
  }
1341
1192
  catch (err) {
1342
- return resp
1343
- .status(400)
1344
- .type("application/json")
1345
- .send({ error: err.message });
1193
+ return resp.status(400).type('application/json').send({ error: err.message });
1346
1194
  }
1347
1195
  });
1348
1196
  // ==================== Tags ====================
1349
1197
  // All tags with the commit each points at (annotated tags are peeled, so the
1350
1198
  // client can always look the tagged commit up by hash). Sorted by name.
1351
- app.get("/api/tags", async (_req, resp) => {
1199
+ app.get('/api/tags', async (_req, resp) => {
1352
1200
  try {
1353
1201
  const raw = await git.raw([
1354
- "tag",
1355
- "--list",
1356
- "--sort=refname",
1357
- "--format=%(refname:short)%09%(*objectname)%09%(objectname)",
1202
+ 'tag',
1203
+ '--list',
1204
+ '--sort=refname',
1205
+ '--format=%(refname:short)%09%(*objectname)%09%(objectname)',
1358
1206
  ]);
1359
1207
  const tags = raw
1360
- .split("\n")
1361
- .map((line) => line.split("\t"))
1208
+ .split('\n')
1209
+ .map((line) => line.split('\t'))
1362
1210
  .filter(([name]) => Boolean(name.trim()))
1363
1211
  .map(([name, peeled, direct]) => ({ name, hash: peeled || direct }));
1364
- return resp.type("application/json").send(tags);
1212
+ return resp.type('application/json').send(tags);
1365
1213
  }
1366
1214
  catch (err) {
1367
- return resp
1368
- .status(400)
1369
- .type("application/json")
1370
- .send({ error: err.message });
1215
+ return resp.status(400).type('application/json').send({ error: err.message });
1371
1216
  }
1372
1217
  });
1373
- app.post("/api/tag/create", async (req, resp) => {
1218
+ app.post('/api/tag/create', async (req, resp) => {
1374
1219
  try {
1375
1220
  const { name, message, commit } = req.body;
1376
1221
  if (message) {
1377
- await git.tag(["-a", name, "-m", message, ...(commit ? [commit] : [])]);
1222
+ await git.tag(['-a', name, '-m', message, ...(commit ? [commit] : [])]);
1378
1223
  }
1379
1224
  else {
1380
1225
  await git.tag([name, ...(commit ? [commit] : [])]);
1381
1226
  }
1382
- return resp.type("application/json").send({ success: true });
1227
+ return resp.type('application/json').send({ success: true });
1383
1228
  }
1384
1229
  catch (err) {
1385
- return resp
1386
- .status(400)
1387
- .type("application/json")
1388
- .send({ error: err.message });
1230
+ return resp.status(400).type('application/json').send({ error: err.message });
1389
1231
  }
1390
1232
  });
1391
- app.post("/api/tag/delete", async (req, resp) => {
1233
+ app.post('/api/tag/delete', async (req, resp) => {
1392
1234
  try {
1393
1235
  const { name } = req.body;
1394
- await git.tag(["-d", name]);
1395
- return resp.type("application/json").send({ success: true });
1236
+ await git.tag(['-d', name]);
1237
+ return resp.type('application/json').send({ success: true });
1396
1238
  }
1397
1239
  catch (err) {
1398
- return resp
1399
- .status(400)
1400
- .type("application/json")
1401
- .send({ error: err.message });
1240
+ return resp.status(400).type('application/json').send({ error: err.message });
1402
1241
  }
1403
1242
  });
1404
- app.post("/api/tag/push", async (req, resp) => {
1243
+ app.post('/api/tag/push', async (req, resp) => {
1405
1244
  try {
1406
1245
  const { name, remote } = req.body;
1407
1246
  if (!name) {
1408
- return resp
1409
- .status(400)
1410
- .type("application/json")
1411
- .send({ error: "name required" });
1247
+ return resp.status(400).type('application/json').send({ error: 'name required' });
1412
1248
  }
1413
- await git.push(remote || "origin", `refs/tags/${name}`);
1414
- return resp.type("application/json").send({ success: true });
1249
+ await git.push(remote || 'origin', `refs/tags/${name}`);
1250
+ return resp.type('application/json').send({ success: true });
1415
1251
  }
1416
1252
  catch (err) {
1417
- return resp
1418
- .status(400)
1419
- .type("application/json")
1420
- .send({ error: err.message });
1253
+ return resp.status(400).type('application/json').send({ error: err.message });
1421
1254
  }
1422
1255
  });
1423
1256
  // ==================== Remote Operations ====================
1424
- app.get("/api/fetch", async (req, resp) => {
1257
+ app.get('/api/fetch', async (req, resp) => {
1425
1258
  try {
1426
- const prune = ["1", "true", "yes"].includes(String(req.query?.prune ?? "").toLowerCase());
1427
- await git.fetch(prune ? { "--prune": null } : {});
1428
- return resp.type("application/json").send({ success: true });
1259
+ const prune = ['1', 'true', 'yes'].includes(String(req.query?.prune ?? '').toLowerCase());
1260
+ await git.fetch(prune ? { '--prune': null } : {});
1261
+ return resp.type('application/json').send({ success: true });
1429
1262
  }
1430
1263
  catch (err) {
1431
- return resp
1432
- .status(400)
1433
- .type("application/json")
1434
- .send({ error: err.message });
1264
+ return resp.status(400).type('application/json').send({ error: err.message });
1435
1265
  }
1436
1266
  });
1437
- app.post("/api/pull", async (req, resp) => {
1267
+ app.post('/api/pull', async (req, resp) => {
1438
1268
  try {
1439
1269
  const { remote, branch, rebase } = req.body;
1440
- await git.pull(remote || "origin", branch || undefined, rebase ? { "--rebase": null } : {});
1441
- return resp.type("application/json").send({ success: true });
1270
+ await git.pull(remote || 'origin', branch || undefined, rebase ? { '--rebase': null } : {});
1271
+ return resp.type('application/json').send({ success: true });
1442
1272
  }
1443
1273
  catch (err) {
1444
- return resp
1445
- .status(400)
1446
- .type("application/json")
1447
- .send({ error: err.message });
1274
+ return resp.status(400).type('application/json').send({ error: err.message });
1448
1275
  }
1449
1276
  });
1450
- app.post("/api/push", async (req, resp) => {
1277
+ app.post('/api/push', async (req, resp) => {
1451
1278
  try {
1452
1279
  const { remote, branch, force } = req.body;
1453
- await git.push(remote || "origin", branch || undefined, force ? { "-f": null } : {});
1454
- return resp.type("application/json").send({ success: true });
1280
+ await git.push(remote || 'origin', branch || undefined, force ? { '-f': null } : {});
1281
+ return resp.type('application/json').send({ success: true });
1455
1282
  }
1456
1283
  catch (err) {
1457
- return resp
1458
- .status(400)
1459
- .type("application/json")
1460
- .send({ error: err.message });
1284
+ return resp.status(400).type('application/json').send({ error: err.message });
1461
1285
  }
1462
1286
  });
1463
- app.post("/api/sync", async (_req, resp) => {
1287
+ app.post('/api/sync', async (_req, resp) => {
1464
1288
  try {
1465
- await git.pull("origin", undefined, {});
1466
- await git.push("origin", undefined, {});
1467
- return resp.type("application/json").send({ success: true });
1289
+ await git.pull('origin', undefined, {});
1290
+ await git.push('origin', undefined, {});
1291
+ return resp.type('application/json').send({ success: true });
1468
1292
  }
1469
1293
  catch (err) {
1470
- return resp
1471
- .status(400)
1472
- .type("application/json")
1473
- .send({ error: err.message });
1294
+ return resp.status(400).type('application/json').send({ error: err.message });
1474
1295
  }
1475
1296
  });
1476
- app.post("/api/prune", async (req, resp) => {
1297
+ app.post('/api/prune', async (req, resp) => {
1477
1298
  try {
1478
1299
  const { remote } = req.body;
1479
- await git.remote(["prune", remote || "origin"]);
1480
- return resp.type("application/json").send({ success: true });
1300
+ await git.remote(['prune', remote || 'origin']);
1301
+ return resp.type('application/json').send({ success: true });
1481
1302
  }
1482
1303
  catch (err) {
1483
- return resp
1484
- .status(400)
1485
- .type("application/json")
1486
- .send({ error: err.message });
1304
+ return resp.status(400).type('application/json').send({ error: err.message });
1487
1305
  }
1488
1306
  });
1489
1307
  const listRemotes = async () => {
1490
- const names = (await git.raw(["remote"]))
1308
+ const names = (await git.raw(['remote']))
1491
1309
  .split(/\r?\n/)
1492
1310
  .map((name) => name.trim())
1493
1311
  .filter(Boolean);
1494
1312
  return Promise.all(names.map(async (name) => {
1495
- const fetchUrl = (await git.raw(["remote", "get-url", name])).trim();
1496
- const pushUrl = (await git
1497
- .raw(["remote", "get-url", "--push", name])
1498
- .catch(() => fetchUrl)).trim();
1313
+ const fetchUrl = (await git.raw(['remote', 'get-url', name])).trim();
1314
+ const pushUrl = (await git.raw(['remote', 'get-url', '--push', name]).catch(() => fetchUrl)).trim();
1499
1315
  return { name, fetchUrl, pushUrl };
1500
1316
  }));
1501
1317
  };
1502
- app.get("/api/remotes", async (_req, resp) => {
1318
+ app.get('/api/remotes', async (_req, resp) => {
1503
1319
  try {
1504
- return resp.type("application/json").send(await listRemotes());
1320
+ return resp.type('application/json').send(await listRemotes());
1505
1321
  }
1506
1322
  catch (err) {
1507
1323
  return resp.status(400).send({ error: err.message });
1508
1324
  }
1509
1325
  });
1510
- app.post("/api/remotes", async (req, resp) => {
1326
+ app.post('/api/remotes', async (req, resp) => {
1511
1327
  try {
1512
- const name = String(req.body?.name ?? "").trim();
1513
- const originalName = String(req.body?.originalName ?? "").trim();
1514
- const fetchUrl = String(req.body?.fetchUrl ?? "").trim();
1515
- const pushUrl = String(req.body?.pushUrl ?? "").trim() || fetchUrl;
1328
+ const name = String(req.body?.name ?? '').trim();
1329
+ const originalName = String(req.body?.originalName ?? '').trim();
1330
+ const fetchUrl = String(req.body?.fetchUrl ?? '').trim();
1331
+ const pushUrl = String(req.body?.pushUrl ?? '').trim() || fetchUrl;
1516
1332
  if (!/^[A-Za-z0-9._-]+$/.test(name) || !fetchUrl) {
1517
- return resp
1518
- .status(400)
1519
- .send({ error: "A valid remote name and fetch URL are required." });
1333
+ return resp.status(400).send({ error: 'A valid remote name and fetch URL are required.' });
1520
1334
  }
1521
1335
  if (!originalName) {
1522
- await git.raw(["remote", "add", name, fetchUrl]);
1336
+ await git.raw(['remote', 'add', name, fetchUrl]);
1523
1337
  }
1524
1338
  else {
1525
1339
  if (originalName !== name)
1526
- await git.raw(["remote", "rename", originalName, name]);
1527
- await git.raw(["remote", "set-url", name, fetchUrl]);
1340
+ await git.raw(['remote', 'rename', originalName, name]);
1341
+ await git.raw(['remote', 'set-url', name, fetchUrl]);
1528
1342
  }
1529
- await git.raw(["remote", "set-url", "--push", name, pushUrl]);
1343
+ await git.raw(['remote', 'set-url', '--push', name, pushUrl]);
1530
1344
  return resp.send(await listRemotes());
1531
1345
  }
1532
1346
  catch (err) {
1533
1347
  return resp.status(400).send({ error: err.message });
1534
1348
  }
1535
1349
  });
1536
- app.delete("/api/remotes/:name", async (req, resp) => {
1350
+ app.delete('/api/remotes/:name', async (req, resp) => {
1537
1351
  try {
1538
- const name = String(req.params?.name ?? "").trim();
1352
+ const name = String(req.params?.name ?? '').trim();
1539
1353
  if (!/^[A-Za-z0-9._-]+$/.test(name)) {
1540
- return resp.status(400).send({ error: "Invalid remote name." });
1354
+ return resp.status(400).send({ error: 'Invalid remote name.' });
1541
1355
  }
1542
- await git.raw(["remote", "remove", name]);
1356
+ await git.raw(['remote', 'remove', name]);
1543
1357
  return resp.send(await listRemotes());
1544
1358
  }
1545
1359
  catch (err) {
1546
1360
  return resp.status(400).send({ error: err.message });
1547
1361
  }
1548
1362
  });
1549
- app.post("/api/identity", async (req, resp) => {
1363
+ app.post('/api/identity', async (req, resp) => {
1550
1364
  try {
1551
- const name = String(req.body?.name ?? "").trim();
1552
- const email = String(req.body?.email ?? "").trim();
1365
+ const name = String(req.body?.name ?? '').trim();
1366
+ const email = String(req.body?.email ?? '').trim();
1553
1367
  if (!name || !email) {
1554
- return resp
1555
- .status(400)
1556
- .send({ error: "User name and email are required." });
1368
+ return resp.status(400).send({ error: 'User name and email are required.' });
1557
1369
  }
1558
- await git.raw(["config", "--local", "user.name", name]);
1559
- await git.raw(["config", "--local", "user.email", email]);
1370
+ await git.raw(['config', '--local', 'user.name', name]);
1371
+ await git.raw(['config', '--local', 'user.email', email]);
1560
1372
  return resp.send(await configuredIdentity());
1561
1373
  }
1562
1374
  catch (err) {
1563
1375
  return resp.status(400).send({ error: err.message });
1564
1376
  }
1565
1377
  });
1566
- app.delete("/api/identity", async (_req, resp) => {
1378
+ app.delete('/api/identity', async (_req, resp) => {
1567
1379
  try {
1568
- await git
1569
- .raw(["config", "--local", "--unset-all", "user.name"])
1570
- .catch(() => "");
1571
- await git
1572
- .raw(["config", "--local", "--unset-all", "user.email"])
1573
- .catch(() => "");
1380
+ await git.raw(['config', '--local', '--unset-all', 'user.name']).catch(() => '');
1381
+ await git.raw(['config', '--local', '--unset-all', 'user.email']).catch(() => '');
1574
1382
  return resp.send(await configuredIdentity());
1575
1383
  }
1576
1384
  catch (err) {
@@ -1578,122 +1386,120 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1578
1386
  }
1579
1387
  });
1580
1388
  // ==================== Settings ====================
1581
- app.get("/api/settings", async (_req, resp) => {
1389
+ app.get('/api/settings', async (_req, resp) => {
1582
1390
  try {
1583
1391
  const settings = await effectiveSettings();
1584
- return resp.type("application/json").send(settings);
1392
+ return resp.type('application/json').send(settings);
1585
1393
  }
1586
1394
  catch (err) {
1587
- return resp
1588
- .status(400)
1589
- .type("application/json")
1590
- .send({ error: err.message });
1395
+ return resp.status(400).type('application/json').send({ error: err.message });
1591
1396
  }
1592
1397
  });
1593
- app.post("/api/settings", async (req, resp) => {
1398
+ app.post('/api/settings', async (req, resp) => {
1594
1399
  try {
1595
1400
  const body = req.body ?? {};
1596
1401
  const settings = await readSettings();
1597
- if ("azureDevOpsUrl" in body) {
1598
- const url = String(body.azureDevOpsUrl ?? "").trim();
1402
+ if ('azureDevOpsUrl' in body) {
1403
+ const url = String(body.azureDevOpsUrl ?? '').trim();
1599
1404
  if (url && !/^https?:\/\//i.test(url)) {
1600
- return resp.status(400).type("application/json").send({
1601
- error: "The Azure DevOps URL must start with http:// or https://.",
1405
+ return resp.status(400).type('application/json').send({
1406
+ error: 'The Azure DevOps URL must start with http:// or https://.',
1602
1407
  });
1603
1408
  }
1604
1409
  settings.azureDevOpsUrl = url;
1605
1410
  }
1606
- if ("prBranchNameTemplate" in body) {
1607
- const template = String(body.prBranchNameTemplate ?? "").trim();
1411
+ if ('prBranchNameTemplate' in body) {
1412
+ const template = String(body.prBranchNameTemplate ?? '').trim();
1608
1413
  if (template.length > 200) {
1609
1414
  return resp.status(400).send({
1610
- error: "The pull request branch template must be 200 characters or fewer.",
1415
+ error: 'The pull request branch template must be 200 characters or fewer.',
1611
1416
  });
1612
1417
  }
1613
1418
  try {
1614
1419
  formatPrBranchName(template, {
1615
- username: "username",
1616
- randomstring: "randomstring",
1617
- branch: "branch",
1618
- targetbranch: "targetbranch",
1619
- title: "title",
1620
- repository: "repository",
1621
- date: "2026-09-12",
1622
- time: "120000",
1623
- timestamp: "1789214400000",
1420
+ username: 'username',
1421
+ randomstring: 'randomstring',
1422
+ branch: 'branch',
1423
+ targetbranch: 'targetbranch',
1424
+ title: 'title',
1425
+ repository: 'repository',
1426
+ date: '2026-09-12',
1427
+ time: '120000',
1428
+ timestamp: '1789214400000',
1624
1429
  });
1625
1430
  }
1626
1431
  catch (error) {
1627
1432
  return resp.status(400).send({ error: error.message });
1628
1433
  }
1629
- settings.prBranchNameTemplate =
1630
- template || DEFAULT_PR_BRANCH_NAME_TEMPLATE;
1434
+ settings.prBranchNameTemplate = template || DEFAULT_PR_BRANCH_NAME_TEMPLATE;
1631
1435
  }
1632
- if (typeof body.showGraph === "boolean") {
1436
+ if (typeof body.showGraph === 'boolean') {
1633
1437
  settings.showGraph = body.showGraph;
1634
1438
  }
1635
- if (typeof body.autoReload === "boolean") {
1439
+ if (typeof body.autoReload === 'boolean') {
1636
1440
  settings.autoReload = body.autoReload;
1637
1441
  }
1638
- if (typeof body.showStashes === "boolean") {
1442
+ if (typeof body.showStashes === 'boolean') {
1639
1443
  settings.showStashes = body.showStashes;
1640
1444
  }
1641
- if (typeof body.showTags === "boolean") {
1445
+ if (typeof body.showTags === 'boolean') {
1642
1446
  settings.showTags = body.showTags;
1643
1447
  }
1644
- if (typeof body.showRemoteBranches === "boolean") {
1448
+ if (typeof body.showRemoteBranches === 'boolean') {
1645
1449
  settings.showRemoteBranches = body.showRemoteBranches;
1646
1450
  }
1647
- if (body.fileListView === "tree" || body.fileListView === "flat") {
1451
+ if (body.fileListView === 'tree' || body.fileListView === 'flat') {
1648
1452
  settings.fileListView = body.fileListView;
1649
1453
  }
1650
- if (body.searchMode === "filter" || body.searchMode === "navigate") {
1454
+ if (body.refListView === 'tree' || body.refListView === 'flat') {
1455
+ settings.refListView = body.refListView;
1456
+ }
1457
+ if (typeof body.sidePanelSectionsExpanded === 'boolean') {
1458
+ settings.sidePanelSectionsExpanded = body.sidePanelSectionsExpanded;
1459
+ }
1460
+ if (body.searchMode === 'filter' || body.searchMode === 'navigate') {
1651
1461
  settings.searchMode = body.searchMode;
1652
1462
  }
1653
- if (typeof body.searchCaseSensitive === "boolean") {
1463
+ if (typeof body.searchCaseSensitive === 'boolean') {
1654
1464
  settings.searchCaseSensitive = body.searchCaseSensitive;
1655
1465
  }
1656
- if (typeof body.allowMerge === "boolean") {
1466
+ if (typeof body.allowMerge === 'boolean') {
1657
1467
  settings.allowMerge = body.allowMerge;
1658
1468
  }
1659
- if ("issueLinking" in body) {
1469
+ if ('issueLinking' in body) {
1660
1470
  if (body.issueLinking === null) {
1661
1471
  if (body.issueLinkingGlobal) {
1662
1472
  await git
1663
- .raw(["config", "--global", "--unset-all", "guito.issueRegex"])
1664
- .catch(() => "");
1665
- await git
1666
- .raw(["config", "--global", "--unset-all", "guito.issueUrl"])
1667
- .catch(() => "");
1473
+ .raw(['config', '--global', '--unset-all', 'guito.issueRegex'])
1474
+ .catch(() => '');
1475
+ await git.raw(['config', '--global', '--unset-all', 'guito.issueUrl']).catch(() => '');
1668
1476
  }
1669
1477
  else {
1670
1478
  delete settings.issueLinking;
1671
1479
  }
1672
1480
  }
1673
1481
  else {
1674
- const regex = String(body.issueLinking?.regex ?? "").trim();
1675
- const url = String(body.issueLinking?.url ?? "").trim();
1482
+ const regex = String(body.issueLinking?.regex ?? '').trim();
1483
+ const url = String(body.issueLinking?.url ?? '').trim();
1676
1484
  if (!regex || !url) {
1677
- return resp
1678
- .status(400)
1679
- .send({ error: "Issue regex and URL are required." });
1485
+ return resp.status(400).send({ error: 'Issue regex and URL are required.' });
1680
1486
  }
1681
1487
  try {
1682
1488
  new RegExp(regex);
1683
1489
  }
1684
1490
  catch {
1685
1491
  return resp.status(400).send({
1686
- error: "Issue regex is not a valid regular expression.",
1492
+ error: 'Issue regex is not a valid regular expression.',
1687
1493
  });
1688
1494
  }
1689
1495
  if (!/^https?:\/\//i.test(url)) {
1690
1496
  return resp.status(400).send({
1691
- error: "Issue URL must start with http:// or https://.",
1497
+ error: 'Issue URL must start with http:// or https://.',
1692
1498
  });
1693
1499
  }
1694
1500
  if (body.issueLinking.useGlobally) {
1695
- await git.raw(["config", "--global", "guito.issueRegex", regex]);
1696
- await git.raw(["config", "--global", "guito.issueUrl", url]);
1501
+ await git.raw(['config', '--global', 'guito.issueRegex', regex]);
1502
+ await git.raw(['config', '--global', 'guito.issueUrl', url]);
1697
1503
  delete settings.issueLinking;
1698
1504
  }
1699
1505
  else {
@@ -1703,40 +1509,34 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1703
1509
  }
1704
1510
  await writeSettings(settings);
1705
1511
  const effective = await effectiveSettings();
1706
- return resp.type("application/json").send(effective);
1512
+ return resp.type('application/json').send(effective);
1707
1513
  }
1708
1514
  catch (err) {
1709
- return resp
1710
- .status(400)
1711
- .type("application/json")
1712
- .send({ error: err.message });
1515
+ return resp.status(400).type('application/json').send({ error: err.message });
1713
1516
  }
1714
1517
  });
1715
1518
  // ==================== Azure DevOps Pull Requests ====================
1716
- app.post("/api/azure-devops/pullrequest", async (req, resp) => {
1519
+ app.post('/api/azure-devops/pullrequest', async (req, resp) => {
1717
1520
  try {
1718
1521
  const { sourceBranch, targetBranch, title, description, newBranch } = req.body ?? {};
1719
1522
  if (!targetBranch) {
1720
- return resp
1721
- .status(400)
1722
- .type("application/json")
1723
- .send({ error: "targetBranch required" });
1523
+ return resp.status(400).type('application/json').send({ error: 'targetBranch required' });
1724
1524
  }
1725
1525
  const { prefix, remoteUrl } = await azureProjectContext();
1726
1526
  const info = parseAzureRemoteUrl(remoteUrl);
1727
1527
  if (!info) {
1728
- return resp.status(400).type("application/json").send({
1729
- error: "Unable to build the Azure DevOps pull request URL.",
1528
+ return resp.status(400).type('application/json').send({
1529
+ error: 'Unable to build the Azure DevOps pull request URL.',
1730
1530
  });
1731
1531
  }
1732
1532
  const prUrl = `${prefix}/_apis/git/repositories/${encodeURIComponent(info.repo)}/pullrequests?api-version=${AZURE_API_VERSION}`;
1733
- let source = String(sourceBranch ?? "").trim();
1533
+ let source = String(sourceBranch ?? '').trim();
1734
1534
  if (newBranch) {
1735
1535
  // Create a remote-only branch from the current HEAD; the local
1736
1536
  // repository and the checked-out branch stay untouched.
1737
1537
  const now = new Date();
1738
1538
  const currentSettings = await effectiveSettings();
1739
- let computerUsername = process.env.USERNAME || process.env.USER || "";
1539
+ let computerUsername = process.env.USERNAME || process.env.USER || '';
1740
1540
  try {
1741
1541
  computerUsername = userInfo().username || computerUsername;
1742
1542
  }
@@ -1746,18 +1546,18 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1746
1546
  const name = formatPrBranchName(currentSettings.prBranchNameTemplate, {
1747
1547
  username: computerUsername,
1748
1548
  randomstring: randomBranchSuffix(),
1749
- branch: (await git.revparse(["--abbrev-ref", "HEAD"])).trim(),
1549
+ branch: (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(),
1750
1550
  targetbranch: String(targetBranch)
1751
- .replace(/^refs\/heads\//, "")
1752
- .replace(/^origin\//, ""),
1753
- title: String(title ?? "").trim() || "pull-request",
1551
+ .replace(/^refs\/heads\//, '')
1552
+ .replace(/^origin\//, ''),
1553
+ title: String(title ?? '').trim() || 'pull-request',
1754
1554
  repository: basename(await repoRoot()),
1755
1555
  date: now.toISOString().slice(0, 10),
1756
- time: now.toISOString().slice(11, 19).replace(/:/g, ""),
1556
+ time: now.toISOString().slice(11, 19).replace(/:/g, ''),
1757
1557
  timestamp: String(now.getTime()),
1758
1558
  });
1759
1559
  try {
1760
- await git.raw(["check-ref-format", "--branch", name]);
1560
+ await git.raw(['check-ref-format', '--branch', name]);
1761
1561
  }
1762
1562
  catch {
1763
1563
  return resp.status(400).send({
@@ -1765,26 +1565,23 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1765
1565
  });
1766
1566
  }
1767
1567
  const existingBranch = await git
1768
- .raw(["ls-remote", "--heads", "origin", `refs/heads/${name}`])
1769
- .catch(() => "");
1568
+ .raw(['ls-remote', '--heads', 'origin', `refs/heads/${name}`])
1569
+ .catch(() => '');
1770
1570
  if (existingBranch.trim()) {
1771
1571
  return resp.status(400).send({
1772
1572
  error: `The pull request branch already exists on origin: ${name}`,
1773
1573
  });
1774
1574
  }
1775
- await git.push(["origin", `HEAD:refs/heads/${name}`]);
1575
+ await git.push(['origin', `HEAD:refs/heads/${name}`]);
1776
1576
  source = name;
1777
1577
  }
1778
1578
  else {
1779
1579
  if (!source) {
1780
- return resp
1781
- .status(400)
1782
- .type("application/json")
1783
- .send({ error: "sourceBranch required" });
1580
+ return resp.status(400).type('application/json').send({ error: 'sourceBranch required' });
1784
1581
  }
1785
- if (source.startsWith("origin/")) {
1582
+ if (source.startsWith('origin/')) {
1786
1583
  // Remote-tracking branch: the PR source is the branch name on the remote.
1787
- source = source.slice("origin/".length);
1584
+ source = source.slice('origin/'.length);
1788
1585
  }
1789
1586
  else {
1790
1587
  // Publish the branch first when the remote does not have it yet.
@@ -1793,15 +1590,15 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1793
1590
  // rev-parse --verify --quiet does not fail reliably through
1794
1591
  // simple-git, so list the tracking refs instead.
1795
1592
  const trackingRefs = await git.raw([
1796
- "for-each-ref",
1797
- "refs/remotes/origin",
1798
- "--format=%(refname)",
1593
+ 'for-each-ref',
1594
+ 'refs/remotes/origin',
1595
+ '--format=%(refname)',
1799
1596
  ]);
1800
1597
  const published = trackingRefs
1801
- .split("\n")
1598
+ .split('\n')
1802
1599
  .some((line) => line.trim() === `refs/remotes/origin/${source}`);
1803
1600
  if (!published) {
1804
- await git.push(["origin", source]);
1601
+ await git.push(['origin', source]);
1805
1602
  }
1806
1603
  }
1807
1604
  }
@@ -1809,32 +1606,28 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1809
1606
  isDraft: req.body?.isDraft === true,
1810
1607
  sourceRefName: `refs/heads/${source}`,
1811
1608
  targetRefName: `refs/heads/${String(targetBranch)
1812
- .replace(/^refs\/heads\//, "")
1813
- .replace(/^origin\//, "")}`,
1609
+ .replace(/^refs\/heads\//, '')
1610
+ .replace(/^origin\//, '')}`,
1814
1611
  };
1815
- const trimmedTitle = String(title ?? "").trim();
1816
- const trimmedDescription = String(description ?? "").trim();
1612
+ const trimmedTitle = String(title ?? '').trim();
1613
+ const trimmedDescription = String(description ?? '').trim();
1817
1614
  if (trimmedTitle) {
1818
1615
  payload.title = trimmedTitle;
1819
1616
  }
1820
1617
  if (trimmedDescription) {
1821
1618
  payload.description = trimmedDescription;
1822
1619
  }
1823
- const reviewerList = Array.isArray(req.body?.reviewers)
1824
- ? req.body.reviewers
1825
- : [];
1620
+ const reviewerList = Array.isArray(req.body?.reviewers) ? req.body.reviewers : [];
1826
1621
  const payloadReviewers = reviewerList
1827
1622
  .map((entry) => ({
1828
- id: String(entry?.id ?? ""),
1623
+ id: String(entry?.id ?? ''),
1829
1624
  isRequired: !!entry?.required,
1830
1625
  }))
1831
1626
  .filter((entry) => entry.id);
1832
1627
  if (payloadReviewers.length) {
1833
1628
  payload.reviewers = payloadReviewers;
1834
1629
  }
1835
- const workItemList = Array.isArray(req.body?.workItems)
1836
- ? req.body.workItems
1837
- : [];
1630
+ const workItemList = Array.isArray(req.body?.workItems) ? req.body.workItems : [];
1838
1631
  const payloadWorkItems = workItemList
1839
1632
  .map((id) => Number(id))
1840
1633
  .filter((id) => Number.isInteger(id) && id > 0);
@@ -1843,20 +1636,19 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1843
1636
  id: String(id),
1844
1637
  }));
1845
1638
  }
1846
- const result = await azureRequest("POST", prUrl, JSON.stringify(payload));
1639
+ const result = await azureRequest('POST', prUrl, JSON.stringify(payload));
1847
1640
  if (result.status < 200 || result.status >= 300) {
1848
1641
  return resp
1849
1642
  .status(400)
1850
- .type("application/json")
1643
+ .type('application/json')
1851
1644
  .send({
1852
- error: azureErrorMessage(result) ||
1853
- `Azure DevOps returned HTTP ${result.status}.`,
1645
+ error: azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`,
1854
1646
  });
1855
1647
  }
1856
1648
  const created = JSON.parse(result.body);
1857
1649
  // Tags (labels) are a separate resource; add them after the PR exists.
1858
1650
  const labelNames = (Array.isArray(req.body?.labels) ? req.body.labels : [])
1859
- .map((name) => String(name ?? "").trim())
1651
+ .map((name) => String(name ?? '').trim())
1860
1652
  .filter(Boolean);
1861
1653
  const warnings = [];
1862
1654
  if (labelNames.length && created.pullRequestId) {
@@ -1865,7 +1657,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1865
1657
  `/pullrequests/${created.pullRequestId}/labels?api-version=${AZURE_API_VERSION}`;
1866
1658
  for (const name of labelNames) {
1867
1659
  try {
1868
- const labelResult = await azureRequest("POST", labelsUrl, JSON.stringify({ name }));
1660
+ const labelResult = await azureRequest('POST', labelsUrl, JSON.stringify({ name }));
1869
1661
  if (labelResult.status < 200 || labelResult.status >= 300) {
1870
1662
  warnings.push(`Could not add tag "${name}" (HTTP ${labelResult.status}).`);
1871
1663
  }
@@ -1875,99 +1667,87 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1875
1667
  }
1876
1668
  }
1877
1669
  }
1878
- return resp.type("application/json").send({
1670
+ return resp.type('application/json').send({
1879
1671
  id: created.pullRequestId,
1880
- url: created._links?.web?.href ?? "",
1672
+ url: created._links?.web?.href ?? '',
1881
1673
  branch: source,
1882
1674
  ...(warnings.length ? { warnings } : {}),
1883
1675
  });
1884
1676
  }
1885
1677
  catch (err) {
1886
- return resp
1887
- .status(400)
1888
- .type("application/json")
1889
- .send({ error: err.message });
1678
+ return resp.status(400).type('application/json').send({ error: err.message });
1890
1679
  }
1891
1680
  });
1892
1681
  // ==================== Azure DevOps PR metadata ====================
1893
1682
  // Collection-scoped identity IDs can be passed directly as PR reviewers.
1894
- app.get("/api/azure-devops/reviewers", async (req, resp) => {
1683
+ app.get('/api/azure-devops/reviewers', async (req, resp) => {
1895
1684
  try {
1896
- const query = String((req.query ?? {}).query ?? "").trim();
1685
+ const query = String((req.query ?? {}).query ?? '').trim();
1897
1686
  if (!query) {
1898
- return resp.type("application/json").send({ reviewers: [] });
1687
+ return resp.type('application/json').send({ reviewers: [] });
1899
1688
  }
1900
1689
  const { prefix } = await azureProjectContext();
1901
- const collection = prefix.slice(0, prefix.lastIndexOf("/"));
1902
- const identityBase = collection.replace("https://dev.azure.com/", "https://vssps.dev.azure.com/");
1690
+ const collection = prefix.slice(0, prefix.lastIndexOf('/'));
1691
+ const identityBase = collection.replace('https://dev.azure.com/', 'https://vssps.dev.azure.com/');
1903
1692
  const url = `${identityBase}/_apis/identities?api-version=5.0-preview` +
1904
1693
  `&searchFilter=General&filterValue=${encodeURIComponent(query)}&queryMembership=None`;
1905
- const result = await azureRequest("GET", url, "");
1694
+ const result = await azureRequest('GET', url, '');
1906
1695
  if (result.status < 200 || result.status >= 300) {
1907
1696
  return resp
1908
1697
  .status(400)
1909
- .type("application/json")
1698
+ .type('application/json')
1910
1699
  .send({
1911
- error: azureErrorMessage(result) ||
1912
- `Azure DevOps returned HTTP ${result.status}.`,
1700
+ error: azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`,
1913
1701
  });
1914
1702
  }
1915
1703
  const data = JSON.parse(result.body);
1916
- const list = Array.isArray(data)
1917
- ? data
1918
- : (data.value ?? data.identities ?? []);
1704
+ const list = Array.isArray(data) ? data : (data.value ?? data.identities ?? []);
1919
1705
  const reviewers = list
1920
1706
  .map((entry) => ({
1921
- id: String(entry?.id ?? entry?.identity?.id ?? ""),
1707
+ id: String(entry?.id ?? entry?.identity?.id ?? ''),
1922
1708
  label: String(entry?.customDisplayName ||
1923
1709
  entry?.providerDisplayName ||
1924
1710
  entry?.label ||
1925
1711
  entry?.displayName ||
1926
1712
  entry?.name ||
1927
- ""),
1713
+ ''),
1928
1714
  description: entry?.properties?.Mail?.$value ??
1929
1715
  entry?.properties?.Account?.$value ??
1930
1716
  entry?.description ??
1931
1717
  undefined,
1932
1718
  }))
1933
1719
  .filter((entry) => entry.id && entry.label);
1934
- return resp.type("application/json").send({ reviewers });
1720
+ return resp.type('application/json').send({ reviewers });
1935
1721
  }
1936
1722
  catch (err) {
1937
- return resp
1938
- .status(400)
1939
- .type("application/json")
1940
- .send({ error: err.message });
1723
+ return resp.status(400).type('application/json').send({ error: err.message });
1941
1724
  }
1942
1725
  });
1943
1726
  // Work item search by id or title: a WIQL query returns ids, a batch read
1944
1727
  // then fetches title/state for the first matches.
1945
- app.get("/api/azure-devops/workitems", async (req, resp) => {
1728
+ app.get('/api/azure-devops/workitems', async (req, resp) => {
1946
1729
  try {
1947
- const query = String((req.query ?? {}).query ?? "")
1730
+ const query = String((req.query ?? {}).query ?? '')
1948
1731
  .trim()
1949
- .replace(/^#(?=\d+$)/, "");
1732
+ .replace(/^#(?=\d+$)/, '');
1950
1733
  if (!query) {
1951
- return resp.type("application/json").send({ workItems: [] });
1734
+ return resp.type('application/json').send({ workItems: [] });
1952
1735
  }
1953
1736
  const { prefix } = await azureProjectContext();
1954
- const clauses = [
1955
- `[System.Title] CONTAINS '${query.replace(/'/g, "''")}'`,
1956
- ];
1737
+ const clauses = [`[System.Title] CONTAINS '${query.replace(/'/g, "''")}'`];
1957
1738
  if (/^\d+$/.test(query)) {
1958
1739
  clauses.push(`[System.Id] = ${Number(query)}`);
1959
1740
  }
1960
- const wiql = "SELECT [System.Id], [System.Title], [System.State] FROM WorkItems " +
1961
- `WHERE [System.TeamProject] = @project AND (${clauses.map((clause) => `(${clause})`).join(" OR ")}) ` +
1962
- "ORDER BY [System.ChangedDate] DESC";
1963
- const wiqlResult = await azureRequest("POST", `${prefix}/_apis/wit/wiql?$top=20&api-version=${AZURE_API_VERSION}`, JSON.stringify({ query: wiql }));
1741
+ const wiql = 'SELECT [System.Id], [System.Title], [System.State] FROM WorkItems ' +
1742
+ `WHERE [System.TeamProject] = @project AND (${clauses.map((clause) => `(${clause})`).join(' OR ')}) ` +
1743
+ 'ORDER BY [System.ChangedDate] DESC';
1744
+ const wiqlResult = await azureRequest('POST', `${prefix}/_apis/wit/wiql?$top=20&api-version=${AZURE_API_VERSION}`, JSON.stringify({ query: wiql }));
1964
1745
  if (wiqlResult.status < 200 || wiqlResult.status >= 300) {
1965
1746
  return resp
1966
1747
  .status(400)
1967
- .type("application/json")
1748
+ .type('application/json')
1968
1749
  .send({
1969
- error: azureErrorMessage(wiqlResult) ||
1970
- `Azure DevOps returned HTTP ${wiqlResult.status}.`,
1750
+ error: azureErrorMessage(wiqlResult) || `Azure DevOps returned HTTP ${wiqlResult.status}.`,
1971
1751
  });
1972
1752
  }
1973
1753
  const ids = (JSON.parse(wiqlResult.body).workItems ?? [])
@@ -1975,46 +1755,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
1975
1755
  .filter((id) => Number.isInteger(id) && id > 0)
1976
1756
  .slice(0, 20);
1977
1757
  if (!ids.length) {
1978
- return resp.type("application/json").send({ workItems: [] });
1758
+ return resp.type('application/json').send({ workItems: [] });
1979
1759
  }
1980
- const batchResult = await azureRequest("GET", `${prefix}/_apis/wit/workitems?ids=${ids.join(",")}` +
1981
- `&fields=System.Id,System.Title,System.State&api-version=${AZURE_API_VERSION}`, "");
1760
+ const batchResult = await azureRequest('GET', `${prefix}/_apis/wit/workitems?ids=${ids.join(',')}` +
1761
+ `&fields=System.Id,System.Title,System.State&api-version=${AZURE_API_VERSION}`, '');
1982
1762
  if (batchResult.status < 200 || batchResult.status >= 300) {
1983
1763
  return resp
1984
1764
  .status(400)
1985
- .type("application/json")
1765
+ .type('application/json')
1986
1766
  .send({
1987
- error: azureErrorMessage(batchResult) ||
1988
- `Azure DevOps returned HTTP ${batchResult.status}.`,
1767
+ error: azureErrorMessage(batchResult) || `Azure DevOps returned HTTP ${batchResult.status}.`,
1989
1768
  });
1990
1769
  }
1991
1770
  const workItems = (JSON.parse(batchResult.body).value ?? []).map((item) => ({
1992
1771
  id: Number(item?.id),
1993
- title: String(item?.fields?.["System.Title"] ?? ""),
1994
- state: String(item?.fields?.["System.State"] ?? ""),
1772
+ title: String(item?.fields?.['System.Title'] ?? ''),
1773
+ state: String(item?.fields?.['System.State'] ?? ''),
1995
1774
  }));
1996
- return resp.type("application/json").send({ workItems });
1775
+ return resp.type('application/json').send({ workItems });
1997
1776
  }
1998
1777
  catch (err) {
1999
- return resp
2000
- .status(400)
2001
- .type("application/json")
2002
- .send({ error: err.message });
1778
+ return resp.status(400).type('application/json').send({ error: err.message });
2003
1779
  }
2004
1780
  });
2005
1781
  // Existing pull request labels for the tag autocomplete.
2006
- app.get("/api/azure-devops/tags", async (_req, resp) => {
1782
+ app.get('/api/azure-devops/tags', async (_req, resp) => {
2007
1783
  try {
2008
1784
  const { prefix, remoteUrl } = await azureProjectContext();
2009
1785
  const repo = parseAzureRemoteUrl(remoteUrl).repo;
2010
1786
  // PR labels are distinct from Azure Boards work item tags.
2011
1787
  const names = new Set();
2012
1788
  for (let skip = 0;; skip += 100) {
2013
- const result = await azureRequest("GET", `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests` +
2014
- `?searchCriteria.status=all&$top=100&$skip=${skip}&api-version=${AZURE_API_VERSION}`, "");
1789
+ const result = await azureRequest('GET', `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests` +
1790
+ `?searchCriteria.status=all&$top=100&$skip=${skip}&api-version=${AZURE_API_VERSION}`, '');
2015
1791
  if (result.status < 200 || result.status >= 300) {
2016
- throw new Error(azureErrorMessage(result) ||
2017
- `Azure DevOps returned HTTP ${result.status}.`);
1792
+ throw new Error(azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`);
2018
1793
  }
2019
1794
  const data = JSON.parse(result.body);
2020
1795
  const requests = Array.isArray(data) ? data : (data.value ?? []);
@@ -2026,17 +1801,16 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2026
1801
  return request.labels;
2027
1802
  if (!Number.isInteger(request.pullRequestId))
2028
1803
  return [];
2029
- const result = await azureRequest("GET", `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}` +
2030
- `/pullrequests/${request.pullRequestId}/labels?api-version=${AZURE_API_VERSION}`, "");
1804
+ const result = await azureRequest('GET', `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}` +
1805
+ `/pullrequests/${request.pullRequestId}/labels?api-version=${AZURE_API_VERSION}`, '');
2031
1806
  if (result.status < 200 || result.status >= 300) {
2032
- throw new Error(azureErrorMessage(result) ||
2033
- `Azure DevOps returned HTTP ${result.status}.`);
1807
+ throw new Error(azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`);
2034
1808
  }
2035
1809
  const data = JSON.parse(result.body);
2036
1810
  return Array.isArray(data) ? data : (data.value ?? []);
2037
1811
  }));
2038
1812
  for (const label of labels.flat()) {
2039
- if (typeof label.name === "string" && label.name.trim())
1813
+ if (typeof label.name === 'string' && label.name.trim())
2040
1814
  names.add(label.name.trim());
2041
1815
  }
2042
1816
  }
@@ -2044,35 +1818,29 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2044
1818
  break;
2045
1819
  }
2046
1820
  const tags = [...names].sort((a, b) => a.localeCompare(b));
2047
- return resp.type("application/json").send({ tags });
1821
+ return resp.type('application/json').send({ tags });
2048
1822
  }
2049
1823
  catch (err) {
2050
- return resp
2051
- .status(400)
2052
- .type("application/json")
2053
- .send({ error: err.message });
1824
+ return resp.status(400).type('application/json').send({ error: err.message });
2054
1825
  }
2055
1826
  });
2056
1827
  // ==================== Azure DevOps pull request review ====================
2057
1828
  // Everything the PR sidebar list and the PR detail dialog need: identity,
2058
1829
  // listing, editing, votes, reviewers, auto-complete, completion, comment
2059
1830
  // threads, and per-file diffs.
2060
- app.get("/api/azure-devops/me", async (_req, resp) => {
1831
+ app.get('/api/azure-devops/me', async (_req, resp) => {
2061
1832
  try {
2062
- return resp.type("application/json").send(await azureMe());
1833
+ return resp.type('application/json').send(await azureMe());
2063
1834
  }
2064
1835
  catch (err) {
2065
- return resp
2066
- .status(400)
2067
- .type("application/json")
2068
- .send({ error: err.message });
1836
+ return resp.status(400).type('application/json').send({ error: err.message });
2069
1837
  }
2070
1838
  });
2071
1839
  // Pull requests the current user created or reviews ("mine"), merged and
2072
1840
  // de-duplicated. Status defaults to active.
2073
- app.get("/api/azure-devops/pullrequests", async (req, resp) => {
1841
+ app.get('/api/azure-devops/pullrequests', async (req, resp) => {
2074
1842
  try {
2075
- const status = String(req.query?.status ?? "active").trim() || "active";
1843
+ const status = String(req.query?.status ?? 'active').trim() || 'active';
2076
1844
  const { prefix, remoteUrl } = await azureProjectContext();
2077
1845
  const repo = parseAzureRemoteUrl(remoteUrl).repo;
2078
1846
  const me = await azureMe();
@@ -2082,7 +1850,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2082
1850
  const url = `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests` +
2083
1851
  `?searchCriteria.status=${encodeURIComponent(status)}&searchCriteria.${criteria}` +
2084
1852
  `&$top=100&$skip=${skip}&api-version=${AZURE_API_VERSION}`;
2085
- const data = await azureJson("GET", url);
1853
+ const data = await azureJson('GET', url);
2086
1854
  const requests = Array.isArray(data?.value) ? data.value : [];
2087
1855
  for (const request of requests) {
2088
1856
  const id = Number(request?.pullRequestId);
@@ -2103,20 +1871,17 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2103
1871
  const pullRequests = [...merged.values()]
2104
1872
  .map((request) => mapAzurePullRequest(request, prefix, remoteUrl, me))
2105
1873
  .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
2106
- return resp.type("application/json").send({ pullRequests });
1874
+ return resp.type('application/json').send({ pullRequests });
2107
1875
  }
2108
1876
  catch (err) {
2109
- return resp
2110
- .status(400)
2111
- .type("application/json")
2112
- .send({ error: err.message });
1877
+ return resp.status(400).type('application/json').send({ error: err.message });
2113
1878
  }
2114
1879
  });
2115
- app.get("/api/azure-devops/pullrequests/:id", async (req, resp) => {
1880
+ app.get('/api/azure-devops/pullrequests/:id', async (req, resp) => {
2116
1881
  try {
2117
1882
  const { base, prefix, remoteUrl } = await azurePrApiBase(Number(req.params?.id));
2118
1883
  const [pr, me] = await Promise.all([
2119
- azureJson("GET", `${base}?api-version=${AZURE_API_VERSION}`),
1884
+ azureJson('GET', `${base}?api-version=${AZURE_API_VERSION}`),
2120
1885
  azureMe(),
2121
1886
  ]);
2122
1887
  const summary = mapAzurePullRequest(pr, prefix, remoteUrl, me);
@@ -2124,44 +1889,41 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2124
1889
  // the labels resource explicitly in that case instead of reporting a
2125
1890
  // false empty tag list.
2126
1891
  let labels = (Array.isArray(pr?.labels) ? pr.labels : [])
2127
- .map((label) => String(label?.name ?? ""))
1892
+ .map((label) => String(label?.name ?? ''))
2128
1893
  .filter(Boolean);
2129
1894
  if (!Array.isArray(pr?.labels)) {
2130
- const labelsResult = await azureRequest("GET", `${base}/labels?api-version=${AZURE_API_VERSION}`, "");
1895
+ const labelsResult = await azureRequest('GET', `${base}/labels?api-version=${AZURE_API_VERSION}`, '');
2131
1896
  if (labelsResult.status < 200 || labelsResult.status >= 300) {
2132
1897
  throw new Error(azureErrorMessage(labelsResult) ||
2133
1898
  `Azure DevOps returned HTTP ${labelsResult.status} while reading pull request tags.`);
2134
1899
  }
2135
1900
  const labelData = JSON.parse(labelsResult.body);
2136
1901
  labels = (Array.isArray(labelData) ? labelData : (labelData.value ?? []))
2137
- .map((label) => String(label?.name ?? ""))
1902
+ .map((label) => String(label?.name ?? ''))
2138
1903
  .filter(Boolean);
2139
1904
  }
2140
- return resp.type("application/json").send({
1905
+ return resp.type('application/json').send({
2141
1906
  ...summary,
2142
- description: String(pr?.description ?? ""),
1907
+ description: String(pr?.description ?? ''),
2143
1908
  autoCompleteSetBy: pr?.autoCompleteSetBy
2144
1909
  ? {
2145
- id: String(pr.autoCompleteSetBy.id ?? ""),
2146
- name: String(pr.autoCompleteSetBy.displayName ?? ""),
1910
+ id: String(pr.autoCompleteSetBy.id ?? ''),
1911
+ name: String(pr.autoCompleteSetBy.displayName ?? ''),
2147
1912
  }
2148
1913
  : null,
2149
1914
  completionOptions: pr?.completionOptions ?? null,
2150
- lastMergeSourceCommit: String(pr?.lastMergeSourceCommit?.commitId ?? ""),
2151
- lastMergeTargetCommit: String(pr?.lastMergeTargetCommit?.commitId ?? ""),
1915
+ lastMergeSourceCommit: String(pr?.lastMergeSourceCommit?.commitId ?? ''),
1916
+ lastMergeTargetCommit: String(pr?.lastMergeTargetCommit?.commitId ?? ''),
2152
1917
  labels,
2153
1918
  mergeStatus: azureMergeStatus(pr),
2154
1919
  });
2155
1920
  }
2156
1921
  catch (err) {
2157
- return resp
2158
- .status(400)
2159
- .type("application/json")
2160
- .send({ error: err.message });
1922
+ return resp.status(400).type('application/json').send({ error: err.message });
2161
1923
  }
2162
1924
  });
2163
1925
  // Edits title/description or flips the draft flag ("publish"/"convert to draft").
2164
- app.patch("/api/azure-devops/pullrequests/:id", async (req, resp) => {
1926
+ app.patch('/api/azure-devops/pullrequests/:id', async (req, resp) => {
2165
1927
  try {
2166
1928
  const { title, description, isDraft } = req.body ?? {};
2167
1929
  const payload = {};
@@ -2172,83 +1934,67 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2172
1934
  if (isDraft !== undefined)
2173
1935
  payload.isDraft = isDraft === true;
2174
1936
  if (!Object.keys(payload).length) {
2175
- return resp
2176
- .status(400)
2177
- .type("application/json")
2178
- .send({ error: "Nothing to update." });
1937
+ return resp.status(400).type('application/json').send({ error: 'Nothing to update.' });
2179
1938
  }
2180
1939
  const { base } = await azurePrApiBase(Number(req.params?.id));
2181
- await azureJson("PATCH", `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
2182
- return resp.type("application/json").send({ ok: true });
1940
+ await azureJson('PATCH', `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
1941
+ return resp.type('application/json').send({ ok: true });
2183
1942
  }
2184
1943
  catch (err) {
2185
- return resp
2186
- .status(400)
2187
- .type("application/json")
2188
- .send({ error: err.message });
1944
+ return resp.status(400).type('application/json').send({ error: err.message });
2189
1945
  }
2190
1946
  });
2191
1947
  // Records the current user's vote on the pull request.
2192
- app.post("/api/azure-devops/pullrequests/:id/vote", async (req, resp) => {
1948
+ app.post('/api/azure-devops/pullrequests/:id/vote', async (req, resp) => {
2193
1949
  try {
2194
- const vote = String(req.body?.vote ?? "");
1950
+ const vote = String(req.body?.vote ?? '');
2195
1951
  if (!(vote in AZURE_VOTES)) {
2196
1952
  return resp
2197
1953
  .status(400)
2198
- .type("application/json")
1954
+ .type('application/json')
2199
1955
  .send({ error: `Unknown vote "${vote}".` });
2200
1956
  }
2201
1957
  const { base } = await azurePrApiBase(Number(req.params?.id));
2202
1958
  const me = await azureMe();
2203
- await azureJson("PUT", `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote] }));
2204
- return resp.type("application/json").send({ ok: true });
1959
+ await azureJson('PUT', `${base}/reviewers/${encodeURIComponent(me.id)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ vote: AZURE_VOTES[vote] }));
1960
+ return resp.type('application/json').send({ ok: true });
2205
1961
  }
2206
1962
  catch (err) {
2207
- return resp
2208
- .status(400)
2209
- .type("application/json")
2210
- .send({ error: err.message });
1963
+ return resp.status(400).type('application/json').send({ error: err.message });
2211
1964
  }
2212
1965
  });
2213
1966
  // Adds or updates a reviewer (optionally required), or removes one.
2214
- app.post("/api/azure-devops/pullrequests/:id/reviewers", async (req, resp) => {
1967
+ app.post('/api/azure-devops/pullrequests/:id/reviewers', async (req, resp) => {
2215
1968
  try {
2216
- const id = String(req.body?.id ?? "");
1969
+ const id = String(req.body?.id ?? '');
2217
1970
  if (!id) {
2218
- return resp
2219
- .status(400)
2220
- .type("application/json")
2221
- .send({ error: "Reviewer id required." });
1971
+ return resp.status(400).type('application/json').send({ error: 'Reviewer id required.' });
2222
1972
  }
2223
1973
  const { base } = await azurePrApiBase(Number(req.params?.id));
2224
1974
  const reviewerUrl = `${base}/reviewers/${encodeURIComponent(id)}?api-version=${AZURE_API_VERSION}`;
2225
1975
  if (req.body?.remove === true) {
2226
- await azureJson("DELETE", reviewerUrl);
2227
- return resp.type("application/json").send({ ok: true });
1976
+ await azureJson('DELETE', reviewerUrl);
1977
+ return resp.type('application/json').send({ ok: true });
2228
1978
  }
2229
1979
  // The PUT replaces the reviewer record, so an existing vote is passed
2230
1980
  // through to survive an isRequired toggle.
2231
1981
  const payload = {
2232
1982
  isRequired: req.body?.required === true,
2233
1983
  };
2234
- if (req.body?.vote !== undefined &&
2235
- Number.isFinite(Number(req.body.vote))) {
1984
+ if (req.body?.vote !== undefined && Number.isFinite(Number(req.body.vote))) {
2236
1985
  payload.vote = Number(req.body.vote);
2237
1986
  }
2238
- await azureJson("PUT", reviewerUrl, JSON.stringify(payload));
2239
- return resp.type("application/json").send({ ok: true });
1987
+ await azureJson('PUT', reviewerUrl, JSON.stringify(payload));
1988
+ return resp.type('application/json').send({ ok: true });
2240
1989
  }
2241
1990
  catch (err) {
2242
- return resp
2243
- .status(400)
2244
- .type("application/json")
2245
- .send({ error: err.message });
1991
+ return resp.status(400).type('application/json').send({ error: err.message });
2246
1992
  }
2247
1993
  });
2248
1994
  /** Maps dialog completion options onto the Azure DevOps payload shape. */
2249
1995
  const completionOptionsOf = (body) => {
2250
- const strategies = ["noFastForward", "squash", "rebase", "rebaseMerge"];
2251
- const mergeStrategy = String(body?.mergeStrategy ?? "");
1996
+ const strategies = ['noFastForward', 'squash', 'rebase', 'rebaseMerge'];
1997
+ const mergeStrategy = String(body?.mergeStrategy ?? '');
2252
1998
  const options = {};
2253
1999
  if (strategies.includes(mergeStrategy))
2254
2000
  options.mergeStrategy = mergeStrategy;
@@ -2265,7 +2011,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2265
2011
  return Object.keys(options).length ? options : null;
2266
2012
  };
2267
2013
  // Sets or clears auto-complete with optional completion options.
2268
- app.post("/api/azure-devops/pullrequests/:id/autocomplete", async (req, resp) => {
2014
+ app.post('/api/azure-devops/pullrequests/:id/autocomplete', async (req, resp) => {
2269
2015
  try {
2270
2016
  const enabled = req.body?.enabled === true;
2271
2017
  const { base } = await azurePrApiBase(Number(req.params?.id));
@@ -2285,188 +2031,157 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2285
2031
  // Azure DevOps clears auto-complete for an empty identity; some Server
2286
2032
  // releases only accept an explicit null, so retry that shape on failure.
2287
2033
  try {
2288
- await azureJson("PATCH", url, buildPayload(enabled && me ? { id: me.id } : { id: "" }));
2034
+ await azureJson('PATCH', url, buildPayload(enabled && me ? { id: me.id } : { id: '' }));
2289
2035
  }
2290
2036
  catch (firstError) {
2291
2037
  if (enabled)
2292
2038
  throw firstError;
2293
- await azureJson("PATCH", url, buildPayload(null));
2039
+ await azureJson('PATCH', url, buildPayload(null));
2294
2040
  }
2295
- return resp.type("application/json").send({ ok: true });
2041
+ return resp.type('application/json').send({ ok: true });
2296
2042
  }
2297
2043
  catch (err) {
2298
- return resp
2299
- .status(400)
2300
- .type("application/json")
2301
- .send({ error: err.message });
2044
+ return resp.status(400).type('application/json').send({ error: err.message });
2302
2045
  }
2303
2046
  });
2304
2047
  // Completes the pull request (merge). Requires the merge source commit for
2305
2048
  // optimistic concurrency, exactly like the Azure DevOps web dialog.
2306
- app.post("/api/azure-devops/pullrequests/:id/complete", async (req, resp) => {
2049
+ app.post('/api/azure-devops/pullrequests/:id/complete', async (req, resp) => {
2307
2050
  try {
2308
2051
  const { base } = await azurePrApiBase(Number(req.params?.id));
2309
- const pr = await azureJson("GET", `${base}?api-version=${AZURE_API_VERSION}`);
2310
- const commitId = String(pr?.lastMergeSourceCommit?.commitId ?? "");
2052
+ const pr = await azureJson('GET', `${base}?api-version=${AZURE_API_VERSION}`);
2053
+ const commitId = String(pr?.lastMergeSourceCommit?.commitId ?? '');
2311
2054
  if (!commitId) {
2312
- throw new Error("The pull request has no merge commit to complete yet.");
2055
+ throw new Error('The pull request has no merge commit to complete yet.');
2313
2056
  }
2314
2057
  const payload = {
2315
- status: "completed",
2058
+ status: 'completed',
2316
2059
  lastMergeSourceCommit: { commitId },
2317
2060
  };
2318
2061
  const options = completionOptionsOf(req.body);
2319
2062
  if (options)
2320
2063
  payload.completionOptions = options;
2321
- await azureJson("PATCH", `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
2322
- return resp.type("application/json").send({ ok: true });
2064
+ await azureJson('PATCH', `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
2065
+ return resp.type('application/json').send({ ok: true });
2323
2066
  }
2324
2067
  catch (err) {
2325
- return resp
2326
- .status(400)
2327
- .type("application/json")
2328
- .send({ error: err.message });
2068
+ return resp.status(400).type('application/json').send({ error: err.message });
2329
2069
  }
2330
2070
  });
2331
- app.get("/api/azure-devops/pullrequests/:id/threads", async (req, resp) => {
2071
+ app.get('/api/azure-devops/pullrequests/:id/threads', async (req, resp) => {
2332
2072
  try {
2333
2073
  const { base } = await azurePrApiBase(Number(req.params?.id));
2334
- const data = await azureJson("GET", `${base}/threads?api-version=${AZURE_API_VERSION}`);
2074
+ const data = await azureJson('GET', `${base}/threads?api-version=${AZURE_API_VERSION}`);
2335
2075
  const threads = (Array.isArray(data?.value) ? data.value : [])
2336
2076
  .filter((thread) => thread?.isDeleted !== true)
2337
2077
  .map((thread) => {
2338
2078
  const context = thread?.threadContext ?? {};
2339
- const line = context.rightFileStart?.line ??
2340
- context.leftFileStart?.line ??
2341
- null;
2079
+ const line = context.rightFileStart?.line ?? context.leftFileStart?.line ?? null;
2342
2080
  return {
2343
2081
  id: Number(thread?.id ?? 0),
2344
2082
  status: azureThreadStatusName(Number(thread?.status ?? 0)),
2345
2083
  isDeleted: false,
2346
- filePath: typeof context.filePath === "string"
2347
- ? context.filePath.replace(/^\//, "")
2348
- : null,
2084
+ filePath: typeof context.filePath === 'string' ? context.filePath.replace(/^\//, '') : null,
2349
2085
  line: Number.isInteger(line) ? line : null,
2350
- side: context.rightFileStart
2351
- ? "right"
2352
- : context.leftFileStart
2353
- ? "left"
2354
- : null,
2086
+ side: context.rightFileStart ? 'right' : context.leftFileStart ? 'left' : null,
2355
2087
  comments: (Array.isArray(thread?.comments) ? thread.comments : [])
2356
2088
  .filter((comment) => comment?.isDeleted !== true)
2357
2089
  .map((comment) => ({
2358
2090
  id: Number(comment?.id ?? 0),
2359
2091
  author: {
2360
- name: String(comment?.author?.displayName ?? ""),
2361
- email: String(comment?.author?.uniqueName ?? ""),
2092
+ name: String(comment?.author?.displayName ?? ''),
2093
+ email: String(comment?.author?.uniqueName ?? ''),
2362
2094
  },
2363
- content: String(comment?.content ?? ""),
2364
- createdAt: String(comment?.publishedDate ?? ""),
2095
+ content: String(comment?.content ?? ''),
2096
+ createdAt: String(comment?.publishedDate ?? ''),
2365
2097
  isDeleted: false,
2366
2098
  })),
2367
2099
  };
2368
2100
  });
2369
- return resp.type("application/json").send({ threads });
2101
+ return resp.type('application/json').send({ threads });
2370
2102
  }
2371
2103
  catch (err) {
2372
- return resp
2373
- .status(400)
2374
- .type("application/json")
2375
- .send({ error: err.message });
2104
+ return resp.status(400).type('application/json').send({ error: err.message });
2376
2105
  }
2377
2106
  });
2378
2107
  // Adds a comment: a reply to a thread, a general PR comment, or an inline
2379
2108
  // comment anchored to a file line (thread context).
2380
- app.post("/api/azure-devops/pullrequests/:id/threads", async (req, resp) => {
2109
+ app.post('/api/azure-devops/pullrequests/:id/threads', async (req, resp) => {
2381
2110
  try {
2382
- const content = String(req.body?.content ?? "").trim();
2111
+ const content = String(req.body?.content ?? '').trim();
2383
2112
  if (!content) {
2384
- return resp
2385
- .status(400)
2386
- .type("application/json")
2387
- .send({ error: "Comment text required." });
2113
+ return resp.status(400).type('application/json').send({ error: 'Comment text required.' });
2388
2114
  }
2389
2115
  const { base } = await azurePrApiBase(Number(req.params?.id));
2390
2116
  const threadId = Number(req.body?.threadId ?? 0);
2391
2117
  if (Number.isInteger(threadId) && threadId > 0) {
2392
- await azureJson("POST", `${base}/threads/${threadId}/comments?api-version=${AZURE_API_VERSION}`, JSON.stringify({ content }));
2393
- return resp.type("application/json").send({ ok: true });
2118
+ await azureJson('POST', `${base}/threads/${threadId}/comments?api-version=${AZURE_API_VERSION}`, JSON.stringify({ content }));
2119
+ return resp.type('application/json').send({ ok: true });
2394
2120
  }
2395
2121
  const payload = {
2396
2122
  status: AZURE_THREAD_STATUSES.active,
2397
2123
  comments: [{ content, parentCommentId: 0 }],
2398
2124
  };
2399
- const filePath = String(req.body?.filePath ?? "").trim();
2125
+ const filePath = String(req.body?.filePath ?? '').trim();
2400
2126
  const line = Number(req.body?.line ?? 0);
2401
2127
  const requestedEndLine = Number(req.body?.endLine ?? line);
2402
- const endLine = Number.isInteger(requestedEndLine) && requestedEndLine >= line
2403
- ? requestedEndLine
2404
- : line;
2405
- const left = req.body?.side === "left";
2128
+ const endLine = Number.isInteger(requestedEndLine) && requestedEndLine >= line ? requestedEndLine : line;
2129
+ const left = req.body?.side === 'left';
2406
2130
  if (filePath && Number.isInteger(line) && line > 0) {
2407
2131
  // Azure anchors a whole-line comment from offset 1 to "end of line".
2408
2132
  payload.threadContext = {
2409
- filePath: `/${filePath.replace(/^\//, "")}`,
2410
- [left ? "leftFileStart" : "rightFileStart"]: { line, offset: 1 },
2411
- [left ? "leftFileEnd" : "rightFileEnd"]: {
2133
+ filePath: `/${filePath.replace(/^\//, '')}`,
2134
+ [left ? 'leftFileStart' : 'rightFileStart']: { line, offset: 1 },
2135
+ [left ? 'leftFileEnd' : 'rightFileEnd']: {
2412
2136
  line: endLine,
2413
2137
  offset: 2147483647,
2414
2138
  },
2415
2139
  };
2416
2140
  }
2417
- await azureJson("POST", `${base}/threads?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
2418
- return resp.type("application/json").send({ ok: true });
2141
+ await azureJson('POST', `${base}/threads?api-version=${AZURE_API_VERSION}`, JSON.stringify(payload));
2142
+ return resp.type('application/json').send({ ok: true });
2419
2143
  }
2420
2144
  catch (err) {
2421
- return resp
2422
- .status(400)
2423
- .type("application/json")
2424
- .send({ error: err.message });
2145
+ return resp.status(400).type('application/json').send({ error: err.message });
2425
2146
  }
2426
2147
  });
2427
2148
  // Resolves, reactivates or closes a comment thread.
2428
- app.post("/api/azure-devops/pullrequests/:id/threads/:threadId/status", async (req, resp) => {
2149
+ app.post('/api/azure-devops/pullrequests/:id/threads/:threadId/status', async (req, resp) => {
2429
2150
  try {
2430
- const status = String(req.body?.status ?? "");
2431
- if (!(status in AZURE_THREAD_STATUSES) || status === "unknown") {
2151
+ const status = String(req.body?.status ?? '');
2152
+ if (!(status in AZURE_THREAD_STATUSES) || status === 'unknown') {
2432
2153
  return resp
2433
2154
  .status(400)
2434
- .type("application/json")
2155
+ .type('application/json')
2435
2156
  .send({ error: `Unknown thread status "${status}".` });
2436
2157
  }
2437
2158
  const { base } = await azurePrApiBase(Number(req.params?.id));
2438
- await azureJson("PATCH", `${base}/threads/${Number(req.params?.threadId)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ status: AZURE_THREAD_STATUSES[status] }));
2439
- return resp.type("application/json").send({ ok: true });
2159
+ await azureJson('PATCH', `${base}/threads/${Number(req.params?.threadId)}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ status: AZURE_THREAD_STATUSES[status] }));
2160
+ return resp.type('application/json').send({ ok: true });
2440
2161
  }
2441
2162
  catch (err) {
2442
- return resp
2443
- .status(400)
2444
- .type("application/json")
2445
- .send({ error: err.message });
2163
+ return resp.status(400).type('application/json').send({ error: err.message });
2446
2164
  }
2447
2165
  });
2448
2166
  // Changed files of the latest PR iteration (names and change types only;
2449
2167
  // content is fetched per file on demand by /file-diff).
2450
- app.get("/api/azure-devops/pullrequests/:id/changes", async (req, resp) => {
2168
+ app.get('/api/azure-devops/pullrequests/:id/changes', async (req, resp) => {
2451
2169
  try {
2452
2170
  const { base } = await azurePrApiBase(Number(req.params?.id));
2453
- const iterations = await azureJson("GET", `${base}/iterations?api-version=${AZURE_API_VERSION}`);
2171
+ const iterations = await azureJson('GET', `${base}/iterations?api-version=${AZURE_API_VERSION}`);
2454
2172
  const list = Array.isArray(iterations?.value) ? iterations.value : [];
2455
2173
  const latest = list.length
2456
- ? list.reduce((newest, entry) => String(entry?.createdDate ?? "") >
2457
- String(newest?.createdDate ?? "")
2458
- ? entry
2459
- : newest)
2174
+ ? list.reduce((newest, entry) => String(entry?.createdDate ?? '') > String(newest?.createdDate ?? '') ? entry : newest)
2460
2175
  : null;
2461
2176
  if (!latest) {
2462
- return resp.type("application/json").send({ files: [] });
2177
+ return resp.type('application/json').send({ files: [] });
2463
2178
  }
2464
2179
  const changeStatus = {
2465
- add: "added",
2466
- edit: "modified",
2467
- rename: "renamed",
2468
- delete: "deleted",
2469
- undelete: "added",
2180
+ add: 'added',
2181
+ edit: 'modified',
2182
+ rename: 'renamed',
2183
+ delete: 'deleted',
2184
+ undelete: 'added',
2470
2185
  };
2471
2186
  // Azure returns the changed files under "changeEntries" and pages
2472
2187
  // them (nextSkip); some releases use "value" instead. Follow the
@@ -2475,7 +2190,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2475
2190
  const entries = [];
2476
2191
  let skip = 0;
2477
2192
  for (;;) {
2478
- const data = await azureJson("GET", `${base}/iterations/${latest.id}/changes?$top=1000&$skip=${skip}&api-version=${AZURE_API_VERSION}`);
2193
+ const data = await azureJson('GET', `${base}/iterations/${latest.id}/changes?$top=1000&$skip=${skip}&api-version=${AZURE_API_VERSION}`);
2479
2194
  const page = Array.isArray(data?.changeEntries)
2480
2195
  ? data.changeEntries
2481
2196
  : Array.isArray(data?.value)
@@ -2496,55 +2211,52 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2496
2211
  const files = entries
2497
2212
  .filter((entry) => entry?.item?.isFolder !== true)
2498
2213
  .map((entry) => {
2499
- const path = String(entry?.item?.path ?? "").replace(/^\//, "");
2500
- const changeType = changeStatus[String(entry?.changeType ?? "")] ?? "modified";
2501
- const oldPath = changeType === "renamed"
2502
- ? String(entry?.sourceServerItem ?? "").replace(/^\//, "")
2503
- : "";
2214
+ const path = String(entry?.item?.path ?? '').replace(/^\//, '');
2215
+ const changeType = changeStatus[String(entry?.changeType ?? '')] ?? 'modified';
2216
+ const oldPath = changeType === 'renamed'
2217
+ ? String(entry?.sourceServerItem ?? '').replace(/^\//, '')
2218
+ : '';
2504
2219
  return { path, oldPath, changeType };
2505
2220
  })
2506
2221
  .filter((file) => Boolean(file.path));
2507
- return resp.type("application/json").send({ files });
2222
+ return resp.type('application/json').send({ files });
2508
2223
  }
2509
2224
  catch (err) {
2510
- return resp
2511
- .status(400)
2512
- .type("application/json")
2513
- .send({ error: err.message });
2225
+ return resp.status(400).type('application/json').send({ error: err.message });
2514
2226
  }
2515
2227
  });
2516
2228
  /** Maps a policy evaluation status state onto the four UI states. */
2517
2229
  const azurePolicyState = (state) => {
2518
- switch (String(state ?? "").toLowerCase()) {
2519
- case "approved":
2520
- return "succeeded";
2521
- case "rejected":
2522
- case "broken":
2523
- return "failed";
2524
- case "notapplicable":
2525
- return "notApplicable";
2230
+ switch (String(state ?? '').toLowerCase()) {
2231
+ case 'approved':
2232
+ return 'succeeded';
2233
+ case 'rejected':
2234
+ case 'broken':
2235
+ return 'failed';
2236
+ case 'notapplicable':
2237
+ return 'notApplicable';
2526
2238
  default:
2527
2239
  // pending, deferred, queued and unknown states all still gate merges.
2528
- return "pending";
2240
+ return 'pending';
2529
2241
  }
2530
2242
  };
2531
2243
  /** Maps a native PR status state onto the four UI states. */
2532
2244
  const azureStatusState = (state) => {
2533
- switch (String(state ?? "").toLowerCase()) {
2534
- case "succeeded":
2535
- return "succeeded";
2536
- case "failed":
2537
- case "error":
2538
- return "failed";
2245
+ switch (String(state ?? '').toLowerCase()) {
2246
+ case 'succeeded':
2247
+ return 'succeeded';
2248
+ case 'failed':
2249
+ case 'error':
2250
+ return 'failed';
2539
2251
  default:
2540
2252
  // notSet and pending: posted but undecided.
2541
- return "pending";
2253
+ return 'pending';
2542
2254
  }
2543
2255
  };
2544
2256
  /** Reads every policy evaluation of one pull request (paged). */
2545
2257
  const fetchPolicyEvaluations = async (prefix, projectId, prId) => {
2546
2258
  if (!projectId || !prId) {
2547
- throw new Error("The pull request does not expose its project identity.");
2259
+ throw new Error('The pull request does not expose its project identity.');
2548
2260
  }
2549
2261
  // The artifact id Azure keys policy evaluations by. The documented
2550
2262
  // template is "CodeReviewId"; the "CodeReviewIdentity" spelling is
@@ -2553,7 +2265,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2553
2265
  const evaluations = [];
2554
2266
  let skip = 0;
2555
2267
  for (;;) {
2556
- const data = await azureJson("GET", `${prefix}/_apis/policy/evaluations?artifactId=${encodeURIComponent(artifactId)}` +
2268
+ const data = await azureJson('GET', `${prefix}/_apis/policy/evaluations?artifactId=${encodeURIComponent(artifactId)}` +
2557
2269
  `&$skip=${skip}&$top=1000&api-version=5.0-preview.1`);
2558
2270
  // Versions disagree on the envelope: "evaluations" (preview shape), the
2559
2271
  // generic "count"/"value" list (seen live on TFS 5.0-preview.1) or a
@@ -2579,10 +2291,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2579
2291
  if (!(moreByCount || moreBySkip) || evaluations.length === before) {
2580
2292
  break;
2581
2293
  }
2582
- skip =
2583
- Number.isInteger(nextSkip) && nextSkip > skip
2584
- ? nextSkip
2585
- : evaluations.length;
2294
+ skip = Number.isInteger(nextSkip) && nextSkip > skip ? nextSkip : evaluations.length;
2586
2295
  if (skip > 20000) {
2587
2296
  break;
2588
2297
  }
@@ -2591,7 +2300,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2591
2300
  };
2592
2301
  /** Reads the native statuses posted directly on the pull request. */
2593
2302
  const fetchPullRequestStatuses = async (base) => {
2594
- const data = await azureJson("GET", `${base}/statuses?api-version=5.0-preview.1`);
2303
+ const data = await azureJson('GET', `${base}/statuses?api-version=5.0-preview.1`);
2595
2304
  return Array.isArray(data?.value) ? data.value : [];
2596
2305
  };
2597
2306
  /** Turns one policy evaluation into a check row (null when unnameable). */
@@ -2603,176 +2312,152 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2603
2312
  const status = evaluation?.status;
2604
2313
  // 5.0-preview answers status as a plain state string; newer versions may
2605
2314
  // answer a { state, message } object. Support both.
2606
- const state = typeof status === "string" ? status : status?.state;
2607
- const message = typeof status === "string" ? "" : status?.message;
2608
- const typeName = String(type?.displayName ?? "").trim();
2315
+ const state = typeof status === 'string' ? status : status?.state;
2316
+ const message = typeof status === 'string' ? '' : status?.message;
2317
+ const typeName = String(type?.displayName ?? '').trim();
2609
2318
  const isBuild = /build/i.test(typeName);
2610
- const name = String(settings?.displayName ?? "").trim() ||
2611
- (isBuild ? String(context?.buildDefinitionName ?? "").trim() : "") ||
2319
+ const name = String(settings?.displayName ?? '').trim() ||
2320
+ (isBuild ? String(context?.buildDefinitionName ?? '').trim() : '') ||
2612
2321
  typeName ||
2613
- "Policy";
2322
+ 'Policy';
2614
2323
  if (!name) {
2615
2324
  return null;
2616
2325
  }
2617
2326
  const gate = `${typeName} ${name}`;
2618
2327
  const buildId = Number(context?.buildId ?? settings?.buildId);
2619
- const evaluationId = String(evaluation?.evaluationId ?? status?.evaluationId ?? "").trim();
2328
+ const evaluationId = String(evaluation?.evaluationId ?? status?.evaluationId ?? '').trim();
2620
2329
  const check = {
2621
2330
  id: `policy:${evaluationId || index}`,
2622
2331
  name,
2623
- kind: /build/i.test(gate)
2624
- ? "build"
2625
- : /reviewer/i.test(gate)
2626
- ? "reviewer"
2627
- : "policy",
2332
+ kind: /build/i.test(gate) ? 'build' : /reviewer/i.test(gate) ? 'reviewer' : 'policy',
2628
2333
  state: azurePolicyState(state),
2629
- required: configuration?.isBlocking === true ||
2630
- configuration?.isRequired === true,
2334
+ required: configuration?.isBlocking === true || configuration?.isRequired === true,
2631
2335
  };
2632
- const detail = String(message ?? "").trim();
2336
+ const detail = String(message ?? '').trim();
2633
2337
  if (detail) {
2634
2338
  check.detail = detail;
2635
2339
  }
2636
2340
  if (context?.isExpired === true) {
2637
- check.detail = detail ? `${detail} (build expired)` : "Build expired";
2341
+ check.detail = detail ? `${detail} (build expired)` : 'Build expired';
2638
2342
  }
2639
2343
  if (Number.isInteger(buildId) && buildId > 0) {
2640
2344
  check.url = `${prefix}/_build/results?buildId=${buildId}`;
2641
2345
  }
2642
2346
  // Only build gates can be re-run, and only through their evaluation id.
2643
- if (check.kind === "build" && evaluationId) {
2347
+ if (check.kind === 'build' && evaluationId) {
2644
2348
  check.evaluationId = evaluationId;
2645
2349
  }
2646
2350
  return check;
2647
2351
  };
2648
2352
  // Related work items of the pull request, resolved to title/state through
2649
2353
  // the work-item batch API. Unresolvable refs degrade to an id-only row.
2650
- app.get("/api/azure-devops/pullrequests/:id/workitems", async (req, resp) => {
2354
+ app.get('/api/azure-devops/pullrequests/:id/workitems', async (req, resp) => {
2651
2355
  try {
2652
2356
  const { base, prefix } = await azurePrApiBase(Number(req.params?.id));
2653
- const refs = await azureJson("GET", `${base}/workitems?api-version=${AZURE_API_VERSION}`);
2357
+ const refs = await azureJson('GET', `${base}/workitems?api-version=${AZURE_API_VERSION}`);
2654
2358
  const ids = [
2655
2359
  ...new Set((Array.isArray(refs?.value) ? refs.value : [])
2656
2360
  .map((entry) => Number(entry?.id))
2657
2361
  .filter((id) => Number.isInteger(id) && id > 0)),
2658
2362
  ];
2659
2363
  if (!ids.length) {
2660
- return resp.type("application/json").send({ workItems: [] });
2364
+ return resp.type('application/json').send({ workItems: [] });
2661
2365
  }
2662
- const batch = await azureRequest("GET", `${prefix}/_apis/wit/workitems?ids=${ids.join(",")}` +
2663
- `&fields=System.Id,System.Title,System.State&api-version=${AZURE_API_VERSION}`, "");
2366
+ const batch = await azureRequest('GET', `${prefix}/_apis/wit/workitems?ids=${ids.join(',')}` +
2367
+ `&fields=System.Id,System.Title,System.State&api-version=${AZURE_API_VERSION}`, '');
2664
2368
  const details = new Map();
2665
2369
  if (batch.status >= 200 && batch.status < 300) {
2666
2370
  const data = JSON.parse(batch.body);
2667
2371
  for (const item of Array.isArray(data?.value) ? data.value : []) {
2668
- details.set(String(item?.id ?? ""), {
2669
- title: String(item?.fields?.["System.Title"] ?? ""),
2670
- state: String(item?.fields?.["System.State"] ?? ""),
2372
+ details.set(String(item?.id ?? ''), {
2373
+ title: String(item?.fields?.['System.Title'] ?? ''),
2374
+ state: String(item?.fields?.['System.State'] ?? ''),
2671
2375
  });
2672
2376
  }
2673
2377
  }
2674
2378
  const workItems = ids.map((id) => ({
2675
2379
  id,
2676
2380
  title: details.get(String(id))?.title || `Work item ${id}`,
2677
- state: details.get(String(id))?.state ?? "",
2381
+ state: details.get(String(id))?.state ?? '',
2678
2382
  url: `${prefix}/_workitems/edit/${id}`,
2679
2383
  }));
2680
- return resp.type("application/json").send({ workItems });
2384
+ return resp.type('application/json').send({ workItems });
2681
2385
  }
2682
2386
  catch (err) {
2683
- return resp
2684
- .status(400)
2685
- .type("application/json")
2686
- .send({ error: err.message });
2387
+ return resp.status(400).type('application/json').send({ error: err.message });
2687
2388
  }
2688
2389
  });
2689
2390
  // Tags are the PR labels resource; the side panel adds and removes them
2690
2391
  // directly, so both directions are served here.
2691
- app.post("/api/azure-devops/pullrequests/:id/labels", async (req, resp) => {
2392
+ app.post('/api/azure-devops/pullrequests/:id/labels', async (req, resp) => {
2692
2393
  try {
2693
- const name = String(req.body?.name ?? "").trim();
2394
+ const name = String(req.body?.name ?? '').trim();
2694
2395
  if (!name) {
2695
- return resp
2696
- .status(400)
2697
- .type("application/json")
2698
- .send({ error: "A tag name is required." });
2396
+ return resp.status(400).type('application/json').send({ error: 'A tag name is required.' });
2699
2397
  }
2700
2398
  const { base } = await azurePrApiBase(Number(req.params?.id));
2701
- await azureJson("POST", `${base}/labels?api-version=${AZURE_API_VERSION}`, JSON.stringify({ name }));
2702
- return resp.type("application/json").send({ success: true });
2399
+ await azureJson('POST', `${base}/labels?api-version=${AZURE_API_VERSION}`, JSON.stringify({ name }));
2400
+ return resp.type('application/json').send({ success: true });
2703
2401
  }
2704
2402
  catch (err) {
2705
- return resp
2706
- .status(400)
2707
- .type("application/json")
2708
- .send({ error: err.message });
2403
+ return resp.status(400).type('application/json').send({ error: err.message });
2709
2404
  }
2710
2405
  });
2711
- app.delete("/api/azure-devops/pullrequests/:id/labels/:name", async (req, resp) => {
2406
+ app.delete('/api/azure-devops/pullrequests/:id/labels/:name', async (req, resp) => {
2712
2407
  try {
2713
- const name = String(req.params?.name ?? "");
2408
+ const name = String(req.params?.name ?? '');
2714
2409
  if (!name) {
2715
- return resp
2716
- .status(400)
2717
- .type("application/json")
2718
- .send({ error: "A tag name is required." });
2410
+ return resp.status(400).type('application/json').send({ error: 'A tag name is required.' });
2719
2411
  }
2720
2412
  const { base } = await azurePrApiBase(Number(req.params?.id));
2721
- await azureJson("DELETE", `${base}/labels/${encodeURIComponent(name)}?api-version=${AZURE_API_VERSION}`, "");
2722
- return resp.type("application/json").send({ success: true });
2413
+ await azureJson('DELETE', `${base}/labels/${encodeURIComponent(name)}?api-version=${AZURE_API_VERSION}`, '');
2414
+ return resp.type('application/json').send({ success: true });
2723
2415
  }
2724
2416
  catch (err) {
2725
- return resp
2726
- .status(400)
2727
- .type("application/json")
2728
- .send({ error: err.message });
2417
+ return resp.status(400).type('application/json').send({ error: err.message });
2729
2418
  }
2730
2419
  });
2731
2420
  // Work items are linked from the work item side: the link lives on the work
2732
2421
  // item as an ArtifactLink relation pointing at the PR's artifact URI, so
2733
2422
  // linking and unlinking both patch the work item rather than the PR.
2734
- app.post("/api/azure-devops/pullrequests/:id/workitems", async (req, resp) => {
2423
+ app.post('/api/azure-devops/pullrequests/:id/workitems', async (req, resp) => {
2735
2424
  try {
2736
2425
  const workItemId = Number(req.body?.id);
2737
2426
  if (!Number.isInteger(workItemId) || workItemId <= 0) {
2738
2427
  return resp
2739
2428
  .status(400)
2740
- .type("application/json")
2741
- .send({ error: "A work item id is required." });
2429
+ .type('application/json')
2430
+ .send({ error: 'A work item id is required.' });
2742
2431
  }
2743
2432
  const { prefix, artifactUrl } = await azurePrArtifactOf(Number(req.params?.id));
2744
- const alreadyLinked = (await azureWorkItemRelations(prefix, workItemId)).some((relation) => String(relation?.url ?? "").toLowerCase() ===
2745
- artifactUrl.toLowerCase());
2433
+ const alreadyLinked = (await azureWorkItemRelations(prefix, workItemId)).some((relation) => String(relation?.url ?? '').toLowerCase() === artifactUrl.toLowerCase());
2746
2434
  if (!alreadyLinked) {
2747
2435
  await azurePatchWorkItemRelations(prefix, workItemId, [
2748
2436
  {
2749
- op: "add",
2750
- path: "/relations/-",
2437
+ op: 'add',
2438
+ path: '/relations/-',
2751
2439
  value: {
2752
- rel: "ArtifactLink",
2440
+ rel: 'ArtifactLink',
2753
2441
  url: artifactUrl,
2754
- attributes: { name: "Pull Request" },
2442
+ attributes: { name: 'Pull Request' },
2755
2443
  },
2756
2444
  },
2757
2445
  ]);
2758
2446
  }
2759
- return resp.type("application/json").send({ success: true });
2447
+ return resp.type('application/json').send({ success: true });
2760
2448
  }
2761
2449
  catch (err) {
2762
- return resp
2763
- .status(400)
2764
- .type("application/json")
2765
- .send({ error: err.message });
2450
+ return resp.status(400).type('application/json').send({ error: err.message });
2766
2451
  }
2767
2452
  });
2768
- app.delete("/api/azure-devops/pullrequests/:id/workitems/:workItemId", async (req, resp) => {
2453
+ app.delete('/api/azure-devops/pullrequests/:id/workitems/:workItemId', async (req, resp) => {
2769
2454
  try {
2770
2455
  const workItemId = Number(req.params?.workItemId);
2771
2456
  if (!Number.isInteger(workItemId) || workItemId <= 0) {
2772
2457
  return resp
2773
2458
  .status(400)
2774
- .type("application/json")
2775
- .send({ error: "A work item id is required." });
2459
+ .type('application/json')
2460
+ .send({ error: 'A work item id is required.' });
2776
2461
  }
2777
2462
  const { prefix, artifactUrl } = await azurePrArtifactOf(Number(req.params?.id));
2778
2463
  // Relations are removed by index, so confirm the link and its index in
@@ -2780,44 +2465,40 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2780
2465
  // Azure applies the patch atomically, so a relation list that moved
2781
2466
  // underneath this call is rejected instead of removing a neighbour.
2782
2467
  const relations = await azureWorkItemRelations(prefix, workItemId);
2783
- const index = relations.findIndex((relation) => String(relation?.url ?? "").toLowerCase() ===
2784
- artifactUrl.toLowerCase());
2468
+ const index = relations.findIndex((relation) => String(relation?.url ?? '').toLowerCase() === artifactUrl.toLowerCase());
2785
2469
  if (index >= 0) {
2786
2470
  await azurePatchWorkItemRelations(prefix, workItemId, [
2787
2471
  {
2788
- op: "test",
2472
+ op: 'test',
2789
2473
  path: `/relations/${index}/url`,
2790
- value: String(relations[index]?.url ?? ""),
2474
+ value: String(relations[index]?.url ?? ''),
2791
2475
  },
2792
- { op: "remove", path: `/relations/${index}` },
2476
+ { op: 'remove', path: `/relations/${index}` },
2793
2477
  ]);
2794
2478
  }
2795
- return resp.type("application/json").send({ success: true });
2479
+ return resp.type('application/json').send({ success: true });
2796
2480
  }
2797
2481
  catch (err) {
2798
- return resp
2799
- .status(400)
2800
- .type("application/json")
2801
- .send({ error: err.message });
2482
+ return resp.status(400).type('application/json').send({ error: err.message });
2802
2483
  }
2803
2484
  });
2804
2485
  // Merge gates of the pull request: Azure policy evaluations (builds,
2805
2486
  // required reviewers, comments, work items, ...) plus the native statuses
2806
2487
  // posted on the PR. One failing source degrades to a warning so the other
2807
2488
  // still reaches the dialog; only when both fail is the request an error.
2808
- app.get("/api/azure-devops/pullrequests/:id/checks", async (req, resp) => {
2489
+ app.get('/api/azure-devops/pullrequests/:id/checks', async (req, resp) => {
2809
2490
  const describe = (error) => error instanceof Error ? error.message : String(error);
2810
2491
  try {
2811
2492
  const { base, prefix } = await azurePrApiBase(Number(req.params?.id));
2812
- const pr = await azureJson("GET", `${base}?api-version=${AZURE_API_VERSION}`);
2493
+ const pr = await azureJson('GET', `${base}?api-version=${AZURE_API_VERSION}`);
2813
2494
  const warnings = [];
2814
2495
  const checks = new Map();
2815
2496
  const [evaluations, statuses] = await Promise.all([
2816
- fetchPolicyEvaluations(prefix, String(pr?.repository?.project?.id ?? ""), Number(pr?.pullRequestId ?? 0)).catch((error) => ({ error })),
2497
+ fetchPolicyEvaluations(prefix, String(pr?.repository?.project?.id ?? ''), Number(pr?.pullRequestId ?? 0)).catch((error) => ({ error })),
2817
2498
  fetchPullRequestStatuses(base).catch((error) => ({ error })),
2818
2499
  ]);
2819
- const policyError = "error" in evaluations ? evaluations.error : null;
2820
- const statusError = "error" in statuses ? statuses.error : null;
2500
+ const policyError = 'error' in evaluations ? evaluations.error : null;
2501
+ const statusError = 'error' in statuses ? statuses.error : null;
2821
2502
  if (policyError && statusError) {
2822
2503
  throw new Error(`Policy evaluations failed (${describe(policyError)}); pull request statuses failed (${describe(statusError)}).`);
2823
2504
  }
@@ -2839,120 +2520,104 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2839
2520
  // Statuses repeat on re-post; keep only the latest per genre/name.
2840
2521
  const latestStatuses = new Map();
2841
2522
  for (const status of statuses) {
2842
- const key = `${String(status?.context?.genre ?? "")}/${String(status?.context?.name ?? "")}`;
2523
+ const key = `${String(status?.context?.genre ?? '')}/${String(status?.context?.name ?? '')}`;
2843
2524
  const existing = latestStatuses.get(key);
2844
2525
  if (!existing ||
2845
- String(status?.creationDate ?? "") >
2846
- String(existing?.creationDate ?? "")) {
2526
+ String(status?.creationDate ?? '') > String(existing?.creationDate ?? '')) {
2847
2527
  latestStatuses.set(key, status);
2848
2528
  }
2849
2529
  }
2850
2530
  for (const [key, status] of latestStatuses) {
2851
- const name = String(status?.context?.name ?? "Status");
2531
+ const name = String(status?.context?.name ?? 'Status');
2852
2532
  const row = {
2853
2533
  id: `status:${key}`,
2854
2534
  name,
2855
- kind: "status",
2535
+ kind: 'status',
2856
2536
  state: azureStatusState(status?.state),
2857
2537
  required: false,
2858
2538
  };
2859
- const description = String(status?.description ?? "").trim();
2539
+ const description = String(status?.description ?? '').trim();
2860
2540
  if (description) {
2861
2541
  row.detail = description;
2862
2542
  }
2863
- const target = String(status?.targetUrl ?? "");
2543
+ const target = String(status?.targetUrl ?? '');
2864
2544
  if (target) {
2865
2545
  row.url = target;
2866
2546
  }
2867
2547
  checks.set(row.id, row);
2868
2548
  }
2869
2549
  }
2870
- return resp
2871
- .type("application/json")
2872
- .send({ checks: [...checks.values()], warnings });
2550
+ return resp.type('application/json').send({ checks: [...checks.values()], warnings });
2873
2551
  }
2874
2552
  catch (err) {
2875
- return resp
2876
- .status(400)
2877
- .type("application/json")
2878
- .send({ error: err.message });
2553
+ return resp.status(400).type('application/json').send({ error: err.message });
2879
2554
  }
2880
2555
  });
2881
2556
  // Re-queues the build behind one build policy evaluation, the same action
2882
2557
  // the Azure DevOps checks list offers. The evaluation id comes from the
2883
2558
  // checks endpoint above; Azure answers the refreshed evaluation.
2884
- app.post("/api/azure-devops/pullrequests/:id/checks/:evaluationId/requeue", async (req, resp) => {
2559
+ app.post('/api/azure-devops/pullrequests/:id/checks/:evaluationId/requeue', async (req, resp) => {
2885
2560
  try {
2886
- const evaluationId = String(req.params?.evaluationId ?? "").trim();
2561
+ const evaluationId = String(req.params?.evaluationId ?? '').trim();
2887
2562
  if (!evaluationId) {
2888
- throw new Error("A policy evaluation id is required.");
2563
+ throw new Error('A policy evaluation id is required.');
2889
2564
  }
2890
2565
  const { prefix } = await azurePrApiBase(Number(req.params?.id));
2891
- await azureJson("POST", `${prefix}/_apis/policy/evaluations/${encodeURIComponent(evaluationId)}` +
2566
+ await azureJson('POST', `${prefix}/_apis/policy/evaluations/${encodeURIComponent(evaluationId)}` +
2892
2567
  `?api-version=5.0-preview.1`);
2893
- return resp.type("application/json").send({ ok: true });
2568
+ return resp.type('application/json').send({ ok: true });
2894
2569
  }
2895
2570
  catch (err) {
2896
- return resp
2897
- .status(400)
2898
- .type("application/json")
2899
- .send({ error: err.message });
2571
+ return resp.status(400).type('application/json').send({ error: err.message });
2900
2572
  }
2901
2573
  });
2902
2574
  // Abandons (closes) the pull request without merging.
2903
- app.post("/api/azure-devops/pullrequests/:id/abandon", async (req, resp) => {
2575
+ app.post('/api/azure-devops/pullrequests/:id/abandon', async (req, resp) => {
2904
2576
  try {
2905
2577
  const { base } = await azurePrApiBase(Number(req.params?.id));
2906
- await azureJson("PATCH", `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ status: "abandoned" }));
2907
- return resp.type("application/json").send({ ok: true });
2578
+ await azureJson('PATCH', `${base}?api-version=${AZURE_API_VERSION}`, JSON.stringify({ status: 'abandoned' }));
2579
+ return resp.type('application/json').send({ ok: true });
2908
2580
  }
2909
2581
  catch (err) {
2910
- return resp
2911
- .status(400)
2912
- .type("application/json")
2913
- .send({ error: err.message });
2582
+ return resp.status(400).type('application/json').send({ error: err.message });
2914
2583
  }
2915
2584
  });
2916
2585
  // Unified diff of one PR file, computed from the Azure item contents at the
2917
2586
  // PR merge commits. source/target are optional overrides; default comes from
2918
2587
  // the pull request detail.
2919
- app.get("/api/azure-devops/pullrequests/:id/file-diff", async (req, resp) => {
2588
+ app.get('/api/azure-devops/pullrequests/:id/file-diff', async (req, resp) => {
2920
2589
  try {
2921
- const path = String(req.query?.path ?? "").replace(/^\//, "");
2590
+ const path = String(req.query?.path ?? '').replace(/^\//, '');
2922
2591
  if (!path) {
2923
- return resp
2924
- .status(400)
2925
- .type("application/json")
2926
- .send({ error: "path required" });
2592
+ return resp.status(400).type('application/json').send({ error: 'path required' });
2927
2593
  }
2928
- const oldPath = String(req.query?.oldPath ?? "").replace(/^\//, "");
2594
+ const oldPath = String(req.query?.oldPath ?? '').replace(/^\//, '');
2929
2595
  const { base, prefix, repo } = await azurePrApiBase(Number(req.params?.id));
2930
- let source = String(req.query?.source ?? "");
2931
- let target = String(req.query?.target ?? "");
2596
+ let source = String(req.query?.source ?? '');
2597
+ let target = String(req.query?.target ?? '');
2932
2598
  if (!source || !target) {
2933
- const pr = await azureJson("GET", `${base}?api-version=${AZURE_API_VERSION}`);
2934
- source = source || String(pr?.lastMergeSourceCommit?.commitId ?? "");
2935
- target = target || String(pr?.lastMergeTargetCommit?.commitId ?? "");
2599
+ const pr = await azureJson('GET', `${base}?api-version=${AZURE_API_VERSION}`);
2600
+ source = source || String(pr?.lastMergeSourceCommit?.commitId ?? '');
2601
+ target = target || String(pr?.lastMergeTargetCommit?.commitId ?? '');
2936
2602
  }
2937
2603
  if (!source || !target) {
2938
- throw new Error("The pull request does not have merge commits to diff yet.");
2604
+ throw new Error('The pull request does not have merge commits to diff yet.');
2939
2605
  }
2940
2606
  const fetchContent = async (itemPath, commit) => {
2941
2607
  if (!itemPath)
2942
- return { content: "", binary: false };
2608
+ return { content: '', binary: false };
2943
2609
  const url = `${prefix}/_apis/git/repositories/${encodeURIComponent(repo)}/items` +
2944
2610
  `?path=${encodeURIComponent(`/${itemPath}`)}` +
2945
2611
  `&versionDescriptor.version=${encodeURIComponent(commit)}` +
2946
2612
  `&versionDescriptor.versionType=commit&includeContent=true` +
2947
2613
  `&api-version=${AZURE_API_VERSION}`;
2948
- const result = await azureRequest("GET", url, "");
2614
+ const result = await azureRequest('GET', url, '');
2949
2615
  // 404/203: the file does not exist at that commit (added/deleted file).
2950
2616
  if (result.status === 404 || result.status === 203) {
2951
- return { content: "", binary: false };
2617
+ return { content: '', binary: false };
2952
2618
  }
2953
2619
  if (result.status < 200 || result.status >= 300) {
2954
- throw new Error(azureErrorMessage(result) ||
2955
- `Azure DevOps returned HTTP ${result.status}.`);
2620
+ throw new Error(azureErrorMessage(result) || `Azure DevOps returned HTTP ${result.status}.`);
2956
2621
  }
2957
2622
  let data = null;
2958
2623
  try {
@@ -2963,14 +2628,14 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2963
2628
  // may return the file body directly instead of a JSON envelope.
2964
2629
  }
2965
2630
  const hasContentEnvelope = data !== null &&
2966
- typeof data === "object" &&
2967
- (Object.prototype.hasOwnProperty.call(data, "content") ||
2968
- Object.prototype.hasOwnProperty.call(data, "isBinary"));
2631
+ typeof data === 'object' &&
2632
+ (Object.prototype.hasOwnProperty.call(data, 'content') ||
2633
+ Object.prototype.hasOwnProperty.call(data, 'isBinary'));
2969
2634
  return {
2970
2635
  content: hasContentEnvelope
2971
- ? typeof data.content === "string"
2636
+ ? typeof data.content === 'string'
2972
2637
  ? data.content
2973
- : ""
2638
+ : ''
2974
2639
  : result.body,
2975
2640
  binary: hasContentEnvelope && data.isBinary === true,
2976
2641
  };
@@ -2980,37 +2645,33 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
2980
2645
  fetchContent(path, source),
2981
2646
  ]);
2982
2647
  if (oldFile.binary || newFile.binary) {
2983
- return resp.type("application/json").send({
2648
+ return resp.type('application/json').send({
2984
2649
  path,
2985
2650
  oldPath,
2986
- status: "binary",
2651
+ status: 'binary',
2987
2652
  binary: true,
2988
2653
  additions: 0,
2989
2654
  deletions: 0,
2990
2655
  lines: [],
2991
2656
  });
2992
2657
  }
2993
- const status = String(req.query?.changeType ?? "") ||
2994
- (!oldFile.content
2995
- ? "added"
2996
- : !newFile.content
2997
- ? "deleted"
2998
- : "modified");
2658
+ const status = String(req.query?.changeType ?? '') ||
2659
+ (!oldFile.content ? 'added' : !newFile.content ? 'deleted' : 'modified');
2999
2660
  // jsdiff omits the "diff --git" header parseUnifiedDiff expects, and
3000
2661
  // prefixing it lets the same parser serve both local and PR diffs.
3001
2662
  const patch = createTwoFilesPatch(`a/${oldPath || path}`, `b/${path}`, oldFile.content, newFile.content);
3002
- const raw = `diff --git a/${oldPath || path} b/${path}\n${patch.replace(/^=+\n/, "")}`;
2663
+ const raw = `diff --git a/${oldPath || path} b/${path}\n${patch.replace(/^=+\n/, '')}`;
3003
2664
  // The patch's trailing newline would surface as a phantom context row.
3004
- const [file] = parseUnifiedDiff(raw.replace(/\n+$/, ""));
2665
+ const [file] = parseUnifiedDiff(raw.replace(/\n+$/, ''));
3005
2666
  const maxLines = 10000;
3006
2667
  const lines = (file?.lines ?? []).slice(0, maxLines);
3007
2668
  if ((file?.lines ?? []).length > maxLines) {
3008
2669
  lines.push({
3009
- type: "hunk",
3010
- text: "Diff truncated (file too large).",
2670
+ type: 'hunk',
2671
+ text: 'Diff truncated (file too large).',
3011
2672
  });
3012
2673
  }
3013
- return resp.type("application/json").send({
2674
+ return resp.type('application/json').send({
3014
2675
  path,
3015
2676
  oldPath,
3016
2677
  status,
@@ -3023,166 +2684,146 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3023
2684
  });
3024
2685
  }
3025
2686
  catch (err) {
3026
- return resp
3027
- .status(400)
3028
- .type("application/json")
3029
- .send({ error: err.message });
2687
+ return resp.status(400).type('application/json').send({ error: err.message });
3030
2688
  }
3031
2689
  });
3032
2690
  // ==================== Commit Detail ====================
3033
- app.post("/api/commit/detail", async (req, resp) => {
2691
+ app.post('/api/commit/detail', async (req, resp) => {
3034
2692
  try {
3035
2693
  const { hash } = req.body;
3036
2694
  if (!hash) {
3037
- return resp
3038
- .status(400)
3039
- .type("application/json")
3040
- .send({ error: "hash required" });
2695
+ return resp.status(400).type('application/json').send({ error: 'hash required' });
3041
2696
  }
3042
2697
  // Fields are NUL-separated; %b may span lines and contain anything
3043
2698
  // except NUL, so it cannot be split naively by newline.
3044
2699
  const raw = await git.raw([
3045
- "show",
3046
- "-s",
3047
- "--format=%H%x00%aI%x00%s%x00%b%x00%aN%x00%aE%x00%P",
2700
+ 'show',
2701
+ '-s',
2702
+ '--format=%H%x00%aI%x00%s%x00%b%x00%aN%x00%aE%x00%P',
3048
2703
  hash,
3049
2704
  ]);
3050
- const [fullHash, date, message, body, authorName, authorEmail, parents] = raw.trimEnd().split("\u0000");
3051
- return resp.type("application/json").send({
2705
+ const [fullHash, date, message, body, authorName, authorEmail, parents] = raw
2706
+ .trimEnd()
2707
+ .split('\u0000');
2708
+ return resp.type('application/json').send({
3052
2709
  hash: fullHash,
3053
2710
  date,
3054
2711
  message,
3055
- refs: "",
3056
- body: body ?? "",
2712
+ refs: '',
2713
+ body: body ?? '',
3057
2714
  ...authorIdentity(authorName, authorEmail, await configuredIdentity()),
3058
- parents: String(parents ?? "")
3059
- .split(" ")
2715
+ parents: String(parents ?? '')
2716
+ .split(' ')
3060
2717
  .filter(Boolean),
3061
2718
  });
3062
2719
  }
3063
2720
  catch (err) {
3064
- return resp
3065
- .status(400)
3066
- .type("application/json")
3067
- .send({ error: err.message });
2721
+ return resp.status(400).type('application/json').send({ error: err.message });
3068
2722
  }
3069
2723
  });
3070
2724
  // ==================== Diff Viewer ====================
3071
- app.post("/api/commit/diff", async (req, resp) => {
2725
+ app.post('/api/commit/diff', async (req, resp) => {
3072
2726
  try {
3073
2727
  const { hash } = req.body;
3074
2728
  if (!hash) {
3075
- return resp
3076
- .status(400)
3077
- .type("application/json")
3078
- .send({ error: "hash required" });
2729
+ return resp.status(400).type('application/json').send({ error: 'hash required' });
3079
2730
  }
3080
2731
  // Get the raw unified diff (diff against the first parent for merges)
3081
2732
  const rawDiff = await git.raw([
3082
- "show",
3083
- "--no-color",
3084
- "--pretty=format:",
3085
- "--find-renames",
3086
- "--first-parent",
3087
- "-m",
2733
+ 'show',
2734
+ '--no-color',
2735
+ '--pretty=format:',
2736
+ '--find-renames',
2737
+ '--first-parent',
2738
+ '-m',
3088
2739
  hash,
3089
2740
  ]);
3090
- return resp
3091
- .type("application/json")
3092
- .send({ hash, files: parseUnifiedDiff(rawDiff) });
2741
+ return resp.type('application/json').send({ hash, files: parseUnifiedDiff(rawDiff) });
3093
2742
  }
3094
2743
  catch (err) {
3095
- return resp
3096
- .status(400)
3097
- .type("application/json")
3098
- .send({ error: err.message });
2744
+ return resp.status(400).type('application/json').send({ error: err.message });
3099
2745
  }
3100
2746
  });
3101
2747
  // ==================== Working Changes ====================
3102
- app.get("/api/working-changes", async (_req, resp) => {
2748
+ app.get('/api/working-changes', async (_req, resp) => {
3103
2749
  try {
3104
2750
  await mutationQueue;
3105
- return resp.type("application/json").send(await working.snapshot());
2751
+ return resp.type('application/json').send(await working.snapshot());
3106
2752
  }
3107
2753
  catch (err) {
3108
- return resp
3109
- .status(400)
3110
- .type("application/json")
3111
- .send({ error: err.message });
2754
+ return resp.status(400).type('application/json').send({ error: err.message });
2755
+ }
2756
+ });
2757
+ app.get('/api/conflict', async (req, resp) => {
2758
+ try {
2759
+ await mutationQueue;
2760
+ return resp.type('application/json').send(await working.conflict(req.query?.path));
2761
+ }
2762
+ catch (err) {
2763
+ return resp.status(400).type('application/json').send({ error: err.message });
2764
+ }
2765
+ });
2766
+ app.post('/api/conflict/resolve', async (req, resp) => {
2767
+ try {
2768
+ const { path, resolution, content } = req.body ?? {};
2769
+ await mutate(() => working.resolveConflict(path, resolution, content));
2770
+ return resp.type('application/json').send({ success: true });
2771
+ }
2772
+ catch (err) {
2773
+ return resp.status(400).type('application/json').send({ error: err.message });
3112
2774
  }
3113
2775
  });
3114
2776
  // ==================== File Content ====================
3115
- app.post("/api/file-content", async (req, resp) => {
2777
+ app.post('/api/file-content', async (req, resp) => {
3116
2778
  try {
3117
2779
  const { path, ref } = req.body;
3118
2780
  if (!path || !ref) {
3119
- return resp
3120
- .status(400)
3121
- .type("application/json")
3122
- .send({ error: "path and ref required" });
2781
+ return resp.status(400).type('application/json').send({ error: 'path and ref required' });
3123
2782
  }
3124
- if (ref === "WORKING") {
2783
+ if (ref === 'WORKING') {
3125
2784
  const root = await repoRoot();
3126
2785
  const abs = resolve(root, path);
3127
2786
  const rel = relative(root, abs);
3128
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
3129
- return resp
3130
- .status(400)
3131
- .type("application/json")
3132
- .send({ error: "invalid path" });
2787
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
2788
+ return resp.status(400).type('application/json').send({ error: 'invalid path' });
3133
2789
  }
3134
2790
  try {
3135
2791
  const buffer = await readFile(abs);
3136
2792
  if (buffer.includes(0)) {
3137
- return resp
3138
- .type("application/json")
3139
- .send({ binary: true, content: "" });
2793
+ return resp.type('application/json').send({ binary: true, content: '' });
3140
2794
  }
3141
- return resp
3142
- .type("application/json")
3143
- .send({ content: buffer.toString("utf8") });
2795
+ return resp.type('application/json').send({ content: buffer.toString('utf8') });
3144
2796
  }
3145
2797
  catch {
3146
2798
  // File no longer exists on disk (deleted).
3147
- return resp.type("application/json").send({ content: "" });
2799
+ return resp.type('application/json').send({ content: '' });
3148
2800
  }
3149
2801
  }
3150
- if (ref === "EMPTY") {
3151
- return resp.type("application/json").send({ content: "" });
2802
+ if (ref === 'EMPTY') {
2803
+ return resp.type('application/json').send({ content: '' });
3152
2804
  }
3153
2805
  try {
3154
- const content = await git.raw([
3155
- "show",
3156
- ref === "INDEX" ? `:${path}` : `${ref}:${path}`,
3157
- ]);
3158
- if (content.includes("\u0000")) {
3159
- return resp
3160
- .type("application/json")
3161
- .send({ binary: true, content: "" });
2806
+ const content = await git.raw(['show', ref === 'INDEX' ? `:${path}` : `${ref}:${path}`]);
2807
+ if (content.includes('\u0000')) {
2808
+ return resp.type('application/json').send({ binary: true, content: '' });
3162
2809
  }
3163
- return resp.type("application/json").send({ content });
2810
+ return resp.type('application/json').send({ content });
3164
2811
  }
3165
2812
  catch {
3166
2813
  // Path did not exist at that revision (added file).
3167
- return resp.type("application/json").send({ content: "" });
2814
+ return resp.type('application/json').send({ content: '' });
3168
2815
  }
3169
2816
  }
3170
2817
  catch (err) {
3171
- return resp
3172
- .status(400)
3173
- .type("application/json")
3174
- .send({ error: err.message });
2818
+ return resp.status(400).type('application/json').send({ error: err.message });
3175
2819
  }
3176
2820
  });
3177
2821
  // ==================== Discard ====================
3178
- app.post("/api/discard", async (req, resp) => {
2822
+ app.post('/api/discard', async (req, resp) => {
3179
2823
  try {
3180
2824
  const { files, mode } = req.body;
3181
2825
  if (!Array.isArray(files) || files.length === 0) {
3182
- return resp
3183
- .status(400)
3184
- .type("application/json")
3185
- .send({ error: "files required" });
2826
+ return resp.status(400).type('application/json').send({ error: 'files required' });
3186
2827
  }
3187
2828
  const root = await repoRoot();
3188
2829
  const status = await git.status();
@@ -3190,56 +2831,47 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3190
2831
  const tracked = files.filter((file) => !untracked.has(file));
3191
2832
  const removed = files.filter((file) => untracked.has(file));
3192
2833
  if (tracked.length > 0) {
3193
- if (mode === "unstaged") {
2834
+ if (mode === 'unstaged') {
3194
2835
  // Restore from the index so staged changes survive; unmerged (conflicted)
3195
2836
  // paths cannot be restored from the index and fall back to HEAD.
3196
2837
  const conflicted = new Set(status.conflicted ?? []);
3197
2838
  const restorable = tracked.filter((file) => !conflicted.has(file));
3198
2839
  const unmerged = tracked.filter((file) => conflicted.has(file));
3199
2840
  if (restorable.length > 0)
3200
- await git.raw(["checkout", "--", ...restorable]);
2841
+ await git.raw(['checkout', '--', ...restorable]);
3201
2842
  if (unmerged.length > 0)
3202
- await git.raw(["checkout", "HEAD", "--", ...unmerged]);
2843
+ await git.raw(['checkout', 'HEAD', '--', ...unmerged]);
3203
2844
  }
3204
2845
  else {
3205
- await git.raw(["checkout", "HEAD", "--", ...tracked]);
2846
+ await git.raw(['checkout', 'HEAD', '--', ...tracked]);
3206
2847
  }
3207
2848
  }
3208
2849
  for (const file of removed) {
3209
2850
  await rm(join(root, file), { force: true, recursive: true });
3210
2851
  }
3211
- return resp.type("application/json").send({ success: true });
2852
+ return resp.type('application/json').send({ success: true });
3212
2853
  }
3213
2854
  catch (err) {
3214
- return resp
3215
- .status(400)
3216
- .type("application/json")
3217
- .send({ error: err.message });
2855
+ return resp.status(400).type('application/json').send({ error: err.message });
3218
2856
  }
3219
2857
  });
3220
2858
  // ==================== Working Tree Actions ====================
3221
- app.post("/api/reset", async (_req, resp) => {
2859
+ app.post('/api/reset', async (_req, resp) => {
3222
2860
  try {
3223
- await git.raw(["reset", "--hard", "HEAD"]);
3224
- return resp.type("application/json").send({ ok: true });
2861
+ await git.raw(['reset', '--hard', 'HEAD']);
2862
+ return resp.type('application/json').send({ ok: true });
3225
2863
  }
3226
2864
  catch (err) {
3227
- return resp
3228
- .status(400)
3229
- .type("application/json")
3230
- .send({ error: err.message });
2865
+ return resp.status(400).type('application/json').send({ error: err.message });
3231
2866
  }
3232
2867
  });
3233
- app.post("/api/clean", async (_req, resp) => {
2868
+ app.post('/api/clean', async (_req, resp) => {
3234
2869
  try {
3235
- await git.raw(["clean", "-fd"]);
3236
- return resp.type("application/json").send({ ok: true });
2870
+ await git.raw(['clean', '-fd']);
2871
+ return resp.type('application/json').send({ ok: true });
3237
2872
  }
3238
2873
  catch (err) {
3239
- return resp
3240
- .status(400)
3241
- .type("application/json")
3242
- .send({ error: err.message });
2874
+ return resp.status(400).type('application/json').send({ error: err.message });
3243
2875
  }
3244
2876
  });
3245
2877
  const address = await app.listen({ port, ...(host ? { host } : {}) });
@@ -3254,6 +2886,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3254
2886
  showGraph = settings.showGraph;
3255
2887
  showStashes = settings.showStashes;
3256
2888
  fileListView = settings.fileListView;
2889
+ sidePanelSectionsExpanded = settings.sidePanelSectionsExpanded;
3257
2890
  searchMode = settings.searchMode;
3258
2891
  searchCaseSensitive = settings.searchCaseSensitive;
3259
2892
  showTags = settings.showTags;