@a9n-shoji/rvw 0.4.0 → 0.4.2

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.
package/dist/cli.mjs CHANGED
@@ -18947,6 +18947,13 @@ function secureNewPath(targetPath, expectedMode, label, chmod = chmodSync) {
18947
18947
  }
18948
18948
  assertSecureExistingPath(targetPath, expectedMode, label);
18949
18949
  }
18950
+ var domainRevisionMetaKeys = {
18951
+ pullRequests: "revision_pull_requests",
18952
+ pullRequestContent: "revision_pull_request_content",
18953
+ comments: "revision_comments",
18954
+ walkthroughs: "revision_walkthroughs",
18955
+ structures: "revision_structures"
18956
+ };
18950
18957
  var RvwDatabase = class {
18951
18958
  filePath;
18952
18959
  configuredPath;
@@ -19090,10 +19097,69 @@ var RvwDatabase = class {
19090
19097
  ).run();
19091
19098
  return this.getChangeSequence();
19092
19099
  }
19100
+ incrementDomainRevisions(domains) {
19101
+ const uniqueDomains = [...new Set(domains)];
19102
+ this.incrementChangeSequence();
19103
+ const increment2 = this.database.prepare(
19104
+ "UPDATE app_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE key = ?"
19105
+ );
19106
+ for (const domain2 of uniqueDomains) increment2.run(domainRevisionMetaKeys[domain2]);
19107
+ return this.getChangeSequence();
19108
+ }
19093
19109
  getChangeSequence() {
19094
19110
  const row = this.database.prepare("SELECT value FROM app_meta WHERE key = 'change_sequence'").get();
19095
19111
  return Number(stringValue(row, "value"));
19096
19112
  }
19113
+ getRevisionSnapshot() {
19114
+ const rows = this.database.prepare(
19115
+ `SELECT key, value
19116
+ FROM app_meta
19117
+ WHERE key IN (
19118
+ 'change_sequence',
19119
+ 'revision_pull_requests',
19120
+ 'revision_pull_request_content',
19121
+ 'revision_comments',
19122
+ 'revision_walkthroughs',
19123
+ 'revision_structures'
19124
+ )`
19125
+ ).all();
19126
+ const values = new Map(rows.map((row) => [stringValue(row, "key"), stringValue(row, "value")]));
19127
+ const value = (key2) => {
19128
+ const raw3 = values.get(key2);
19129
+ if (raw3 === void 0) throw new RvwError("DATABASE_ERROR", `revision\u304C\u3042\u308A\u307E\u305B\u3093: ${key2}`);
19130
+ return Number(raw3);
19131
+ };
19132
+ return {
19133
+ changeSequence: value("change_sequence"),
19134
+ revisions: {
19135
+ pullRequests: value(domainRevisionMetaKeys.pullRequests),
19136
+ pullRequestContent: value(domainRevisionMetaKeys.pullRequestContent),
19137
+ comments: value(domainRevisionMetaKeys.comments),
19138
+ walkthroughs: value(domainRevisionMetaKeys.walkthroughs),
19139
+ structures: value(domainRevisionMetaKeys.structures)
19140
+ }
19141
+ };
19142
+ }
19143
+ getDomainRevisions() {
19144
+ return this.getRevisionSnapshot().revisions;
19145
+ }
19146
+ getPullRequestRevisionSnapshot(id) {
19147
+ this.database.exec("BEGIN");
19148
+ try {
19149
+ const result = {
19150
+ pullRequest: this.getPullRequest(id),
19151
+ revisionSnapshot: this.getRevisionSnapshot()
19152
+ };
19153
+ this.database.exec("COMMIT");
19154
+ return result;
19155
+ } catch (error51) {
19156
+ try {
19157
+ this.database.exec("ROLLBACK");
19158
+ } catch {
19159
+ }
19160
+ throw error51;
19161
+ }
19162
+ }
19097
19163
  getCommentWatchDatabaseId() {
19098
19164
  const row = this.database.prepare("SELECT value FROM app_meta WHERE key = 'comment_watch_database_id'").get();
19099
19165
  if (!row) throw new RvwError("DATABASE_ERROR", "comment watch\u306Edatabase ID\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
@@ -19246,7 +19312,19 @@ var RvwDatabase = class {
19246
19312
  const statement = this.database.prepare(
19247
19313
  "UPDATE pull_requests SET github_state = ?, github_is_draft = ? WHERE id = ?"
19248
19314
  );
19315
+ let changed = false;
19249
19316
  for (const update of updates) {
19317
+ const current = this.getPullRequest(update.pullRequestId);
19318
+ if (!current) {
19319
+ throw new RvwError(
19320
+ "PR_NOT_FOUND",
19321
+ `Pull Request\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${update.pullRequestId}`,
19322
+ { status: 404 }
19323
+ );
19324
+ }
19325
+ if (current.githubState === update.state && current.githubIsDraft === update.isDraft) {
19326
+ continue;
19327
+ }
19250
19328
  const result = statement.run(update.state, update.isDraft ? 1 : 0, update.pullRequestId);
19251
19329
  if (Number(result.changes) !== 1) {
19252
19330
  throw new RvwError(
@@ -19255,18 +19333,24 @@ var RvwDatabase = class {
19255
19333
  { status: 404 }
19256
19334
  );
19257
19335
  }
19336
+ changed = true;
19258
19337
  }
19259
- this.incrementChangeSequence();
19338
+ if (changed) this.incrementDomainRevisions(["pullRequests"]);
19260
19339
  });
19261
19340
  }
19262
19341
  updateRepositoryLocation(id, repository) {
19342
+ const existing = this.getPullRequest(id);
19343
+ if (!existing) throw new RvwError("PR_NOT_FOUND", "Pull Request\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
19344
+ if (existing.localRepositoryPath === repository.localRepositoryPath && existing.gitCommonDir === repository.gitCommonDir) {
19345
+ return existing;
19346
+ }
19263
19347
  this.immediateTransaction(() => {
19264
19348
  const result = this.database.prepare(
19265
19349
  "UPDATE pull_requests SET local_repository_path = ?, git_common_dir = ?, updated_at = ? WHERE id = ?"
19266
19350
  ).run(repository.localRepositoryPath, repository.gitCommonDir, (/* @__PURE__ */ new Date()).toISOString(), id);
19267
19351
  if (Number(result.changes) === 0)
19268
19352
  throw new RvwError("PR_NOT_FOUND", "Pull Request\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
19269
- this.incrementChangeSequence();
19353
+ this.incrementDomainRevisions(["pullRequests"]);
19270
19354
  });
19271
19355
  const pullRequest = this.getPullRequest(id);
19272
19356
  if (!pullRequest) throw new RvwError("PR_NOT_FOUND", "Pull Request\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
@@ -19276,6 +19360,8 @@ var RvwDatabase = class {
19276
19360
  const now = (/* @__PURE__ */ new Date()).toISOString();
19277
19361
  const existing = this.findPullRequestByIdentity(github.owner, github.repository, github.number);
19278
19362
  const id = existing?.id ?? randomUUID();
19363
+ const contentChanged = !existing || existing.latestTitle !== github.title || existing.latestBody !== github.body;
19364
+ const semanticChanged = !existing || existing.url !== github.url || existing.latestAuthorLogin !== github.authorLogin || existing.latestHeadRepositoryOwner !== github.headRepositoryOwner || existing.latestHeadRepositoryName !== github.headRepositoryName || existing.localRepositoryPath !== repository.localRepositoryPath || existing.gitCommonDir !== repository.gitCommonDir || contentChanged || existing.latestBaseRefName !== github.baseRefName || existing.latestHeadRefName !== github.headRefName || existing.latestBaseOid !== github.baseOid || existing.latestComparisonBaseOid !== comparisonBaseOid || existing.latestHeadOid !== github.headOid || existing.githubCreatedAt !== github.createdAt || existing.githubUpdatedAt !== github.updatedAt || existing.githubState !== github.state || existing.githubIsDraft !== github.isDraft;
19279
19365
  this.database.prepare(
19280
19366
  `INSERT INTO pull_requests(
19281
19367
  id, host, owner, repository, number, github_url,
@@ -19329,16 +19415,19 @@ var RvwDatabase = class {
19329
19415
  github.isDraft ? 1 : 0,
19330
19416
  now,
19331
19417
  existing?.createdAt ?? now,
19332
- now,
19418
+ semanticChanged ? now : existing.updatedAt,
19333
19419
  comparisonBaseOid
19334
19420
  );
19335
- return id;
19421
+ return { id, semanticChanged, contentChanged };
19336
19422
  }
19337
19423
  upsertPullRequest(github, repository, comparisonBaseOid) {
19338
19424
  const id = this.immediateTransaction(() => {
19339
- const writtenId = this.writePullRequest(github, repository, comparisonBaseOid);
19340
- this.incrementChangeSequence();
19341
- return writtenId;
19425
+ const write = this.writePullRequest(github, repository, comparisonBaseOid);
19426
+ const domains = [];
19427
+ if (write.semanticChanged) domains.push("pullRequests");
19428
+ if (write.contentChanged) domains.push("pullRequestContent");
19429
+ if (domains.length > 0) this.incrementDomainRevisions(domains);
19430
+ return write.id;
19342
19431
  });
19343
19432
  const pullRequest = this.getPullRequest(id);
19344
19433
  if (!pullRequest)
@@ -19347,10 +19436,14 @@ var RvwDatabase = class {
19347
19436
  }
19348
19437
  syncPullRequestAndComments(github, repository, comparisonBaseOid, updates) {
19349
19438
  const id = this.immediateTransaction(() => {
19350
- const writtenId = this.writePullRequest(github, repository, comparisonBaseOid);
19351
- this.applyCommentUpdates(updates, github.headOid);
19352
- this.incrementChangeSequence();
19353
- return writtenId;
19439
+ const write = this.writePullRequest(github, repository, comparisonBaseOid);
19440
+ const commentsChanged = this.applyCommentUpdates(updates, github.headOid);
19441
+ const domains = [];
19442
+ if (write.semanticChanged) domains.push("pullRequests");
19443
+ if (write.contentChanged) domains.push("pullRequestContent");
19444
+ if (commentsChanged) domains.push("comments");
19445
+ if (domains.length > 0) this.incrementDomainRevisions(domains);
19446
+ return write.id;
19354
19447
  });
19355
19448
  const pullRequest = this.getPullRequest(id);
19356
19449
  if (!pullRequest)
@@ -19359,10 +19452,16 @@ var RvwDatabase = class {
19359
19452
  }
19360
19453
  resetPullRequest(github, repository, comparisonBaseOid) {
19361
19454
  const id = this.immediateTransaction(() => {
19362
- const writtenId = this.writePullRequest(github, repository, comparisonBaseOid);
19363
- this.deletePullRequestHistory(writtenId);
19364
- this.incrementChangeSequence();
19365
- return writtenId;
19455
+ const write = this.writePullRequest(github, repository, comparisonBaseOid);
19456
+ this.deletePullRequestHistory(write.id);
19457
+ this.incrementDomainRevisions([
19458
+ "pullRequests",
19459
+ "pullRequestContent",
19460
+ "comments",
19461
+ "walkthroughs",
19462
+ "structures"
19463
+ ]);
19464
+ return write.id;
19366
19465
  });
19367
19466
  const pullRequest = this.getPullRequest(id);
19368
19467
  if (!pullRequest)
@@ -19496,7 +19595,7 @@ var RvwDatabase = class {
19496
19595
  key_hash, request_hash, structure_id, created_at
19497
19596
  ) VALUES (?, ?, ?, ?)`
19498
19597
  ).run(keyHash, input.idempotencyRequestHash, id, now);
19499
- this.incrementChangeSequence();
19598
+ this.incrementDomainRevisions(["structures"]);
19500
19599
  structureId = id;
19501
19600
  });
19502
19601
  if (!structureId) {
@@ -19579,7 +19678,7 @@ var RvwDatabase = class {
19579
19678
  "INSERT OR IGNORE INTO structure_retired_edge_ids(structure_id, edge_id, retired_at) VALUES (?, ?, ?)"
19580
19679
  );
19581
19680
  for (const edgeId of retiredEdgesAtThisUpdate) retireEdge.run(id, edgeId, now);
19582
- this.incrementChangeSequence();
19681
+ this.incrementDomainRevisions(["structures"]);
19583
19682
  });
19584
19683
  const structure = this.getStructure(id);
19585
19684
  if (!structure) throw new RvwError("DATABASE_ERROR", "\u66F4\u65B0\u3057\u305FStructure\u3092\u8AAD\u307F\u51FA\u305B\u307E\u305B\u3093\u3002");
@@ -19619,7 +19718,7 @@ var RvwDatabase = class {
19619
19718
  { status: 409, details: { expectedUpdatedAt: expectedUpdatedAt2 } }
19620
19719
  );
19621
19720
  }
19622
- this.incrementChangeSequence();
19721
+ this.incrementDomainRevisions(["structures"]);
19623
19722
  return {
19624
19723
  id: structure.id,
19625
19724
  ref: structure.ref,
@@ -19715,7 +19814,7 @@ var RvwDatabase = class {
19715
19814
  now
19716
19815
  );
19717
19816
  this.insertCodeReferences("walkthrough", id, input.references);
19718
- this.incrementChangeSequence();
19817
+ this.incrementDomainRevisions(["walkthroughs"]);
19719
19818
  });
19720
19819
  const walkthrough = this.getWalkthrough(id);
19721
19820
  if (!walkthrough) {
@@ -19742,7 +19841,7 @@ var RvwDatabase = class {
19742
19841
  }
19743
19842
  this.database.prepare("DELETE FROM walkthrough_references WHERE walkthrough_id = ?").run(id);
19744
19843
  this.insertCodeReferences("walkthrough", id, input.references);
19745
- this.incrementChangeSequence();
19844
+ this.incrementDomainRevisions(["walkthroughs"]);
19746
19845
  });
19747
19846
  const walkthrough = this.getWalkthrough(id);
19748
19847
  if (!walkthrough) {
@@ -19778,7 +19877,9 @@ var RvwDatabase = class {
19778
19877
  WHERE id IN (SELECT comment_id FROM comment_targets WHERE walkthrough_id = ?)`
19779
19878
  ).run(id);
19780
19879
  this.database.prepare("DELETE FROM walkthroughs WHERE id = ?").run(id);
19781
- this.incrementChangeSequence();
19880
+ this.incrementDomainRevisions(
19881
+ counts.comments === 0 ? ["walkthroughs"] : ["walkthroughs", "comments"]
19882
+ );
19782
19883
  return {
19783
19884
  id: walkthrough.id,
19784
19885
  ref: walkthrough.ref,
@@ -19816,7 +19917,7 @@ var RvwDatabase = class {
19816
19917
  post_id, comment_ref, pull_request_url, created_at
19817
19918
  ) VALUES (?, ?, ?, ?)`
19818
19919
  ).run(postId, formatCommentUri(id), pullRequest.url, now);
19819
- this.incrementChangeSequence();
19920
+ this.incrementDomainRevisions(["comments"]);
19820
19921
  });
19821
19922
  const comment3 = this.getComment(id);
19822
19923
  if (!comment3) throw new RvwError("DATABASE_ERROR", "\u4FDD\u5B58\u3057\u305F\u30B3\u30E1\u30F3\u30C8\u3092\u8AAD\u307F\u51FA\u305B\u307E\u305B\u3093\u3002");
@@ -19935,7 +20036,7 @@ var RvwDatabase = class {
19935
20036
  FROM comments c
19936
20037
  JOIN comment_targets t ON t.comment_id = c.id
19937
20038
  LEFT JOIN walkthroughs w ON w.id = t.walkthrough_id
19938
- WHERE c.pull_request_id = ?${where} ORDER BY c.updated_at DESC`
20039
+ WHERE c.pull_request_id = ?${where} ORDER BY c.updated_at DESC, c.id DESC`
19939
20040
  ).all(pullRequestId).map((row) => this.mapComment(row));
19940
20041
  }
19941
20042
  listCommentPage(pullRequestId, resolved, limit, offset) {
@@ -20061,7 +20162,7 @@ var RvwDatabase = class {
20061
20162
  ).run(idempotencyKeyHash, idempotencyRequestHash2, id, now);
20062
20163
  }
20063
20164
  this.database.prepare("UPDATE comments SET updated_at = ? WHERE id = ?").run(now, commentId);
20064
- if (incrementSequence) this.incrementChangeSequence();
20165
+ if (incrementSequence) this.incrementDomainRevisions(["comments"]);
20065
20166
  result = {
20066
20167
  id,
20067
20168
  commentId,
@@ -20096,7 +20197,7 @@ var RvwDatabase = class {
20096
20197
  this.database.prepare("DELETE FROM comment_post_references WHERE post_id = ?").run(postId);
20097
20198
  this.insertCodeReferences("comment-post", postId, references);
20098
20199
  this.database.prepare("UPDATE comments SET updated_at = ? WHERE id = ?").run(now, commentId);
20099
- this.incrementChangeSequence();
20200
+ this.incrementDomainRevisions(["comments"]);
20100
20201
  });
20101
20202
  const post = this.getCommentPost(postId);
20102
20203
  if (!post) {
@@ -20128,7 +20229,7 @@ var RvwDatabase = class {
20128
20229
  this.immediateTransaction(() => {
20129
20230
  this.database.prepare("DELETE FROM comment_posts WHERE id = ? AND comment_id = ?").run(postId, commentId);
20130
20231
  this.database.prepare("UPDATE comments SET updated_at = ? WHERE id = ?").run(now, commentId);
20131
- this.incrementChangeSequence();
20232
+ this.incrementDomainRevisions(["comments"]);
20132
20233
  });
20133
20234
  return { commentId, postId };
20134
20235
  }
@@ -20138,7 +20239,7 @@ var RvwDatabase = class {
20138
20239
  const result = this.database.prepare("UPDATE comments SET resolved_at = ?, updated_at = ? WHERE id = ?").run(resolved ? now : null, now, commentId);
20139
20240
  if (Number(result.changes) === 0)
20140
20241
  throw new RvwError("COMMENT_NOT_FOUND", "\u30B3\u30E1\u30F3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
20141
- if (incrementSequence) this.incrementChangeSequence();
20242
+ if (incrementSequence) this.incrementDomainRevisions(["comments"]);
20142
20243
  };
20143
20244
  if (incrementSequence) this.immediateTransaction(write);
20144
20245
  else write();
@@ -20153,14 +20254,18 @@ var RvwDatabase = class {
20153
20254
  }
20154
20255
  this.immediateTransaction(() => {
20155
20256
  this.database.prepare("DELETE FROM comments WHERE id = ?").run(commentId);
20156
- this.incrementChangeSequence();
20257
+ this.incrementDomainRevisions(["comments"]);
20157
20258
  });
20158
20259
  return { id: comment3.id, ref: comment3.ref };
20159
20260
  }
20160
20261
  applyCommentUpdates(updates, relatedCommitOid) {
20262
+ let changed = false;
20161
20263
  for (const update of updates) {
20264
+ const current = this.getComment(update.commentId);
20265
+ if (!current) throw new RvwError("COMMENT_NOT_FOUND", "\u30B3\u30E1\u30F3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
20162
20266
  if (update.reply.trim().length > 0) {
20163
- this.insertReply(
20267
+ const existingPostIds = new Set(current.posts.map(({ id }) => id));
20268
+ const reply = this.insertReply(
20164
20269
  update.commentId,
20165
20270
  {
20166
20271
  body: update.reply,
@@ -20173,11 +20278,14 @@ var RvwDatabase = class {
20173
20278
  },
20174
20279
  false
20175
20280
  );
20176
- } else if (!this.getComment(update.commentId)) {
20177
- throw new RvwError("COMMENT_NOT_FOUND", "\u30B3\u30E1\u30F3\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002");
20281
+ if (!existingPostIds.has(reply.id)) changed = true;
20282
+ }
20283
+ if (update.resolve && current.resolvedAt === null) {
20284
+ this.setCommentResolved(update.commentId, true, false);
20285
+ changed = true;
20178
20286
  }
20179
- if (update.resolve) this.setCommentResolved(update.commentId, true, false);
20180
20287
  }
20288
+ return changed;
20181
20289
  }
20182
20290
  };
20183
20291
 
@@ -20187,12 +20295,13 @@ import path5 from "node:path";
20187
20295
  import { TextDecoder as TextDecoder2 } from "node:util";
20188
20296
 
20189
20297
  // src/shared/constants.ts
20190
- var APP_VERSION = "0.4.0";
20298
+ var APP_VERSION = "0.4.2";
20191
20299
  var PROTOCOL_VERSION = 4;
20192
20300
  var VIEWER_ID_HEADER = "x-rvw-viewer-id";
20193
20301
  var VIEWER_OPEN_LEASE_HEADER = "x-rvw-viewer-open-lease";
20194
20302
  var VIEWER_OPEN_LEASE_QUERY = "viewerLease";
20195
20303
  var GIT_OBJECT_ID_PATTERN = /^[0-9a-f]{40,64}$/i;
20304
+ var CONTENT_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
20196
20305
  var MAX_TEXT_DOCUMENT_BYTES = 1024 * 1024;
20197
20306
  var MAX_MARKDOWN_ASSET_BYTES = 5 * 1024 * 1024;
20198
20307
  var MAX_GITHUB_ATTACHMENT_BYTES = 10 * 1024 * 1024;
@@ -28386,6 +28495,9 @@ ${normalizeLf(body3)}`;
28386
28495
  function hashDocument(value) {
28387
28496
  return createHash2("sha256").update(normalizeLf(value), "utf8").digest("hex");
28388
28497
  }
28498
+ function pullRequestContentFingerprint(title, body3) {
28499
+ return hashDocument(buildPullRequestMarkdown(title, body3));
28500
+ }
28389
28501
  function selectedLineText(value, startLine, endLine) {
28390
28502
  const lines2 = normalizeLf(value).split("\n");
28391
28503
  if (startLine < 1 || endLine < startLine || endLine > lines2.length) return null;
@@ -28477,6 +28589,34 @@ function structureSourceAnchor(structure, locator) {
28477
28589
  const edge = structure.edges.find((candidate) => candidate.id === locator.edgeId);
28478
28590
  return edge?.anchors[locator.anchorIndex] ?? null;
28479
28591
  }
28592
+ function compareStableIds(left, right) {
28593
+ if (left === right) return 0;
28594
+ return left < right ? -1 : 1;
28595
+ }
28596
+ function closestMatchingStructureNode(structure, matchingNodeIds) {
28597
+ if (matchingNodeIds.size === 0) return null;
28598
+ const distances = /* @__PURE__ */ new Map([[structure.originNodeId, 0]]);
28599
+ const neighbors = new Map(structure.nodes.map((node2) => [node2.id, /* @__PURE__ */ new Set()]));
28600
+ for (const edge of structure.edges) {
28601
+ neighbors.get(edge.from)?.add(edge.to);
28602
+ neighbors.get(edge.to)?.add(edge.from);
28603
+ }
28604
+ const queue = [structure.originNodeId];
28605
+ for (let index2 = 0; index2 < queue.length; index2 += 1) {
28606
+ const nodeId = queue[index2];
28607
+ const distance = distances.get(nodeId);
28608
+ for (const neighbor of neighbors.get(nodeId) ?? []) {
28609
+ if (distances.has(neighbor)) continue;
28610
+ distances.set(neighbor, distance + 1);
28611
+ queue.push(neighbor);
28612
+ }
28613
+ }
28614
+ return structure.nodes.filter((node2) => matchingNodeIds.has(node2.id)).sort((left, right) => {
28615
+ const leftDistance = distances.get(left.id) ?? Number.POSITIVE_INFINITY;
28616
+ const rightDistance = distances.get(right.id) ?? Number.POSITIVE_INFINITY;
28617
+ return leftDistance - rightDistance || compareStableIds(left.id, right.id);
28618
+ })[0] ?? null;
28619
+ }
28480
28620
 
28481
28621
  // src/domain/source-excerpt.ts
28482
28622
  var SOURCE_EXCERPT_CONTEXT_LINES = 20;
@@ -41445,6 +41585,18 @@ function assertLinePair(startLine, endLine) {
41445
41585
  }
41446
41586
  var codeReferenceIdPattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41447
41587
  var walkthroughDiagramNodePattern = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41588
+ function structureSummary(structure) {
41589
+ return {
41590
+ id: structure.id,
41591
+ ref: structure.ref,
41592
+ pullRequestId: structure.pullRequestId,
41593
+ sourceOid: structure.sourceOid,
41594
+ title: structure.title,
41595
+ scope: structure.scope,
41596
+ createdAt: structure.createdAt,
41597
+ updatedAt: structure.updatedAt
41598
+ };
41599
+ }
41448
41600
  var mermaidIdentifierPattern = /[A-Za-z][A-Za-z0-9_-]{0,63}/g;
41449
41601
  var mermaidEdgePattern = /[<|o*x]*[-.=~]{2,}[|o*x>]*/g;
41450
41602
  var mermaidClassShorthandPattern = /:::[A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*/g;
@@ -41666,6 +41818,20 @@ function assertCodeReferencePath(filePath) {
41666
41818
  throw new RvwError("INVALID_INPUT", `code reference\u306Epath\u304C\u4E0D\u6B63\u3067\u3059: ${filePath}`);
41667
41819
  }
41668
41820
  }
41821
+ async function mapWithConcurrency(values, concurrency, mapper) {
41822
+ const results = new Array(values.length);
41823
+ let nextIndex = 0;
41824
+ const worker = async () => {
41825
+ while (nextIndex < values.length) {
41826
+ const index2 = nextIndex++;
41827
+ results[index2] = await mapper(values[index2], index2);
41828
+ }
41829
+ };
41830
+ await Promise.all(
41831
+ Array.from({ length: Math.min(concurrency, values.length) }, async () => await worker())
41832
+ );
41833
+ return results;
41834
+ }
41669
41835
  var RvwService = class {
41670
41836
  constructor(database, git, github) {
41671
41837
  this.database = database;
@@ -42053,7 +42219,10 @@ var RvwService = class {
42053
42219
  });
42054
42220
  }
42055
42221
  async getPullRequestView(id) {
42056
- const pullRequest = this.getPullRequest(id);
42222
+ const { pullRequest, revisionSnapshot } = this.database.getPullRequestRevisionSnapshot(id);
42223
+ if (!pullRequest) {
42224
+ throw new RvwError("PR_NOT_FOUND", "Pull Request\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002", { status: 404 });
42225
+ }
42057
42226
  const commits = await this.git.commits(
42058
42227
  pullRequest.localRepositoryPath,
42059
42228
  pullRequest.latestComparisonBaseOid,
@@ -42061,11 +42230,30 @@ var RvwService = class {
42061
42230
  );
42062
42231
  return {
42063
42232
  pullRequest,
42233
+ pullRequestContentFingerprint: pullRequestContentFingerprint(
42234
+ pullRequest.latestTitle,
42235
+ pullRequest.latestBody
42236
+ ),
42064
42237
  comparisonBaseOid: pullRequest.latestComparisonBaseOid,
42065
42238
  headOid: pullRequest.latestHeadOid,
42066
- commits
42239
+ commits,
42240
+ ...revisionSnapshot
42067
42241
  };
42068
42242
  }
42243
+ assertPullRequestContentFingerprint(pullRequest, expectedFingerprint) {
42244
+ const actualFingerprint = pullRequestContentFingerprint(
42245
+ pullRequest.latestTitle,
42246
+ pullRequest.latestBody
42247
+ );
42248
+ if (expectedFingerprint !== void 0 && expectedFingerprint !== actualFingerprint) {
42249
+ throw new RvwError("STALE_CONTENT", "Pull Request\u672C\u6587\u304C\u66F4\u65B0\u3055\u308C\u3066\u3044\u307E\u3059\u3002", {
42250
+ status: 409,
42251
+ details: { expectedFingerprint, actualFingerprint },
42252
+ suggestions: ["\u6700\u65B0\u306EPull Request\u672C\u6587\u3092\u518D\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002"]
42253
+ });
42254
+ }
42255
+ return actualFingerprint;
42256
+ }
42069
42257
  async assertCommitAvailable(pullRequest, oid) {
42070
42258
  if (!GIT_OBJECT_ID_PATTERN.test(oid) || !await this.git.hasObject(pullRequest.localRepositoryPath, oid)) {
42071
42259
  throw new RvwError("COMMIT_NOT_FOUND", `Git commit\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${oid}`, { status: 404 });
@@ -42122,6 +42310,26 @@ var RvwService = class {
42122
42310
  );
42123
42311
  return { ref, ...content3 };
42124
42312
  }
42313
+ async getDocumentWithContentFingerprint(ref, expectedFingerprint) {
42314
+ if (ref.kind !== "pull-request-markdown") {
42315
+ return { document: await this.getDocument(ref), pullRequestContentFingerprint: null };
42316
+ }
42317
+ const pullRequest = this.getPullRequest(ref.pullRequestId);
42318
+ const fingerprint = this.assertPullRequestContentFingerprint(pullRequest, expectedFingerprint);
42319
+ const text5 = buildPullRequestMarkdown(pullRequest.latestTitle, pullRequest.latestBody);
42320
+ return {
42321
+ document: {
42322
+ ref,
42323
+ availability: "available",
42324
+ text: text5,
42325
+ byteLength: Buffer.byteLength(text5, "utf8"),
42326
+ entryKind: "virtual",
42327
+ normalizedLineEndings: false,
42328
+ oid: null
42329
+ },
42330
+ pullRequestContentFingerprint: fingerprint
42331
+ };
42332
+ }
42125
42333
  async getRepositoryAsset(pullRequestId, sourceOid, filePath) {
42126
42334
  const pullRequest = this.getPullRequest(pullRequestId);
42127
42335
  await this.assertCommitAvailable(pullRequest, sourceOid);
@@ -42149,7 +42357,7 @@ var RvwService = class {
42149
42357
  ]);
42150
42358
  return { ref, old, new: next2 };
42151
42359
  }
42152
- async search(pullRequestId, oid, query, options) {
42360
+ async search(pullRequestId, oid, query, options, expectedPullRequestContentFingerprint) {
42153
42361
  const queryBytes = Buffer.byteLength(query, "utf8");
42154
42362
  if (queryBytes === 0 || queryBytes > MAX_SEARCH_QUERY_BYTES || query.includes("\n") || query.includes("\r")) {
42155
42363
  throw new RvwError(
@@ -42158,6 +42366,10 @@ var RvwService = class {
42158
42366
  );
42159
42367
  }
42160
42368
  const pullRequest = this.getPullRequest(pullRequestId);
42369
+ const contentFingerprint = this.assertPullRequestContentFingerprint(
42370
+ pullRequest,
42371
+ expectedPullRequestContentFingerprint
42372
+ );
42161
42373
  await this.assertCommitAvailable(pullRequest, oid);
42162
42374
  const prMarkdown = buildPullRequestMarkdown(pullRequest.latestTitle, pullRequest.latestBody);
42163
42375
  const results = [];
@@ -42183,6 +42395,7 @@ var RvwService = class {
42183
42395
  });
42184
42396
  }
42185
42397
  return {
42398
+ pullRequestContentFingerprint: contentFingerprint,
42186
42399
  results,
42187
42400
  matchCount: results.reduce((count, result) => count + result.matches.length, 0),
42188
42401
  truncated: gitResult.truncated || results.length >= MAX_SEARCH_RESULTS && (gitResult.results.length > 0 || prMarkdown.split("\n").some((line) => findFixedStringMatches(line, query, options).length > 0)),
@@ -42509,6 +42722,85 @@ var RvwService = class {
42509
42722
  this.getPullRequest(pullRequestId);
42510
42723
  return this.database.listStructures(pullRequestId);
42511
42724
  }
42725
+ async listFileStructureReferences(pullRequestId, targetSourceOid, targetPath) {
42726
+ assertCodeReferencePath(targetPath);
42727
+ const index2 = await this.listFileStructureReferenceIndex(pullRequestId, targetSourceOid);
42728
+ return index2.entries.find(({ path: candidate }) => candidate === targetPath)?.references ?? [];
42729
+ }
42730
+ async listFileStructureReferenceIndex(pullRequestId, targetSourceOid) {
42731
+ const pullRequest = this.getPullRequest(pullRequestId);
42732
+ const structures = this.database.listStructures(pullRequestId).flatMap((summary) => {
42733
+ const structure = this.database.getStructure(summary.id);
42734
+ return structure?.pullRequestId === pullRequestId ? [structure] : [];
42735
+ });
42736
+ if (structures.length === 0) return { sourceOid: targetSourceOid, entries: [] };
42737
+ await this.assertCommitAvailable(pullRequest, targetSourceOid);
42738
+ const treePaths = (await this.git.tree(pullRequest.localRepositoryPath, targetSourceOid)).map(
42739
+ (entry) => entry.path
42740
+ );
42741
+ const targetPaths = new Set(treePaths);
42742
+ const successorIndexes = /* @__PURE__ */ new Map();
42743
+ const successorIndex = (sourceOid) => {
42744
+ const key2 = JSON.stringify([sourceOid, targetSourceOid]);
42745
+ const cached2 = successorIndexes.get(key2);
42746
+ if (cached2) return cached2;
42747
+ const index2 = this.git.changedFilesWithCopies(pullRequest.localRepositoryPath, sourceOid, targetSourceOid).then((changes) => {
42748
+ const successors = /* @__PURE__ */ new Map();
42749
+ for (const candidate of changes) {
42750
+ if (!candidate.status.startsWith("R") && !candidate.status.startsWith("C") || candidate.oldPath === null || candidate.newPath === null) {
42751
+ continue;
42752
+ }
42753
+ const paths = successors.get(candidate.oldPath) ?? /* @__PURE__ */ new Set();
42754
+ paths.add(candidate.newPath);
42755
+ successors.set(candidate.oldPath, paths);
42756
+ }
42757
+ return successors;
42758
+ });
42759
+ successorIndexes.set(key2, index2);
42760
+ return index2;
42761
+ };
42762
+ const resolvePath = (sourceOid, sourcePath) => {
42763
+ if (targetPaths.has(sourcePath)) return Promise.resolve(sourcePath);
42764
+ if (sourceOid === targetSourceOid) return Promise.resolve(null);
42765
+ return successorIndex(sourceOid).then((index2) => {
42766
+ const successors = index2.get(sourcePath);
42767
+ if (successors?.size !== 1) return null;
42768
+ const successor = [...successors][0];
42769
+ return targetPaths.has(successor) ? successor : null;
42770
+ });
42771
+ };
42772
+ const referencesByPath = /* @__PURE__ */ new Map();
42773
+ for (const structure of structures) {
42774
+ const matchingNodeIdsByPath = /* @__PURE__ */ new Map();
42775
+ for (const node2 of structure.nodes) {
42776
+ if (!node2.anchor) continue;
42777
+ const resolvedPath = await resolvePath(structure.sourceOid, node2.anchor.path);
42778
+ if (!resolvedPath) continue;
42779
+ const nodeIds = matchingNodeIdsByPath.get(resolvedPath) ?? /* @__PURE__ */ new Set();
42780
+ nodeIds.add(node2.id);
42781
+ matchingNodeIdsByPath.set(resolvedPath, nodeIds);
42782
+ }
42783
+ for (const [resolvedPath, matchingNodeIds] of matchingNodeIdsByPath) {
42784
+ const targetNode = closestMatchingStructureNode(structure, matchingNodeIds);
42785
+ if (!targetNode) continue;
42786
+ const references = referencesByPath.get(resolvedPath) ?? [];
42787
+ references.push({
42788
+ structure: structureSummary(structure),
42789
+ targetNodeId: targetNode.id,
42790
+ targetNodeLabel: targetNode.label,
42791
+ matchingNodeCount: matchingNodeIds.size
42792
+ });
42793
+ referencesByPath.set(resolvedPath, references);
42794
+ }
42795
+ }
42796
+ return {
42797
+ sourceOid: targetSourceOid,
42798
+ entries: treePaths.flatMap((path11) => {
42799
+ const references = referencesByPath.get(path11);
42800
+ return references ? [{ path: path11, references }] : [];
42801
+ })
42802
+ };
42803
+ }
42512
42804
  listStructuresByReference(reference) {
42513
42805
  const pullRequest = this.resolveStoredPullRequest(reference);
42514
42806
  return { pullRequest, structures: this.database.listStructures(pullRequest.id) };
@@ -42867,38 +43159,15 @@ var RvwService = class {
42867
43159
  path: reference.path
42868
43160
  });
42869
43161
  const latestHeadOid = pullRequest.latestHeadOid;
42870
- let latestPath = reference.path;
42871
- let latestDocument;
42872
- if (sourceOid === latestHeadOid) {
42873
- latestDocument = sourceDocument;
42874
- } else {
42875
- latestDocument = await this.getDocument({
42876
- kind: "repository-file",
42877
- pullRequestId: pullRequest.id,
42878
- sourceOid: latestHeadOid,
42879
- path: reference.path
42880
- });
42881
- if (latestDocument.availability === "missing") {
42882
- const successorPaths = [
42883
- ...new Set(
42884
- (await this.git.changedFilesWithCopies(
42885
- pullRequest.localRepositoryPath,
42886
- sourceOid,
42887
- latestHeadOid
42888
- )).filter(
42889
- (candidate) => (candidate.status.startsWith("R") || candidate.status.startsWith("C")) && candidate.oldPath === reference.path && candidate.newPath !== null
42890
- ).map((candidate) => candidate.newPath)
42891
- )
42892
- ];
42893
- latestPath = successorPaths.length === 1 ? successorPaths[0] : null;
42894
- latestDocument = latestPath === null ? null : await this.getDocument({
42895
- kind: "repository-file",
42896
- pullRequestId: pullRequest.id,
42897
- sourceOid: latestHeadOid,
42898
- path: latestPath
42899
- });
42900
- }
42901
- }
43162
+ const resolvedLatestFile = await this.resolveSourceFileAtCommit(
43163
+ pullRequest,
43164
+ sourceOid,
43165
+ reference.path,
43166
+ latestHeadOid,
43167
+ sourceDocument
43168
+ );
43169
+ const latestPath = resolvedLatestFile?.path ?? null;
43170
+ const latestDocument = resolvedLatestFile?.document ?? null;
42902
43171
  const latestFileExists = latestDocument !== null && latestDocument.availability !== "missing";
42903
43172
  const latestFileDisplayable = latestDocument?.availability === "available";
42904
43173
  const mappedRange = reference.startLine === null || reference.endLine === null ? latestFileDisplayable ? null : void 0 : sourceDocument.availability === "available" && latestDocument?.availability === "available" ? mapUnchangedLineRange(
@@ -42955,6 +43224,38 @@ var RvwService = class {
42955
43224
  document: sourceDocument
42956
43225
  };
42957
43226
  }
43227
+ async resolveSourceFileAtCommit(pullRequest, sourceOid, sourcePath, targetSourceOid, sourceDocument) {
43228
+ const directDocument = sourceOid === targetSourceOid ? sourceDocument : await this.getDocument({
43229
+ kind: "repository-file",
43230
+ pullRequestId: pullRequest.id,
43231
+ sourceOid: targetSourceOid,
43232
+ path: sourcePath
43233
+ });
43234
+ if (directDocument.availability !== "missing") {
43235
+ return { path: sourcePath, document: directDocument };
43236
+ }
43237
+ if (sourceOid === targetSourceOid) return null;
43238
+ const successorPaths = [
43239
+ ...new Set(
43240
+ (await this.git.changedFilesWithCopies(
43241
+ pullRequest.localRepositoryPath,
43242
+ sourceOid,
43243
+ targetSourceOid
43244
+ )).filter(
43245
+ (candidate) => (candidate.status.startsWith("R") || candidate.status.startsWith("C")) && candidate.oldPath === sourcePath && candidate.newPath !== null
43246
+ ).map((candidate) => candidate.newPath)
43247
+ )
43248
+ ];
43249
+ if (successorPaths.length !== 1) return null;
43250
+ const successorPath = successorPaths[0];
43251
+ const successorDocument = await this.getDocument({
43252
+ kind: "repository-file",
43253
+ pullRequestId: pullRequest.id,
43254
+ sourceOid: targetSourceOid,
43255
+ path: successorPath
43256
+ });
43257
+ return successorDocument.availability === "missing" ? null : { path: successorPath, document: successorDocument };
43258
+ }
42958
43259
  getWalkthroughByUri(uri) {
42959
43260
  const walkthrough = this.database.getWalkthrough(parseWalkthroughUri(uri));
42960
43261
  if (!walkthrough) {
@@ -43204,40 +43505,54 @@ var RvwService = class {
43204
43505
  }
43205
43506
  return { ...placeMutableDocumentComment(comment3.target, walkthrough.body), path: null };
43206
43507
  }
43207
- async resolveRepositoryCommentPath(pullRequest, source, destinationOid) {
43508
+ async resolveRepositoryCommentPath(pullRequest, source, destinationOid, context) {
43208
43509
  if (source.sourceOid === destinationOid) {
43209
43510
  return { path: source.path, deleted: false };
43210
43511
  }
43211
- const changes = await this.git.changedFiles(
43212
- pullRequest.localRepositoryPath,
43213
- source.sourceOid,
43214
- destinationOid
43215
- );
43512
+ const key2 = JSON.stringify([source.sourceOid, destinationOid]);
43513
+ let changesPromise = context.changedFiles.get(key2);
43514
+ if (!changesPromise) {
43515
+ changesPromise = this.git.changedFiles(
43516
+ pullRequest.localRepositoryPath,
43517
+ source.sourceOid,
43518
+ destinationOid
43519
+ );
43520
+ context.changedFiles.set(key2, changesPromise);
43521
+ }
43522
+ const changes = await changesPromise;
43216
43523
  const change = changes.find((candidate) => candidate.oldPath === source.path);
43217
43524
  if (change?.kind === "deleted" || change && change.newPath === null) {
43218
43525
  return { path: source.path, deleted: true };
43219
43526
  }
43220
43527
  return { path: change?.newPath ?? source.path, deleted: false };
43221
43528
  }
43222
- async placeRepositoryCommentAtPath(pullRequest, source, destinationOid, destinationPath) {
43223
- const destinationContent = await this.getDocument({
43224
- kind: "repository-file",
43225
- pullRequestId: pullRequest.id,
43226
- sourceOid: destinationOid,
43227
- path: destinationPath
43228
- });
43529
+ async placeRepositoryCommentAtPath(pullRequest, source, destinationOid, destinationPath, context) {
43530
+ const destinationContent = await this.getPlacementDocument(
43531
+ pullRequest,
43532
+ {
43533
+ kind: "repository-file",
43534
+ pullRequestId: pullRequest.id,
43535
+ sourceOid: destinationOid,
43536
+ path: destinationPath
43537
+ },
43538
+ context
43539
+ );
43229
43540
  if (source.startLine === null || source.endLine === null) {
43230
43541
  return destinationContent.availability === "missing" ? { outdated: true, range: null, path: destinationPath } : { outdated: false, range: null, path: destinationPath };
43231
43542
  }
43232
43543
  if (destinationContent.availability !== "available") {
43233
43544
  return { outdated: true, range: null, path: destinationPath };
43234
43545
  }
43235
- const sourceContent = await this.getDocument({
43236
- kind: "repository-file",
43237
- pullRequestId: pullRequest.id,
43238
- sourceOid: source.sourceOid,
43239
- path: source.path
43240
- });
43546
+ const sourceContent = await this.getPlacementDocument(
43547
+ pullRequest,
43548
+ {
43549
+ kind: "repository-file",
43550
+ pullRequestId: pullRequest.id,
43551
+ sourceOid: source.sourceOid,
43552
+ path: source.path
43553
+ },
43554
+ context
43555
+ );
43241
43556
  if (sourceContent.availability !== "available") {
43242
43557
  return { outdated: true, range: null, path: destinationPath };
43243
43558
  }
@@ -43250,55 +43565,171 @@ var RvwService = class {
43250
43565
  return range ? { outdated: false, range, path: destinationPath } : { outdated: true, range: null, path: destinationPath };
43251
43566
  }
43252
43567
  async placeComment(comment3, destination) {
43253
- if (comment3.pullRequestId !== destination.pullRequestId) {
43568
+ const pullRequest = this.getPullRequest(comment3.pullRequestId);
43569
+ return await this.placeCommentForDestination(
43570
+ comment3,
43571
+ pullRequest,
43572
+ { kind: "document", ref: destination },
43573
+ this.createCommentPlacementRequestContext()
43574
+ );
43575
+ }
43576
+ createCommentPlacementRequestContext() {
43577
+ return {
43578
+ commitAvailability: /* @__PURE__ */ new Map(),
43579
+ changedFiles: /* @__PURE__ */ new Map(),
43580
+ documents: /* @__PURE__ */ new Map()
43581
+ };
43582
+ }
43583
+ async assertPlacementCommitAvailable(pullRequest, oid, context) {
43584
+ let available = context.commitAvailability.get(oid);
43585
+ if (!available) {
43586
+ available = this.assertCommitAvailable(pullRequest, oid);
43587
+ context.commitAvailability.set(oid, available);
43588
+ }
43589
+ await available;
43590
+ }
43591
+ async getPlacementDocument(pullRequest, ref, context) {
43592
+ const key2 = JSON.stringify(ref);
43593
+ let document3 = context.documents.get(key2);
43594
+ if (!document3) {
43595
+ document3 = (async () => {
43596
+ if (ref.kind === "pull-request-markdown") {
43597
+ const text5 = buildPullRequestMarkdown(pullRequest.latestTitle, pullRequest.latestBody);
43598
+ return {
43599
+ ref,
43600
+ availability: "available",
43601
+ text: text5,
43602
+ byteLength: Buffer.byteLength(text5, "utf8"),
43603
+ entryKind: "virtual",
43604
+ normalizedLineEndings: false,
43605
+ oid: null
43606
+ };
43607
+ }
43608
+ await this.assertPlacementCommitAvailable(pullRequest, ref.sourceOid, context);
43609
+ const content3 = await this.git.readDocument(
43610
+ pullRequest.localRepositoryPath,
43611
+ ref.sourceOid,
43612
+ ref.path
43613
+ );
43614
+ return { ref, ...content3 };
43615
+ })();
43616
+ context.documents.set(key2, document3);
43617
+ }
43618
+ return await document3;
43619
+ }
43620
+ async placeCommentForDestination(comment3, pullRequest, destination, context) {
43621
+ if (destination.kind === "walkthrough") {
43622
+ return this.placeWalkthroughComment(comment3, destination.walkthroughId);
43623
+ }
43624
+ if (destination.kind === "commit") {
43625
+ await this.assertPlacementCommitAvailable(pullRequest, destination.oid, context);
43626
+ }
43627
+ const documentDestination = destination.kind === "document" ? destination.ref : null;
43628
+ const destinationOid = destination.kind === "commit" ? destination.oid : documentDestination?.kind === "repository-file" ? documentDestination.sourceOid : null;
43629
+ if (documentDestination !== null && comment3.pullRequestId !== documentDestination.pullRequestId) {
43254
43630
  return { outdated: true, range: null, path: null };
43255
43631
  }
43256
43632
  if (comment3.target.kind === "pull-request") return { outdated: false, range: null, path: null };
43257
43633
  if (comment3.target.kind === "walkthrough") {
43258
43634
  return this.placeWalkthroughComment(comment3);
43259
43635
  }
43260
- const pullRequest = this.getPullRequest(comment3.pullRequestId);
43261
43636
  if (comment3.target.documentKind === "pull-request-markdown") {
43262
- return destination.kind === "pull-request-markdown" ? this.placePullRequestMarkdownComment(comment3, pullRequest) : { outdated: true, range: null, path: null };
43637
+ return destination.kind === "commit" || documentDestination?.kind === "pull-request-markdown" ? this.placePullRequestMarkdownComment(comment3, pullRequest) : { outdated: true, range: null, path: null };
43263
43638
  }
43264
- if (destination.kind !== "repository-file") return { outdated: true, range: null, path: null };
43639
+ if (destinationOid === null) return { outdated: true, range: null, path: null };
43265
43640
  const source = comment3.target;
43641
+ await this.assertPlacementCommitAvailable(pullRequest, source.sourceOid, context);
43266
43642
  const resolved = await this.resolveRepositoryCommentPath(
43267
43643
  pullRequest,
43268
43644
  source,
43269
- destination.sourceOid
43645
+ destinationOid,
43646
+ context
43270
43647
  );
43271
43648
  if (resolved.deleted) return { outdated: true, range: null, path: source.path };
43272
- if (resolved.path !== destination.path) {
43649
+ if (documentDestination?.kind === "repository-file" && resolved.path !== documentDestination.path) {
43273
43650
  return { outdated: true, range: null, path: resolved.path };
43274
43651
  }
43275
43652
  return await this.placeRepositoryCommentAtPath(
43276
43653
  pullRequest,
43277
43654
  source,
43278
- destination.sourceOid,
43279
- resolved.path
43655
+ destinationOid,
43656
+ resolved.path,
43657
+ context
43280
43658
  );
43281
43659
  }
43282
43660
  async placeCommentAtCommit(comment3, destinationOid) {
43283
43661
  const pullRequest = this.getPullRequest(comment3.pullRequestId);
43284
- await this.assertCommitAvailable(pullRequest, destinationOid);
43285
- if (comment3.target.kind === "pull-request") return { outdated: false, range: null, path: null };
43286
- if (comment3.target.kind === "walkthrough") {
43287
- return this.placeWalkthroughComment(comment3);
43288
- }
43289
- if (comment3.target.documentKind === "pull-request-markdown") {
43290
- return this.placePullRequestMarkdownComment(comment3, pullRequest);
43291
- }
43292
- const source = comment3.target;
43293
- const resolved = await this.resolveRepositoryCommentPath(pullRequest, source, destinationOid);
43294
- if (resolved.deleted) return { outdated: true, range: null, path: source.path };
43295
- return await this.placeRepositoryCommentAtPath(
43662
+ return await this.placeCommentForDestination(
43663
+ comment3,
43296
43664
  pullRequest,
43297
- source,
43298
- destinationOid,
43299
- resolved.path
43665
+ { kind: "commit", oid: destinationOid },
43666
+ this.createCommentPlacementRequestContext()
43300
43667
  );
43301
43668
  }
43669
+ async resolveCommentPlacements(pullRequestId, commentIds, destinations, expectedPullRequestContentFingerprint) {
43670
+ const pullRequest = this.getPullRequest(pullRequestId);
43671
+ const contentFingerprint = this.assertPullRequestContentFingerprint(
43672
+ pullRequest,
43673
+ expectedPullRequestContentFingerprint
43674
+ );
43675
+ const uniqueCommentIds = [...new Set(commentIds)];
43676
+ const comments = [];
43677
+ const missingCommentIds = [];
43678
+ for (const commentId of uniqueCommentIds) {
43679
+ const comment3 = this.database.getComment(commentId);
43680
+ if (!comment3) {
43681
+ missingCommentIds.push(commentId);
43682
+ continue;
43683
+ }
43684
+ if (comment3.pullRequestId !== pullRequestId) {
43685
+ throw new RvwError(
43686
+ "INVALID_INPUT",
43687
+ `\u5225\u306EPull Request\u306E\u30B3\u30E1\u30F3\u30C8\u306F\u4E00\u62EC\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093: ${commentId}`
43688
+ );
43689
+ }
43690
+ comments.push(comment3);
43691
+ }
43692
+ const context = this.createCommentPlacementRequestContext();
43693
+ if (comments.length > 0) {
43694
+ for (const destination of destinations) {
43695
+ if (destination.kind === "commit") {
43696
+ await this.assertPlacementCommitAvailable(pullRequest, destination.oid, context);
43697
+ } else if (destination.kind === "document" && destination.ref.kind === "repository-file") {
43698
+ await this.assertPlacementCommitAvailable(
43699
+ pullRequest,
43700
+ destination.ref.sourceOid,
43701
+ context
43702
+ );
43703
+ }
43704
+ }
43705
+ }
43706
+ return {
43707
+ pullRequestContentFingerprint: contentFingerprint,
43708
+ comments: await mapWithConcurrency(comments, 4, async (comment3) => {
43709
+ const placements = [];
43710
+ const failures = [];
43711
+ for (const destination of destinations) {
43712
+ try {
43713
+ placements.push({
43714
+ destination,
43715
+ placement: await this.placeCommentForDestination(
43716
+ comment3,
43717
+ pullRequest,
43718
+ destination,
43719
+ context
43720
+ )
43721
+ });
43722
+ } catch (error51) {
43723
+ const placementError = asRvwError(error51);
43724
+ if (placementError.code !== "COMMIT_NOT_FOUND") throw error51;
43725
+ failures.push({ destination, error: placementError.toJSON() });
43726
+ }
43727
+ }
43728
+ return { commentId: comment3.id, placements, failures };
43729
+ }),
43730
+ missingCommentIds
43731
+ };
43732
+ }
43302
43733
  async getResetPreview(pullRequestId) {
43303
43734
  const pullRequest = this.getPullRequest(pullRequestId);
43304
43735
  const refs = await this.git.listRefsByPrefix(
@@ -47596,9 +48027,30 @@ var editCommentPostSchema = external_exports.object({
47596
48027
  body: external_exports.string().min(1).refine((value) => Buffer.byteLength(value, "utf8") <= MAX_COMMENT_BODY_BYTES),
47597
48028
  references: external_exports.array(codeReferenceInputSchema).optional()
47598
48029
  });
48030
+ var placementDocumentRefSchema = external_exports.discriminatedUnion("kind", [
48031
+ external_exports.object({ kind: external_exports.literal("pull-request-markdown"), pullRequestId: external_exports.uuid() }),
48032
+ external_exports.object({
48033
+ kind: external_exports.literal("repository-file"),
48034
+ pullRequestId: external_exports.uuid(),
48035
+ sourceOid: external_exports.string().regex(GIT_OBJECT_ID_PATTERN),
48036
+ path: external_exports.string().min(1)
48037
+ })
48038
+ ]);
48039
+ var resolveCommentPlacementsSchema = external_exports.object({
48040
+ commentIds: external_exports.array(external_exports.uuid()),
48041
+ expectedPullRequestContentFingerprint: external_exports.string().regex(CONTENT_FINGERPRINT_PATTERN).optional(),
48042
+ destinations: external_exports.array(
48043
+ external_exports.union([
48044
+ external_exports.object({ kind: external_exports.literal("document"), ref: placementDocumentRefSchema }),
48045
+ external_exports.object({ kind: external_exports.literal("commit"), oid: external_exports.string().regex(GIT_OBJECT_ID_PATTERN) }),
48046
+ external_exports.object({ kind: external_exports.literal("walkthrough"), walkthroughId: external_exports.uuid() })
48047
+ ])
48048
+ ).min(1).max(4)
48049
+ });
47599
48050
 
47600
48051
  // src/server/app.ts
47601
48052
  var oidSchema = external_exports.string().regex(GIT_OBJECT_ID_PATTERN);
48053
+ var contentFingerprintSchema = external_exports.string().regex(CONTENT_FINGERPRINT_PATTERN);
47602
48054
  var nonnegativeIndexSchema = external_exports.coerce.number().int().nonnegative();
47603
48055
  var svgAssetContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; sandbox";
47604
48056
  function requiredQuery(value, name) {
@@ -47608,6 +48060,9 @@ function requiredQuery(value, name) {
47608
48060
  function oidQuery(value, name) {
47609
48061
  return oidSchema.parse(requiredQuery(value, name));
47610
48062
  }
48063
+ function optionalContentFingerprint(value) {
48064
+ return value === void 0 ? void 0 : contentFingerprintSchema.parse(value);
48065
+ }
47611
48066
  function structureSourceLocator(query) {
47612
48067
  if (query.locatorKind === "node") {
47613
48068
  return { kind: "node", nodeId: requiredQuery(query.nodeId, "nodeId") };
@@ -47717,7 +48172,7 @@ function createApp(service, options) {
47717
48172
  rawViewerLease === void 0 ? void 0 : viewerIdSchema.parse(rawViewerLease)
47718
48173
  );
47719
48174
  }
47720
- return context.json({ ok: true, changeSequence: service.database.getChangeSequence() });
48175
+ return context.json({ ok: true, ...service.database.getRevisionSnapshot() });
47721
48176
  });
47722
48177
  app.post("/api/meta/viewers/release", async (context) => {
47723
48178
  const input = viewerReleaseSchema.parse(await context.req.json());
@@ -47758,10 +48213,10 @@ function createApp(service, options) {
47758
48213
  ...await service.openPullRequest(input.reference, input.cwd)
47759
48214
  });
47760
48215
  });
47761
- app.post(
47762
- "/api/pull-requests/:id/refresh",
47763
- async (context) => context.json({ ok: true, ...await service.refreshPullRequest(context.req.param("id")) })
47764
- );
48216
+ app.post("/api/pull-requests/:id/refresh", async (context) => {
48217
+ const result = await service.refreshPullRequest(context.req.param("id"));
48218
+ return context.json({ ok: true, ...result });
48219
+ });
47765
48220
  app.post("/api/pull-requests/:id/reset", async (context) => {
47766
48221
  const input = resetSchema.parse(await context.req.json());
47767
48222
  if (!input.yes) {
@@ -47799,7 +48254,13 @@ function createApp(service, options) {
47799
48254
  });
47800
48255
  app.get("/api/pull-requests/:id/document", async (context) => {
47801
48256
  const ref = documentRefFromQuery(context.req.param("id"), context.req.query());
47802
- return context.json({ ok: true, document: await service.getDocument(ref) });
48257
+ return context.json({
48258
+ ok: true,
48259
+ ...await service.getDocumentWithContentFingerprint(
48260
+ ref,
48261
+ optionalContentFingerprint(context.req.query("expectedPullRequestContentFingerprint"))
48262
+ )
48263
+ });
47803
48264
  });
47804
48265
  app.on(["GET", "HEAD"], "/api/pull-requests/:id/markdown-asset", async (context) => {
47805
48266
  const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
@@ -47858,10 +48319,16 @@ function createApp(service, options) {
47858
48319
  const wholeWord = parseBooleanQuery(context.req.query("wholeWord"), "wholeWord");
47859
48320
  return context.json({
47860
48321
  ok: true,
47861
- ...await service.search(context.req.param("id"), oid, query, {
47862
- matchCase,
47863
- wholeWord
47864
- })
48322
+ ...await service.search(
48323
+ context.req.param("id"),
48324
+ oid,
48325
+ query,
48326
+ {
48327
+ matchCase,
48328
+ wholeWord
48329
+ },
48330
+ optionalContentFingerprint(context.req.query("expectedPullRequestContentFingerprint"))
48331
+ )
47865
48332
  });
47866
48333
  });
47867
48334
  app.get("/api/pull-requests/:id/comments", (context) => {
@@ -47916,6 +48383,25 @@ function createApp(service, options) {
47916
48383
  structures: service.listStructures(context.req.param("id"))
47917
48384
  })
47918
48385
  );
48386
+ app.get("/api/pull-requests/:id/structure-references", async (context) => {
48387
+ const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
48388
+ const filePath = requiredQuery(context.req.query("path"), "path");
48389
+ return context.json({
48390
+ ok: true,
48391
+ references: await service.listFileStructureReferences(
48392
+ context.req.param("id"),
48393
+ sourceOid,
48394
+ filePath
48395
+ )
48396
+ });
48397
+ });
48398
+ app.get("/api/pull-requests/:id/structure-reference-index", async (context) => {
48399
+ const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
48400
+ return context.json({
48401
+ ok: true,
48402
+ index: await service.listFileStructureReferenceIndex(context.req.param("id"), sourceOid)
48403
+ });
48404
+ });
47919
48405
  app.get(
47920
48406
  "/api/pull-requests/:id/structures/:structureId",
47921
48407
  (context) => context.json({
@@ -47969,43 +48455,51 @@ function createApp(service, options) {
47969
48455
  });
47970
48456
  app.post("/api/comments/:id/posts", async (context) => {
47971
48457
  const input = replySchema.parse(await context.req.json());
48458
+ const commentId = context.req.param("id");
48459
+ const post = await service.replyToComment(commentId, {
48460
+ body: input.body,
48461
+ ...input.relatedCommitOid === void 0 ? {} : { relatedCommitOid: input.relatedCommitOid },
48462
+ ...input.authorLabel === void 0 ? {} : { authorLabel: input.authorLabel },
48463
+ lastModifiedBy: "human",
48464
+ ...input.references === void 0 ? {} : { references: input.references }
48465
+ });
47972
48466
  return context.json(
47973
48467
  {
47974
48468
  ok: true,
47975
- post: await service.replyToComment(context.req.param("id"), {
47976
- body: input.body,
47977
- ...input.relatedCommitOid === void 0 ? {} : { relatedCommitOid: input.relatedCommitOid },
47978
- ...input.authorLabel === void 0 ? {} : { authorLabel: input.authorLabel },
47979
- lastModifiedBy: "human",
47980
- ...input.references === void 0 ? {} : { references: input.references }
47981
- })
48469
+ post,
48470
+ comment: service.database.getComment(commentId)
47982
48471
  },
47983
48472
  201
47984
48473
  );
47985
48474
  });
47986
48475
  app.patch("/api/comments/:id/posts/:postId", async (context) => {
47987
48476
  const input = editCommentPostSchema.parse(await context.req.json());
48477
+ const commentId = context.req.param("id");
48478
+ const post = input.references === void 0 ? await service.updateCommentPost(
48479
+ commentId,
48480
+ context.req.param("postId"),
48481
+ input.body,
48482
+ "human"
48483
+ ) : await service.editCommentPost(commentId, context.req.param("postId"), {
48484
+ body: input.body,
48485
+ references: input.references,
48486
+ lastModifiedBy: "human"
48487
+ });
47988
48488
  return context.json({
47989
48489
  ok: true,
47990
- post: input.references === void 0 ? await service.updateCommentPost(
47991
- context.req.param("id"),
47992
- context.req.param("postId"),
47993
- input.body,
47994
- "human"
47995
- ) : await service.editCommentPost(context.req.param("id"), context.req.param("postId"), {
47996
- body: input.body,
47997
- references: input.references,
47998
- lastModifiedBy: "human"
47999
- })
48490
+ post,
48491
+ comment: service.database.getComment(commentId)
48000
48492
  });
48001
48493
  });
48002
- app.delete(
48003
- "/api/comments/:id/posts/:postId",
48004
- (context) => context.json({
48494
+ app.delete("/api/comments/:id/posts/:postId", (context) => {
48495
+ const commentId = context.req.param("id");
48496
+ const deleted = service.deleteReply(commentId, context.req.param("postId"));
48497
+ return context.json({
48005
48498
  ok: true,
48006
- deleted: service.deleteReply(context.req.param("id"), context.req.param("postId"))
48007
- })
48008
- );
48499
+ deleted,
48500
+ comment: service.database.getComment(commentId)
48501
+ });
48502
+ });
48009
48503
  app.post(
48010
48504
  "/api/comments/:id/resolve",
48011
48505
  (context) => context.json({ ok: true, comment: service.setCommentResolved(context.req.param("id"), true) })
@@ -48018,6 +48512,18 @@ function createApp(service, options) {
48018
48512
  "/api/comments/:id",
48019
48513
  (context) => context.json({ ok: true, deleted: service.deleteComment(context.req.param("id")) })
48020
48514
  );
48515
+ app.post("/api/pull-requests/:pullRequestId/comment-placements/resolve", async (context) => {
48516
+ const input = resolveCommentPlacementsSchema.parse(await context.req.json());
48517
+ return context.json({
48518
+ ok: true,
48519
+ ...await service.resolveCommentPlacements(
48520
+ context.req.param("pullRequestId"),
48521
+ input.commentIds,
48522
+ input.destinations,
48523
+ input.expectedPullRequestContentFingerprint
48524
+ )
48525
+ });
48526
+ });
48021
48527
  app.get("/api/comments/:id/placement", async (context) => {
48022
48528
  const comment3 = service.database.getComment(context.req.param("id"));
48023
48529
  if (!comment3)
@@ -48029,22 +48535,21 @@ function createApp(service, options) {
48029
48535
  placement: { outdated: true, range: null, path: null }
48030
48536
  });
48031
48537
  }
48538
+ let destination;
48032
48539
  if (context.req.query("kind") === "commit") {
48033
48540
  const oid = oidQuery(context.req.query("oid"), "oid");
48034
- return context.json({
48035
- ok: true,
48036
- placement: await service.placeCommentAtCommit(comment3, oid)
48037
- });
48038
- }
48039
- if (context.req.query("kind") === "walkthrough") {
48541
+ destination = { kind: "commit", oid };
48542
+ } else if (context.req.query("kind") === "walkthrough") {
48040
48543
  const walkthroughId = requiredQuery(context.req.query("walkthroughId"), "walkthroughId");
48041
- return context.json({
48042
- ok: true,
48043
- placement: service.placeWalkthroughComment(comment3, walkthroughId)
48044
- });
48544
+ destination = { kind: "walkthrough", walkthroughId };
48545
+ } else {
48546
+ destination = {
48547
+ kind: "document",
48548
+ ref: documentRefFromQuery(pullRequestId, context.req.query())
48549
+ };
48045
48550
  }
48046
- const destination = documentRefFromQuery(pullRequestId, context.req.query());
48047
- return context.json({ ok: true, placement: await service.placeComment(comment3, destination) });
48551
+ const placement = destination.kind === "commit" ? await service.placeCommentAtCommit(comment3, destination.oid) : destination.kind === "walkthrough" ? service.placeWalkthroughComment(comment3, destination.walkthroughId) : await service.placeComment(comment3, destination.ref);
48552
+ return context.json({ ok: true, placement });
48048
48553
  });
48049
48554
  if (options.staticDirectory && existsSync4(options.staticDirectory)) {
48050
48555
  app.use("*", serveStatic({ root: options.staticDirectory }));