mobbdev 1.2.6 → 1.2.8

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
@@ -846,13 +846,12 @@ var init_client_generates = __esm({
846
846
  }
847
847
  `;
848
848
  AnalyzeCommitForExtensionAiBlameDocument = `
849
- mutation AnalyzeCommitForExtensionAIBlame($repositoryURL: String!, $commitSha: String!, $organizationId: String!, $commitTimestamp: Timestamp, $parentCommits: [ParentCommitInput!]) {
849
+ mutation AnalyzeCommitForExtensionAIBlame($repositoryURL: String!, $commitSha: String!, $organizationId: String!, $commitTimestamp: Timestamp) {
850
850
  analyzeCommitForAIBlame(
851
851
  repositoryURL: $repositoryURL
852
852
  commitSha: $commitSha
853
853
  organizationId: $organizationId
854
854
  commitTimestamp: $commitTimestamp
855
- parentCommits: $parentCommits
856
855
  ) {
857
856
  __typename
858
857
  ... on ProcessAIBlameFinalResult {
@@ -3801,34 +3800,6 @@ ${rootContent}`;
3801
3800
  throw new Error(errorMessage);
3802
3801
  }
3803
3802
  }
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
3803
  /**
3833
3804
  * Gets local commit data including diff, timestamp, and parent commits.
3834
3805
  * Used by Tracy extension to send commit data directly without requiring SCM token.
@@ -3873,18 +3844,14 @@ ${rootContent}`;
3873
3844
  }
3874
3845
  const timestampStr = metadataLines[0];
3875
3846
  const timestamp = new Date(timestampStr);
3876
- const parentShas = (metadataLines[1] ?? "").trim().split(/\s+/).filter(Boolean);
3877
- const parentCommits = await this.getParentCommitTimestamps(parentShas);
3878
3847
  this.log("[GitService] Local commit data retrieved", "debug", {
3879
3848
  commitSha,
3880
3849
  diffSizeBytes,
3881
- timestamp: timestamp.toISOString(),
3882
- parentCommitCount: parentCommits?.length ?? 0
3850
+ timestamp: timestamp.toISOString()
3883
3851
  });
3884
3852
  return {
3885
3853
  diff,
3886
- timestamp,
3887
- parentCommits
3854
+ timestamp
3888
3855
  };
3889
3856
  } catch (error) {
3890
3857
  const errorMessage = `Failed to get local commit data: ${error.message}`;
@@ -6109,6 +6076,34 @@ var GetReferenceResultZ = z13.object({
6109
6076
  type: z13.nativeEnum(ReferenceType)
6110
6077
  });
6111
6078
 
6079
+ // src/features/analysis/scm/utils/diffUtils.ts
6080
+ import parseDiff from "parse-diff";
6081
+ function parseAddedLinesByFile(diff) {
6082
+ const result = /* @__PURE__ */ new Map();
6083
+ const parsedDiff = parseDiff(diff);
6084
+ for (const file of parsedDiff) {
6085
+ if (!file.to || file.to === "/dev/null") {
6086
+ continue;
6087
+ }
6088
+ const filePath = file.to;
6089
+ const addedLines = [];
6090
+ if (file.chunks) {
6091
+ for (const chunk of file.chunks) {
6092
+ for (const change of chunk.changes) {
6093
+ if (change.type === "add") {
6094
+ const addChange = change;
6095
+ addedLines.push(addChange.ln);
6096
+ }
6097
+ }
6098
+ }
6099
+ }
6100
+ if (addedLines.length > 0) {
6101
+ result.set(filePath, addedLines);
6102
+ }
6103
+ }
6104
+ return result;
6105
+ }
6106
+
6112
6107
  // src/features/analysis/scm/utils/scm.ts
6113
6108
  var safeBody = (body, maxBodyLength) => {
6114
6109
  const truncationNotice = "\n\n... Message was cut here because it is too long";
@@ -6201,6 +6196,36 @@ function getCommitIssueUrl(params) {
6201
6196
  analysisId
6202
6197
  })}/commit?${searchParams.toString()}`;
6203
6198
  }
6199
+ function extractLinearTicketsFromBody(body, seen) {
6200
+ const tickets = [];
6201
+ const htmlPattern = /<a href="(https:\/\/linear\.app\/[^"]+)">([A-Z]+-\d+)<\/a>/g;
6202
+ let match;
6203
+ while ((match = htmlPattern.exec(body)) !== null) {
6204
+ const ticket = parseLinearTicket(match[1], match[2]);
6205
+ if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
6206
+ seen.add(`${ticket.name}|${ticket.url}`);
6207
+ tickets.push(ticket);
6208
+ }
6209
+ }
6210
+ const markdownPattern = /\[([A-Z]+-\d+)\]\((https:\/\/linear\.app\/[^)]+)\)/g;
6211
+ while ((match = markdownPattern.exec(body)) !== null) {
6212
+ const ticket = parseLinearTicket(match[2], match[1]);
6213
+ if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
6214
+ seen.add(`${ticket.name}|${ticket.url}`);
6215
+ tickets.push(ticket);
6216
+ }
6217
+ }
6218
+ return tickets;
6219
+ }
6220
+ function parseLinearTicket(url, name) {
6221
+ if (!name || !url) {
6222
+ return null;
6223
+ }
6224
+ const urlParts = url.split("/");
6225
+ const titleSlug = urlParts[urlParts.length - 1] || "";
6226
+ const title = titleSlug.replace(/-/g, " ");
6227
+ return { name, title, url };
6228
+ }
6204
6229
  var userNamePattern = /^(https?:\/\/)([^@]+@)?([^/]+\/.+)$/;
6205
6230
  var sshPattern = /^git@([\w.-]+):([\w./-]+)$/;
6206
6231
  function normalizeUrl(repoUrl) {
@@ -6894,9 +6919,6 @@ async function getAdoSdk(params) {
6894
6919
  return commitRes.value;
6895
6920
  }
6896
6921
  throw new RefNotFoundError(`ref: ${ref} does not exist`);
6897
- },
6898
- getAdoBlameRanges() {
6899
- return [];
6900
6922
  }
6901
6923
  };
6902
6924
  }
@@ -7016,7 +7038,6 @@ var SCMLib = class {
7016
7038
  * IMPORTANT: Sort order must remain consistent across paginated requests
7017
7039
  * for cursor-based pagination to work correctly.
7018
7040
  *
7019
- * Default implementation uses getSubmitRequests and applies filters/sorting in-memory.
7020
7041
  * Override in subclasses for provider-specific optimizations (e.g., GitHub Search API).
7021
7042
  *
7022
7043
  * @param params - Search parameters including filters, sort, and pagination
@@ -7098,6 +7119,14 @@ var SCMLib = class {
7098
7119
  static async getIsValidBranchName(branchName) {
7099
7120
  return isValidBranchName(branchName);
7100
7121
  }
7122
+ /**
7123
+ * Extract Linear ticket links from PR/MR comments.
7124
+ * Default implementation returns empty array - subclasses can override.
7125
+ * Public so it can be reused by backend services.
7126
+ */
7127
+ extractLinearTicketsFromComments(_comments) {
7128
+ return [];
7129
+ }
7101
7130
  _validateAccessTokenAndUrl() {
7102
7131
  this._validateAccessToken();
7103
7132
  this._validateUrl();
@@ -7250,10 +7279,6 @@ var AdoSCMLib = class extends SCMLib {
7250
7279
  throw new Error(`unknown state ${state}`);
7251
7280
  }
7252
7281
  }
7253
- async getRepoBlameRanges(_ref, _path) {
7254
- const adoSdk = await this.getAdoSdk();
7255
- return await adoSdk.getAdoBlameRanges();
7256
- }
7257
7282
  async getReferenceData(ref) {
7258
7283
  this._validateUrl();
7259
7284
  const adoSdk = await this.getAdoSdk();
@@ -7312,9 +7337,6 @@ var AdoSCMLib = class extends SCMLib {
7312
7337
  async getSubmitRequestDiff(_submitRequestId) {
7313
7338
  throw new Error("getSubmitRequestDiff not implemented for ADO");
7314
7339
  }
7315
- async getSubmitRequests(_repoUrl) {
7316
- throw new Error("getSubmitRequests not implemented for ADO");
7317
- }
7318
7340
  async searchSubmitRequests(_params) {
7319
7341
  throw new Error("searchSubmitRequests not implemented for ADO");
7320
7342
  }
@@ -7326,6 +7348,12 @@ var AdoSCMLib = class extends SCMLib {
7326
7348
  async getPullRequestMetrics(_prNumber) {
7327
7349
  throw new Error("getPullRequestMetrics not implemented for ADO");
7328
7350
  }
7351
+ async getRecentCommits(_since) {
7352
+ throw new Error("getRecentCommits not implemented for ADO");
7353
+ }
7354
+ async getRateLimitStatus() {
7355
+ return null;
7356
+ }
7329
7357
  };
7330
7358
 
7331
7359
  // src/features/analysis/scm/bitbucket/bitbucket.ts
@@ -7843,9 +7871,6 @@ var BitbucketSCMLib = class extends SCMLib {
7843
7871
  throw new Error(`unknown state ${pullRequestRes.state} `);
7844
7872
  }
7845
7873
  }
7846
- async getRepoBlameRanges(_ref, _path) {
7847
- return [];
7848
- }
7849
7874
  async getReferenceData(ref) {
7850
7875
  this._validateUrl();
7851
7876
  return this.bitbucketSdk.getReferenceData({ url: this.url, ref });
@@ -7894,9 +7919,6 @@ var BitbucketSCMLib = class extends SCMLib {
7894
7919
  async getSubmitRequestDiff(_submitRequestId) {
7895
7920
  throw new Error("getSubmitRequestDiff not implemented for Bitbucket");
7896
7921
  }
7897
- async getSubmitRequests(_repoUrl) {
7898
- throw new Error("getSubmitRequests not implemented for Bitbucket");
7899
- }
7900
7922
  async searchSubmitRequests(_params) {
7901
7923
  throw new Error("searchSubmitRequests not implemented for Bitbucket");
7902
7924
  }
@@ -7908,6 +7930,12 @@ var BitbucketSCMLib = class extends SCMLib {
7908
7930
  async getPullRequestMetrics(_prNumber) {
7909
7931
  throw new Error("getPullRequestMetrics not implemented for Bitbucket");
7910
7932
  }
7933
+ async getRecentCommits(_since) {
7934
+ throw new Error("getRecentCommits not implemented for Bitbucket");
7935
+ }
7936
+ async getRateLimitStatus() {
7937
+ return null;
7938
+ }
7911
7939
  };
7912
7940
 
7913
7941
  // src/features/analysis/scm/constants.ts
@@ -7920,7 +7948,7 @@ init_env();
7920
7948
 
7921
7949
  // src/features/analysis/scm/github/GithubSCMLib.ts
7922
7950
  init_env();
7923
- import pLimit2 from "p-limit";
7951
+ import pLimit from "p-limit";
7924
7952
  import { z as z21 } from "zod";
7925
7953
  init_client_generates();
7926
7954
 
@@ -7939,59 +7967,6 @@ function parseCursorSafe(cursor, defaultValue = 0, maxValue = MAX_CURSOR_VALUE)
7939
7967
 
7940
7968
  // src/features/analysis/scm/github/github.ts
7941
7969
  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
7970
 
7996
7971
  // src/features/analysis/scm/github/consts.ts
7997
7972
  var POST_COMMENT_PATH = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments";
@@ -8009,35 +7984,6 @@ var GET_A_REPOSITORY_PUBLIC_KEY = "GET /repos/{owner}/{repo}/actions/secrets/pub
8009
7984
  var GET_USER = "GET /user";
8010
7985
  var GET_USER_REPOS = "GET /user/repos";
8011
7986
  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
7987
  var GITHUB_GRAPHQL_FRAGMENTS = {
8042
7988
  /**
8043
7989
  * Fragment for fetching PR additions/deletions.
@@ -8061,30 +8007,6 @@ var GITHUB_GRAPHQL_FRAGMENTS = {
8061
8007
  body
8062
8008
  }
8063
8009
  }
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
8010
  `
8089
8011
  };
8090
8012
  var GET_PR_METRICS_QUERY = `
@@ -8296,112 +8218,6 @@ async function githubValidateParams(url, accessToken) {
8296
8218
 
8297
8219
  // src/features/analysis/scm/github/github.ts
8298
8220
  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
8221
  async function executeBatchGraphQL(octokit, owner, repo, config2) {
8406
8222
  const { items, aliasPrefix, buildFragment, extractResult } = config2;
8407
8223
  if (items.length === 0) {
@@ -8716,29 +8532,6 @@ function getGithubSdk(params = {}) {
8716
8532
  sha: res.data.sha
8717
8533
  };
8718
8534
  },
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
8535
  /**
8743
8536
  * Fetches commits for multiple PRs in a single GraphQL request.
8744
8537
  * This is much more efficient than making N separate REST API calls.
@@ -9022,232 +8815,6 @@ function getGithubSdk(params = {}) {
9022
8815
  }
9023
8816
  });
9024
8817
  },
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
8818
  async getPRMetricsGraphQL(params2) {
9252
8819
  const res = await octokit.graphql(
9253
8820
  GET_PR_METRICS_QUERY,
@@ -9452,10 +9019,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9452
9019
  async getRecentCommits(since) {
9453
9020
  this._validateAccessTokenAndUrl();
9454
9021
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9455
- return await this.githubSdk.getRecentCommits({ owner, repo, since });
9456
- }
9022
+ const result = await this.githubSdk.getRecentCommits({ owner, repo, since });
9023
+ return {
9024
+ data: result.data.map((c) => ({
9025
+ sha: c.sha,
9026
+ commit: {
9027
+ committer: c.commit.committer ? { date: c.commit.committer.date } : void 0,
9028
+ author: c.commit.author ? { email: c.commit.author.email, name: c.commit.author.name } : void 0,
9029
+ message: c.commit.message
9030
+ },
9031
+ parents: c.parents?.map((p) => ({ sha: p.sha }))
9032
+ }))
9033
+ };
9034
+ }
9457
9035
  async getRateLimitStatus() {
9458
- return await this.githubSdk.getRateLimitStatus();
9036
+ const result = await this.githubSdk.getRateLimitStatus();
9037
+ return {
9038
+ remaining: result.remaining,
9039
+ reset: result.reset,
9040
+ limit: result.limit
9041
+ };
9459
9042
  }
9460
9043
  get scmLibType() {
9461
9044
  return "GITHUB" /* GITHUB */;
@@ -9513,14 +9096,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9513
9096
  markdownComment: comment
9514
9097
  });
9515
9098
  }
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
9099
  async getReferenceData(ref) {
9525
9100
  this._validateUrl();
9526
9101
  return await this.githubSdk.getGithubReferenceData({
@@ -9610,37 +9185,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9610
9185
  commitSha
9611
9186
  });
9612
9187
  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
9188
  let repositoryCreatedAt = options?.repositoryCreatedAt;
9645
9189
  if (repositoryCreatedAt === void 0) {
9646
9190
  try {
@@ -9662,7 +9206,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9662
9206
  authorName: commit.commit.author?.name,
9663
9207
  authorEmail: commit.commit.author?.email,
9664
9208
  message: commit.commit.message,
9665
- parentCommits,
9666
9209
  repositoryCreatedAt
9667
9210
  };
9668
9211
  }
@@ -9670,46 +9213,31 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9670
9213
  this._validateAccessTokenAndUrl();
9671
9214
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9672
9215
  const prNumber = Number(submitRequestId);
9673
- const [prRes, commitsRes, filesRes, repoData] = await Promise.all([
9216
+ const [prRes, commitsRes, repoData, prDiff] = await Promise.all([
9674
9217
  this.githubSdk.getPr({ owner, repo, pull_number: prNumber }),
9675
9218
  this.githubSdk.getPrCommits({ owner, repo, pull_number: prNumber }),
9676
- this.githubSdk.listPRFiles({ owner, repo, pull_number: prNumber }),
9677
- this.githubSdk.getRepository({ owner, repo })
9219
+ this.githubSdk.getRepository({ owner, repo }),
9220
+ this.getPrDiff({ pull_number: prNumber })
9678
9221
  ]);
9679
9222
  const pr = prRes.data;
9680
9223
  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);
9224
+ const limit = pLimit(GITHUB_API_CONCURRENCY);
9698
9225
  const commits = await Promise.all(
9699
9226
  commitsRes.data.map(
9700
9227
  (commit) => limit(
9701
9228
  () => this.getCommitDiff(commit.sha, {
9702
- repositoryCreatedAt,
9703
- parentCommitTimestamps
9229
+ repositoryCreatedAt
9704
9230
  })
9705
9231
  )
9706
9232
  )
9707
9233
  );
9708
- const diffLines = filesRes ? await this._attributeLinesViaBlame({
9709
- headSha: pr.head.sha,
9710
- changedFiles: filesRes.data,
9711
- prCommits: commits
9712
- }) : [];
9234
+ const addedLinesByFile = parseAddedLinesByFile(prDiff);
9235
+ const diffLines = [];
9236
+ for (const [file, lines] of addedLinesByFile) {
9237
+ for (const line of lines) {
9238
+ diffLines.push({ file, line });
9239
+ }
9240
+ }
9713
9241
  return {
9714
9242
  diff: prDiff,
9715
9243
  createdAt: new Date(pr.created_at),
@@ -9726,47 +9254,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9726
9254
  diffLines
9727
9255
  };
9728
9256
  }
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
9257
  /**
9771
9258
  * Override searchSubmitRequests to use GitHub's Search API for efficient pagination.
9772
9259
  * This is much faster than fetching all PRs and filtering in-memory.
@@ -9985,146 +9472,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9985
9472
  };
9986
9473
  }
9987
9474
  /**
9988
- * Parse a Linear ticket from URL and name
9989
- * Returns null if invalid or missing data
9475
+ * Extract Linear ticket links from pre-fetched comments (pure function, no API calls)
9476
+ * Instance method that overrides base class - can also be called statically for backwards compatibility.
9990
9477
  */
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 };
9478
+ extractLinearTicketsFromComments(comments) {
9479
+ return _GithubSCMLib._extractLinearTicketsFromCommentsImpl(comments);
9999
9480
  }
10000
9481
  /**
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.
9482
+ * Static implementation for backwards compatibility and reuse.
9483
+ * Called by both the instance method and direct static calls.
10003
9484
  */
10004
- static extractLinearTicketsFromComments(comments) {
9485
+ static _extractLinearTicketsFromCommentsImpl(comments) {
10005
9486
  const tickets = [];
10006
9487
  const seen = /* @__PURE__ */ new Set();
10007
9488
  for (const comment of comments) {
10008
9489
  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
- }
9490
+ tickets.push(...extractLinearTicketsFromBody(comment.body || "", seen));
10027
9491
  }
10028
9492
  }
10029
9493
  return tickets;
10030
9494
  }
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
9495
  };
10129
9496
 
10130
9497
  // src/features/analysis/scm/gitlab/gitlab.ts
@@ -10137,11 +9504,72 @@ import {
10137
9504
  Gitlab
10138
9505
  } from "@gitbeaker/rest";
10139
9506
  import Debug3 from "debug";
9507
+ import pLimit2 from "p-limit";
10140
9508
  import {
10141
9509
  Agent,
10142
9510
  fetch as undiciFetch,
10143
9511
  ProxyAgent as ProxyAgent2
10144
9512
  } from "undici";
9513
+
9514
+ // src/utils/contextLogger.ts
9515
+ import debugModule from "debug";
9516
+ var debug3 = debugModule("mobb:shared");
9517
+ var _contextLogger = null;
9518
+ var createContextLogger = async () => {
9519
+ if (_contextLogger) return _contextLogger;
9520
+ try {
9521
+ let logger2;
9522
+ try {
9523
+ let module;
9524
+ try {
9525
+ const buildPath = "../../../../../tscommon/backend/build/src/utils/logger";
9526
+ module = await import(buildPath);
9527
+ } catch (e) {
9528
+ const sourcePath = "../../../../../tscommon/backend/src/utils/logger";
9529
+ module = await import(sourcePath);
9530
+ }
9531
+ logger2 = module.logger;
9532
+ } catch {
9533
+ }
9534
+ if (logger2) {
9535
+ _contextLogger = {
9536
+ info: (message, data) => data ? logger2.info(data, message) : logger2.info(message),
9537
+ warn: (message, data) => data ? logger2.warn(data, message) : logger2.warn(message),
9538
+ debug: (message, data) => data ? logger2.debug(data, message) : logger2.debug(message),
9539
+ error: (message, data) => data ? logger2.error(data, message) : logger2.error(message)
9540
+ };
9541
+ return _contextLogger;
9542
+ }
9543
+ } catch {
9544
+ }
9545
+ _contextLogger = {
9546
+ info: (message, data) => debug3(message, data),
9547
+ warn: (message, data) => debug3(message, data),
9548
+ debug: (message, data) => debug3(message, data),
9549
+ error: (message, data) => debug3(message, data)
9550
+ };
9551
+ return _contextLogger;
9552
+ };
9553
+ var contextLogger = {
9554
+ info: async (message, data) => {
9555
+ const logger2 = await createContextLogger();
9556
+ return logger2.info(message, data);
9557
+ },
9558
+ debug: async (message, data) => {
9559
+ const logger2 = await createContextLogger();
9560
+ return logger2.debug(message, data);
9561
+ },
9562
+ warn: async (message, data) => {
9563
+ const logger2 = await createContextLogger();
9564
+ return logger2.warn(message, data);
9565
+ },
9566
+ error: async (message, data) => {
9567
+ const logger2 = await createContextLogger();
9568
+ return logger2.error(message, data);
9569
+ }
9570
+ };
9571
+
9572
+ // src/features/analysis/scm/gitlab/gitlab.ts
10145
9573
  init_env();
10146
9574
 
10147
9575
  // src/features/analysis/scm/gitlab/types.ts
@@ -10158,6 +9586,14 @@ function removeTrailingSlash2(str) {
10158
9586
  return str.trim().replace(/\/+$/, "");
10159
9587
  }
10160
9588
  var MAX_GITLAB_PR_BODY_LENGTH = 1048576;
9589
+ function buildUnifiedDiff(diffs) {
9590
+ return diffs.filter((d) => d.diff).map((d) => {
9591
+ const oldPath = d.old_path || d.new_path || "";
9592
+ const newPath = d.new_path || d.old_path || "";
9593
+ return `diff --git a/${oldPath} b/${newPath}
9594
+ ${d.diff}`;
9595
+ }).join("\n");
9596
+ }
10161
9597
  function getRandomGitlabCloudAnonToken() {
10162
9598
  if (!GITLAB_API_TOKEN || typeof GITLAB_API_TOKEN !== "string") {
10163
9599
  return void 0;
@@ -10211,7 +9647,10 @@ async function gitlabValidateParams({
10211
9647
  if (code === 404 || description.includes("404") || description.includes("Not Found")) {
10212
9648
  throw new InvalidRepoUrlError(`invalid gitlab repo URL: ${url}`);
10213
9649
  }
10214
- console.log("gitlabValidateParams error", e);
9650
+ contextLogger.warn("[gitlabValidateParams] Error validating params", {
9651
+ url,
9652
+ error: e
9653
+ });
10215
9654
  throw new InvalidRepoUrlError(
10216
9655
  `cannot access gitlab repo URL: ${url} with the provided access token`
10217
9656
  );
@@ -10240,6 +9679,13 @@ async function getGitlabIsUserCollaborator({
10240
9679
  ];
10241
9680
  return accessLevelWithWriteAccess.includes(groupAccess) || accessLevelWithWriteAccess.includes(projectAccess);
10242
9681
  } catch (e) {
9682
+ contextLogger.warn(
9683
+ "[getGitlabIsUserCollaborator] Error checking collaborator status",
9684
+ {
9685
+ error: e instanceof Error ? e.message : String(e),
9686
+ repoUrl
9687
+ }
9688
+ );
10243
9689
  return false;
10244
9690
  }
10245
9691
  }
@@ -10286,6 +9732,14 @@ async function getGitlabIsRemoteBranch({
10286
9732
  const res = await api2.Branches.show(projectPath, branch);
10287
9733
  return res.name === branch;
10288
9734
  } catch (e) {
9735
+ contextLogger.warn(
9736
+ "[getGitlabIsRemoteBranch] Error checking remote branch",
9737
+ {
9738
+ error: e instanceof Error ? e.message : String(e),
9739
+ repoUrl,
9740
+ branch
9741
+ }
9742
+ );
10289
9743
  return false;
10290
9744
  }
10291
9745
  }
@@ -10318,49 +9772,257 @@ async function getGitlabRepoList(url, accessToken) {
10318
9772
  })
10319
9773
  );
10320
9774
  }
10321
- async function getGitlabBranchList({
10322
- accessToken,
10323
- repoUrl
9775
+ async function getGitlabBranchList({
9776
+ accessToken,
9777
+ repoUrl
9778
+ }) {
9779
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9780
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9781
+ try {
9782
+ const res = await api2.Branches.all(projectPath, {
9783
+ //keyset API pagination is not supported by GL for the branch list (at least not the on-prem version)
9784
+ //so for now we stick with the default pagination and just return the first page and limit the results to 1000 entries.
9785
+ //This is a temporary solution until we implement list branches with name search.
9786
+ perPage: MAX_BRANCHES_FETCH,
9787
+ page: 1
9788
+ });
9789
+ res.sort((a, b) => {
9790
+ if (!a.commit?.committed_date || !b.commit?.committed_date) {
9791
+ return 0;
9792
+ }
9793
+ return new Date(b.commit?.committed_date).getTime() - new Date(a.commit?.committed_date).getTime();
9794
+ });
9795
+ return res.map((branch) => branch.name).slice(0, MAX_BRANCHES_FETCH);
9796
+ } catch (e) {
9797
+ contextLogger.warn("[getGitlabBranchList] Error fetching branch list", {
9798
+ error: e instanceof Error ? e.message : String(e),
9799
+ repoUrl
9800
+ });
9801
+ return [];
9802
+ }
9803
+ }
9804
+ async function createMergeRequest(options) {
9805
+ const { projectPath } = parseGitlabOwnerAndRepo(options.repoUrl);
9806
+ const api2 = getGitBeaker({
9807
+ url: options.repoUrl,
9808
+ gitlabAuthToken: options.accessToken
9809
+ });
9810
+ const res = await api2.MergeRequests.create(
9811
+ projectPath,
9812
+ options.sourceBranchName,
9813
+ options.targetBranchName,
9814
+ options.title,
9815
+ {
9816
+ description: safeBody(options.body, MAX_GITLAB_PR_BODY_LENGTH)
9817
+ }
9818
+ );
9819
+ return res.iid;
9820
+ }
9821
+ async function getGitlabMergeRequest({
9822
+ url,
9823
+ prNumber,
9824
+ accessToken
9825
+ }) {
9826
+ const { projectPath } = parseGitlabOwnerAndRepo(url);
9827
+ const api2 = getGitBeaker({
9828
+ url,
9829
+ gitlabAuthToken: accessToken
9830
+ });
9831
+ return await api2.MergeRequests.show(projectPath, prNumber);
9832
+ }
9833
+ async function searchGitlabMergeRequests({
9834
+ repoUrl,
9835
+ accessToken,
9836
+ state,
9837
+ updatedAfter,
9838
+ orderBy = "updated_at",
9839
+ sort = "desc",
9840
+ perPage = GITLAB_PER_PAGE,
9841
+ page = 1
9842
+ }) {
9843
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9844
+ debug4(
9845
+ "[searchGitlabMergeRequests] Fetching MRs for %s (page=%d, perPage=%d)",
9846
+ projectPath,
9847
+ page,
9848
+ perPage
9849
+ );
9850
+ const api2 = getGitBeaker({
9851
+ url: repoUrl,
9852
+ gitlabAuthToken: accessToken
9853
+ });
9854
+ const mergeRequests = await api2.MergeRequests.all({
9855
+ projectId: projectPath,
9856
+ state: state === "all" ? void 0 : state,
9857
+ updatedAfter: updatedAfter?.toISOString(),
9858
+ orderBy,
9859
+ sort,
9860
+ perPage,
9861
+ page
9862
+ });
9863
+ const items = mergeRequests.map((mr) => ({
9864
+ iid: mr.iid,
9865
+ title: mr.title,
9866
+ state: mr.state,
9867
+ sourceBranch: mr.source_branch,
9868
+ targetBranch: mr.target_branch,
9869
+ authorUsername: mr.author?.username,
9870
+ createdAt: mr.created_at,
9871
+ updatedAt: mr.updated_at,
9872
+ description: mr.description
9873
+ }));
9874
+ debug4(
9875
+ "[searchGitlabMergeRequests] Found %d MRs on page %d",
9876
+ items.length,
9877
+ page
9878
+ );
9879
+ return {
9880
+ items,
9881
+ hasMore: mergeRequests.length === perPage
9882
+ };
9883
+ }
9884
+ var GITLAB_API_CONCURRENCY = 5;
9885
+ async function getGitlabMrCommitsBatch({
9886
+ repoUrl,
9887
+ accessToken,
9888
+ mrNumbers
9889
+ }) {
9890
+ if (mrNumbers.length === 0) {
9891
+ return /* @__PURE__ */ new Map();
9892
+ }
9893
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9894
+ const api2 = getGitBeaker({
9895
+ url: repoUrl,
9896
+ gitlabAuthToken: accessToken
9897
+ });
9898
+ const limit = pLimit2(GITLAB_API_CONCURRENCY);
9899
+ const results = await Promise.all(
9900
+ mrNumbers.map(
9901
+ (mrNumber) => limit(async () => {
9902
+ try {
9903
+ const commits = await api2.MergeRequests.allCommits(
9904
+ projectPath,
9905
+ mrNumber
9906
+ );
9907
+ return [mrNumber, commits.map((c) => c.id)];
9908
+ } catch (error) {
9909
+ contextLogger.warn(
9910
+ "[getGitlabMrCommitsBatch] Failed to fetch commits for MR",
9911
+ {
9912
+ error: error instanceof Error ? error.message : String(error),
9913
+ mrNumber,
9914
+ repoUrl
9915
+ }
9916
+ );
9917
+ return [mrNumber, []];
9918
+ }
9919
+ })
9920
+ )
9921
+ );
9922
+ return new Map(results);
9923
+ }
9924
+ async function getGitlabMrDataBatch({
9925
+ repoUrl,
9926
+ accessToken,
9927
+ mrNumbers
9928
+ }) {
9929
+ if (mrNumbers.length === 0) {
9930
+ return /* @__PURE__ */ new Map();
9931
+ }
9932
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9933
+ const api2 = getGitBeaker({
9934
+ url: repoUrl,
9935
+ gitlabAuthToken: accessToken
9936
+ });
9937
+ const limit = pLimit2(GITLAB_API_CONCURRENCY);
9938
+ const results = await Promise.all(
9939
+ mrNumbers.map(
9940
+ (mrNumber) => limit(async () => {
9941
+ try {
9942
+ const [diffs, notes] = await Promise.all([
9943
+ api2.MergeRequests.allDiffs(projectPath, mrNumber),
9944
+ api2.MergeRequestNotes.all(projectPath, mrNumber)
9945
+ ]);
9946
+ let additions = 0;
9947
+ let deletions = 0;
9948
+ for (const diff of diffs) {
9949
+ if (diff.diff) {
9950
+ for (const line of diff.diff.split("\n")) {
9951
+ if (line.startsWith("+") && !line.startsWith("+++")) {
9952
+ additions++;
9953
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
9954
+ deletions++;
9955
+ }
9956
+ }
9957
+ }
9958
+ }
9959
+ const comments = notes.map((note) => ({
9960
+ author: note.author ? {
9961
+ login: note.author.username,
9962
+ type: note.author.username.endsWith("[bot]") || note.author.username.toLowerCase() === "linear" ? "Bot" : "User"
9963
+ } : null,
9964
+ body: note.body
9965
+ }));
9966
+ return [
9967
+ mrNumber,
9968
+ { changedLines: { additions, deletions }, comments }
9969
+ ];
9970
+ } catch (error) {
9971
+ contextLogger.warn(
9972
+ "[getGitlabMrDataBatch] Failed to fetch data for MR",
9973
+ {
9974
+ error: error instanceof Error ? error.message : String(error),
9975
+ mrNumber,
9976
+ repoUrl
9977
+ }
9978
+ );
9979
+ return [
9980
+ mrNumber,
9981
+ {
9982
+ changedLines: { additions: 0, deletions: 0 },
9983
+ comments: []
9984
+ }
9985
+ ];
9986
+ }
9987
+ })
9988
+ )
9989
+ );
9990
+ return new Map(results);
9991
+ }
9992
+ async function getGitlabMergeRequestLinesAdded({
9993
+ url,
9994
+ prNumber,
9995
+ accessToken
10324
9996
  }) {
10325
- const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10326
- const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10327
9997
  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
9998
+ const { projectPath } = parseGitlabOwnerAndRepo(url);
9999
+ const api2 = getGitBeaker({
10000
+ url,
10001
+ gitlabAuthToken: accessToken
10334
10002
  });
10335
- res.sort((a, b) => {
10336
- if (!a.commit?.committed_date || !b.commit?.committed_date) {
10337
- return 0;
10003
+ const diffs = await api2.MergeRequests.allDiffs(projectPath, prNumber);
10004
+ let linesAdded = 0;
10005
+ for (const diff of diffs) {
10006
+ if (diff.diff) {
10007
+ const addedLines = diff.diff.split("\n").filter(
10008
+ (line) => line.startsWith("+") && !line.startsWith("+++")
10009
+ ).length;
10010
+ linesAdded += addedLines;
10338
10011
  }
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
10012
  }
10360
- );
10361
- return res.iid;
10013
+ return linesAdded;
10014
+ } catch (error) {
10015
+ contextLogger.warn(
10016
+ "[getGitlabMergeRequestLinesAdded] Failed to fetch diffs for MR",
10017
+ {
10018
+ prNumber,
10019
+ error
10020
+ }
10021
+ );
10022
+ return 0;
10023
+ }
10362
10024
  }
10363
- async function getGitlabMergeRequest({
10025
+ async function getGitlabMergeRequestMetrics({
10364
10026
  url,
10365
10027
  prNumber,
10366
10028
  accessToken
@@ -10370,7 +10032,28 @@ async function getGitlabMergeRequest({
10370
10032
  url,
10371
10033
  gitlabAuthToken: accessToken
10372
10034
  });
10373
- return await api2.MergeRequests.show(projectPath, prNumber);
10035
+ const [mr, commits, linesAdded, notes] = await Promise.all([
10036
+ api2.MergeRequests.show(projectPath, prNumber),
10037
+ api2.MergeRequests.allCommits(projectPath, prNumber),
10038
+ getGitlabMergeRequestLinesAdded({ url, prNumber, accessToken }),
10039
+ api2.MergeRequestNotes.all(projectPath, prNumber)
10040
+ ]);
10041
+ const sortedCommits = [...commits].sort(
10042
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
10043
+ );
10044
+ const firstCommitDate = sortedCommits[0]?.created_at ?? null;
10045
+ const commentIds = notes.filter((note) => !note.system).map((note) => String(note.id));
10046
+ return {
10047
+ state: mr.state,
10048
+ isDraft: mr.draft ?? false,
10049
+ createdAt: mr.created_at,
10050
+ mergedAt: mr.merged_at ?? null,
10051
+ linesAdded,
10052
+ commitsCount: commits.length,
10053
+ commitShas: commits.map((c) => c.id),
10054
+ firstCommitDate,
10055
+ commentIds
10056
+ };
10374
10057
  }
10375
10058
  async function getGitlabCommitUrl({
10376
10059
  url,
@@ -10449,26 +10132,125 @@ function parseGitlabOwnerAndRepo(gitlabUrl) {
10449
10132
  const { organization, repoName, projectPath } = parsingResult;
10450
10133
  return { owner: organization, repo: repoName, projectPath };
10451
10134
  }
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;
10135
+ var GITLAB_MAX_RESULTS_LIMIT = 1024;
10136
+ var GITLAB_PER_PAGE = 128;
10137
+ async function getGitlabRecentCommits({
10138
+ repoUrl,
10139
+ accessToken,
10140
+ since
10141
+ }) {
10142
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10143
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10144
+ const allCommits = [];
10145
+ let page = 1;
10146
+ let hasMore = true;
10147
+ while (hasMore && allCommits.length < GITLAB_MAX_RESULTS_LIMIT) {
10148
+ const commits = await api2.Commits.all(projectPath, {
10149
+ since,
10150
+ perPage: GITLAB_PER_PAGE,
10151
+ page
10152
+ });
10153
+ if (commits.length === 0) {
10154
+ hasMore = false;
10155
+ break;
10156
+ }
10157
+ for (const commit of commits) {
10158
+ if (allCommits.length >= GITLAB_MAX_RESULTS_LIMIT) {
10159
+ break;
10160
+ }
10161
+ allCommits.push({
10162
+ sha: commit.id,
10163
+ commit: {
10164
+ committer: commit.committed_date ? { date: commit.committed_date } : void 0,
10165
+ author: {
10166
+ email: commit.author_email,
10167
+ name: commit.author_name
10168
+ },
10169
+ message: commit.message
10170
+ },
10171
+ parents: commit.parent_ids?.map((sha) => ({ sha })) || []
10172
+ });
10173
+ }
10174
+ if (commits.length < GITLAB_PER_PAGE) {
10175
+ hasMore = false;
10176
+ } else {
10177
+ page++;
10178
+ }
10179
+ }
10180
+ if (allCommits.length >= GITLAB_MAX_RESULTS_LIMIT) {
10181
+ contextLogger.warn("[getGitlabRecentCommits] Hit commit pagination limit", {
10182
+ limit: GITLAB_MAX_RESULTS_LIMIT,
10183
+ count: allCommits.length,
10184
+ repoUrl,
10185
+ since
10186
+ });
10187
+ }
10188
+ return allCommits;
10189
+ }
10190
+ async function getGitlabCommitDiff({
10191
+ repoUrl,
10192
+ accessToken,
10193
+ commitSha
10194
+ }) {
10195
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10196
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10197
+ const [commit, diffs] = await Promise.all([
10198
+ api2.Commits.show(projectPath, commitSha),
10199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10200
+ api2.Commits.showDiff(projectPath, commitSha, { unidiff: true })
10201
+ ]);
10202
+ const diffString = buildUnifiedDiff(diffs);
10203
+ const commitTimestamp = commit.committed_date ? new Date(commit.committed_date) : /* @__PURE__ */ new Date();
10204
+ return {
10205
+ diff: diffString,
10206
+ commitTimestamp,
10207
+ commitSha: commit.id,
10208
+ authorName: commit.author_name,
10209
+ authorEmail: commit.author_email,
10210
+ message: commit.message
10211
+ };
10212
+ }
10213
+ async function getGitlabRateLimitStatus({
10214
+ repoUrl,
10215
+ accessToken
10216
+ }) {
10217
+ try {
10218
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10219
+ const response = await api2.Users.showCurrentUser({ showExpanded: true });
10220
+ const headers = response.headers;
10221
+ if (!headers) {
10222
+ return null;
10223
+ }
10224
+ const remaining = headers["ratelimit-remaining"];
10225
+ const reset = headers["ratelimit-reset"];
10226
+ const limit = headers["ratelimit-limit"];
10227
+ if (!remaining || !reset) {
10228
+ return null;
10229
+ }
10230
+ const remainingNum = parseInt(remaining, 10);
10231
+ const resetNum = parseInt(reset, 10);
10232
+ if (isNaN(remainingNum) || isNaN(resetNum)) {
10233
+ contextLogger.warn(
10234
+ "[getGitlabRateLimitStatus] Malformed rate limit headers",
10235
+ { remaining, reset, repoUrl }
10236
+ );
10237
+ return null;
10238
+ }
10466
10239
  return {
10467
- startingLine: oldLineNumber,
10468
- endingLine: lineNumber - 1,
10469
- commitSha: range.commit.id
10240
+ remaining: remainingNum,
10241
+ reset: new Date(resetNum * 1e3),
10242
+ limit: limit ? parseInt(limit, 10) : void 0
10470
10243
  };
10471
- });
10244
+ } catch (error) {
10245
+ contextLogger.warn(
10246
+ "[getGitlabRateLimitStatus] Error fetching rate limit status",
10247
+ {
10248
+ error: error instanceof Error ? error.message : String(error),
10249
+ repoUrl
10250
+ }
10251
+ );
10252
+ return null;
10253
+ }
10472
10254
  }
10473
10255
  async function processBody(response) {
10474
10256
  const headers = response.headers;
@@ -10509,8 +10291,90 @@ async function brokerRequestHandler(endpoint, options) {
10509
10291
  };
10510
10292
  throw new Error(`gitbeaker: ${response.statusText}`);
10511
10293
  }
10294
+ async function getGitlabMergeRequestDiff({
10295
+ repoUrl,
10296
+ accessToken,
10297
+ mrNumber
10298
+ }) {
10299
+ debug4("[getGitlabMergeRequestDiff] Starting for MR #%d", mrNumber);
10300
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10301
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10302
+ debug4(
10303
+ "[getGitlabMergeRequestDiff] Fetching MR details, diffs, and commits..."
10304
+ );
10305
+ const startMrFetch = Date.now();
10306
+ const [mr, mrDiffs, mrCommitsRaw] = await Promise.all([
10307
+ api2.MergeRequests.show(projectPath, mrNumber),
10308
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10309
+ api2.MergeRequests.allDiffs(projectPath, mrNumber, { unidiff: true }),
10310
+ api2.MergeRequests.allCommits(projectPath, mrNumber)
10311
+ ]);
10312
+ debug4(
10313
+ "[getGitlabMergeRequestDiff] MR fetch took %dms. Diffs: %d, Commits: %d",
10314
+ Date.now() - startMrFetch,
10315
+ mrDiffs.length,
10316
+ mrCommitsRaw.length
10317
+ );
10318
+ const diffString = buildUnifiedDiff(mrDiffs);
10319
+ debug4(
10320
+ "[getGitlabMergeRequestDiff] Fetching commit diffs for %d commits...",
10321
+ mrCommitsRaw.length
10322
+ );
10323
+ const startCommitFetch = Date.now();
10324
+ const commitDiffLimit = pLimit2(GITLAB_API_CONCURRENCY);
10325
+ const commits = await Promise.all(
10326
+ mrCommitsRaw.map(
10327
+ (commit) => commitDiffLimit(async () => {
10328
+ const commitDiff = await getGitlabCommitDiff({
10329
+ repoUrl,
10330
+ accessToken,
10331
+ commitSha: commit.id
10332
+ });
10333
+ return {
10334
+ diff: commitDiff.diff,
10335
+ commitTimestamp: commitDiff.commitTimestamp,
10336
+ commitSha: commitDiff.commitSha,
10337
+ authorName: commitDiff.authorName,
10338
+ authorEmail: commitDiff.authorEmail,
10339
+ message: commitDiff.message
10340
+ };
10341
+ })
10342
+ )
10343
+ );
10344
+ commits.sort(
10345
+ (a, b) => new Date(a.commitTimestamp).getTime() - new Date(b.commitTimestamp).getTime()
10346
+ );
10347
+ debug4(
10348
+ "[getGitlabMergeRequestDiff] Commit diffs fetch took %dms",
10349
+ Date.now() - startCommitFetch
10350
+ );
10351
+ const addedLinesByFile = parseAddedLinesByFile(diffString);
10352
+ const diffLines = [];
10353
+ for (const [file, lines] of addedLinesByFile) {
10354
+ for (const line of lines) {
10355
+ diffLines.push({ file, line });
10356
+ }
10357
+ }
10358
+ return {
10359
+ diff: diffString,
10360
+ createdAt: new Date(mr.created_at),
10361
+ updatedAt: new Date(mr.updated_at),
10362
+ submitRequestId: String(mrNumber),
10363
+ submitRequestNumber: mrNumber,
10364
+ sourceBranch: mr.source_branch,
10365
+ targetBranch: mr.target_branch,
10366
+ authorName: mr.author?.name || mr.author?.username,
10367
+ authorEmail: void 0,
10368
+ // GitLab MR API doesn't expose author email directly
10369
+ title: mr.title,
10370
+ description: mr.description || void 0,
10371
+ commits,
10372
+ diffLines
10373
+ };
10374
+ }
10512
10375
 
10513
10376
  // src/features/analysis/scm/gitlab/GitlabSCMLib.ts
10377
+ init_client_generates();
10514
10378
  var GitlabSCMLib = class extends SCMLib {
10515
10379
  constructor(url, accessToken, scmOrg) {
10516
10380
  super(url, accessToken, scmOrg);
@@ -10537,7 +10401,7 @@ var GitlabSCMLib = class extends SCMLib {
10537
10401
  }
10538
10402
  async getRepoList(_scmOrg) {
10539
10403
  if (!this.accessToken) {
10540
- console.error("no access token");
10404
+ contextLogger.warn("[GitlabSCMLib.getRepoList] No access token provided");
10541
10405
  throw new Error("no access token");
10542
10406
  }
10543
10407
  return getGitlabRepoList(this.url, this.accessToken);
@@ -10629,16 +10493,6 @@ var GitlabSCMLib = class extends SCMLib {
10629
10493
  markdownComment: comment
10630
10494
  });
10631
10495
  }
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
10496
  async getReferenceData(ref) {
10643
10497
  this._validateUrl();
10644
10498
  return await getGitlabReferenceData(
@@ -10682,25 +10536,180 @@ var GitlabSCMLib = class extends SCMLib {
10682
10536
  this._validateAccessTokenAndUrl();
10683
10537
  return `${this.url}/-/commits/${branchName}`;
10684
10538
  }
10685
- async getCommitDiff(_commitSha) {
10686
- throw new Error("getCommitDiff not implemented for GitLab");
10539
+ async getCommitDiff(commitSha) {
10540
+ this._validateAccessTokenAndUrl();
10541
+ const result = await getGitlabCommitDiff({
10542
+ repoUrl: this.url,
10543
+ accessToken: this.accessToken,
10544
+ commitSha
10545
+ });
10546
+ return {
10547
+ diff: result.diff,
10548
+ commitTimestamp: result.commitTimestamp,
10549
+ commitSha: result.commitSha,
10550
+ authorName: result.authorName,
10551
+ authorEmail: result.authorEmail,
10552
+ message: result.message
10553
+ };
10687
10554
  }
10688
- async getSubmitRequestDiff(_submitRequestId) {
10689
- throw new Error("getSubmitRequestDiff not implemented for GitLab");
10555
+ async getSubmitRequestDiff(submitRequestId) {
10556
+ this._validateAccessTokenAndUrl();
10557
+ const mrNumber = parseInt(submitRequestId, 10);
10558
+ if (isNaN(mrNumber) || mrNumber <= 0) {
10559
+ throw new Error(`Invalid merge request ID: ${submitRequestId}`);
10560
+ }
10561
+ return getGitlabMergeRequestDiff({
10562
+ repoUrl: this.url,
10563
+ accessToken: this.accessToken,
10564
+ mrNumber
10565
+ });
10566
+ }
10567
+ async searchSubmitRequests(params) {
10568
+ this._validateAccessTokenAndUrl();
10569
+ const page = parseCursorSafe(params.cursor, 1);
10570
+ const perPage = params.limit || 10;
10571
+ const sort = params.sort || { field: "updated", order: "desc" };
10572
+ const orderBy = sort.field === "created" ? "created_at" : "updated_at";
10573
+ let gitlabState;
10574
+ if (params.filters?.state === "open") {
10575
+ gitlabState = "opened";
10576
+ } else if (params.filters?.state === "closed") {
10577
+ gitlabState = "closed";
10578
+ } else {
10579
+ gitlabState = "all";
10580
+ }
10581
+ const searchResult = await searchGitlabMergeRequests({
10582
+ repoUrl: this.url,
10583
+ accessToken: this.accessToken,
10584
+ state: gitlabState,
10585
+ updatedAfter: params.filters?.updatedAfter,
10586
+ orderBy,
10587
+ sort: sort.order,
10588
+ perPage,
10589
+ page
10590
+ });
10591
+ const results = searchResult.items.map((mr) => {
10592
+ let status = "open";
10593
+ if (mr.state === "merged") {
10594
+ status = "merged";
10595
+ } else if (mr.state === "closed") {
10596
+ status = "closed";
10597
+ }
10598
+ return {
10599
+ submitRequestId: String(mr.iid),
10600
+ submitRequestNumber: mr.iid,
10601
+ title: mr.title,
10602
+ status,
10603
+ sourceBranch: mr.sourceBranch,
10604
+ targetBranch: mr.targetBranch,
10605
+ authorName: mr.authorUsername,
10606
+ authorEmail: void 0,
10607
+ createdAt: new Date(mr.createdAt),
10608
+ updatedAt: new Date(mr.updatedAt),
10609
+ description: mr.description || void 0,
10610
+ tickets: [],
10611
+ changedLines: { added: 0, removed: 0 }
10612
+ };
10613
+ });
10614
+ const MAX_TOTAL_RESULTS = 1024;
10615
+ const totalFetchedSoFar = page * perPage;
10616
+ const reachedLimit = totalFetchedSoFar >= MAX_TOTAL_RESULTS;
10617
+ if (reachedLimit && searchResult.hasMore) {
10618
+ contextLogger.warn(
10619
+ "[searchSubmitRequests] Hit limit of merge requests for GitLab repo",
10620
+ {
10621
+ limit: MAX_TOTAL_RESULTS
10622
+ }
10623
+ );
10624
+ }
10625
+ return {
10626
+ results,
10627
+ nextCursor: searchResult.hasMore && !reachedLimit ? String(page + 1) : void 0,
10628
+ hasMore: searchResult.hasMore && !reachedLimit
10629
+ };
10690
10630
  }
10691
- async getSubmitRequests(_repoUrl) {
10692
- throw new Error("getSubmitRequests not implemented for GitLab");
10631
+ async getPrCommitsBatch(_repoUrl, prNumbers) {
10632
+ this._validateAccessTokenAndUrl();
10633
+ return getGitlabMrCommitsBatch({
10634
+ repoUrl: this.url,
10635
+ accessToken: this.accessToken,
10636
+ mrNumbers: prNumbers
10637
+ });
10693
10638
  }
10694
- async searchSubmitRequests(_params) {
10695
- throw new Error("searchSubmitRequests not implemented for GitLab");
10639
+ async getPrDataBatch(_repoUrl, prNumbers) {
10640
+ this._validateAccessTokenAndUrl();
10641
+ return getGitlabMrDataBatch({
10642
+ repoUrl: this.url,
10643
+ accessToken: this.accessToken,
10644
+ mrNumbers: prNumbers
10645
+ });
10696
10646
  }
10697
10647
  async searchRepos(_params) {
10698
10648
  throw new Error("searchRepos not implemented for GitLab");
10699
10649
  }
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");
10650
+ async getPullRequestMetrics(prNumber) {
10651
+ this._validateAccessTokenAndUrl();
10652
+ const metrics = await getGitlabMergeRequestMetrics({
10653
+ url: this.url,
10654
+ prNumber,
10655
+ accessToken: this.accessToken
10656
+ });
10657
+ let prStatus;
10658
+ switch (metrics.state) {
10659
+ case "merged":
10660
+ prStatus = "MERGED" /* Merged */;
10661
+ break;
10662
+ case "closed":
10663
+ prStatus = "CLOSED" /* Closed */;
10664
+ break;
10665
+ default:
10666
+ prStatus = metrics.isDraft ? "DRAFT" /* Draft */ : "ACTIVE" /* Active */;
10667
+ }
10668
+ return {
10669
+ prId: String(prNumber),
10670
+ repositoryUrl: this.url,
10671
+ prCreatedAt: new Date(metrics.createdAt),
10672
+ prMergedAt: metrics.mergedAt ? new Date(metrics.mergedAt) : null,
10673
+ firstCommitDate: metrics.firstCommitDate ? new Date(metrics.firstCommitDate) : null,
10674
+ linesAdded: metrics.linesAdded,
10675
+ commitsCount: metrics.commitsCount,
10676
+ commitShas: metrics.commitShas,
10677
+ prStatus,
10678
+ commentIds: metrics.commentIds
10679
+ };
10680
+ }
10681
+ async getRecentCommits(since) {
10682
+ this._validateAccessTokenAndUrl();
10683
+ const commits = await getGitlabRecentCommits({
10684
+ repoUrl: this.url,
10685
+ accessToken: this.accessToken,
10686
+ since
10687
+ });
10688
+ return { data: commits };
10689
+ }
10690
+ async getRateLimitStatus() {
10691
+ this._validateAccessTokenAndUrl();
10692
+ return getGitlabRateLimitStatus({
10693
+ repoUrl: this.url,
10694
+ accessToken: this.accessToken
10695
+ });
10696
+ }
10697
+ /**
10698
+ * Extract Linear ticket links from pre-fetched comments (pure function, no API calls).
10699
+ * Linear bot uses the same comment format on GitLab as on GitHub.
10700
+ * Bot username may be 'linear' or 'linear[bot]' on GitLab.
10701
+ */
10702
+ extractLinearTicketsFromComments(comments) {
10703
+ const tickets = [];
10704
+ const seen = /* @__PURE__ */ new Set();
10705
+ for (const comment of comments) {
10706
+ const authorLogin = comment.author?.login?.toLowerCase() || "";
10707
+ const isLinearBot = authorLogin === "linear" || authorLogin === "linear[bot]" || comment.author?.type === "Bot" && authorLogin.includes("linear");
10708
+ if (isLinearBot) {
10709
+ tickets.push(...extractLinearTicketsFromBody(comment.body || "", seen));
10710
+ }
10711
+ }
10712
+ return tickets;
10704
10713
  }
10705
10714
  };
10706
10715
 
@@ -10759,10 +10768,6 @@ var StubSCMLib = class extends SCMLib {
10759
10768
  console.warn("getUserHasAccessToRepo() returning false");
10760
10769
  return false;
10761
10770
  }
10762
- async getRepoBlameRanges(_ref, _path) {
10763
- console.warn("getRepoBlameRanges() returning empty array");
10764
- return [];
10765
- }
10766
10771
  async getReferenceData(_ref) {
10767
10772
  console.warn("getReferenceData() returning null/empty defaults");
10768
10773
  return {
@@ -10827,14 +10832,18 @@ var StubSCMLib = class extends SCMLib {
10827
10832
  diffLines: []
10828
10833
  };
10829
10834
  }
10830
- async getSubmitRequests(_repoUrl) {
10831
- console.warn("getSubmitRequests() returning empty array");
10832
- return [];
10833
- }
10834
10835
  async getPullRequestMetrics(_prNumber) {
10835
10836
  console.warn("getPullRequestMetrics() returning empty object");
10836
10837
  throw new Error("getPullRequestMetrics() not implemented");
10837
10838
  }
10839
+ async getRecentCommits(_since) {
10840
+ console.warn("getRecentCommits() returning empty array");
10841
+ return { data: [] };
10842
+ }
10843
+ async getRateLimitStatus() {
10844
+ console.warn("getRateLimitStatus() returning null");
10845
+ return null;
10846
+ }
10838
10847
  };
10839
10848
 
10840
10849
  // src/features/analysis/scm/scmFactory.ts
@@ -12855,7 +12864,7 @@ import Debug10 from "debug";
12855
12864
 
12856
12865
  // src/features/analysis/add_fix_comments_for_pr/utils/utils.ts
12857
12866
  import Debug9 from "debug";
12858
- import parseDiff from "parse-diff";
12867
+ import parseDiff2 from "parse-diff";
12859
12868
  import { z as z27 } from "zod";
12860
12869
 
12861
12870
  // src/features/analysis/utils/by_key.ts
@@ -13228,7 +13237,7 @@ Refresh the page in order to see the changes.`,
13228
13237
  }
13229
13238
  async function getRelevantVulenrabilitiesFromDiff(params) {
13230
13239
  const { gqlClient, diff, vulnerabilityReportId } = params;
13231
- const parsedDiff = parseDiff(diff);
13240
+ const parsedDiff = parseDiff2(diff);
13232
13241
  const fileHunks = parsedDiff.map((file) => {
13233
13242
  const fileNumbers = file.chunks.flatMap((chunk) => chunk.changes).filter((change) => change.type === "add").map((_change) => {
13234
13243
  const change = _change;
@@ -15329,7 +15338,7 @@ async function getRepositoryUrl() {
15329
15338
  }
15330
15339
  const remoteUrl = await gitService.getRemoteUrl();
15331
15340
  const parsed = parseScmURL(remoteUrl);
15332
- return parsed?.scmType === "GitHub" /* GitHub */ ? remoteUrl : null;
15341
+ return parsed?.scmType === "GitHub" /* GitHub */ || parsed?.scmType === "GitLab" /* GitLab */ ? remoteUrl : null;
15333
15342
  } catch {
15334
15343
  return null;
15335
15344
  }
@@ -21495,7 +21504,7 @@ import {
21495
21504
  writeFileSync
21496
21505
  } from "fs";
21497
21506
  import fs21 from "fs/promises";
21498
- import parseDiff2 from "parse-diff";
21507
+ import parseDiff3 from "parse-diff";
21499
21508
  import path20 from "path";
21500
21509
  var PatchApplicationService = class {
21501
21510
  /**
@@ -22179,7 +22188,7 @@ var PatchApplicationService = class {
22179
22188
  fixId,
22180
22189
  scanContext
22181
22190
  }) {
22182
- const parsedPatch = parseDiff2(patch);
22191
+ const parsedPatch = parseDiff3(patch);
22183
22192
  if (!parsedPatch || parsedPatch.length === 0) {
22184
22193
  throw new Error("Failed to parse patch - no changes found");
22185
22194
  }