mobbdev 1.2.6 → 1.2.19

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/index.mjs CHANGED
@@ -277,6 +277,7 @@ var init_client_generates = __esm({
277
277
  IssueType_Enum2["MissingTemplateStringIndicator"] = "MISSING_TEMPLATE_STRING_INDICATOR";
278
278
  IssueType_Enum2["MissingUser"] = "MISSING_USER";
279
279
  IssueType_Enum2["MissingWhitespace"] = "MISSING_WHITESPACE";
280
+ IssueType_Enum2["MissingWorkflowPermissions"] = "MISSING_WORKFLOW_PERMISSIONS";
280
281
  IssueType_Enum2["ModifiedDefaultParam"] = "MODIFIED_DEFAULT_PARAM";
281
282
  IssueType_Enum2["NonFinalPublicStaticField"] = "NON_FINAL_PUBLIC_STATIC_FIELD";
282
283
  IssueType_Enum2["NonReadonlyField"] = "NON_READONLY_FIELD";
@@ -846,13 +847,12 @@ var init_client_generates = __esm({
846
847
  }
847
848
  `;
848
849
  AnalyzeCommitForExtensionAiBlameDocument = `
849
- mutation AnalyzeCommitForExtensionAIBlame($repositoryURL: String!, $commitSha: String!, $organizationId: String!, $commitTimestamp: Timestamp, $parentCommits: [ParentCommitInput!]) {
850
+ mutation AnalyzeCommitForExtensionAIBlame($repositoryURL: String!, $commitSha: String!, $organizationId: String!, $commitTimestamp: Timestamp) {
850
851
  analyzeCommitForAIBlame(
851
852
  repositoryURL: $repositoryURL
852
853
  commitSha: $commitSha
853
854
  organizationId: $organizationId
854
855
  commitTimestamp: $commitTimestamp
855
- parentCommits: $parentCommits
856
856
  ) {
857
857
  __typename
858
858
  ... on ProcessAIBlameFinalResult {
@@ -1381,7 +1381,8 @@ var init_getIssueType = __esm({
1381
1381
  ["RETURN_IN_INIT" /* ReturnInInit */]: "Return in Init",
1382
1382
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: "Action Not Pinned to Commit Sha",
1383
1383
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: "Django Blank Field Needs Null or Default",
1384
- ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: "Redundant Nil Error Check"
1384
+ ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: "Redundant Nil Error Check",
1385
+ ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: "Missing Workflow Permissions"
1385
1386
  };
1386
1387
  issueTypeZ = z.nativeEnum(IssueType_Enum);
1387
1388
  getIssueTypeFriendlyString = (issueType) => {
@@ -1508,6 +1509,7 @@ var init_fix = __esm({
1508
1509
  });
1509
1510
  IssueSharedStateZ = z7.object({
1510
1511
  id: z7.string(),
1512
+ createdAt: z7.string(),
1511
1513
  isArchived: z7.boolean(),
1512
1514
  ticketIntegrationId: z7.string().nullable(),
1513
1515
  ticketIntegrations: z7.array(
@@ -3801,34 +3803,6 @@ ${rootContent}`;
3801
3803
  throw new Error(errorMessage);
3802
3804
  }
3803
3805
  }
3804
- /**
3805
- * Gets timestamps for parent commits in a single git call.
3806
- * @param parentShas Array of parent commit SHAs
3807
- * @returns Array of parent commits with timestamps, or undefined if unavailable
3808
- */
3809
- async getParentCommitTimestamps(parentShas) {
3810
- if (parentShas.length === 0) {
3811
- return void 0;
3812
- }
3813
- try {
3814
- const output = await this.git.raw([
3815
- "log",
3816
- "--format=%H %cI",
3817
- "--no-walk",
3818
- ...parentShas
3819
- ]);
3820
- const parentCommits = output.trim().split("\n").filter(Boolean).map((line) => {
3821
- const [sha, ts] = line.split(" ");
3822
- return { sha: sha ?? "", timestamp: new Date(ts ?? "") };
3823
- }).filter((p) => p.sha !== "");
3824
- return parentCommits.length > 0 ? parentCommits : void 0;
3825
- } catch {
3826
- this.log("[GitService] Could not get parent commit timestamps", "debug", {
3827
- parentShas
3828
- });
3829
- return void 0;
3830
- }
3831
- }
3832
3806
  /**
3833
3807
  * Gets local commit data including diff, timestamp, and parent commits.
3834
3808
  * Used by Tracy extension to send commit data directly without requiring SCM token.
@@ -3873,18 +3847,14 @@ ${rootContent}`;
3873
3847
  }
3874
3848
  const timestampStr = metadataLines[0];
3875
3849
  const timestamp = new Date(timestampStr);
3876
- const parentShas = (metadataLines[1] ?? "").trim().split(/\s+/).filter(Boolean);
3877
- const parentCommits = await this.getParentCommitTimestamps(parentShas);
3878
3850
  this.log("[GitService] Local commit data retrieved", "debug", {
3879
3851
  commitSha,
3880
3852
  diffSizeBytes,
3881
- timestamp: timestamp.toISOString(),
3882
- parentCommitCount: parentCommits?.length ?? 0
3853
+ timestamp: timestamp.toISOString()
3883
3854
  });
3884
3855
  return {
3885
3856
  diff,
3886
- timestamp,
3887
- parentCommits
3857
+ timestamp
3888
3858
  };
3889
3859
  } catch (error) {
3890
3860
  const errorMessage = `Failed to get local commit data: ${error.message}`;
@@ -4274,7 +4244,8 @@ var fixDetailsData = {
4274
4244
  ["RETURN_IN_INIT" /* ReturnInInit */]: void 0,
4275
4245
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: void 0,
4276
4246
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: void 0,
4277
- ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: void 0
4247
+ ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: void 0,
4248
+ ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: void 0
4278
4249
  };
4279
4250
 
4280
4251
  // src/features/analysis/scm/shared/src/commitDescriptionMarkup.ts
@@ -6109,6 +6080,34 @@ var GetReferenceResultZ = z13.object({
6109
6080
  type: z13.nativeEnum(ReferenceType)
6110
6081
  });
6111
6082
 
6083
+ // src/features/analysis/scm/utils/diffUtils.ts
6084
+ import parseDiff from "parse-diff";
6085
+ function parseAddedLinesByFile(diff) {
6086
+ const result = /* @__PURE__ */ new Map();
6087
+ const parsedDiff = parseDiff(diff);
6088
+ for (const file of parsedDiff) {
6089
+ if (!file.to || file.to === "/dev/null") {
6090
+ continue;
6091
+ }
6092
+ const filePath = file.to;
6093
+ const addedLines = [];
6094
+ if (file.chunks) {
6095
+ for (const chunk of file.chunks) {
6096
+ for (const change of chunk.changes) {
6097
+ if (change.type === "add") {
6098
+ const addChange = change;
6099
+ addedLines.push(addChange.ln);
6100
+ }
6101
+ }
6102
+ }
6103
+ }
6104
+ if (addedLines.length > 0) {
6105
+ result.set(filePath, addedLines);
6106
+ }
6107
+ }
6108
+ return result;
6109
+ }
6110
+
6112
6111
  // src/features/analysis/scm/utils/scm.ts
6113
6112
  var safeBody = (body, maxBodyLength) => {
6114
6113
  const truncationNotice = "\n\n... Message was cut here because it is too long";
@@ -6201,6 +6200,36 @@ function getCommitIssueUrl(params) {
6201
6200
  analysisId
6202
6201
  })}/commit?${searchParams.toString()}`;
6203
6202
  }
6203
+ function extractLinearTicketsFromBody(body, seen) {
6204
+ const tickets = [];
6205
+ const htmlPattern = /<a href="(https:\/\/linear\.app\/[^"]+)">([A-Z]+-\d+)<\/a>/g;
6206
+ let match;
6207
+ while ((match = htmlPattern.exec(body)) !== null) {
6208
+ const ticket = parseLinearTicket(match[1], match[2]);
6209
+ if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
6210
+ seen.add(`${ticket.name}|${ticket.url}`);
6211
+ tickets.push(ticket);
6212
+ }
6213
+ }
6214
+ const markdownPattern = /\[([A-Z]+-\d+)\]\((https:\/\/linear\.app\/[^)]+)\)/g;
6215
+ while ((match = markdownPattern.exec(body)) !== null) {
6216
+ const ticket = parseLinearTicket(match[2], match[1]);
6217
+ if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
6218
+ seen.add(`${ticket.name}|${ticket.url}`);
6219
+ tickets.push(ticket);
6220
+ }
6221
+ }
6222
+ return tickets;
6223
+ }
6224
+ function parseLinearTicket(url, name) {
6225
+ if (!name || !url) {
6226
+ return null;
6227
+ }
6228
+ const urlParts = url.split("/");
6229
+ const titleSlug = urlParts[urlParts.length - 1] || "";
6230
+ const title = titleSlug.replace(/-/g, " ");
6231
+ return { name, title, url };
6232
+ }
6204
6233
  var userNamePattern = /^(https?:\/\/)([^@]+@)?([^/]+\/.+)$/;
6205
6234
  var sshPattern = /^git@([\w.-]+):([\w./-]+)$/;
6206
6235
  function normalizeUrl(repoUrl) {
@@ -6894,9 +6923,6 @@ async function getAdoSdk(params) {
6894
6923
  return commitRes.value;
6895
6924
  }
6896
6925
  throw new RefNotFoundError(`ref: ${ref} does not exist`);
6897
- },
6898
- getAdoBlameRanges() {
6899
- return [];
6900
6926
  }
6901
6927
  };
6902
6928
  }
@@ -7016,7 +7042,6 @@ var SCMLib = class {
7016
7042
  * IMPORTANT: Sort order must remain consistent across paginated requests
7017
7043
  * for cursor-based pagination to work correctly.
7018
7044
  *
7019
- * Default implementation uses getSubmitRequests and applies filters/sorting in-memory.
7020
7045
  * Override in subclasses for provider-specific optimizations (e.g., GitHub Search API).
7021
7046
  *
7022
7047
  * @param params - Search parameters including filters, sort, and pagination
@@ -7098,6 +7123,14 @@ var SCMLib = class {
7098
7123
  static async getIsValidBranchName(branchName) {
7099
7124
  return isValidBranchName(branchName);
7100
7125
  }
7126
+ /**
7127
+ * Extract Linear ticket links from PR/MR comments.
7128
+ * Default implementation returns empty array - subclasses can override.
7129
+ * Public so it can be reused by backend services.
7130
+ */
7131
+ extractLinearTicketsFromComments(_comments) {
7132
+ return [];
7133
+ }
7101
7134
  _validateAccessTokenAndUrl() {
7102
7135
  this._validateAccessToken();
7103
7136
  this._validateUrl();
@@ -7250,10 +7283,6 @@ var AdoSCMLib = class extends SCMLib {
7250
7283
  throw new Error(`unknown state ${state}`);
7251
7284
  }
7252
7285
  }
7253
- async getRepoBlameRanges(_ref, _path) {
7254
- const adoSdk = await this.getAdoSdk();
7255
- return await adoSdk.getAdoBlameRanges();
7256
- }
7257
7286
  async getReferenceData(ref) {
7258
7287
  this._validateUrl();
7259
7288
  const adoSdk = await this.getAdoSdk();
@@ -7312,9 +7341,6 @@ var AdoSCMLib = class extends SCMLib {
7312
7341
  async getSubmitRequestDiff(_submitRequestId) {
7313
7342
  throw new Error("getSubmitRequestDiff not implemented for ADO");
7314
7343
  }
7315
- async getSubmitRequests(_repoUrl) {
7316
- throw new Error("getSubmitRequests not implemented for ADO");
7317
- }
7318
7344
  async searchSubmitRequests(_params) {
7319
7345
  throw new Error("searchSubmitRequests not implemented for ADO");
7320
7346
  }
@@ -7326,6 +7352,12 @@ var AdoSCMLib = class extends SCMLib {
7326
7352
  async getPullRequestMetrics(_prNumber) {
7327
7353
  throw new Error("getPullRequestMetrics not implemented for ADO");
7328
7354
  }
7355
+ async getRecentCommits(_since) {
7356
+ throw new Error("getRecentCommits not implemented for ADO");
7357
+ }
7358
+ async getRateLimitStatus() {
7359
+ return null;
7360
+ }
7329
7361
  };
7330
7362
 
7331
7363
  // src/features/analysis/scm/bitbucket/bitbucket.ts
@@ -7843,9 +7875,6 @@ var BitbucketSCMLib = class extends SCMLib {
7843
7875
  throw new Error(`unknown state ${pullRequestRes.state} `);
7844
7876
  }
7845
7877
  }
7846
- async getRepoBlameRanges(_ref, _path) {
7847
- return [];
7848
- }
7849
7878
  async getReferenceData(ref) {
7850
7879
  this._validateUrl();
7851
7880
  return this.bitbucketSdk.getReferenceData({ url: this.url, ref });
@@ -7894,9 +7923,6 @@ var BitbucketSCMLib = class extends SCMLib {
7894
7923
  async getSubmitRequestDiff(_submitRequestId) {
7895
7924
  throw new Error("getSubmitRequestDiff not implemented for Bitbucket");
7896
7925
  }
7897
- async getSubmitRequests(_repoUrl) {
7898
- throw new Error("getSubmitRequests not implemented for Bitbucket");
7899
- }
7900
7926
  async searchSubmitRequests(_params) {
7901
7927
  throw new Error("searchSubmitRequests not implemented for Bitbucket");
7902
7928
  }
@@ -7908,6 +7934,12 @@ var BitbucketSCMLib = class extends SCMLib {
7908
7934
  async getPullRequestMetrics(_prNumber) {
7909
7935
  throw new Error("getPullRequestMetrics not implemented for Bitbucket");
7910
7936
  }
7937
+ async getRecentCommits(_since) {
7938
+ throw new Error("getRecentCommits not implemented for Bitbucket");
7939
+ }
7940
+ async getRateLimitStatus() {
7941
+ return null;
7942
+ }
7911
7943
  };
7912
7944
 
7913
7945
  // src/features/analysis/scm/constants.ts
@@ -7920,7 +7952,7 @@ init_env();
7920
7952
 
7921
7953
  // src/features/analysis/scm/github/GithubSCMLib.ts
7922
7954
  init_env();
7923
- import pLimit2 from "p-limit";
7955
+ import pLimit from "p-limit";
7924
7956
  import { z as z21 } from "zod";
7925
7957
  init_client_generates();
7926
7958
 
@@ -7939,59 +7971,6 @@ function parseCursorSafe(cursor, defaultValue = 0, maxValue = MAX_CURSOR_VALUE)
7939
7971
 
7940
7972
  // src/features/analysis/scm/github/github.ts
7941
7973
  import { RequestError } from "@octokit/request-error";
7942
- import pLimit from "p-limit";
7943
-
7944
- // src/utils/contextLogger.ts
7945
- import debugModule from "debug";
7946
- var debug3 = debugModule("mobb:shared");
7947
- var _contextLogger = null;
7948
- var createContextLogger = async () => {
7949
- if (_contextLogger) return _contextLogger;
7950
- try {
7951
- let logger2;
7952
- try {
7953
- let module;
7954
- try {
7955
- const buildPath = "../../../../../tscommon/backend/build/src/utils/logger";
7956
- module = await import(buildPath);
7957
- } catch (e) {
7958
- const sourcePath = "../../../../../tscommon/backend/src/utils/logger";
7959
- module = await import(sourcePath);
7960
- }
7961
- logger2 = module.logger;
7962
- } catch {
7963
- }
7964
- if (logger2) {
7965
- _contextLogger = {
7966
- info: (message, data) => data ? logger2.info(data, message) : logger2.info(message),
7967
- debug: (message, data) => data ? logger2.debug(data, message) : logger2.debug(message),
7968
- error: (message, data) => data ? logger2.error(data, message) : logger2.error(message)
7969
- };
7970
- return _contextLogger;
7971
- }
7972
- } catch {
7973
- }
7974
- _contextLogger = {
7975
- info: (message, data) => debug3(message, data),
7976
- debug: (message, data) => debug3(message, data),
7977
- error: (message, data) => debug3(message, data)
7978
- };
7979
- return _contextLogger;
7980
- };
7981
- var contextLogger = {
7982
- info: async (message, data) => {
7983
- const logger2 = await createContextLogger();
7984
- return logger2.info(message, data);
7985
- },
7986
- debug: async (message, data) => {
7987
- const logger2 = await createContextLogger();
7988
- return logger2.debug(message, data);
7989
- },
7990
- error: async (message, data) => {
7991
- const logger2 = await createContextLogger();
7992
- return logger2.error(message, data);
7993
- }
7994
- };
7995
7974
 
7996
7975
  // src/features/analysis/scm/github/consts.ts
7997
7976
  var POST_COMMENT_PATH = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments";
@@ -8009,35 +7988,6 @@ var GET_A_REPOSITORY_PUBLIC_KEY = "GET /repos/{owner}/{repo}/actions/secrets/pub
8009
7988
  var GET_USER = "GET /user";
8010
7989
  var GET_USER_REPOS = "GET /user/repos";
8011
7990
  var GET_REPO_BRANCHES = "GET /repos/{owner}/{repo}/branches";
8012
- var GET_BLAME_DOCUMENT = `
8013
- query GetBlame(
8014
- $owner: String!
8015
- $repo: String!
8016
- $ref: String!
8017
- $path: String!
8018
- ) {
8019
- repository(name: $repo, owner: $owner) {
8020
- # branch name
8021
- object(expression: $ref) {
8022
- # cast Target to a Commit
8023
- ... on Commit {
8024
- # full repo-relative path to blame file
8025
- blame(path: $path) {
8026
- ranges {
8027
- commit {
8028
- oid
8029
- }
8030
- startingLine
8031
- endingLine
8032
- age
8033
- }
8034
- }
8035
- }
8036
-
8037
- }
8038
- }
8039
- }
8040
- `;
8041
7991
  var GITHUB_GRAPHQL_FRAGMENTS = {
8042
7992
  /**
8043
7993
  * Fragment for fetching PR additions/deletions.
@@ -8061,30 +8011,6 @@ var GITHUB_GRAPHQL_FRAGMENTS = {
8061
8011
  body
8062
8012
  }
8063
8013
  }
8064
- `,
8065
- /**
8066
- * Fragment for fetching blame data.
8067
- * Use with object(expression: $ref) on Commit type.
8068
- * Note: $path placeholder must be replaced with actual file path.
8069
- */
8070
- BLAME_RANGES: `
8071
- blame(path: "$path") {
8072
- ranges {
8073
- startingLine
8074
- endingLine
8075
- commit {
8076
- oid
8077
- }
8078
- }
8079
- }
8080
- `,
8081
- /**
8082
- * Fragment for fetching commit timestamp.
8083
- * Use with object(oid: $sha) on Commit type.
8084
- */
8085
- COMMIT_TIMESTAMP: `
8086
- oid
8087
- committedDate
8088
8014
  `
8089
8015
  };
8090
8016
  var GET_PR_METRICS_QUERY = `
@@ -8296,112 +8222,6 @@ async function githubValidateParams(url, accessToken) {
8296
8222
 
8297
8223
  // src/features/analysis/scm/github/github.ts
8298
8224
  var MAX_GH_PR_BODY_LENGTH = 65536;
8299
- var BLAME_LARGE_FILE_THRESHOLD_BYTES = 1e6;
8300
- var BLAME_THRESHOLD_REDUCTION_BYTES = 1e5;
8301
- var BLAME_MIN_THRESHOLD_BYTES = 1e5;
8302
- var GRAPHQL_INPUT_PATTERNS = {
8303
- // File paths: most printable ASCII chars, unicode letters/numbers
8304
- // Allows: letters, numbers, spaces, common punctuation, path separators
8305
- // Disallows: control characters, null bytes
8306
- path: /^[\p{L}\p{N}\p{Zs}\-._/@+#~%()[\]{}=!,;'&]+$/u,
8307
- // Git refs: branch/tag names follow git-check-ref-format rules
8308
- // Allows: letters, numbers, slashes, dots, hyphens, underscores
8309
- // Can also be "ref:path" format for expressions
8310
- ref: /^[\p{L}\p{N}\-._/:@]+$/u,
8311
- // Git SHAs: strictly hexadecimal (short or full)
8312
- sha: /^[0-9a-fA-F]+$/
8313
- };
8314
- function validateGraphQLInput(value, type2) {
8315
- const pattern = GRAPHQL_INPUT_PATTERNS[type2];
8316
- if (!pattern.test(value)) {
8317
- void contextLogger.info(
8318
- "[GraphQL] Input contains unexpected characters, proceeding with escaping",
8319
- {
8320
- type: type2,
8321
- valueLength: value.length,
8322
- // Log first 100 chars to help debug without exposing full value
8323
- valueSample: value.slice(0, 100)
8324
- }
8325
- );
8326
- return false;
8327
- }
8328
- return true;
8329
- }
8330
- function escapeGraphQLString(value) {
8331
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\f/g, "\\f").replace(/[\b]/g, "\\b");
8332
- }
8333
- function safeGraphQLString(value, type2) {
8334
- validateGraphQLInput(value, type2);
8335
- return escapeGraphQLString(value);
8336
- }
8337
- function extractBlameRanges(data) {
8338
- const fileData = data;
8339
- if (fileData.blame?.ranges) {
8340
- return fileData.blame.ranges.map((range) => ({
8341
- startingLine: range.startingLine,
8342
- endingLine: range.endingLine,
8343
- commitSha: range.commit.oid
8344
- }));
8345
- }
8346
- return void 0;
8347
- }
8348
- function buildBlameFragment(ref) {
8349
- const escapedRef = safeGraphQLString(ref, "ref");
8350
- return (path25, index) => {
8351
- const escapedPath = safeGraphQLString(path25, "path");
8352
- return `
8353
- file${index}: object(expression: "${escapedRef}") {
8354
- ... on Commit {
8355
- ${GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES.replace("$path", escapedPath)}
8356
- }
8357
- }`;
8358
- };
8359
- }
8360
- function createBatchesByTotalSize(files, threshold) {
8361
- const batches = [];
8362
- let currentBatch = [];
8363
- let currentBatchSize = 0;
8364
- for (const file of files) {
8365
- if (currentBatchSize + file.size > threshold && currentBatch.length > 0) {
8366
- batches.push(currentBatch);
8367
- currentBatch = [];
8368
- currentBatchSize = 0;
8369
- }
8370
- currentBatch.push(file);
8371
- currentBatchSize += file.size;
8372
- }
8373
- if (currentBatch.length > 0) {
8374
- batches.push(currentBatch);
8375
- }
8376
- return batches;
8377
- }
8378
- async function fetchBlameForBatch(octokit, owner, repo, ref, files) {
8379
- if (files.length === 0) {
8380
- return /* @__PURE__ */ new Map();
8381
- }
8382
- return executeBatchGraphQL(octokit, owner, repo, {
8383
- items: files.map((f) => f.path),
8384
- aliasPrefix: "file",
8385
- buildFragment: buildBlameFragment(ref),
8386
- extractResult: extractBlameRanges
8387
- });
8388
- }
8389
- async function processBlameAttempt(params) {
8390
- const { octokit, owner, repo, ref, batches, concurrency } = params;
8391
- const result = /* @__PURE__ */ new Map();
8392
- const limit = pLimit(concurrency);
8393
- const batchResults = await Promise.all(
8394
- batches.map(
8395
- (batch) => limit(() => fetchBlameForBatch(octokit, owner, repo, ref, batch))
8396
- )
8397
- );
8398
- for (const batchResult of batchResults) {
8399
- for (const [path25, blameData] of batchResult) {
8400
- result.set(path25, blameData);
8401
- }
8402
- }
8403
- return result;
8404
- }
8405
8225
  async function executeBatchGraphQL(octokit, owner, repo, config2) {
8406
8226
  const { items, aliasPrefix, buildFragment, extractResult } = config2;
8407
8227
  if (items.length === 0) {
@@ -8716,29 +8536,6 @@ function getGithubSdk(params = {}) {
8716
8536
  sha: res.data.sha
8717
8537
  };
8718
8538
  },
8719
- async getGithubBlameRanges(params2) {
8720
- const { ref, gitHubUrl, path: path25 } = params2;
8721
- const { owner, repo } = parseGithubOwnerAndRepo(gitHubUrl);
8722
- const res = await octokit.graphql(
8723
- GET_BLAME_DOCUMENT,
8724
- {
8725
- owner,
8726
- repo,
8727
- path: path25,
8728
- ref
8729
- }
8730
- );
8731
- if (!res?.repository?.object?.blame?.ranges) {
8732
- return [];
8733
- }
8734
- return res.repository.object.blame.ranges.map(
8735
- (range) => ({
8736
- startingLine: range.startingLine,
8737
- endingLine: range.endingLine,
8738
- commitSha: range.commit.oid
8739
- })
8740
- );
8741
- },
8742
8539
  /**
8743
8540
  * Fetches commits for multiple PRs in a single GraphQL request.
8744
8541
  * This is much more efficient than making N separate REST API calls.
@@ -9022,232 +8819,6 @@ function getGithubSdk(params = {}) {
9022
8819
  }
9023
8820
  });
9024
8821
  },
9025
- /**
9026
- * Batch fetch blob sizes for multiple files via GraphQL.
9027
- * Used to determine which files are too large to batch in blame queries.
9028
- */
9029
- async getBlobSizesBatch(params2) {
9030
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9031
- items: params2.blobShas,
9032
- aliasPrefix: "blob",
9033
- buildFragment: (sha, index) => {
9034
- const escapedSha = safeGraphQLString(sha, "sha");
9035
- return `
9036
- blob${index}: object(oid: "${escapedSha}") {
9037
- ... on Blob {
9038
- byteSize
9039
- }
9040
- }`;
9041
- },
9042
- extractResult: (data) => {
9043
- const blobData = data;
9044
- if (blobData.byteSize !== void 0) {
9045
- return blobData.byteSize;
9046
- }
9047
- return void 0;
9048
- }
9049
- });
9050
- },
9051
- /**
9052
- * Batch fetch blame data for multiple files via GraphQL.
9053
- * Uses GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES for the field selection.
9054
- *
9055
- * Optimized to handle large files with retry logic:
9056
- * - Files above threshold are processed individually with rate limiting
9057
- * - On failure, retries with reduced threshold (-100KB) and concurrency (-1)
9058
- * - Continues until success or threshold < 100KB
9059
- *
9060
- * @param params.files - Array of files with path and blobSha for size lookup
9061
- * @param params.concurrency - Max concurrent requests for large files (default: 2)
9062
- */
9063
- async getBlameBatch(params2) {
9064
- const {
9065
- owner,
9066
- repo,
9067
- ref,
9068
- files,
9069
- concurrency: initialConcurrency = 2
9070
- } = params2;
9071
- if (files.length === 0) {
9072
- return /* @__PURE__ */ new Map();
9073
- }
9074
- const filesWithSizes = await this.fetchFilesWithSizes(owner, repo, files);
9075
- return this.executeBlameWithRetries({
9076
- owner,
9077
- repo,
9078
- ref,
9079
- filesWithSizes,
9080
- initialConcurrency
9081
- });
9082
- },
9083
- /**
9084
- * Fetches blob sizes and creates a list of files with their sizes.
9085
- */
9086
- async fetchFilesWithSizes(owner, repo, files) {
9087
- const blobShas = files.map((f) => f.blobSha);
9088
- const blobSizes = await this.getBlobSizesBatch({ owner, repo, blobShas });
9089
- return files.map((file) => ({
9090
- ...file,
9091
- size: blobSizes.get(file.blobSha) ?? 0
9092
- }));
9093
- },
9094
- /**
9095
- * Executes blame fetching with retry logic on failure.
9096
- * Reduces threshold and concurrency on each retry attempt.
9097
- */
9098
- async executeBlameWithRetries(params2) {
9099
- const { owner, repo, ref, filesWithSizes, initialConcurrency } = params2;
9100
- let threshold = BLAME_LARGE_FILE_THRESHOLD_BYTES;
9101
- let concurrency = initialConcurrency;
9102
- let attempt = 1;
9103
- let lastError = null;
9104
- while (threshold >= BLAME_MIN_THRESHOLD_BYTES) {
9105
- const batches = createBatchesByTotalSize(filesWithSizes, threshold);
9106
- this.logBlameAttemptStart(
9107
- attempt,
9108
- threshold,
9109
- concurrency,
9110
- filesWithSizes.length,
9111
- batches.length,
9112
- owner,
9113
- repo,
9114
- ref
9115
- );
9116
- try {
9117
- const result = await processBlameAttempt({
9118
- octokit,
9119
- owner,
9120
- repo,
9121
- ref,
9122
- batches,
9123
- concurrency
9124
- });
9125
- this.logBlameAttemptSuccess(attempt, result.size, owner, repo);
9126
- return result;
9127
- } catch (error) {
9128
- lastError = error instanceof Error ? error : new Error(String(error));
9129
- this.logBlameAttemptFailure(
9130
- attempt,
9131
- threshold,
9132
- concurrency,
9133
- lastError.message,
9134
- owner,
9135
- repo
9136
- );
9137
- threshold -= BLAME_THRESHOLD_REDUCTION_BYTES;
9138
- concurrency = Math.max(1, concurrency - 1);
9139
- attempt++;
9140
- }
9141
- }
9142
- void contextLogger.error("[getBlameBatch] Exhausted all retries", {
9143
- attempts: attempt - 1,
9144
- repo: `${owner}/${repo}`,
9145
- ref,
9146
- error: lastError?.message || "unknown"
9147
- });
9148
- throw lastError || new Error("getBlameBatch failed after all retries");
9149
- },
9150
- /**
9151
- * Logs the start of a blame batch attempt.
9152
- */
9153
- logBlameAttemptStart(attempt, threshold, concurrency, totalFiles, batchCount, owner, repo, ref) {
9154
- void contextLogger.debug("[getBlameBatch] Processing attempt", {
9155
- attempt,
9156
- threshold,
9157
- concurrency,
9158
- totalFiles,
9159
- batchCount,
9160
- repo: `${owner}/${repo}`,
9161
- ref
9162
- });
9163
- },
9164
- /**
9165
- * Logs a successful blame batch attempt.
9166
- */
9167
- logBlameAttemptSuccess(attempt, filesProcessed, owner, repo) {
9168
- void contextLogger.debug("[getBlameBatch] Successfully processed batch", {
9169
- attempt,
9170
- filesProcessed,
9171
- repo: `${owner}/${repo}`
9172
- });
9173
- },
9174
- /**
9175
- * Logs a failed blame batch attempt.
9176
- */
9177
- logBlameAttemptFailure(attempt, threshold, concurrency, errorMessage, owner, repo) {
9178
- void contextLogger.debug(
9179
- "[getBlameBatch] Attempt failed, retrying with reduced threshold",
9180
- {
9181
- attempt,
9182
- threshold,
9183
- concurrency,
9184
- error: errorMessage,
9185
- repo: `${owner}/${repo}`
9186
- }
9187
- );
9188
- },
9189
- /**
9190
- * Batch fetch blame data for multiple files via GraphQL (legacy interface).
9191
- * This is a convenience wrapper that accepts file paths without blob SHAs.
9192
- * Note: This does NOT perform size-based optimization. Use getBlameBatch with
9193
- * files array including blobSha for optimized large file handling.
9194
- */
9195
- async getBlameBatchByPaths(params2) {
9196
- const escapedRef = safeGraphQLString(params2.ref, "ref");
9197
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9198
- items: params2.filePaths,
9199
- aliasPrefix: "file",
9200
- buildFragment: (path25, index) => {
9201
- const escapedPath = safeGraphQLString(path25, "path");
9202
- return `
9203
- file${index}: object(expression: "${escapedRef}") {
9204
- ... on Commit {
9205
- ${GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES.replace("$path", escapedPath)}
9206
- }
9207
- }`;
9208
- },
9209
- extractResult: (data) => {
9210
- const fileData = data;
9211
- if (fileData.blame?.ranges) {
9212
- return fileData.blame.ranges.map((range) => ({
9213
- startingLine: range.startingLine,
9214
- endingLine: range.endingLine,
9215
- commitSha: range.commit.oid
9216
- }));
9217
- }
9218
- return void 0;
9219
- }
9220
- });
9221
- },
9222
- /**
9223
- * Batch fetch commit timestamps for multiple commits via GraphQL.
9224
- * Uses GITHUB_GRAPHQL_FRAGMENTS.COMMIT_TIMESTAMP for the field selection.
9225
- */
9226
- async getCommitsBatch(params2) {
9227
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9228
- items: params2.commitShas,
9229
- aliasPrefix: "commit",
9230
- buildFragment: (sha, index) => {
9231
- const escapedSha = safeGraphQLString(sha, "sha");
9232
- return `
9233
- commit${index}: object(oid: "${escapedSha}") {
9234
- ... on Commit {
9235
- ${GITHUB_GRAPHQL_FRAGMENTS.COMMIT_TIMESTAMP}
9236
- }
9237
- }`;
9238
- },
9239
- extractResult: (data) => {
9240
- const commitData = data;
9241
- if (commitData.oid && commitData.committedDate) {
9242
- return {
9243
- sha: commitData.oid,
9244
- timestamp: new Date(commitData.committedDate)
9245
- };
9246
- }
9247
- return void 0;
9248
- }
9249
- });
9250
- },
9251
8822
  async getPRMetricsGraphQL(params2) {
9252
8823
  const res = await octokit.graphql(
9253
8824
  GET_PR_METRICS_QUERY,
@@ -9452,10 +9023,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9452
9023
  async getRecentCommits(since) {
9453
9024
  this._validateAccessTokenAndUrl();
9454
9025
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9455
- return await this.githubSdk.getRecentCommits({ owner, repo, since });
9456
- }
9026
+ const result = await this.githubSdk.getRecentCommits({ owner, repo, since });
9027
+ return {
9028
+ data: result.data.map((c) => ({
9029
+ sha: c.sha,
9030
+ commit: {
9031
+ committer: c.commit.committer ? { date: c.commit.committer.date } : void 0,
9032
+ author: c.commit.author ? { email: c.commit.author.email, name: c.commit.author.name } : void 0,
9033
+ message: c.commit.message
9034
+ },
9035
+ parents: c.parents?.map((p) => ({ sha: p.sha }))
9036
+ }))
9037
+ };
9038
+ }
9457
9039
  async getRateLimitStatus() {
9458
- return await this.githubSdk.getRateLimitStatus();
9040
+ const result = await this.githubSdk.getRateLimitStatus();
9041
+ return {
9042
+ remaining: result.remaining,
9043
+ reset: result.reset,
9044
+ limit: result.limit
9045
+ };
9459
9046
  }
9460
9047
  get scmLibType() {
9461
9048
  return "GITHUB" /* GITHUB */;
@@ -9513,14 +9100,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9513
9100
  markdownComment: comment
9514
9101
  });
9515
9102
  }
9516
- async getRepoBlameRanges(ref, path25) {
9517
- this._validateUrl();
9518
- return await this.githubSdk.getGithubBlameRanges({
9519
- ref,
9520
- path: path25,
9521
- gitHubUrl: this.url
9522
- });
9523
- }
9524
9103
  async getReferenceData(ref) {
9525
9104
  this._validateUrl();
9526
9105
  return await this.githubSdk.getGithubReferenceData({
@@ -9610,37 +9189,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9610
9189
  commitSha
9611
9190
  });
9612
9191
  const commitTimestamp = commit.commit.committer?.date ? new Date(commit.commit.committer.date) : new Date(commit.commit.author?.date || Date.now());
9613
- let parentCommits;
9614
- if (commit.parents && commit.parents.length > 0) {
9615
- if (options?.parentCommitTimestamps) {
9616
- parentCommits = commit.parents.map((p) => options.parentCommitTimestamps.get(p.sha)).filter((p) => p !== void 0);
9617
- } else {
9618
- try {
9619
- parentCommits = await Promise.all(
9620
- commit.parents.map(async (parent) => {
9621
- const parentCommit = await this.githubSdk.getCommit({
9622
- owner,
9623
- repo,
9624
- commitSha: parent.sha
9625
- });
9626
- const parentTimestamp = parentCommit.data.committer?.date ? new Date(parentCommit.data.committer.date) : new Date(Date.now());
9627
- return {
9628
- sha: parent.sha,
9629
- timestamp: parentTimestamp
9630
- };
9631
- })
9632
- );
9633
- } catch (error) {
9634
- console.error("Failed to fetch parent commit timestamps", {
9635
- error,
9636
- commitSha,
9637
- owner,
9638
- repo
9639
- });
9640
- parentCommits = void 0;
9641
- }
9642
- }
9643
- }
9644
9192
  let repositoryCreatedAt = options?.repositoryCreatedAt;
9645
9193
  if (repositoryCreatedAt === void 0) {
9646
9194
  try {
@@ -9662,7 +9210,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9662
9210
  authorName: commit.commit.author?.name,
9663
9211
  authorEmail: commit.commit.author?.email,
9664
9212
  message: commit.commit.message,
9665
- parentCommits,
9666
9213
  repositoryCreatedAt
9667
9214
  };
9668
9215
  }
@@ -9670,46 +9217,31 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9670
9217
  this._validateAccessTokenAndUrl();
9671
9218
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9672
9219
  const prNumber = Number(submitRequestId);
9673
- const [prRes, commitsRes, filesRes, repoData] = await Promise.all([
9220
+ const [prRes, commitsRes, repoData, prDiff] = await Promise.all([
9674
9221
  this.githubSdk.getPr({ owner, repo, pull_number: prNumber }),
9675
9222
  this.githubSdk.getPrCommits({ owner, repo, pull_number: prNumber }),
9676
- this.githubSdk.listPRFiles({ owner, repo, pull_number: prNumber }),
9677
- this.githubSdk.getRepository({ owner, repo })
9223
+ this.githubSdk.getRepository({ owner, repo }),
9224
+ this.getPrDiff({ pull_number: prNumber })
9678
9225
  ]);
9679
9226
  const pr = prRes.data;
9680
9227
  const repositoryCreatedAt = repoData.data.created_at ? new Date(repoData.data.created_at) : void 0;
9681
- const allParentShas = /* @__PURE__ */ new Set();
9682
- for (const commit of commitsRes.data) {
9683
- if (commit.parents) {
9684
- for (const parent of commit.parents) {
9685
- allParentShas.add(parent.sha);
9686
- }
9687
- }
9688
- }
9689
- const [parentCommitTimestamps, prDiff] = await Promise.all([
9690
- this.githubSdk.getCommitsBatch({
9691
- owner,
9692
- repo,
9693
- commitShas: Array.from(allParentShas)
9694
- }),
9695
- this.getPrDiff({ pull_number: prNumber })
9696
- ]);
9697
- const limit = pLimit2(GITHUB_API_CONCURRENCY);
9228
+ const limit = pLimit(GITHUB_API_CONCURRENCY);
9698
9229
  const commits = await Promise.all(
9699
9230
  commitsRes.data.map(
9700
9231
  (commit) => limit(
9701
9232
  () => this.getCommitDiff(commit.sha, {
9702
- repositoryCreatedAt,
9703
- parentCommitTimestamps
9233
+ repositoryCreatedAt
9704
9234
  })
9705
9235
  )
9706
9236
  )
9707
9237
  );
9708
- const diffLines = filesRes ? await this._attributeLinesViaBlame({
9709
- headSha: pr.head.sha,
9710
- changedFiles: filesRes.data,
9711
- prCommits: commits
9712
- }) : [];
9238
+ const addedLinesByFile = parseAddedLinesByFile(prDiff);
9239
+ const diffLines = [];
9240
+ for (const [file, lines] of addedLinesByFile) {
9241
+ for (const line of lines) {
9242
+ diffLines.push({ file, line });
9243
+ }
9244
+ }
9713
9245
  return {
9714
9246
  diff: prDiff,
9715
9247
  createdAt: new Date(pr.created_at),
@@ -9726,47 +9258,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9726
9258
  diffLines
9727
9259
  };
9728
9260
  }
9729
- async getSubmitRequests(repoUrl) {
9730
- this._validateAccessToken();
9731
- const { owner, repo } = parseGithubOwnerAndRepo(repoUrl);
9732
- const pullsRes = await this.githubSdk.getRepoPullRequests({ owner, repo });
9733
- const prNumbers = pullsRes.data.map((pr) => pr.number);
9734
- const [additionsDeletionsMap, commentsMap] = await Promise.all([
9735
- this.githubSdk.getPrAdditionsDeletionsBatch({ owner, repo, prNumbers }),
9736
- this.githubSdk.getPrCommentsBatch({ owner, repo, prNumbers })
9737
- ]);
9738
- const submitRequests = pullsRes.data.map((pr) => {
9739
- let status = "open";
9740
- if (pr.state === "closed") {
9741
- status = pr.merged_at ? "merged" : "closed";
9742
- } else if (pr.draft) {
9743
- status = "draft";
9744
- }
9745
- const changedLinesData = additionsDeletionsMap.get(pr.number);
9746
- const changedLines = changedLinesData ? {
9747
- added: changedLinesData.additions,
9748
- removed: changedLinesData.deletions
9749
- } : { added: 0, removed: 0 };
9750
- const comments = commentsMap.get(pr.number) || [];
9751
- const tickets = _GithubSCMLib.extractLinearTicketsFromComments(comments);
9752
- return {
9753
- submitRequestId: String(pr.number),
9754
- submitRequestNumber: pr.number,
9755
- title: pr.title,
9756
- status,
9757
- sourceBranch: pr.head.ref,
9758
- targetBranch: pr.base.ref,
9759
- authorName: pr.user?.name || pr.user?.login,
9760
- authorEmail: pr.user?.email || void 0,
9761
- createdAt: new Date(pr.created_at),
9762
- updatedAt: new Date(pr.updated_at),
9763
- description: pr.body || void 0,
9764
- tickets,
9765
- changedLines
9766
- };
9767
- });
9768
- return submitRequests;
9769
- }
9770
9261
  /**
9771
9262
  * Override searchSubmitRequests to use GitHub's Search API for efficient pagination.
9772
9263
  * This is much faster than fetching all PRs and filtering in-memory.
@@ -9985,146 +9476,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9985
9476
  };
9986
9477
  }
9987
9478
  /**
9988
- * Parse a Linear ticket from URL and name
9989
- * Returns null if invalid or missing data
9479
+ * Extract Linear ticket links from pre-fetched comments (pure function, no API calls)
9480
+ * Instance method that overrides base class - can also be called statically for backwards compatibility.
9990
9481
  */
9991
- static _parseLinearTicket(url, name) {
9992
- if (!name || !url) {
9993
- return null;
9994
- }
9995
- const urlParts = url.split("/");
9996
- const titleSlug = urlParts[urlParts.length - 1] || "";
9997
- const title = titleSlug.replace(/-/g, " ");
9998
- return { name, title, url };
9482
+ extractLinearTicketsFromComments(comments) {
9483
+ return _GithubSCMLib._extractLinearTicketsFromCommentsImpl(comments);
9999
9484
  }
10000
9485
  /**
10001
- * Extract Linear ticket links from pre-fetched comments (pure function, no API calls)
10002
- * Public static method so it can be reused by backend services.
9486
+ * Static implementation for backwards compatibility and reuse.
9487
+ * Called by both the instance method and direct static calls.
10003
9488
  */
10004
- static extractLinearTicketsFromComments(comments) {
9489
+ static _extractLinearTicketsFromCommentsImpl(comments) {
10005
9490
  const tickets = [];
10006
9491
  const seen = /* @__PURE__ */ new Set();
10007
9492
  for (const comment of comments) {
10008
9493
  if (comment.author?.login === "linear[bot]" || comment.author?.type === "Bot") {
10009
- const body = comment.body || "";
10010
- const htmlPattern = /<a href="(https:\/\/linear\.app\/[^"]+)">([A-Z]+-\d+)<\/a>/g;
10011
- let match;
10012
- while ((match = htmlPattern.exec(body)) !== null) {
10013
- const ticket = _GithubSCMLib._parseLinearTicket(match[1], match[2]);
10014
- if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
10015
- seen.add(`${ticket.name}|${ticket.url}`);
10016
- tickets.push(ticket);
10017
- }
10018
- }
10019
- const markdownPattern = /\[([A-Z]+-\d+)\]\((https:\/\/linear\.app\/[^)]+)\)/g;
10020
- while ((match = markdownPattern.exec(body)) !== null) {
10021
- const ticket = _GithubSCMLib._parseLinearTicket(match[2], match[1]);
10022
- if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
10023
- seen.add(`${ticket.name}|${ticket.url}`);
10024
- tickets.push(ticket);
10025
- }
10026
- }
9494
+ tickets.push(...extractLinearTicketsFromBody(comment.body || "", seen));
10027
9495
  }
10028
9496
  }
10029
9497
  return tickets;
10030
9498
  }
10031
- /**
10032
- * Optimized helper to parse added line numbers from a unified diff patch
10033
- * Single-pass parsing for minimal CPU usage
10034
- */
10035
- _parseAddedLinesFromPatch(patch) {
10036
- const addedLines = [];
10037
- const lines = patch.split("\n");
10038
- let currentLineNumber = 0;
10039
- for (const line of lines) {
10040
- if (line.startsWith("@@")) {
10041
- const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
10042
- if (match?.[1]) {
10043
- currentLineNumber = parseInt(match[1], 10);
10044
- }
10045
- continue;
10046
- }
10047
- if (line.startsWith("+") && !line.startsWith("+++")) {
10048
- addedLines.push(currentLineNumber);
10049
- currentLineNumber++;
10050
- } else if (!line.startsWith("-")) {
10051
- currentLineNumber++;
10052
- }
10053
- }
10054
- return addedLines;
10055
- }
10056
- /**
10057
- * Process blame data for a single file to attribute lines to commits
10058
- * Uses pre-fetched blame data instead of making API calls
10059
- */
10060
- _processFileBlameSafe(file, blameData, prCommitShas) {
10061
- const addedLines = this._parseAddedLinesFromPatch(file.patch);
10062
- const addedLinesSet = new Set(addedLines);
10063
- const fileAttributions = [];
10064
- for (const blameRange of blameData) {
10065
- if (!prCommitShas.has(blameRange.commitSha)) {
10066
- continue;
10067
- }
10068
- for (let lineNum = blameRange.startingLine; lineNum <= blameRange.endingLine; lineNum++) {
10069
- if (addedLinesSet.has(lineNum)) {
10070
- fileAttributions.push({
10071
- file: file.filename,
10072
- line: lineNum,
10073
- commitSha: blameRange.commitSha
10074
- });
10075
- }
10076
- }
10077
- }
10078
- return fileAttributions;
10079
- }
10080
- /**
10081
- * Optimized helper to attribute PR lines to commits using blame API
10082
- * Batch blame queries for minimal API call time (1 call instead of M calls)
10083
- *
10084
- * Uses size-based batching to handle large files:
10085
- * - Files > 1MB are processed individually with rate limiting
10086
- * - Smaller files are batched together in a single request
10087
- * This prevents GitHub API timeouts (~10s) on large generated files.
10088
- */
10089
- async _attributeLinesViaBlame(params) {
10090
- const { headSha, changedFiles, prCommits } = params;
10091
- const prCommitShas = new Set(prCommits.map((c) => c.commitSha));
10092
- const filesWithAdditions = changedFiles.filter((file) => {
10093
- if (!file.patch || file.patch.trim().length === 0) {
10094
- return false;
10095
- }
10096
- if (!file.sha) {
10097
- return false;
10098
- }
10099
- return true;
10100
- });
10101
- if (filesWithAdditions.length === 0) {
10102
- return [];
10103
- }
10104
- const { owner, repo } = parseGithubOwnerAndRepo(this.url);
10105
- const blameMap = await this.githubSdk.getBlameBatch({
10106
- owner,
10107
- repo,
10108
- ref: headSha,
10109
- // Use commit SHA directly from PR.head.sha
10110
- files: filesWithAdditions.map((f) => ({
10111
- path: f.filename,
10112
- blobSha: f.sha
10113
- })),
10114
- concurrency: GITHUB_API_CONCURRENCY
10115
- });
10116
- const allAttributions = [];
10117
- for (const file of filesWithAdditions) {
10118
- const blameData = blameMap.get(file.filename) || [];
10119
- const fileAttributions = this._processFileBlameSafe(
10120
- file,
10121
- blameData,
10122
- prCommitShas
10123
- );
10124
- allAttributions.push(...fileAttributions);
10125
- }
10126
- return allAttributions;
10127
- }
10128
9499
  };
10129
9500
 
10130
9501
  // src/features/analysis/scm/gitlab/gitlab.ts
@@ -10137,11 +9508,72 @@ import {
10137
9508
  Gitlab
10138
9509
  } from "@gitbeaker/rest";
10139
9510
  import Debug3 from "debug";
9511
+ import pLimit2 from "p-limit";
10140
9512
  import {
10141
9513
  Agent,
10142
9514
  fetch as undiciFetch,
10143
9515
  ProxyAgent as ProxyAgent2
10144
9516
  } from "undici";
9517
+
9518
+ // src/utils/contextLogger.ts
9519
+ import debugModule from "debug";
9520
+ var debug3 = debugModule("mobb:shared");
9521
+ var _contextLogger = null;
9522
+ var createContextLogger = async () => {
9523
+ if (_contextLogger) return _contextLogger;
9524
+ try {
9525
+ let logger2;
9526
+ try {
9527
+ let module;
9528
+ try {
9529
+ const buildPath = "../../../../../tscommon/backend/build/src/utils/logger";
9530
+ module = await import(buildPath);
9531
+ } catch (e) {
9532
+ const sourcePath = "../../../../../tscommon/backend/src/utils/logger";
9533
+ module = await import(sourcePath);
9534
+ }
9535
+ logger2 = module.logger;
9536
+ } catch {
9537
+ }
9538
+ if (logger2) {
9539
+ _contextLogger = {
9540
+ info: (message, data) => data ? logger2.info(data, message) : logger2.info(message),
9541
+ warn: (message, data) => data ? logger2.warn(data, message) : logger2.warn(message),
9542
+ debug: (message, data) => data ? logger2.debug(data, message) : logger2.debug(message),
9543
+ error: (message, data) => data ? logger2.error(data, message) : logger2.error(message)
9544
+ };
9545
+ return _contextLogger;
9546
+ }
9547
+ } catch {
9548
+ }
9549
+ _contextLogger = {
9550
+ info: (message, data) => debug3(message, data),
9551
+ warn: (message, data) => debug3(message, data),
9552
+ debug: (message, data) => debug3(message, data),
9553
+ error: (message, data) => debug3(message, data)
9554
+ };
9555
+ return _contextLogger;
9556
+ };
9557
+ var contextLogger = {
9558
+ info: async (message, data) => {
9559
+ const logger2 = await createContextLogger();
9560
+ return logger2.info(message, data);
9561
+ },
9562
+ debug: async (message, data) => {
9563
+ const logger2 = await createContextLogger();
9564
+ return logger2.debug(message, data);
9565
+ },
9566
+ warn: async (message, data) => {
9567
+ const logger2 = await createContextLogger();
9568
+ return logger2.warn(message, data);
9569
+ },
9570
+ error: async (message, data) => {
9571
+ const logger2 = await createContextLogger();
9572
+ return logger2.error(message, data);
9573
+ }
9574
+ };
9575
+
9576
+ // src/features/analysis/scm/gitlab/gitlab.ts
10145
9577
  init_env();
10146
9578
 
10147
9579
  // src/features/analysis/scm/gitlab/types.ts
@@ -10158,6 +9590,14 @@ function removeTrailingSlash2(str) {
10158
9590
  return str.trim().replace(/\/+$/, "");
10159
9591
  }
10160
9592
  var MAX_GITLAB_PR_BODY_LENGTH = 1048576;
9593
+ function buildUnifiedDiff(diffs) {
9594
+ return diffs.filter((d) => d.diff).map((d) => {
9595
+ const oldPath = d.old_path || d.new_path || "";
9596
+ const newPath = d.new_path || d.old_path || "";
9597
+ return `diff --git a/${oldPath} b/${newPath}
9598
+ ${d.diff}`;
9599
+ }).join("\n");
9600
+ }
10161
9601
  function getRandomGitlabCloudAnonToken() {
10162
9602
  if (!GITLAB_API_TOKEN || typeof GITLAB_API_TOKEN !== "string") {
10163
9603
  return void 0;
@@ -10211,7 +9651,10 @@ async function gitlabValidateParams({
10211
9651
  if (code === 404 || description.includes("404") || description.includes("Not Found")) {
10212
9652
  throw new InvalidRepoUrlError(`invalid gitlab repo URL: ${url}`);
10213
9653
  }
10214
- console.log("gitlabValidateParams error", e);
9654
+ contextLogger.warn("[gitlabValidateParams] Error validating params", {
9655
+ url,
9656
+ error: e
9657
+ });
10215
9658
  throw new InvalidRepoUrlError(
10216
9659
  `cannot access gitlab repo URL: ${url} with the provided access token`
10217
9660
  );
@@ -10240,6 +9683,13 @@ async function getGitlabIsUserCollaborator({
10240
9683
  ];
10241
9684
  return accessLevelWithWriteAccess.includes(groupAccess) || accessLevelWithWriteAccess.includes(projectAccess);
10242
9685
  } catch (e) {
9686
+ contextLogger.warn(
9687
+ "[getGitlabIsUserCollaborator] Error checking collaborator status",
9688
+ {
9689
+ error: e instanceof Error ? e.message : String(e),
9690
+ repoUrl
9691
+ }
9692
+ );
10243
9693
  return false;
10244
9694
  }
10245
9695
  }
@@ -10286,6 +9736,14 @@ async function getGitlabIsRemoteBranch({
10286
9736
  const res = await api2.Branches.show(projectPath, branch);
10287
9737
  return res.name === branch;
10288
9738
  } catch (e) {
9739
+ contextLogger.warn(
9740
+ "[getGitlabIsRemoteBranch] Error checking remote branch",
9741
+ {
9742
+ error: e instanceof Error ? e.message : String(e),
9743
+ repoUrl,
9744
+ branch
9745
+ }
9746
+ );
10289
9747
  return false;
10290
9748
  }
10291
9749
  }
@@ -10318,49 +9776,257 @@ async function getGitlabRepoList(url, accessToken) {
10318
9776
  })
10319
9777
  );
10320
9778
  }
10321
- async function getGitlabBranchList({
10322
- accessToken,
10323
- repoUrl
9779
+ async function getGitlabBranchList({
9780
+ accessToken,
9781
+ repoUrl
9782
+ }) {
9783
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9784
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9785
+ try {
9786
+ const res = await api2.Branches.all(projectPath, {
9787
+ //keyset API pagination is not supported by GL for the branch list (at least not the on-prem version)
9788
+ //so for now we stick with the default pagination and just return the first page and limit the results to 1000 entries.
9789
+ //This is a temporary solution until we implement list branches with name search.
9790
+ perPage: MAX_BRANCHES_FETCH,
9791
+ page: 1
9792
+ });
9793
+ res.sort((a, b) => {
9794
+ if (!a.commit?.committed_date || !b.commit?.committed_date) {
9795
+ return 0;
9796
+ }
9797
+ return new Date(b.commit?.committed_date).getTime() - new Date(a.commit?.committed_date).getTime();
9798
+ });
9799
+ return res.map((branch) => branch.name).slice(0, MAX_BRANCHES_FETCH);
9800
+ } catch (e) {
9801
+ contextLogger.warn("[getGitlabBranchList] Error fetching branch list", {
9802
+ error: e instanceof Error ? e.message : String(e),
9803
+ repoUrl
9804
+ });
9805
+ return [];
9806
+ }
9807
+ }
9808
+ async function createMergeRequest(options) {
9809
+ const { projectPath } = parseGitlabOwnerAndRepo(options.repoUrl);
9810
+ const api2 = getGitBeaker({
9811
+ url: options.repoUrl,
9812
+ gitlabAuthToken: options.accessToken
9813
+ });
9814
+ const res = await api2.MergeRequests.create(
9815
+ projectPath,
9816
+ options.sourceBranchName,
9817
+ options.targetBranchName,
9818
+ options.title,
9819
+ {
9820
+ description: safeBody(options.body, MAX_GITLAB_PR_BODY_LENGTH)
9821
+ }
9822
+ );
9823
+ return res.iid;
9824
+ }
9825
+ async function getGitlabMergeRequest({
9826
+ url,
9827
+ prNumber,
9828
+ accessToken
9829
+ }) {
9830
+ const { projectPath } = parseGitlabOwnerAndRepo(url);
9831
+ const api2 = getGitBeaker({
9832
+ url,
9833
+ gitlabAuthToken: accessToken
9834
+ });
9835
+ return await api2.MergeRequests.show(projectPath, prNumber);
9836
+ }
9837
+ async function searchGitlabMergeRequests({
9838
+ repoUrl,
9839
+ accessToken,
9840
+ state,
9841
+ updatedAfter,
9842
+ orderBy = "updated_at",
9843
+ sort = "desc",
9844
+ perPage = GITLAB_PER_PAGE,
9845
+ page = 1
9846
+ }) {
9847
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9848
+ debug4(
9849
+ "[searchGitlabMergeRequests] Fetching MRs for %s (page=%d, perPage=%d)",
9850
+ projectPath,
9851
+ page,
9852
+ perPage
9853
+ );
9854
+ const api2 = getGitBeaker({
9855
+ url: repoUrl,
9856
+ gitlabAuthToken: accessToken
9857
+ });
9858
+ const mergeRequests = await api2.MergeRequests.all({
9859
+ projectId: projectPath,
9860
+ state: state === "all" ? void 0 : state,
9861
+ updatedAfter: updatedAfter?.toISOString(),
9862
+ orderBy,
9863
+ sort,
9864
+ perPage,
9865
+ page
9866
+ });
9867
+ const items = mergeRequests.map((mr) => ({
9868
+ iid: mr.iid,
9869
+ title: mr.title,
9870
+ state: mr.state,
9871
+ sourceBranch: mr.source_branch,
9872
+ targetBranch: mr.target_branch,
9873
+ authorUsername: mr.author?.username,
9874
+ createdAt: mr.created_at,
9875
+ updatedAt: mr.updated_at,
9876
+ description: mr.description
9877
+ }));
9878
+ debug4(
9879
+ "[searchGitlabMergeRequests] Found %d MRs on page %d",
9880
+ items.length,
9881
+ page
9882
+ );
9883
+ return {
9884
+ items,
9885
+ hasMore: mergeRequests.length === perPage
9886
+ };
9887
+ }
9888
+ var GITLAB_API_CONCURRENCY = 5;
9889
+ async function getGitlabMrCommitsBatch({
9890
+ repoUrl,
9891
+ accessToken,
9892
+ mrNumbers
9893
+ }) {
9894
+ if (mrNumbers.length === 0) {
9895
+ return /* @__PURE__ */ new Map();
9896
+ }
9897
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9898
+ const api2 = getGitBeaker({
9899
+ url: repoUrl,
9900
+ gitlabAuthToken: accessToken
9901
+ });
9902
+ const limit = pLimit2(GITLAB_API_CONCURRENCY);
9903
+ const results = await Promise.all(
9904
+ mrNumbers.map(
9905
+ (mrNumber) => limit(async () => {
9906
+ try {
9907
+ const commits = await api2.MergeRequests.allCommits(
9908
+ projectPath,
9909
+ mrNumber
9910
+ );
9911
+ return [mrNumber, commits.map((c) => c.id)];
9912
+ } catch (error) {
9913
+ contextLogger.warn(
9914
+ "[getGitlabMrCommitsBatch] Failed to fetch commits for MR",
9915
+ {
9916
+ error: error instanceof Error ? error.message : String(error),
9917
+ mrNumber,
9918
+ repoUrl
9919
+ }
9920
+ );
9921
+ return [mrNumber, []];
9922
+ }
9923
+ })
9924
+ )
9925
+ );
9926
+ return new Map(results);
9927
+ }
9928
+ async function getGitlabMrDataBatch({
9929
+ repoUrl,
9930
+ accessToken,
9931
+ mrNumbers
9932
+ }) {
9933
+ if (mrNumbers.length === 0) {
9934
+ return /* @__PURE__ */ new Map();
9935
+ }
9936
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9937
+ const api2 = getGitBeaker({
9938
+ url: repoUrl,
9939
+ gitlabAuthToken: accessToken
9940
+ });
9941
+ const limit = pLimit2(GITLAB_API_CONCURRENCY);
9942
+ const results = await Promise.all(
9943
+ mrNumbers.map(
9944
+ (mrNumber) => limit(async () => {
9945
+ try {
9946
+ const [diffs, notes] = await Promise.all([
9947
+ api2.MergeRequests.allDiffs(projectPath, mrNumber),
9948
+ api2.MergeRequestNotes.all(projectPath, mrNumber)
9949
+ ]);
9950
+ let additions = 0;
9951
+ let deletions = 0;
9952
+ for (const diff of diffs) {
9953
+ if (diff.diff) {
9954
+ for (const line of diff.diff.split("\n")) {
9955
+ if (line.startsWith("+") && !line.startsWith("+++")) {
9956
+ additions++;
9957
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
9958
+ deletions++;
9959
+ }
9960
+ }
9961
+ }
9962
+ }
9963
+ const comments = notes.map((note) => ({
9964
+ author: note.author ? {
9965
+ login: note.author.username,
9966
+ type: note.author.username.endsWith("[bot]") || note.author.username.toLowerCase() === "linear" ? "Bot" : "User"
9967
+ } : null,
9968
+ body: note.body
9969
+ }));
9970
+ return [
9971
+ mrNumber,
9972
+ { changedLines: { additions, deletions }, comments }
9973
+ ];
9974
+ } catch (error) {
9975
+ contextLogger.warn(
9976
+ "[getGitlabMrDataBatch] Failed to fetch data for MR",
9977
+ {
9978
+ error: error instanceof Error ? error.message : String(error),
9979
+ mrNumber,
9980
+ repoUrl
9981
+ }
9982
+ );
9983
+ return [
9984
+ mrNumber,
9985
+ {
9986
+ changedLines: { additions: 0, deletions: 0 },
9987
+ comments: []
9988
+ }
9989
+ ];
9990
+ }
9991
+ })
9992
+ )
9993
+ );
9994
+ return new Map(results);
9995
+ }
9996
+ async function getGitlabMergeRequestLinesAdded({
9997
+ url,
9998
+ prNumber,
9999
+ accessToken
10324
10000
  }) {
10325
- const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10326
- const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10327
10001
  try {
10328
- const res = await api2.Branches.all(projectPath, {
10329
- //keyset API pagination is not supported by GL for the branch list (at least not the on-prem version)
10330
- //so for now we stick with the default pagination and just return the first page and limit the results to 1000 entries.
10331
- //This is a temporary solution until we implement list branches with name search.
10332
- perPage: MAX_BRANCHES_FETCH,
10333
- page: 1
10002
+ const { projectPath } = parseGitlabOwnerAndRepo(url);
10003
+ const api2 = getGitBeaker({
10004
+ url,
10005
+ gitlabAuthToken: accessToken
10334
10006
  });
10335
- res.sort((a, b) => {
10336
- if (!a.commit?.committed_date || !b.commit?.committed_date) {
10337
- return 0;
10007
+ const diffs = await api2.MergeRequests.allDiffs(projectPath, prNumber);
10008
+ let linesAdded = 0;
10009
+ for (const diff of diffs) {
10010
+ if (diff.diff) {
10011
+ const addedLines = diff.diff.split("\n").filter(
10012
+ (line) => line.startsWith("+") && !line.startsWith("+++")
10013
+ ).length;
10014
+ linesAdded += addedLines;
10338
10015
  }
10339
- return new Date(b.commit?.committed_date).getTime() - new Date(a.commit?.committed_date).getTime();
10340
- });
10341
- return res.map((branch) => branch.name).slice(0, MAX_BRANCHES_FETCH);
10342
- } catch (e) {
10343
- return [];
10344
- }
10345
- }
10346
- async function createMergeRequest(options) {
10347
- const { projectPath } = parseGitlabOwnerAndRepo(options.repoUrl);
10348
- const api2 = getGitBeaker({
10349
- url: options.repoUrl,
10350
- gitlabAuthToken: options.accessToken
10351
- });
10352
- const res = await api2.MergeRequests.create(
10353
- projectPath,
10354
- options.sourceBranchName,
10355
- options.targetBranchName,
10356
- options.title,
10357
- {
10358
- description: safeBody(options.body, MAX_GITLAB_PR_BODY_LENGTH)
10359
10016
  }
10360
- );
10361
- return res.iid;
10017
+ return linesAdded;
10018
+ } catch (error) {
10019
+ contextLogger.warn(
10020
+ "[getGitlabMergeRequestLinesAdded] Failed to fetch diffs for MR",
10021
+ {
10022
+ prNumber,
10023
+ error
10024
+ }
10025
+ );
10026
+ return 0;
10027
+ }
10362
10028
  }
10363
- async function getGitlabMergeRequest({
10029
+ async function getGitlabMergeRequestMetrics({
10364
10030
  url,
10365
10031
  prNumber,
10366
10032
  accessToken
@@ -10370,7 +10036,28 @@ async function getGitlabMergeRequest({
10370
10036
  url,
10371
10037
  gitlabAuthToken: accessToken
10372
10038
  });
10373
- return await api2.MergeRequests.show(projectPath, prNumber);
10039
+ const [mr, commits, linesAdded, notes] = await Promise.all([
10040
+ api2.MergeRequests.show(projectPath, prNumber),
10041
+ api2.MergeRequests.allCommits(projectPath, prNumber),
10042
+ getGitlabMergeRequestLinesAdded({ url, prNumber, accessToken }),
10043
+ api2.MergeRequestNotes.all(projectPath, prNumber)
10044
+ ]);
10045
+ const sortedCommits = [...commits].sort(
10046
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
10047
+ );
10048
+ const firstCommitDate = sortedCommits[0]?.created_at ?? null;
10049
+ const commentIds = notes.filter((note) => !note.system).map((note) => String(note.id));
10050
+ return {
10051
+ state: mr.state,
10052
+ isDraft: mr.draft ?? false,
10053
+ createdAt: mr.created_at,
10054
+ mergedAt: mr.merged_at ?? null,
10055
+ linesAdded,
10056
+ commitsCount: commits.length,
10057
+ commitShas: commits.map((c) => c.id),
10058
+ firstCommitDate,
10059
+ commentIds
10060
+ };
10374
10061
  }
10375
10062
  async function getGitlabCommitUrl({
10376
10063
  url,
@@ -10449,26 +10136,125 @@ function parseGitlabOwnerAndRepo(gitlabUrl) {
10449
10136
  const { organization, repoName, projectPath } = parsingResult;
10450
10137
  return { owner: organization, repo: repoName, projectPath };
10451
10138
  }
10452
- async function getGitlabBlameRanges({ ref, gitlabUrl, path: path25 }, options) {
10453
- const { projectPath } = parseGitlabOwnerAndRepo(gitlabUrl);
10454
- const api2 = getGitBeaker({
10455
- url: gitlabUrl,
10456
- gitlabAuthToken: options?.gitlabAuthToken
10457
- });
10458
- const resp = await api2.RepositoryFiles.allFileBlames(projectPath, path25, ref);
10459
- let lineNumber = 1;
10460
- return resp.filter((range) => range.lines).map((range) => {
10461
- const oldLineNumber = lineNumber;
10462
- if (!range.lines) {
10463
- throw new Error("range.lines should not be undefined");
10464
- }
10465
- lineNumber += range.lines.length;
10139
+ var GITLAB_MAX_RESULTS_LIMIT = 1024;
10140
+ var GITLAB_PER_PAGE = 128;
10141
+ async function getGitlabRecentCommits({
10142
+ repoUrl,
10143
+ accessToken,
10144
+ since
10145
+ }) {
10146
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10147
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10148
+ const allCommits = [];
10149
+ let page = 1;
10150
+ let hasMore = true;
10151
+ while (hasMore && allCommits.length < GITLAB_MAX_RESULTS_LIMIT) {
10152
+ const commits = await api2.Commits.all(projectPath, {
10153
+ since,
10154
+ perPage: GITLAB_PER_PAGE,
10155
+ page
10156
+ });
10157
+ if (commits.length === 0) {
10158
+ hasMore = false;
10159
+ break;
10160
+ }
10161
+ for (const commit of commits) {
10162
+ if (allCommits.length >= GITLAB_MAX_RESULTS_LIMIT) {
10163
+ break;
10164
+ }
10165
+ allCommits.push({
10166
+ sha: commit.id,
10167
+ commit: {
10168
+ committer: commit.committed_date ? { date: commit.committed_date } : void 0,
10169
+ author: {
10170
+ email: commit.author_email,
10171
+ name: commit.author_name
10172
+ },
10173
+ message: commit.message
10174
+ },
10175
+ parents: commit.parent_ids?.map((sha) => ({ sha })) || []
10176
+ });
10177
+ }
10178
+ if (commits.length < GITLAB_PER_PAGE) {
10179
+ hasMore = false;
10180
+ } else {
10181
+ page++;
10182
+ }
10183
+ }
10184
+ if (allCommits.length >= GITLAB_MAX_RESULTS_LIMIT) {
10185
+ contextLogger.warn("[getGitlabRecentCommits] Hit commit pagination limit", {
10186
+ limit: GITLAB_MAX_RESULTS_LIMIT,
10187
+ count: allCommits.length,
10188
+ repoUrl,
10189
+ since
10190
+ });
10191
+ }
10192
+ return allCommits;
10193
+ }
10194
+ async function getGitlabCommitDiff({
10195
+ repoUrl,
10196
+ accessToken,
10197
+ commitSha
10198
+ }) {
10199
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10200
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10201
+ const [commit, diffs] = await Promise.all([
10202
+ api2.Commits.show(projectPath, commitSha),
10203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10204
+ api2.Commits.showDiff(projectPath, commitSha, { unidiff: true })
10205
+ ]);
10206
+ const diffString = buildUnifiedDiff(diffs);
10207
+ const commitTimestamp = commit.committed_date ? new Date(commit.committed_date) : /* @__PURE__ */ new Date();
10208
+ return {
10209
+ diff: diffString,
10210
+ commitTimestamp,
10211
+ commitSha: commit.id,
10212
+ authorName: commit.author_name,
10213
+ authorEmail: commit.author_email,
10214
+ message: commit.message
10215
+ };
10216
+ }
10217
+ async function getGitlabRateLimitStatus({
10218
+ repoUrl,
10219
+ accessToken
10220
+ }) {
10221
+ try {
10222
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10223
+ const response = await api2.Users.showCurrentUser({ showExpanded: true });
10224
+ const headers = response.headers;
10225
+ if (!headers) {
10226
+ return null;
10227
+ }
10228
+ const remaining = headers["ratelimit-remaining"];
10229
+ const reset = headers["ratelimit-reset"];
10230
+ const limit = headers["ratelimit-limit"];
10231
+ if (!remaining || !reset) {
10232
+ return null;
10233
+ }
10234
+ const remainingNum = parseInt(remaining, 10);
10235
+ const resetNum = parseInt(reset, 10);
10236
+ if (isNaN(remainingNum) || isNaN(resetNum)) {
10237
+ contextLogger.warn(
10238
+ "[getGitlabRateLimitStatus] Malformed rate limit headers",
10239
+ { remaining, reset, repoUrl }
10240
+ );
10241
+ return null;
10242
+ }
10466
10243
  return {
10467
- startingLine: oldLineNumber,
10468
- endingLine: lineNumber - 1,
10469
- commitSha: range.commit.id
10244
+ remaining: remainingNum,
10245
+ reset: new Date(resetNum * 1e3),
10246
+ limit: limit ? parseInt(limit, 10) : void 0
10470
10247
  };
10471
- });
10248
+ } catch (error) {
10249
+ contextLogger.warn(
10250
+ "[getGitlabRateLimitStatus] Error fetching rate limit status",
10251
+ {
10252
+ error: error instanceof Error ? error.message : String(error),
10253
+ repoUrl
10254
+ }
10255
+ );
10256
+ return null;
10257
+ }
10472
10258
  }
10473
10259
  async function processBody(response) {
10474
10260
  const headers = response.headers;
@@ -10509,8 +10295,90 @@ async function brokerRequestHandler(endpoint, options) {
10509
10295
  };
10510
10296
  throw new Error(`gitbeaker: ${response.statusText}`);
10511
10297
  }
10298
+ async function getGitlabMergeRequestDiff({
10299
+ repoUrl,
10300
+ accessToken,
10301
+ mrNumber
10302
+ }) {
10303
+ debug4("[getGitlabMergeRequestDiff] Starting for MR #%d", mrNumber);
10304
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10305
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10306
+ debug4(
10307
+ "[getGitlabMergeRequestDiff] Fetching MR details, diffs, and commits..."
10308
+ );
10309
+ const startMrFetch = Date.now();
10310
+ const [mr, mrDiffs, mrCommitsRaw] = await Promise.all([
10311
+ api2.MergeRequests.show(projectPath, mrNumber),
10312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10313
+ api2.MergeRequests.allDiffs(projectPath, mrNumber, { unidiff: true }),
10314
+ api2.MergeRequests.allCommits(projectPath, mrNumber)
10315
+ ]);
10316
+ debug4(
10317
+ "[getGitlabMergeRequestDiff] MR fetch took %dms. Diffs: %d, Commits: %d",
10318
+ Date.now() - startMrFetch,
10319
+ mrDiffs.length,
10320
+ mrCommitsRaw.length
10321
+ );
10322
+ const diffString = buildUnifiedDiff(mrDiffs);
10323
+ debug4(
10324
+ "[getGitlabMergeRequestDiff] Fetching commit diffs for %d commits...",
10325
+ mrCommitsRaw.length
10326
+ );
10327
+ const startCommitFetch = Date.now();
10328
+ const commitDiffLimit = pLimit2(GITLAB_API_CONCURRENCY);
10329
+ const commits = await Promise.all(
10330
+ mrCommitsRaw.map(
10331
+ (commit) => commitDiffLimit(async () => {
10332
+ const commitDiff = await getGitlabCommitDiff({
10333
+ repoUrl,
10334
+ accessToken,
10335
+ commitSha: commit.id
10336
+ });
10337
+ return {
10338
+ diff: commitDiff.diff,
10339
+ commitTimestamp: commitDiff.commitTimestamp,
10340
+ commitSha: commitDiff.commitSha,
10341
+ authorName: commitDiff.authorName,
10342
+ authorEmail: commitDiff.authorEmail,
10343
+ message: commitDiff.message
10344
+ };
10345
+ })
10346
+ )
10347
+ );
10348
+ commits.sort(
10349
+ (a, b) => new Date(a.commitTimestamp).getTime() - new Date(b.commitTimestamp).getTime()
10350
+ );
10351
+ debug4(
10352
+ "[getGitlabMergeRequestDiff] Commit diffs fetch took %dms",
10353
+ Date.now() - startCommitFetch
10354
+ );
10355
+ const addedLinesByFile = parseAddedLinesByFile(diffString);
10356
+ const diffLines = [];
10357
+ for (const [file, lines] of addedLinesByFile) {
10358
+ for (const line of lines) {
10359
+ diffLines.push({ file, line });
10360
+ }
10361
+ }
10362
+ return {
10363
+ diff: diffString,
10364
+ createdAt: new Date(mr.created_at),
10365
+ updatedAt: new Date(mr.updated_at),
10366
+ submitRequestId: String(mrNumber),
10367
+ submitRequestNumber: mrNumber,
10368
+ sourceBranch: mr.source_branch,
10369
+ targetBranch: mr.target_branch,
10370
+ authorName: mr.author?.name || mr.author?.username,
10371
+ authorEmail: void 0,
10372
+ // GitLab MR API doesn't expose author email directly
10373
+ title: mr.title,
10374
+ description: mr.description || void 0,
10375
+ commits,
10376
+ diffLines
10377
+ };
10378
+ }
10512
10379
 
10513
10380
  // src/features/analysis/scm/gitlab/GitlabSCMLib.ts
10381
+ init_client_generates();
10514
10382
  var GitlabSCMLib = class extends SCMLib {
10515
10383
  constructor(url, accessToken, scmOrg) {
10516
10384
  super(url, accessToken, scmOrg);
@@ -10537,7 +10405,7 @@ var GitlabSCMLib = class extends SCMLib {
10537
10405
  }
10538
10406
  async getRepoList(_scmOrg) {
10539
10407
  if (!this.accessToken) {
10540
- console.error("no access token");
10408
+ contextLogger.warn("[GitlabSCMLib.getRepoList] No access token provided");
10541
10409
  throw new Error("no access token");
10542
10410
  }
10543
10411
  return getGitlabRepoList(this.url, this.accessToken);
@@ -10629,16 +10497,6 @@ var GitlabSCMLib = class extends SCMLib {
10629
10497
  markdownComment: comment
10630
10498
  });
10631
10499
  }
10632
- async getRepoBlameRanges(ref, path25) {
10633
- this._validateUrl();
10634
- return await getGitlabBlameRanges(
10635
- { ref, path: path25, gitlabUrl: this.url },
10636
- {
10637
- url: this.url,
10638
- gitlabAuthToken: this.accessToken
10639
- }
10640
- );
10641
- }
10642
10500
  async getReferenceData(ref) {
10643
10501
  this._validateUrl();
10644
10502
  return await getGitlabReferenceData(
@@ -10682,25 +10540,180 @@ var GitlabSCMLib = class extends SCMLib {
10682
10540
  this._validateAccessTokenAndUrl();
10683
10541
  return `${this.url}/-/commits/${branchName}`;
10684
10542
  }
10685
- async getCommitDiff(_commitSha) {
10686
- throw new Error("getCommitDiff not implemented for GitLab");
10543
+ async getCommitDiff(commitSha) {
10544
+ this._validateAccessTokenAndUrl();
10545
+ const result = await getGitlabCommitDiff({
10546
+ repoUrl: this.url,
10547
+ accessToken: this.accessToken,
10548
+ commitSha
10549
+ });
10550
+ return {
10551
+ diff: result.diff,
10552
+ commitTimestamp: result.commitTimestamp,
10553
+ commitSha: result.commitSha,
10554
+ authorName: result.authorName,
10555
+ authorEmail: result.authorEmail,
10556
+ message: result.message
10557
+ };
10687
10558
  }
10688
- async getSubmitRequestDiff(_submitRequestId) {
10689
- throw new Error("getSubmitRequestDiff not implemented for GitLab");
10559
+ async getSubmitRequestDiff(submitRequestId) {
10560
+ this._validateAccessTokenAndUrl();
10561
+ const mrNumber = parseInt(submitRequestId, 10);
10562
+ if (isNaN(mrNumber) || mrNumber <= 0) {
10563
+ throw new Error(`Invalid merge request ID: ${submitRequestId}`);
10564
+ }
10565
+ return getGitlabMergeRequestDiff({
10566
+ repoUrl: this.url,
10567
+ accessToken: this.accessToken,
10568
+ mrNumber
10569
+ });
10570
+ }
10571
+ async searchSubmitRequests(params) {
10572
+ this._validateAccessTokenAndUrl();
10573
+ const page = parseCursorSafe(params.cursor, 1);
10574
+ const perPage = params.limit || 10;
10575
+ const sort = params.sort || { field: "updated", order: "desc" };
10576
+ const orderBy = sort.field === "created" ? "created_at" : "updated_at";
10577
+ let gitlabState;
10578
+ if (params.filters?.state === "open") {
10579
+ gitlabState = "opened";
10580
+ } else if (params.filters?.state === "closed") {
10581
+ gitlabState = "closed";
10582
+ } else {
10583
+ gitlabState = "all";
10584
+ }
10585
+ const searchResult = await searchGitlabMergeRequests({
10586
+ repoUrl: this.url,
10587
+ accessToken: this.accessToken,
10588
+ state: gitlabState,
10589
+ updatedAfter: params.filters?.updatedAfter,
10590
+ orderBy,
10591
+ sort: sort.order,
10592
+ perPage,
10593
+ page
10594
+ });
10595
+ const results = searchResult.items.map((mr) => {
10596
+ let status = "open";
10597
+ if (mr.state === "merged") {
10598
+ status = "merged";
10599
+ } else if (mr.state === "closed") {
10600
+ status = "closed";
10601
+ }
10602
+ return {
10603
+ submitRequestId: String(mr.iid),
10604
+ submitRequestNumber: mr.iid,
10605
+ title: mr.title,
10606
+ status,
10607
+ sourceBranch: mr.sourceBranch,
10608
+ targetBranch: mr.targetBranch,
10609
+ authorName: mr.authorUsername,
10610
+ authorEmail: void 0,
10611
+ createdAt: new Date(mr.createdAt),
10612
+ updatedAt: new Date(mr.updatedAt),
10613
+ description: mr.description || void 0,
10614
+ tickets: [],
10615
+ changedLines: { added: 0, removed: 0 }
10616
+ };
10617
+ });
10618
+ const MAX_TOTAL_RESULTS = 1024;
10619
+ const totalFetchedSoFar = page * perPage;
10620
+ const reachedLimit = totalFetchedSoFar >= MAX_TOTAL_RESULTS;
10621
+ if (reachedLimit && searchResult.hasMore) {
10622
+ contextLogger.warn(
10623
+ "[searchSubmitRequests] Hit limit of merge requests for GitLab repo",
10624
+ {
10625
+ limit: MAX_TOTAL_RESULTS
10626
+ }
10627
+ );
10628
+ }
10629
+ return {
10630
+ results,
10631
+ nextCursor: searchResult.hasMore && !reachedLimit ? String(page + 1) : void 0,
10632
+ hasMore: searchResult.hasMore && !reachedLimit
10633
+ };
10690
10634
  }
10691
- async getSubmitRequests(_repoUrl) {
10692
- throw new Error("getSubmitRequests not implemented for GitLab");
10635
+ async getPrCommitsBatch(_repoUrl, prNumbers) {
10636
+ this._validateAccessTokenAndUrl();
10637
+ return getGitlabMrCommitsBatch({
10638
+ repoUrl: this.url,
10639
+ accessToken: this.accessToken,
10640
+ mrNumbers: prNumbers
10641
+ });
10693
10642
  }
10694
- async searchSubmitRequests(_params) {
10695
- throw new Error("searchSubmitRequests not implemented for GitLab");
10643
+ async getPrDataBatch(_repoUrl, prNumbers) {
10644
+ this._validateAccessTokenAndUrl();
10645
+ return getGitlabMrDataBatch({
10646
+ repoUrl: this.url,
10647
+ accessToken: this.accessToken,
10648
+ mrNumbers: prNumbers
10649
+ });
10696
10650
  }
10697
10651
  async searchRepos(_params) {
10698
10652
  throw new Error("searchRepos not implemented for GitLab");
10699
10653
  }
10700
- // TODO: Add comprehensive tests for getPullRequestMetrics (GitLab)
10701
- // See clients/cli/src/features/analysis/scm/__tests__/github.test.ts:589-648 for reference
10702
- async getPullRequestMetrics(_prNumber) {
10703
- throw new Error("getPullRequestMetrics not implemented for GitLab");
10654
+ async getPullRequestMetrics(prNumber) {
10655
+ this._validateAccessTokenAndUrl();
10656
+ const metrics = await getGitlabMergeRequestMetrics({
10657
+ url: this.url,
10658
+ prNumber,
10659
+ accessToken: this.accessToken
10660
+ });
10661
+ let prStatus;
10662
+ switch (metrics.state) {
10663
+ case "merged":
10664
+ prStatus = "MERGED" /* Merged */;
10665
+ break;
10666
+ case "closed":
10667
+ prStatus = "CLOSED" /* Closed */;
10668
+ break;
10669
+ default:
10670
+ prStatus = metrics.isDraft ? "DRAFT" /* Draft */ : "ACTIVE" /* Active */;
10671
+ }
10672
+ return {
10673
+ prId: String(prNumber),
10674
+ repositoryUrl: this.url,
10675
+ prCreatedAt: new Date(metrics.createdAt),
10676
+ prMergedAt: metrics.mergedAt ? new Date(metrics.mergedAt) : null,
10677
+ firstCommitDate: metrics.firstCommitDate ? new Date(metrics.firstCommitDate) : null,
10678
+ linesAdded: metrics.linesAdded,
10679
+ commitsCount: metrics.commitsCount,
10680
+ commitShas: metrics.commitShas,
10681
+ prStatus,
10682
+ commentIds: metrics.commentIds
10683
+ };
10684
+ }
10685
+ async getRecentCommits(since) {
10686
+ this._validateAccessTokenAndUrl();
10687
+ const commits = await getGitlabRecentCommits({
10688
+ repoUrl: this.url,
10689
+ accessToken: this.accessToken,
10690
+ since
10691
+ });
10692
+ return { data: commits };
10693
+ }
10694
+ async getRateLimitStatus() {
10695
+ this._validateAccessTokenAndUrl();
10696
+ return getGitlabRateLimitStatus({
10697
+ repoUrl: this.url,
10698
+ accessToken: this.accessToken
10699
+ });
10700
+ }
10701
+ /**
10702
+ * Extract Linear ticket links from pre-fetched comments (pure function, no API calls).
10703
+ * Linear bot uses the same comment format on GitLab as on GitHub.
10704
+ * Bot username may be 'linear' or 'linear[bot]' on GitLab.
10705
+ */
10706
+ extractLinearTicketsFromComments(comments) {
10707
+ const tickets = [];
10708
+ const seen = /* @__PURE__ */ new Set();
10709
+ for (const comment of comments) {
10710
+ const authorLogin = comment.author?.login?.toLowerCase() || "";
10711
+ const isLinearBot = authorLogin === "linear" || authorLogin === "linear[bot]" || comment.author?.type === "Bot" && authorLogin.includes("linear");
10712
+ if (isLinearBot) {
10713
+ tickets.push(...extractLinearTicketsFromBody(comment.body || "", seen));
10714
+ }
10715
+ }
10716
+ return tickets;
10704
10717
  }
10705
10718
  };
10706
10719
 
@@ -10759,10 +10772,6 @@ var StubSCMLib = class extends SCMLib {
10759
10772
  console.warn("getUserHasAccessToRepo() returning false");
10760
10773
  return false;
10761
10774
  }
10762
- async getRepoBlameRanges(_ref, _path) {
10763
- console.warn("getRepoBlameRanges() returning empty array");
10764
- return [];
10765
- }
10766
10775
  async getReferenceData(_ref) {
10767
10776
  console.warn("getReferenceData() returning null/empty defaults");
10768
10777
  return {
@@ -10827,14 +10836,18 @@ var StubSCMLib = class extends SCMLib {
10827
10836
  diffLines: []
10828
10837
  };
10829
10838
  }
10830
- async getSubmitRequests(_repoUrl) {
10831
- console.warn("getSubmitRequests() returning empty array");
10832
- return [];
10833
- }
10834
10839
  async getPullRequestMetrics(_prNumber) {
10835
10840
  console.warn("getPullRequestMetrics() returning empty object");
10836
10841
  throw new Error("getPullRequestMetrics() not implemented");
10837
10842
  }
10843
+ async getRecentCommits(_since) {
10844
+ console.warn("getRecentCommits() returning empty array");
10845
+ return { data: [] };
10846
+ }
10847
+ async getRateLimitStatus() {
10848
+ console.warn("getRateLimitStatus() returning null");
10849
+ return null;
10850
+ }
10838
10851
  };
10839
10852
 
10840
10853
  // src/features/analysis/scm/scmFactory.ts
@@ -12855,7 +12868,7 @@ import Debug10 from "debug";
12855
12868
 
12856
12869
  // src/features/analysis/add_fix_comments_for_pr/utils/utils.ts
12857
12870
  import Debug9 from "debug";
12858
- import parseDiff from "parse-diff";
12871
+ import parseDiff2 from "parse-diff";
12859
12872
  import { z as z27 } from "zod";
12860
12873
 
12861
12874
  // src/features/analysis/utils/by_key.ts
@@ -13228,7 +13241,7 @@ Refresh the page in order to see the changes.`,
13228
13241
  }
13229
13242
  async function getRelevantVulenrabilitiesFromDiff(params) {
13230
13243
  const { gqlClient, diff, vulnerabilityReportId } = params;
13231
- const parsedDiff = parseDiff(diff);
13244
+ const parsedDiff = parseDiff2(diff);
13232
13245
  const fileHunks = parsedDiff.map((file) => {
13233
13246
  const fileNumbers = file.chunks.flatMap((chunk) => chunk.changes).filter((change) => change.type === "add").map((_change) => {
13234
13247
  const change = _change;
@@ -15329,7 +15342,7 @@ async function getRepositoryUrl() {
15329
15342
  }
15330
15343
  const remoteUrl = await gitService.getRemoteUrl();
15331
15344
  const parsed = parseScmURL(remoteUrl);
15332
- return parsed?.scmType === "GitHub" /* GitHub */ ? remoteUrl : null;
15345
+ return parsed?.scmType === "GitHub" /* GitHub */ || parsed?.scmType === "GitLab" /* GitLab */ ? remoteUrl : null;
15333
15346
  } catch {
15334
15347
  return null;
15335
15348
  }
@@ -21495,7 +21508,7 @@ import {
21495
21508
  writeFileSync
21496
21509
  } from "fs";
21497
21510
  import fs21 from "fs/promises";
21498
- import parseDiff2 from "parse-diff";
21511
+ import parseDiff3 from "parse-diff";
21499
21512
  import path20 from "path";
21500
21513
  var PatchApplicationService = class {
21501
21514
  /**
@@ -22179,7 +22192,7 @@ var PatchApplicationService = class {
22179
22192
  fixId,
22180
22193
  scanContext
22181
22194
  }) {
22182
- const parsedPatch = parseDiff2(patch);
22195
+ const parsedPatch = parseDiff3(patch);
22183
22196
  if (!parsedPatch || parsedPatch.length === 0) {
22184
22197
  throw new Error("Failed to parse patch - no changes found");
22185
22198
  }