@junctionpanel/server 0.1.79 → 0.1.81

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.
@@ -3,7 +3,7 @@ import { promisify } from "util";
3
3
  import { resolve, dirname, basename } from "path";
4
4
  import { realpathSync } from "fs";
5
5
  import { open as openFile, stat as statFile } from "fs/promises";
6
- import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
6
+ import { parseAndHighlightDiff, parseDiff } from "../server/utils/diff-highlighter.js";
7
7
  import { expandTilde } from "./path.js";
8
8
  import { isJunctionOwnedWorktreeCwd } from "./worktree.js";
9
9
  import { requireJunctionWorktreeBaseRefName } from "./worktree-metadata.js";
@@ -15,8 +15,8 @@ const READ_ONLY_GIT_ENV = {
15
15
  };
16
16
  const SAFE_GIT_REMOTE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
17
17
  const SMALL_OUTPUT_MAX_BUFFER = 20 * 1024 * 1024; // 20MB
18
- async function execGit(command, options) {
19
- return execAsync(command, { ...options, maxBuffer: SMALL_OUTPUT_MAX_BUFFER });
18
+ async function execGit(args, options) {
19
+ return execFileAsync("git", [...args], { ...options, maxBuffer: SMALL_OUTPUT_MAX_BUFFER });
20
20
  }
21
21
  function isSafeRemoteName(remoteName) {
22
22
  return SAFE_GIT_REMOTE_NAME_PATTERN.test(remoteName);
@@ -108,7 +108,7 @@ function normalizeBranchSuggestionName(raw, source) {
108
108
  return normalized;
109
109
  }
110
110
  async function listGitRefs(cwd, refPrefix) {
111
- const { stdout } = await execGit(`git for-each-ref --format="%(refname:short)" ${refPrefix}`, {
111
+ const { stdout } = await execGit(["for-each-ref", "--format=%(refname:short)", refPrefix], {
112
112
  cwd,
113
113
  env: READ_ONLY_GIT_ENV,
114
114
  });
@@ -214,8 +214,7 @@ export async function listBranchSuggestions(cwd, options) {
214
214
  async function listCheckoutFileChanges(cwd, diffArgs, opts) {
215
215
  const changes = [];
216
216
  const includeUntracked = opts?.includeUntracked ?? true;
217
- const diffArgsStr = diffArgs.length > 0 ? ` ${diffArgs.join(" ")}` : "";
218
- const { stdout: nameStatusOut } = await execGit(`git diff --name-status${diffArgsStr}`, {
217
+ const { stdout: nameStatusOut } = await execGit(["diff", "--name-status", ...diffArgs], {
219
218
  cwd,
220
219
  env: READ_ONLY_GIT_ENV,
221
220
  });
@@ -251,7 +250,7 @@ async function listCheckoutFileChanges(cwd, diffArgs, opts) {
251
250
  });
252
251
  }
253
252
  if (includeUntracked) {
254
- const { stdout: untrackedOut } = await execGit("git ls-files --others --exclude-standard", {
253
+ const { stdout: untrackedOut } = await execGit(["ls-files", "--others", "--exclude-standard"], {
255
254
  cwd,
256
255
  env: READ_ONLY_GIT_ENV,
257
256
  });
@@ -281,7 +280,10 @@ async function listCheckoutFileChanges(cwd, diffArgs, opts) {
281
280
  }
282
281
  async function tryResolveMergeBase(cwd, baseRef) {
283
282
  try {
284
- const { stdout } = await execGit(`git merge-base ${baseRef} HEAD`, { cwd, env: READ_ONLY_GIT_ENV });
283
+ const { stdout } = await execGit(["merge-base", baseRef, "HEAD"], {
284
+ cwd,
285
+ env: READ_ONLY_GIT_ENV,
286
+ });
285
287
  const sha = stdout.trim();
286
288
  return sha.length > 0 ? sha : null;
287
289
  }
@@ -341,7 +343,10 @@ async function getTrackedNumstatByPath(cwd, diffArgs) {
341
343
  }
342
344
  stats.set(path, { additions, deletions, isBinary: false });
343
345
  }
344
- return stats;
346
+ return {
347
+ stats,
348
+ truncated: result.truncated,
349
+ };
345
350
  }
346
351
  function isTrackedDiffTooLarge(stat) {
347
352
  if (!stat || stat.isBinary) {
@@ -1130,6 +1135,9 @@ export async function getCheckoutStatusLite(cwd, context) {
1130
1135
  }
1131
1136
  export async function getCheckoutDiff(cwd, compare, context) {
1132
1137
  await requireGitRepo(cwd);
1138
+ const includeStructured = compare.includeStructured === true;
1139
+ const structuredDetail = compare.structuredDetail ?? "full";
1140
+ const includeDiffText = compare.includeDiffText ?? true;
1133
1141
  let diffArgs;
1134
1142
  let includeUntracked;
1135
1143
  if (compare.mode === "uncommitted") {
@@ -1173,20 +1181,29 @@ export async function getCheckoutDiff(cwd, compare, context) {
1173
1181
  if (diffBytes >= TOTAL_DIFF_MAX_BYTES)
1174
1182
  return;
1175
1183
  const buf = Buffer.from(text, "utf8");
1176
- if (diffBytes + buf.length <= TOTAL_DIFF_MAX_BYTES) {
1177
- diffText += text;
1184
+ const remaining = TOTAL_DIFF_MAX_BYTES - diffBytes;
1185
+ if (buf.length <= remaining) {
1178
1186
  diffBytes += buf.length;
1187
+ if (includeDiffText) {
1188
+ diffText += text;
1189
+ }
1179
1190
  return;
1180
1191
  }
1181
- const remaining = TOTAL_DIFF_MAX_BYTES - diffBytes;
1182
1192
  if (remaining > 0) {
1183
- diffText += buf.subarray(0, remaining).toString("utf8");
1184
1193
  diffBytes = TOTAL_DIFF_MAX_BYTES;
1194
+ if (includeDiffText) {
1195
+ diffText += buf.subarray(0, remaining).toString("utf8");
1196
+ }
1185
1197
  }
1186
1198
  };
1187
1199
  const trackedChanges = changes.filter((change) => !change.isUntracked);
1188
1200
  const untrackedChanges = changes.filter((change) => change.isUntracked === true);
1189
- const trackedNumstatByPath = trackedChanges.length > 0 ? await getTrackedNumstatByPath(cwd, diffArgs) : new Map();
1201
+ const needsTrackedDiffText = includeDiffText || (includeStructured && structuredDetail === "full");
1202
+ const trackedNumstatResult = trackedChanges.length > 0
1203
+ ? await getTrackedNumstatByPath(cwd, diffArgs)
1204
+ : { stats: new Map(), truncated: false };
1205
+ const trackedNumstatByPath = trackedNumstatResult.stats;
1206
+ const trackedNumstatTruncated = trackedNumstatResult.truncated;
1190
1207
  const trackedDiffPaths = [];
1191
1208
  const trackedPlaceholderByPath = new Map();
1192
1209
  for (const change of trackedChanges) {
@@ -1203,7 +1220,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
1203
1220
  }
1204
1221
  let trackedDiffText = "";
1205
1222
  let trackedDiffTruncated = false;
1206
- if (trackedDiffPaths.length > 0) {
1223
+ if (trackedDiffPaths.length > 0 && needsTrackedDiffText) {
1207
1224
  const trackedDiffResult = await spawnLimitedText({
1208
1225
  cmd: "git",
1209
1226
  args: ["diff", ...diffArgs, "--", ...trackedDiffPaths],
@@ -1214,7 +1231,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
1214
1231
  trackedDiffText = trackedDiffResult.text;
1215
1232
  trackedDiffTruncated = trackedDiffResult.truncated;
1216
1233
  appendDiff(trackedDiffText);
1217
- if (trackedDiffTruncated) {
1234
+ if (includeDiffText && trackedDiffTruncated) {
1218
1235
  appendDiff("# tracked diff truncated\n");
1219
1236
  }
1220
1237
  }
@@ -1225,8 +1242,10 @@ export async function getCheckoutDiff(cwd, compare, context) {
1225
1242
  }
1226
1243
  appendDiff(`# ${change.path}: diff too large omitted\n`);
1227
1244
  };
1228
- if (compare.includeStructured) {
1229
- const parsedTrackedFiles = trackedDiffText.length > 0 ? await parseAndHighlightDiff(trackedDiffText, cwd) : [];
1245
+ if (includeStructured) {
1246
+ const parsedTrackedFiles = structuredDetail === "full" && trackedDiffText.length > 0
1247
+ ? await parseAndHighlightDiff(trackedDiffText, cwd)
1248
+ : [];
1230
1249
  const parsedTrackedByPath = new Map(parsedTrackedFiles.map((file) => [file.path, file]));
1231
1250
  for (const change of trackedChanges) {
1232
1251
  const placeholder = trackedPlaceholderByPath.get(change.path);
@@ -1235,10 +1254,25 @@ export async function getCheckoutDiff(cwd, compare, context) {
1235
1254
  status: placeholder.status,
1236
1255
  stat: placeholder.stat,
1237
1256
  }));
1238
- appendTrackedPlaceholderComment(change, placeholder.status);
1257
+ if (includeDiffText) {
1258
+ appendTrackedPlaceholderComment(change, placeholder.status);
1259
+ }
1239
1260
  continue;
1240
1261
  }
1241
1262
  const stat = trackedNumstatByPath.get(change.path) ?? null;
1263
+ if (structuredDetail === "summary") {
1264
+ const missingSummaryStat = stat === null && trackedNumstatTruncated;
1265
+ structured.push({
1266
+ path: change.path,
1267
+ isNew: change.isNew,
1268
+ isDeleted: change.isDeleted,
1269
+ additions: stat?.additions ?? 0,
1270
+ deletions: stat?.deletions ?? 0,
1271
+ hunks: [],
1272
+ status: missingSummaryStat ? "too_large" : "ok",
1273
+ });
1274
+ continue;
1275
+ }
1242
1276
  const parsedFile = parsedTrackedByPath.get(change.path);
1243
1277
  if (parsedFile) {
1244
1278
  structured.push({
@@ -1264,7 +1298,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
1264
1298
  else {
1265
1299
  for (const change of trackedChanges) {
1266
1300
  const placeholder = trackedPlaceholderByPath.get(change.path);
1267
- if (placeholder) {
1301
+ if (placeholder && includeDiffText) {
1268
1302
  appendTrackedPlaceholderComment(change, placeholder.status);
1269
1303
  }
1270
1304
  }
@@ -1274,12 +1308,16 @@ export async function getCheckoutDiff(cwd, compare, context) {
1274
1308
  break;
1275
1309
  }
1276
1310
  const { text, truncated, stat } = await getUntrackedDiffText(cwd, change);
1277
- if (!compare.includeStructured) {
1311
+ if (!includeStructured) {
1278
1312
  if (stat?.isBinary) {
1279
- appendDiff(`# ${change.path}: binary diff omitted\n`);
1313
+ if (includeDiffText) {
1314
+ appendDiff(`# ${change.path}: binary diff omitted\n`);
1315
+ }
1280
1316
  }
1281
1317
  else if (truncated) {
1282
- appendDiff(`# ${change.path}: diff too large omitted\n`);
1318
+ if (includeDiffText) {
1319
+ appendDiff(`# ${change.path}: diff too large omitted\n`);
1320
+ }
1283
1321
  }
1284
1322
  else {
1285
1323
  appendDiff(text);
@@ -1288,16 +1326,22 @@ export async function getCheckoutDiff(cwd, compare, context) {
1288
1326
  }
1289
1327
  if (stat?.isBinary) {
1290
1328
  structured.push(buildPlaceholderParsedDiffFile(change, { status: "binary", stat }));
1291
- appendDiff(`# ${change.path}: binary diff omitted\n`);
1329
+ if (includeDiffText) {
1330
+ appendDiff(`# ${change.path}: binary diff omitted\n`);
1331
+ }
1292
1332
  continue;
1293
1333
  }
1294
1334
  if (truncated) {
1295
1335
  structured.push(buildPlaceholderParsedDiffFile(change, { status: "too_large", stat }));
1296
- appendDiff(`# ${change.path}: diff too large omitted\n`);
1336
+ if (includeDiffText) {
1337
+ appendDiff(`# ${change.path}: diff too large omitted\n`);
1338
+ }
1297
1339
  continue;
1298
1340
  }
1299
1341
  appendDiff(text);
1300
- const parsed = await parseAndHighlightDiff(text, cwd);
1342
+ const parsed = structuredDetail === "summary"
1343
+ ? parseDiff(text)
1344
+ : await parseAndHighlightDiff(text, cwd);
1301
1345
  const parsedFile = parsed[0] ??
1302
1346
  {
1303
1347
  path: change.path,
@@ -1312,13 +1356,14 @@ export async function getCheckoutDiff(cwd, compare, context) {
1312
1356
  path: change.path,
1313
1357
  isNew: change.isNew,
1314
1358
  isDeleted: change.isDeleted,
1359
+ hunks: structuredDetail === "summary" ? [] : parsedFile.hunks,
1315
1360
  status: "ok",
1316
1361
  });
1317
1362
  }
1318
- if (compare.includeStructured) {
1319
- return { diff: diffText, structured };
1363
+ if (includeStructured) {
1364
+ return { diff: includeDiffText ? diffText : "", structured };
1320
1365
  }
1321
- return { diff: diffText };
1366
+ return { diff: includeDiffText ? diffText : "" };
1322
1367
  }
1323
1368
  export async function commitChanges(cwd, options) {
1324
1369
  await requireGitRepo(cwd);
@@ -2109,6 +2154,7 @@ async function getPullRequestMergeabilityViaRest(cwd, repo, prNumber) {
2109
2154
  }
2110
2155
  function toPullRequestStatus(baseStatus, checks, mergeability, options) {
2111
2156
  const requiredChecks = checks.filter((check) => check.required);
2157
+ const nonRequiredChecks = checks.filter((check) => !check.required);
2112
2158
  const requiredChecksKnown = options?.requiredChecksKnown ?? true;
2113
2159
  const checksState = baseStatus.isMerged
2114
2160
  ? "passing"
@@ -2125,6 +2171,11 @@ function toPullRequestStatus(baseStatus, checks, mergeability, options) {
2125
2171
  ? true
2126
2172
  : requiredChecks.every((check) => check.bucket === "pass" || check.bucket === "skipping");
2127
2173
  const hasConflicts = baseStatus.isMerged ? false : (mergeability?.hasConflicts ?? false);
2174
+ const hasNonRequiredFailingChecks = baseStatus.isMerged
2175
+ ? false
2176
+ : !requiredChecksKnown
2177
+ ? null
2178
+ : nonRequiredChecks.some((check) => check.bucket === "fail" || check.bucket === "cancel");
2128
2179
  const mergeableKnown = baseStatus.isMerged ? true : (mergeability?.mergeableKnown ?? true);
2129
2180
  const canMerge = baseStatus.isMerged
2130
2181
  ? false
@@ -2146,6 +2197,7 @@ function toPullRequestStatus(baseStatus, checks, mergeability, options) {
2146
2197
  requiredChecksPassed,
2147
2198
  canMerge,
2148
2199
  hasConflicts,
2200
+ hasNonRequiredFailingChecks,
2149
2201
  };
2150
2202
  }
2151
2203
  async function listWorkflowRunsForHead(options) {
@@ -2285,6 +2337,7 @@ function toSummaryOnlyPullRequestStatus(baseStatus) {
2285
2337
  requiredChecksPassed: null,
2286
2338
  canMerge: null,
2287
2339
  hasConflicts: null,
2340
+ hasNonRequiredFailingChecks: null,
2288
2341
  };
2289
2342
  }
2290
2343
  async function loadCurrentPullRequest(cwd, options) {
@@ -2457,6 +2510,31 @@ async function loadCurrentPullRequest(cwd, options) {
2457
2510
  requiredChecksKnown = false;
2458
2511
  checks = await getPullRequestChecksViaRest(cwd, repo, openBaseStatus.headSha ?? localHeadSha ?? openBaseStatus.headRefName);
2459
2512
  }
2513
+ if (requiredChecksKnown
2514
+ && checks.every((check) => check.bucket === "pass" || check.bucket === "skipping")) {
2515
+ try {
2516
+ const rollupChecks = await getPullRequestChecksViaRest(cwd, repo, openBaseStatus.headSha ?? localHeadSha ?? openBaseStatus.headRefName);
2517
+ checks = mergeCheckStatuses(checks, rollupChecks);
2518
+ }
2519
+ catch (error) {
2520
+ if (isGhAuthError(error)) {
2521
+ return {
2522
+ status: null,
2523
+ githubFeaturesEnabled: false,
2524
+ checks: [],
2525
+ headSha: null,
2526
+ repo,
2527
+ detailLevel,
2528
+ fetchedAt: Date.now(),
2529
+ };
2530
+ }
2531
+ if (isGhRateLimitError(error)) {
2532
+ if (compatibleCachedLookup) {
2533
+ return compatibleCachedLookup;
2534
+ }
2535
+ }
2536
+ }
2537
+ }
2460
2538
  try {
2461
2539
  mergeability = await getPullRequestMergeabilityViaRest(cwd, repo, openBaseStatus.number);
2462
2540
  }