guito 1.0.2 → 1.0.3

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