mobbdev 1.2.4 → 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 {
@@ -1623,6 +1622,7 @@ var init_issue = __esm({
1623
1622
  MAX_SOURCE_CODE_FILE_SIZE_IN_BYTES = 1e5;
1624
1623
  VulnerabilityReportIssueSharedStateZ = z9.object({
1625
1624
  id: z9.string().uuid(),
1625
+ createdAt: z9.string(),
1626
1626
  isArchived: z9.boolean(),
1627
1627
  ticketIntegrationId: z9.string().uuid().nullable(),
1628
1628
  ticketIntegrations: z9.array(
@@ -1849,6 +1849,7 @@ var init_types = __esm({
1849
1849
  });
1850
1850
  IssueSharedStateZ2 = z11.object({
1851
1851
  id: z11.string().uuid(),
1852
+ createdAt: z11.string(),
1852
1853
  isArchived: z11.boolean(),
1853
1854
  ticketIntegrationId: z11.string().uuid().nullable(),
1854
1855
  ticketIntegrations: z11.array(
@@ -3799,34 +3800,6 @@ ${rootContent}`;
3799
3800
  throw new Error(errorMessage);
3800
3801
  }
3801
3802
  }
3802
- /**
3803
- * Gets timestamps for parent commits in a single git call.
3804
- * @param parentShas Array of parent commit SHAs
3805
- * @returns Array of parent commits with timestamps, or undefined if unavailable
3806
- */
3807
- async getParentCommitTimestamps(parentShas) {
3808
- if (parentShas.length === 0) {
3809
- return void 0;
3810
- }
3811
- try {
3812
- const output = await this.git.raw([
3813
- "log",
3814
- "--format=%H %cI",
3815
- "--no-walk",
3816
- ...parentShas
3817
- ]);
3818
- const parentCommits = output.trim().split("\n").filter(Boolean).map((line) => {
3819
- const [sha, ts] = line.split(" ");
3820
- return { sha: sha ?? "", timestamp: new Date(ts ?? "") };
3821
- }).filter((p) => p.sha !== "");
3822
- return parentCommits.length > 0 ? parentCommits : void 0;
3823
- } catch {
3824
- this.log("[GitService] Could not get parent commit timestamps", "debug", {
3825
- parentShas
3826
- });
3827
- return void 0;
3828
- }
3829
- }
3830
3803
  /**
3831
3804
  * Gets local commit data including diff, timestamp, and parent commits.
3832
3805
  * Used by Tracy extension to send commit data directly without requiring SCM token.
@@ -3871,18 +3844,14 @@ ${rootContent}`;
3871
3844
  }
3872
3845
  const timestampStr = metadataLines[0];
3873
3846
  const timestamp = new Date(timestampStr);
3874
- const parentShas = (metadataLines[1] ?? "").trim().split(/\s+/).filter(Boolean);
3875
- const parentCommits = await this.getParentCommitTimestamps(parentShas);
3876
3847
  this.log("[GitService] Local commit data retrieved", "debug", {
3877
3848
  commitSha,
3878
3849
  diffSizeBytes,
3879
- timestamp: timestamp.toISOString(),
3880
- parentCommitCount: parentCommits?.length ?? 0
3850
+ timestamp: timestamp.toISOString()
3881
3851
  });
3882
3852
  return {
3883
3853
  diff,
3884
- timestamp,
3885
- parentCommits
3854
+ timestamp
3886
3855
  };
3887
3856
  } catch (error) {
3888
3857
  const errorMessage = `Failed to get local commit data: ${error.message}`;
@@ -6107,6 +6076,34 @@ var GetReferenceResultZ = z13.object({
6107
6076
  type: z13.nativeEnum(ReferenceType)
6108
6077
  });
6109
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
+
6110
6107
  // src/features/analysis/scm/utils/scm.ts
6111
6108
  var safeBody = (body, maxBodyLength) => {
6112
6109
  const truncationNotice = "\n\n... Message was cut here because it is too long";
@@ -6199,6 +6196,36 @@ function getCommitIssueUrl(params) {
6199
6196
  analysisId
6200
6197
  })}/commit?${searchParams.toString()}`;
6201
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
+ }
6202
6229
  var userNamePattern = /^(https?:\/\/)([^@]+@)?([^/]+\/.+)$/;
6203
6230
  var sshPattern = /^git@([\w.-]+):([\w./-]+)$/;
6204
6231
  function normalizeUrl(repoUrl) {
@@ -6892,9 +6919,6 @@ async function getAdoSdk(params) {
6892
6919
  return commitRes.value;
6893
6920
  }
6894
6921
  throw new RefNotFoundError(`ref: ${ref} does not exist`);
6895
- },
6896
- getAdoBlameRanges() {
6897
- return [];
6898
6922
  }
6899
6923
  };
6900
6924
  }
@@ -7014,7 +7038,6 @@ var SCMLib = class {
7014
7038
  * IMPORTANT: Sort order must remain consistent across paginated requests
7015
7039
  * for cursor-based pagination to work correctly.
7016
7040
  *
7017
- * Default implementation uses getSubmitRequests and applies filters/sorting in-memory.
7018
7041
  * Override in subclasses for provider-specific optimizations (e.g., GitHub Search API).
7019
7042
  *
7020
7043
  * @param params - Search parameters including filters, sort, and pagination
@@ -7096,6 +7119,14 @@ var SCMLib = class {
7096
7119
  static async getIsValidBranchName(branchName) {
7097
7120
  return isValidBranchName(branchName);
7098
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
+ }
7099
7130
  _validateAccessTokenAndUrl() {
7100
7131
  this._validateAccessToken();
7101
7132
  this._validateUrl();
@@ -7248,10 +7279,6 @@ var AdoSCMLib = class extends SCMLib {
7248
7279
  throw new Error(`unknown state ${state}`);
7249
7280
  }
7250
7281
  }
7251
- async getRepoBlameRanges(_ref, _path) {
7252
- const adoSdk = await this.getAdoSdk();
7253
- return await adoSdk.getAdoBlameRanges();
7254
- }
7255
7282
  async getReferenceData(ref) {
7256
7283
  this._validateUrl();
7257
7284
  const adoSdk = await this.getAdoSdk();
@@ -7310,9 +7337,6 @@ var AdoSCMLib = class extends SCMLib {
7310
7337
  async getSubmitRequestDiff(_submitRequestId) {
7311
7338
  throw new Error("getSubmitRequestDiff not implemented for ADO");
7312
7339
  }
7313
- async getSubmitRequests(_repoUrl) {
7314
- throw new Error("getSubmitRequests not implemented for ADO");
7315
- }
7316
7340
  async searchSubmitRequests(_params) {
7317
7341
  throw new Error("searchSubmitRequests not implemented for ADO");
7318
7342
  }
@@ -7324,6 +7348,12 @@ var AdoSCMLib = class extends SCMLib {
7324
7348
  async getPullRequestMetrics(_prNumber) {
7325
7349
  throw new Error("getPullRequestMetrics not implemented for ADO");
7326
7350
  }
7351
+ async getRecentCommits(_since) {
7352
+ throw new Error("getRecentCommits not implemented for ADO");
7353
+ }
7354
+ async getRateLimitStatus() {
7355
+ return null;
7356
+ }
7327
7357
  };
7328
7358
 
7329
7359
  // src/features/analysis/scm/bitbucket/bitbucket.ts
@@ -7841,9 +7871,6 @@ var BitbucketSCMLib = class extends SCMLib {
7841
7871
  throw new Error(`unknown state ${pullRequestRes.state} `);
7842
7872
  }
7843
7873
  }
7844
- async getRepoBlameRanges(_ref, _path) {
7845
- return [];
7846
- }
7847
7874
  async getReferenceData(ref) {
7848
7875
  this._validateUrl();
7849
7876
  return this.bitbucketSdk.getReferenceData({ url: this.url, ref });
@@ -7892,9 +7919,6 @@ var BitbucketSCMLib = class extends SCMLib {
7892
7919
  async getSubmitRequestDiff(_submitRequestId) {
7893
7920
  throw new Error("getSubmitRequestDiff not implemented for Bitbucket");
7894
7921
  }
7895
- async getSubmitRequests(_repoUrl) {
7896
- throw new Error("getSubmitRequests not implemented for Bitbucket");
7897
- }
7898
7922
  async searchSubmitRequests(_params) {
7899
7923
  throw new Error("searchSubmitRequests not implemented for Bitbucket");
7900
7924
  }
@@ -7906,6 +7930,12 @@ var BitbucketSCMLib = class extends SCMLib {
7906
7930
  async getPullRequestMetrics(_prNumber) {
7907
7931
  throw new Error("getPullRequestMetrics not implemented for Bitbucket");
7908
7932
  }
7933
+ async getRecentCommits(_since) {
7934
+ throw new Error("getRecentCommits not implemented for Bitbucket");
7935
+ }
7936
+ async getRateLimitStatus() {
7937
+ return null;
7938
+ }
7909
7939
  };
7910
7940
 
7911
7941
  // src/features/analysis/scm/constants.ts
@@ -7918,7 +7948,7 @@ init_env();
7918
7948
 
7919
7949
  // src/features/analysis/scm/github/GithubSCMLib.ts
7920
7950
  init_env();
7921
- import pLimit2 from "p-limit";
7951
+ import pLimit from "p-limit";
7922
7952
  import { z as z21 } from "zod";
7923
7953
  init_client_generates();
7924
7954
 
@@ -7937,59 +7967,6 @@ function parseCursorSafe(cursor, defaultValue = 0, maxValue = MAX_CURSOR_VALUE)
7937
7967
 
7938
7968
  // src/features/analysis/scm/github/github.ts
7939
7969
  import { RequestError } from "@octokit/request-error";
7940
- import pLimit from "p-limit";
7941
-
7942
- // src/utils/contextLogger.ts
7943
- import debugModule from "debug";
7944
- var debug3 = debugModule("mobb:shared");
7945
- var _contextLogger = null;
7946
- var createContextLogger = async () => {
7947
- if (_contextLogger) return _contextLogger;
7948
- try {
7949
- let logger2;
7950
- try {
7951
- let module;
7952
- try {
7953
- const buildPath = "../../../../../tscommon/backend/build/src/utils/logger";
7954
- module = await import(buildPath);
7955
- } catch (e) {
7956
- const sourcePath = "../../../../../tscommon/backend/src/utils/logger";
7957
- module = await import(sourcePath);
7958
- }
7959
- logger2 = module.logger;
7960
- } catch {
7961
- }
7962
- if (logger2) {
7963
- _contextLogger = {
7964
- info: (message, data) => data ? logger2.info(data, message) : logger2.info(message),
7965
- debug: (message, data) => data ? logger2.debug(data, message) : logger2.debug(message),
7966
- error: (message, data) => data ? logger2.error(data, message) : logger2.error(message)
7967
- };
7968
- return _contextLogger;
7969
- }
7970
- } catch {
7971
- }
7972
- _contextLogger = {
7973
- info: (message, data) => debug3(message, data),
7974
- debug: (message, data) => debug3(message, data),
7975
- error: (message, data) => debug3(message, data)
7976
- };
7977
- return _contextLogger;
7978
- };
7979
- var contextLogger = {
7980
- info: async (message, data) => {
7981
- const logger2 = await createContextLogger();
7982
- return logger2.info(message, data);
7983
- },
7984
- debug: async (message, data) => {
7985
- const logger2 = await createContextLogger();
7986
- return logger2.debug(message, data);
7987
- },
7988
- error: async (message, data) => {
7989
- const logger2 = await createContextLogger();
7990
- return logger2.error(message, data);
7991
- }
7992
- };
7993
7970
 
7994
7971
  // src/features/analysis/scm/github/consts.ts
7995
7972
  var POST_COMMENT_PATH = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments";
@@ -8007,35 +7984,6 @@ var GET_A_REPOSITORY_PUBLIC_KEY = "GET /repos/{owner}/{repo}/actions/secrets/pub
8007
7984
  var GET_USER = "GET /user";
8008
7985
  var GET_USER_REPOS = "GET /user/repos";
8009
7986
  var GET_REPO_BRANCHES = "GET /repos/{owner}/{repo}/branches";
8010
- var GET_BLAME_DOCUMENT = `
8011
- query GetBlame(
8012
- $owner: String!
8013
- $repo: String!
8014
- $ref: String!
8015
- $path: String!
8016
- ) {
8017
- repository(name: $repo, owner: $owner) {
8018
- # branch name
8019
- object(expression: $ref) {
8020
- # cast Target to a Commit
8021
- ... on Commit {
8022
- # full repo-relative path to blame file
8023
- blame(path: $path) {
8024
- ranges {
8025
- commit {
8026
- oid
8027
- }
8028
- startingLine
8029
- endingLine
8030
- age
8031
- }
8032
- }
8033
- }
8034
-
8035
- }
8036
- }
8037
- }
8038
- `;
8039
7987
  var GITHUB_GRAPHQL_FRAGMENTS = {
8040
7988
  /**
8041
7989
  * Fragment for fetching PR additions/deletions.
@@ -8059,30 +8007,6 @@ var GITHUB_GRAPHQL_FRAGMENTS = {
8059
8007
  body
8060
8008
  }
8061
8009
  }
8062
- `,
8063
- /**
8064
- * Fragment for fetching blame data.
8065
- * Use with object(expression: $ref) on Commit type.
8066
- * Note: $path placeholder must be replaced with actual file path.
8067
- */
8068
- BLAME_RANGES: `
8069
- blame(path: "$path") {
8070
- ranges {
8071
- startingLine
8072
- endingLine
8073
- commit {
8074
- oid
8075
- }
8076
- }
8077
- }
8078
- `,
8079
- /**
8080
- * Fragment for fetching commit timestamp.
8081
- * Use with object(oid: $sha) on Commit type.
8082
- */
8083
- COMMIT_TIMESTAMP: `
8084
- oid
8085
- committedDate
8086
8010
  `
8087
8011
  };
8088
8012
  var GET_PR_METRICS_QUERY = `
@@ -8294,112 +8218,6 @@ async function githubValidateParams(url, accessToken) {
8294
8218
 
8295
8219
  // src/features/analysis/scm/github/github.ts
8296
8220
  var MAX_GH_PR_BODY_LENGTH = 65536;
8297
- var BLAME_LARGE_FILE_THRESHOLD_BYTES = 1e6;
8298
- var BLAME_THRESHOLD_REDUCTION_BYTES = 1e5;
8299
- var BLAME_MIN_THRESHOLD_BYTES = 1e5;
8300
- var GRAPHQL_INPUT_PATTERNS = {
8301
- // File paths: most printable ASCII chars, unicode letters/numbers
8302
- // Allows: letters, numbers, spaces, common punctuation, path separators
8303
- // Disallows: control characters, null bytes
8304
- path: /^[\p{L}\p{N}\p{Zs}\-._/@+#~%()[\]{}=!,;'&]+$/u,
8305
- // Git refs: branch/tag names follow git-check-ref-format rules
8306
- // Allows: letters, numbers, slashes, dots, hyphens, underscores
8307
- // Can also be "ref:path" format for expressions
8308
- ref: /^[\p{L}\p{N}\-._/:@]+$/u,
8309
- // Git SHAs: strictly hexadecimal (short or full)
8310
- sha: /^[0-9a-fA-F]+$/
8311
- };
8312
- function validateGraphQLInput(value, type2) {
8313
- const pattern = GRAPHQL_INPUT_PATTERNS[type2];
8314
- if (!pattern.test(value)) {
8315
- void contextLogger.info(
8316
- "[GraphQL] Input contains unexpected characters, proceeding with escaping",
8317
- {
8318
- type: type2,
8319
- valueLength: value.length,
8320
- // Log first 100 chars to help debug without exposing full value
8321
- valueSample: value.slice(0, 100)
8322
- }
8323
- );
8324
- return false;
8325
- }
8326
- return true;
8327
- }
8328
- function escapeGraphQLString(value) {
8329
- 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");
8330
- }
8331
- function safeGraphQLString(value, type2) {
8332
- validateGraphQLInput(value, type2);
8333
- return escapeGraphQLString(value);
8334
- }
8335
- function extractBlameRanges(data) {
8336
- const fileData = data;
8337
- if (fileData.blame?.ranges) {
8338
- return fileData.blame.ranges.map((range) => ({
8339
- startingLine: range.startingLine,
8340
- endingLine: range.endingLine,
8341
- commitSha: range.commit.oid
8342
- }));
8343
- }
8344
- return void 0;
8345
- }
8346
- function buildBlameFragment(ref) {
8347
- const escapedRef = safeGraphQLString(ref, "ref");
8348
- return (path25, index) => {
8349
- const escapedPath = safeGraphQLString(path25, "path");
8350
- return `
8351
- file${index}: object(expression: "${escapedRef}") {
8352
- ... on Commit {
8353
- ${GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES.replace("$path", escapedPath)}
8354
- }
8355
- }`;
8356
- };
8357
- }
8358
- function createBatchesByTotalSize(files, threshold) {
8359
- const batches = [];
8360
- let currentBatch = [];
8361
- let currentBatchSize = 0;
8362
- for (const file of files) {
8363
- if (currentBatchSize + file.size > threshold && currentBatch.length > 0) {
8364
- batches.push(currentBatch);
8365
- currentBatch = [];
8366
- currentBatchSize = 0;
8367
- }
8368
- currentBatch.push(file);
8369
- currentBatchSize += file.size;
8370
- }
8371
- if (currentBatch.length > 0) {
8372
- batches.push(currentBatch);
8373
- }
8374
- return batches;
8375
- }
8376
- async function fetchBlameForBatch(octokit, owner, repo, ref, files) {
8377
- if (files.length === 0) {
8378
- return /* @__PURE__ */ new Map();
8379
- }
8380
- return executeBatchGraphQL(octokit, owner, repo, {
8381
- items: files.map((f) => f.path),
8382
- aliasPrefix: "file",
8383
- buildFragment: buildBlameFragment(ref),
8384
- extractResult: extractBlameRanges
8385
- });
8386
- }
8387
- async function processBlameAttempt(params) {
8388
- const { octokit, owner, repo, ref, batches, concurrency } = params;
8389
- const result = /* @__PURE__ */ new Map();
8390
- const limit = pLimit(concurrency);
8391
- const batchResults = await Promise.all(
8392
- batches.map(
8393
- (batch) => limit(() => fetchBlameForBatch(octokit, owner, repo, ref, batch))
8394
- )
8395
- );
8396
- for (const batchResult of batchResults) {
8397
- for (const [path25, blameData] of batchResult) {
8398
- result.set(path25, blameData);
8399
- }
8400
- }
8401
- return result;
8402
- }
8403
8221
  async function executeBatchGraphQL(octokit, owner, repo, config2) {
8404
8222
  const { items, aliasPrefix, buildFragment, extractResult } = config2;
8405
8223
  if (items.length === 0) {
@@ -8714,29 +8532,6 @@ function getGithubSdk(params = {}) {
8714
8532
  sha: res.data.sha
8715
8533
  };
8716
8534
  },
8717
- async getGithubBlameRanges(params2) {
8718
- const { ref, gitHubUrl, path: path25 } = params2;
8719
- const { owner, repo } = parseGithubOwnerAndRepo(gitHubUrl);
8720
- const res = await octokit.graphql(
8721
- GET_BLAME_DOCUMENT,
8722
- {
8723
- owner,
8724
- repo,
8725
- path: path25,
8726
- ref
8727
- }
8728
- );
8729
- if (!res?.repository?.object?.blame?.ranges) {
8730
- return [];
8731
- }
8732
- return res.repository.object.blame.ranges.map(
8733
- (range) => ({
8734
- startingLine: range.startingLine,
8735
- endingLine: range.endingLine,
8736
- commitSha: range.commit.oid
8737
- })
8738
- );
8739
- },
8740
8535
  /**
8741
8536
  * Fetches commits for multiple PRs in a single GraphQL request.
8742
8537
  * This is much more efficient than making N separate REST API calls.
@@ -9020,301 +8815,75 @@ function getGithubSdk(params = {}) {
9020
8815
  }
9021
8816
  });
9022
8817
  },
9023
- /**
9024
- * Batch fetch blob sizes for multiple files via GraphQL.
9025
- * Used to determine which files are too large to batch in blame queries.
9026
- */
9027
- async getBlobSizesBatch(params2) {
9028
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9029
- items: params2.blobShas,
9030
- aliasPrefix: "blob",
9031
- buildFragment: (sha, index) => {
9032
- const escapedSha = safeGraphQLString(sha, "sha");
9033
- return `
9034
- blob${index}: object(oid: "${escapedSha}") {
9035
- ... on Blob {
9036
- byteSize
9037
- }
9038
- }`;
9039
- },
9040
- extractResult: (data) => {
9041
- const blobData = data;
9042
- if (blobData.byteSize !== void 0) {
9043
- return blobData.byteSize;
9044
- }
9045
- return void 0;
8818
+ async getPRMetricsGraphQL(params2) {
8819
+ const res = await octokit.graphql(
8820
+ GET_PR_METRICS_QUERY,
8821
+ {
8822
+ owner: params2.owner,
8823
+ repo: params2.repo,
8824
+ prNumber: params2.prNumber
9046
8825
  }
9047
- });
8826
+ );
8827
+ return res;
9048
8828
  },
9049
8829
  /**
9050
- * Batch fetch blame data for multiple files via GraphQL.
9051
- * Uses GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES for the field selection.
9052
- *
9053
- * Optimized to handle large files with retry logic:
9054
- * - Files above threshold are processed individually with rate limiting
9055
- * - On failure, retries with reduced threshold (-100KB) and concurrency (-1)
9056
- * - Continues until success or threshold < 100KB
9057
- *
9058
- * @param params.files - Array of files with path and blobSha for size lookup
9059
- * @param params.concurrency - Max concurrent requests for large files (default: 2)
8830
+ * Search PRs using GitHub's Search API with sorting
8831
+ * https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests
9060
8832
  */
9061
- async getBlameBatch(params2) {
8833
+ async searchPullRequests(params2) {
9062
8834
  const {
9063
8835
  owner,
9064
8836
  repo,
9065
- ref,
9066
- files,
9067
- concurrency: initialConcurrency = 2
8837
+ updatedAfter,
8838
+ state = "all",
8839
+ sort = { field: "updated", order: "desc" },
8840
+ perPage = 10,
8841
+ page = 1
9068
8842
  } = params2;
9069
- if (files.length === 0) {
9070
- return /* @__PURE__ */ new Map();
8843
+ let query = `repo:${owner}/${repo} is:pr`;
8844
+ if (updatedAfter) {
8845
+ const dateStr = updatedAfter.toISOString().split("T")[0];
8846
+ query += ` updated:>=${dateStr}`;
9071
8847
  }
9072
- const filesWithSizes = await this.fetchFilesWithSizes(owner, repo, files);
9073
- return this.executeBlameWithRetries({
9074
- owner,
9075
- repo,
9076
- ref,
9077
- filesWithSizes,
9078
- initialConcurrency
8848
+ if (state !== "all") {
8849
+ query += ` is:${state}`;
8850
+ }
8851
+ const githubSortField = sort.field === "updated" || sort.field === "created" ? sort.field : "comments";
8852
+ const response = await octokit.rest.search.issuesAndPullRequests({
8853
+ q: query,
8854
+ sort: githubSortField,
8855
+ order: sort.order,
8856
+ per_page: perPage,
8857
+ page
9079
8858
  });
8859
+ return {
8860
+ items: response.data.items,
8861
+ totalCount: response.data.total_count,
8862
+ hasMore: page * perPage < response.data.total_count
8863
+ };
9080
8864
  },
9081
8865
  /**
9082
- * Fetches blob sizes and creates a list of files with their sizes.
9083
- */
9084
- async fetchFilesWithSizes(owner, repo, files) {
9085
- const blobShas = files.map((f) => f.blobSha);
9086
- const blobSizes = await this.getBlobSizesBatch({ owner, repo, blobShas });
9087
- return files.map((file) => ({
9088
- ...file,
9089
- size: blobSizes.get(file.blobSha) ?? 0
9090
- }));
9091
- },
9092
- /**
9093
- * Executes blame fetching with retry logic on failure.
9094
- * Reduces threshold and concurrency on each retry attempt.
8866
+ * Search repositories using GitHub's Search API.
8867
+ * Docs: https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories
9095
8868
  */
9096
- async executeBlameWithRetries(params2) {
9097
- const { owner, repo, ref, filesWithSizes, initialConcurrency } = params2;
9098
- let threshold = BLAME_LARGE_FILE_THRESHOLD_BYTES;
9099
- let concurrency = initialConcurrency;
9100
- let attempt = 1;
9101
- let lastError = null;
9102
- while (threshold >= BLAME_MIN_THRESHOLD_BYTES) {
9103
- const batches = createBatchesByTotalSize(filesWithSizes, threshold);
9104
- this.logBlameAttemptStart(
9105
- attempt,
9106
- threshold,
9107
- concurrency,
9108
- filesWithSizes.length,
9109
- batches.length,
9110
- owner,
9111
- repo,
9112
- ref
9113
- );
9114
- try {
9115
- const result = await processBlameAttempt({
9116
- octokit,
9117
- owner,
9118
- repo,
9119
- ref,
9120
- batches,
9121
- concurrency
9122
- });
9123
- this.logBlameAttemptSuccess(attempt, result.size, owner, repo);
9124
- return result;
9125
- } catch (error) {
9126
- lastError = error instanceof Error ? error : new Error(String(error));
9127
- this.logBlameAttemptFailure(
9128
- attempt,
9129
- threshold,
9130
- concurrency,
9131
- lastError.message,
9132
- owner,
9133
- repo
9134
- );
9135
- threshold -= BLAME_THRESHOLD_REDUCTION_BYTES;
9136
- concurrency = Math.max(1, concurrency - 1);
9137
- attempt++;
9138
- }
8869
+ async searchRepositories(params2) {
8870
+ const {
8871
+ org,
8872
+ sort = { field: "updated", order: "desc" },
8873
+ perPage = 10,
8874
+ page = 1
8875
+ } = params2;
8876
+ if (!org) {
8877
+ throw new Error("Organization is required for repository search");
9139
8878
  }
9140
- void contextLogger.error("[getBlameBatch] Exhausted all retries", {
9141
- attempts: attempt - 1,
9142
- repo: `${owner}/${repo}`,
9143
- ref,
9144
- error: lastError?.message || "unknown"
9145
- });
9146
- throw lastError || new Error("getBlameBatch failed after all retries");
9147
- },
9148
- /**
9149
- * Logs the start of a blame batch attempt.
9150
- */
9151
- logBlameAttemptStart(attempt, threshold, concurrency, totalFiles, batchCount, owner, repo, ref) {
9152
- void contextLogger.debug("[getBlameBatch] Processing attempt", {
9153
- attempt,
9154
- threshold,
9155
- concurrency,
9156
- totalFiles,
9157
- batchCount,
9158
- repo: `${owner}/${repo}`,
9159
- ref
9160
- });
9161
- },
9162
- /**
9163
- * Logs a successful blame batch attempt.
9164
- */
9165
- logBlameAttemptSuccess(attempt, filesProcessed, owner, repo) {
9166
- void contextLogger.debug("[getBlameBatch] Successfully processed batch", {
9167
- attempt,
9168
- filesProcessed,
9169
- repo: `${owner}/${repo}`
9170
- });
9171
- },
9172
- /**
9173
- * Logs a failed blame batch attempt.
9174
- */
9175
- logBlameAttemptFailure(attempt, threshold, concurrency, errorMessage, owner, repo) {
9176
- void contextLogger.debug(
9177
- "[getBlameBatch] Attempt failed, retrying with reduced threshold",
9178
- {
9179
- attempt,
9180
- threshold,
9181
- concurrency,
9182
- error: errorMessage,
9183
- repo: `${owner}/${repo}`
9184
- }
9185
- );
9186
- },
9187
- /**
9188
- * Batch fetch blame data for multiple files via GraphQL (legacy interface).
9189
- * This is a convenience wrapper that accepts file paths without blob SHAs.
9190
- * Note: This does NOT perform size-based optimization. Use getBlameBatch with
9191
- * files array including blobSha for optimized large file handling.
9192
- */
9193
- async getBlameBatchByPaths(params2) {
9194
- const escapedRef = safeGraphQLString(params2.ref, "ref");
9195
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9196
- items: params2.filePaths,
9197
- aliasPrefix: "file",
9198
- buildFragment: (path25, index) => {
9199
- const escapedPath = safeGraphQLString(path25, "path");
9200
- return `
9201
- file${index}: object(expression: "${escapedRef}") {
9202
- ... on Commit {
9203
- ${GITHUB_GRAPHQL_FRAGMENTS.BLAME_RANGES.replace("$path", escapedPath)}
9204
- }
9205
- }`;
9206
- },
9207
- extractResult: (data) => {
9208
- const fileData = data;
9209
- if (fileData.blame?.ranges) {
9210
- return fileData.blame.ranges.map((range) => ({
9211
- startingLine: range.startingLine,
9212
- endingLine: range.endingLine,
9213
- commitSha: range.commit.oid
9214
- }));
9215
- }
9216
- return void 0;
9217
- }
9218
- });
9219
- },
9220
- /**
9221
- * Batch fetch commit timestamps for multiple commits via GraphQL.
9222
- * Uses GITHUB_GRAPHQL_FRAGMENTS.COMMIT_TIMESTAMP for the field selection.
9223
- */
9224
- async getCommitsBatch(params2) {
9225
- return executeBatchGraphQL(octokit, params2.owner, params2.repo, {
9226
- items: params2.commitShas,
9227
- aliasPrefix: "commit",
9228
- buildFragment: (sha, index) => {
9229
- const escapedSha = safeGraphQLString(sha, "sha");
9230
- return `
9231
- commit${index}: object(oid: "${escapedSha}") {
9232
- ... on Commit {
9233
- ${GITHUB_GRAPHQL_FRAGMENTS.COMMIT_TIMESTAMP}
9234
- }
9235
- }`;
9236
- },
9237
- extractResult: (data) => {
9238
- const commitData = data;
9239
- if (commitData.oid && commitData.committedDate) {
9240
- return {
9241
- sha: commitData.oid,
9242
- timestamp: new Date(commitData.committedDate)
9243
- };
9244
- }
9245
- return void 0;
9246
- }
9247
- });
9248
- },
9249
- async getPRMetricsGraphQL(params2) {
9250
- const res = await octokit.graphql(
9251
- GET_PR_METRICS_QUERY,
9252
- {
9253
- owner: params2.owner,
9254
- repo: params2.repo,
9255
- prNumber: params2.prNumber
9256
- }
9257
- );
9258
- return res;
9259
- },
9260
- /**
9261
- * Search PRs using GitHub's Search API with sorting
9262
- * https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests
9263
- */
9264
- async searchPullRequests(params2) {
9265
- const {
9266
- owner,
9267
- repo,
9268
- updatedAfter,
9269
- state = "all",
9270
- sort = { field: "updated", order: "desc" },
9271
- perPage = 10,
9272
- page = 1
9273
- } = params2;
9274
- let query = `repo:${owner}/${repo} is:pr`;
9275
- if (updatedAfter) {
9276
- const dateStr = updatedAfter.toISOString().split("T")[0];
9277
- query += ` updated:>=${dateStr}`;
9278
- }
9279
- if (state !== "all") {
9280
- query += ` is:${state}`;
9281
- }
9282
- const githubSortField = sort.field === "updated" || sort.field === "created" ? sort.field : "comments";
9283
- const response = await octokit.rest.search.issuesAndPullRequests({
9284
- q: query,
9285
- sort: githubSortField,
9286
- order: sort.order,
9287
- per_page: perPage,
9288
- page
9289
- });
9290
- return {
9291
- items: response.data.items,
9292
- totalCount: response.data.total_count,
9293
- hasMore: page * perPage < response.data.total_count
9294
- };
9295
- },
9296
- /**
9297
- * Search repositories using GitHub's Search API.
9298
- * Docs: https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-repositories
9299
- */
9300
- async searchRepositories(params2) {
9301
- const {
9302
- org,
9303
- sort = { field: "updated", order: "desc" },
9304
- perPage = 10,
9305
- page = 1
9306
- } = params2;
9307
- if (!org) {
9308
- throw new Error("Organization is required for repository search");
9309
- }
9310
- const query = `org:${org}`;
9311
- const githubSortField = sort.field === "name" ? void 0 : "updated";
9312
- const response = await octokit.rest.search.repos({
9313
- q: query,
9314
- sort: githubSortField,
9315
- order: sort.order,
9316
- per_page: perPage,
9317
- page
8879
+ const query = `org:${org}`;
8880
+ const githubSortField = sort.field === "name" ? void 0 : "updated";
8881
+ const response = await octokit.rest.search.repos({
8882
+ q: query,
8883
+ sort: githubSortField,
8884
+ order: sort.order,
8885
+ per_page: perPage,
8886
+ page
9318
8887
  });
9319
8888
  return {
9320
8889
  items: response.data.items,
@@ -9450,10 +9019,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9450
9019
  async getRecentCommits(since) {
9451
9020
  this._validateAccessTokenAndUrl();
9452
9021
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9453
- return await this.githubSdk.getRecentCommits({ owner, repo, since });
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
+ };
9454
9034
  }
9455
9035
  async getRateLimitStatus() {
9456
- 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
+ };
9457
9042
  }
9458
9043
  get scmLibType() {
9459
9044
  return "GITHUB" /* GITHUB */;
@@ -9511,14 +9096,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9511
9096
  markdownComment: comment
9512
9097
  });
9513
9098
  }
9514
- async getRepoBlameRanges(ref, path25) {
9515
- this._validateUrl();
9516
- return await this.githubSdk.getGithubBlameRanges({
9517
- ref,
9518
- path: path25,
9519
- gitHubUrl: this.url
9520
- });
9521
- }
9522
9099
  async getReferenceData(ref) {
9523
9100
  this._validateUrl();
9524
9101
  return await this.githubSdk.getGithubReferenceData({
@@ -9608,37 +9185,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9608
9185
  commitSha
9609
9186
  });
9610
9187
  const commitTimestamp = commit.commit.committer?.date ? new Date(commit.commit.committer.date) : new Date(commit.commit.author?.date || Date.now());
9611
- let parentCommits;
9612
- if (commit.parents && commit.parents.length > 0) {
9613
- if (options?.parentCommitTimestamps) {
9614
- parentCommits = commit.parents.map((p) => options.parentCommitTimestamps.get(p.sha)).filter((p) => p !== void 0);
9615
- } else {
9616
- try {
9617
- parentCommits = await Promise.all(
9618
- commit.parents.map(async (parent) => {
9619
- const parentCommit = await this.githubSdk.getCommit({
9620
- owner,
9621
- repo,
9622
- commitSha: parent.sha
9623
- });
9624
- const parentTimestamp = parentCommit.data.committer?.date ? new Date(parentCommit.data.committer.date) : new Date(Date.now());
9625
- return {
9626
- sha: parent.sha,
9627
- timestamp: parentTimestamp
9628
- };
9629
- })
9630
- );
9631
- } catch (error) {
9632
- console.error("Failed to fetch parent commit timestamps", {
9633
- error,
9634
- commitSha,
9635
- owner,
9636
- repo
9637
- });
9638
- parentCommits = void 0;
9639
- }
9640
- }
9641
- }
9642
9188
  let repositoryCreatedAt = options?.repositoryCreatedAt;
9643
9189
  if (repositoryCreatedAt === void 0) {
9644
9190
  try {
@@ -9660,7 +9206,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9660
9206
  authorName: commit.commit.author?.name,
9661
9207
  authorEmail: commit.commit.author?.email,
9662
9208
  message: commit.commit.message,
9663
- parentCommits,
9664
9209
  repositoryCreatedAt
9665
9210
  };
9666
9211
  }
@@ -9668,46 +9213,31 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9668
9213
  this._validateAccessTokenAndUrl();
9669
9214
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
9670
9215
  const prNumber = Number(submitRequestId);
9671
- const [prRes, commitsRes, filesRes, repoData] = await Promise.all([
9216
+ const [prRes, commitsRes, repoData, prDiff] = await Promise.all([
9672
9217
  this.githubSdk.getPr({ owner, repo, pull_number: prNumber }),
9673
9218
  this.githubSdk.getPrCommits({ owner, repo, pull_number: prNumber }),
9674
- this.githubSdk.listPRFiles({ owner, repo, pull_number: prNumber }),
9675
- this.githubSdk.getRepository({ owner, repo })
9219
+ this.githubSdk.getRepository({ owner, repo }),
9220
+ this.getPrDiff({ pull_number: prNumber })
9676
9221
  ]);
9677
9222
  const pr = prRes.data;
9678
9223
  const repositoryCreatedAt = repoData.data.created_at ? new Date(repoData.data.created_at) : void 0;
9679
- const allParentShas = /* @__PURE__ */ new Set();
9680
- for (const commit of commitsRes.data) {
9681
- if (commit.parents) {
9682
- for (const parent of commit.parents) {
9683
- allParentShas.add(parent.sha);
9684
- }
9685
- }
9686
- }
9687
- const [parentCommitTimestamps, prDiff] = await Promise.all([
9688
- this.githubSdk.getCommitsBatch({
9689
- owner,
9690
- repo,
9691
- commitShas: Array.from(allParentShas)
9692
- }),
9693
- this.getPrDiff({ pull_number: prNumber })
9694
- ]);
9695
- const limit = pLimit2(GITHUB_API_CONCURRENCY);
9224
+ const limit = pLimit(GITHUB_API_CONCURRENCY);
9696
9225
  const commits = await Promise.all(
9697
9226
  commitsRes.data.map(
9698
9227
  (commit) => limit(
9699
9228
  () => this.getCommitDiff(commit.sha, {
9700
- repositoryCreatedAt,
9701
- parentCommitTimestamps
9229
+ repositoryCreatedAt
9702
9230
  })
9703
9231
  )
9704
9232
  )
9705
9233
  );
9706
- const diffLines = filesRes ? await this._attributeLinesViaBlame({
9707
- headSha: pr.head.sha,
9708
- changedFiles: filesRes.data,
9709
- prCommits: commits
9710
- }) : [];
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
+ }
9711
9241
  return {
9712
9242
  diff: prDiff,
9713
9243
  createdAt: new Date(pr.created_at),
@@ -9724,47 +9254,6 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9724
9254
  diffLines
9725
9255
  };
9726
9256
  }
9727
- async getSubmitRequests(repoUrl) {
9728
- this._validateAccessToken();
9729
- const { owner, repo } = parseGithubOwnerAndRepo(repoUrl);
9730
- const pullsRes = await this.githubSdk.getRepoPullRequests({ owner, repo });
9731
- const prNumbers = pullsRes.data.map((pr) => pr.number);
9732
- const [additionsDeletionsMap, commentsMap] = await Promise.all([
9733
- this.githubSdk.getPrAdditionsDeletionsBatch({ owner, repo, prNumbers }),
9734
- this.githubSdk.getPrCommentsBatch({ owner, repo, prNumbers })
9735
- ]);
9736
- const submitRequests = pullsRes.data.map((pr) => {
9737
- let status = "open";
9738
- if (pr.state === "closed") {
9739
- status = pr.merged_at ? "merged" : "closed";
9740
- } else if (pr.draft) {
9741
- status = "draft";
9742
- }
9743
- const changedLinesData = additionsDeletionsMap.get(pr.number);
9744
- const changedLines = changedLinesData ? {
9745
- added: changedLinesData.additions,
9746
- removed: changedLinesData.deletions
9747
- } : { added: 0, removed: 0 };
9748
- const comments = commentsMap.get(pr.number) || [];
9749
- const tickets = _GithubSCMLib.extractLinearTicketsFromComments(comments);
9750
- return {
9751
- submitRequestId: String(pr.number),
9752
- submitRequestNumber: pr.number,
9753
- title: pr.title,
9754
- status,
9755
- sourceBranch: pr.head.ref,
9756
- targetBranch: pr.base.ref,
9757
- authorName: pr.user?.name || pr.user?.login,
9758
- authorEmail: pr.user?.email || void 0,
9759
- createdAt: new Date(pr.created_at),
9760
- updatedAt: new Date(pr.updated_at),
9761
- description: pr.body || void 0,
9762
- tickets,
9763
- changedLines
9764
- };
9765
- });
9766
- return submitRequests;
9767
- }
9768
9257
  /**
9769
9258
  * Override searchSubmitRequests to use GitHub's Search API for efficient pagination.
9770
9259
  * This is much faster than fetching all PRs and filtering in-memory.
@@ -9983,146 +9472,26 @@ var GithubSCMLib = class _GithubSCMLib extends SCMLib {
9983
9472
  };
9984
9473
  }
9985
9474
  /**
9986
- * Parse a Linear ticket from URL and name
9987
- * 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.
9988
9477
  */
9989
- static _parseLinearTicket(url, name) {
9990
- if (!name || !url) {
9991
- return null;
9992
- }
9993
- const urlParts = url.split("/");
9994
- const titleSlug = urlParts[urlParts.length - 1] || "";
9995
- const title = titleSlug.replace(/-/g, " ");
9996
- return { name, title, url };
9478
+ extractLinearTicketsFromComments(comments) {
9479
+ return _GithubSCMLib._extractLinearTicketsFromCommentsImpl(comments);
9997
9480
  }
9998
9481
  /**
9999
- * Extract Linear ticket links from pre-fetched comments (pure function, no API calls)
10000
- * 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.
10001
9484
  */
10002
- static extractLinearTicketsFromComments(comments) {
9485
+ static _extractLinearTicketsFromCommentsImpl(comments) {
10003
9486
  const tickets = [];
10004
9487
  const seen = /* @__PURE__ */ new Set();
10005
9488
  for (const comment of comments) {
10006
9489
  if (comment.author?.login === "linear[bot]" || comment.author?.type === "Bot") {
10007
- const body = comment.body || "";
10008
- const htmlPattern = /<a href="(https:\/\/linear\.app\/[^"]+)">([A-Z]+-\d+)<\/a>/g;
10009
- let match;
10010
- while ((match = htmlPattern.exec(body)) !== null) {
10011
- const ticket = _GithubSCMLib._parseLinearTicket(match[1], match[2]);
10012
- if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
10013
- seen.add(`${ticket.name}|${ticket.url}`);
10014
- tickets.push(ticket);
10015
- }
10016
- }
10017
- const markdownPattern = /\[([A-Z]+-\d+)\]\((https:\/\/linear\.app\/[^)]+)\)/g;
10018
- while ((match = markdownPattern.exec(body)) !== null) {
10019
- const ticket = _GithubSCMLib._parseLinearTicket(match[2], match[1]);
10020
- if (ticket && !seen.has(`${ticket.name}|${ticket.url}`)) {
10021
- seen.add(`${ticket.name}|${ticket.url}`);
10022
- tickets.push(ticket);
10023
- }
10024
- }
9490
+ tickets.push(...extractLinearTicketsFromBody(comment.body || "", seen));
10025
9491
  }
10026
9492
  }
10027
9493
  return tickets;
10028
9494
  }
10029
- /**
10030
- * Optimized helper to parse added line numbers from a unified diff patch
10031
- * Single-pass parsing for minimal CPU usage
10032
- */
10033
- _parseAddedLinesFromPatch(patch) {
10034
- const addedLines = [];
10035
- const lines = patch.split("\n");
10036
- let currentLineNumber = 0;
10037
- for (const line of lines) {
10038
- if (line.startsWith("@@")) {
10039
- const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
10040
- if (match?.[1]) {
10041
- currentLineNumber = parseInt(match[1], 10);
10042
- }
10043
- continue;
10044
- }
10045
- if (line.startsWith("+") && !line.startsWith("+++")) {
10046
- addedLines.push(currentLineNumber);
10047
- currentLineNumber++;
10048
- } else if (!line.startsWith("-")) {
10049
- currentLineNumber++;
10050
- }
10051
- }
10052
- return addedLines;
10053
- }
10054
- /**
10055
- * Process blame data for a single file to attribute lines to commits
10056
- * Uses pre-fetched blame data instead of making API calls
10057
- */
10058
- _processFileBlameSafe(file, blameData, prCommitShas) {
10059
- const addedLines = this._parseAddedLinesFromPatch(file.patch);
10060
- const addedLinesSet = new Set(addedLines);
10061
- const fileAttributions = [];
10062
- for (const blameRange of blameData) {
10063
- if (!prCommitShas.has(blameRange.commitSha)) {
10064
- continue;
10065
- }
10066
- for (let lineNum = blameRange.startingLine; lineNum <= blameRange.endingLine; lineNum++) {
10067
- if (addedLinesSet.has(lineNum)) {
10068
- fileAttributions.push({
10069
- file: file.filename,
10070
- line: lineNum,
10071
- commitSha: blameRange.commitSha
10072
- });
10073
- }
10074
- }
10075
- }
10076
- return fileAttributions;
10077
- }
10078
- /**
10079
- * Optimized helper to attribute PR lines to commits using blame API
10080
- * Batch blame queries for minimal API call time (1 call instead of M calls)
10081
- *
10082
- * Uses size-based batching to handle large files:
10083
- * - Files > 1MB are processed individually with rate limiting
10084
- * - Smaller files are batched together in a single request
10085
- * This prevents GitHub API timeouts (~10s) on large generated files.
10086
- */
10087
- async _attributeLinesViaBlame(params) {
10088
- const { headSha, changedFiles, prCommits } = params;
10089
- const prCommitShas = new Set(prCommits.map((c) => c.commitSha));
10090
- const filesWithAdditions = changedFiles.filter((file) => {
10091
- if (!file.patch || file.patch.trim().length === 0) {
10092
- return false;
10093
- }
10094
- if (!file.sha) {
10095
- return false;
10096
- }
10097
- return true;
10098
- });
10099
- if (filesWithAdditions.length === 0) {
10100
- return [];
10101
- }
10102
- const { owner, repo } = parseGithubOwnerAndRepo(this.url);
10103
- const blameMap = await this.githubSdk.getBlameBatch({
10104
- owner,
10105
- repo,
10106
- ref: headSha,
10107
- // Use commit SHA directly from PR.head.sha
10108
- files: filesWithAdditions.map((f) => ({
10109
- path: f.filename,
10110
- blobSha: f.sha
10111
- })),
10112
- concurrency: GITHUB_API_CONCURRENCY
10113
- });
10114
- const allAttributions = [];
10115
- for (const file of filesWithAdditions) {
10116
- const blameData = blameMap.get(file.filename) || [];
10117
- const fileAttributions = this._processFileBlameSafe(
10118
- file,
10119
- blameData,
10120
- prCommitShas
10121
- );
10122
- allAttributions.push(...fileAttributions);
10123
- }
10124
- return allAttributions;
10125
- }
10126
9495
  };
10127
9496
 
10128
9497
  // src/features/analysis/scm/gitlab/gitlab.ts
@@ -10135,11 +9504,72 @@ import {
10135
9504
  Gitlab
10136
9505
  } from "@gitbeaker/rest";
10137
9506
  import Debug3 from "debug";
9507
+ import pLimit2 from "p-limit";
10138
9508
  import {
10139
9509
  Agent,
10140
9510
  fetch as undiciFetch,
10141
9511
  ProxyAgent as ProxyAgent2
10142
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
10143
9573
  init_env();
10144
9574
 
10145
9575
  // src/features/analysis/scm/gitlab/types.ts
@@ -10156,6 +9586,14 @@ function removeTrailingSlash2(str) {
10156
9586
  return str.trim().replace(/\/+$/, "");
10157
9587
  }
10158
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
+ }
10159
9597
  function getRandomGitlabCloudAnonToken() {
10160
9598
  if (!GITLAB_API_TOKEN || typeof GITLAB_API_TOKEN !== "string") {
10161
9599
  return void 0;
@@ -10209,7 +9647,10 @@ async function gitlabValidateParams({
10209
9647
  if (code === 404 || description.includes("404") || description.includes("Not Found")) {
10210
9648
  throw new InvalidRepoUrlError(`invalid gitlab repo URL: ${url}`);
10211
9649
  }
10212
- console.log("gitlabValidateParams error", e);
9650
+ contextLogger.warn("[gitlabValidateParams] Error validating params", {
9651
+ url,
9652
+ error: e
9653
+ });
10213
9654
  throw new InvalidRepoUrlError(
10214
9655
  `cannot access gitlab repo URL: ${url} with the provided access token`
10215
9656
  );
@@ -10238,6 +9679,13 @@ async function getGitlabIsUserCollaborator({
10238
9679
  ];
10239
9680
  return accessLevelWithWriteAccess.includes(groupAccess) || accessLevelWithWriteAccess.includes(projectAccess);
10240
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
+ );
10241
9689
  return false;
10242
9690
  }
10243
9691
  }
@@ -10284,6 +9732,14 @@ async function getGitlabIsRemoteBranch({
10284
9732
  const res = await api2.Branches.show(projectPath, branch);
10285
9733
  return res.name === branch;
10286
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
+ );
10287
9743
  return false;
10288
9744
  }
10289
9745
  }
@@ -10316,49 +9772,257 @@ async function getGitlabRepoList(url, accessToken) {
10316
9772
  })
10317
9773
  );
10318
9774
  }
10319
- async function getGitlabBranchList({
10320
- accessToken,
10321
- 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
10322
9996
  }) {
10323
- const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
10324
- const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
10325
9997
  try {
10326
- const res = await api2.Branches.all(projectPath, {
10327
- //keyset API pagination is not supported by GL for the branch list (at least not the on-prem version)
10328
- //so for now we stick with the default pagination and just return the first page and limit the results to 1000 entries.
10329
- //This is a temporary solution until we implement list branches with name search.
10330
- perPage: MAX_BRANCHES_FETCH,
10331
- page: 1
9998
+ const { projectPath } = parseGitlabOwnerAndRepo(url);
9999
+ const api2 = getGitBeaker({
10000
+ url,
10001
+ gitlabAuthToken: accessToken
10332
10002
  });
10333
- res.sort((a, b) => {
10334
- if (!a.commit?.committed_date || !b.commit?.committed_date) {
10335
- 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;
10336
10011
  }
10337
- return new Date(b.commit?.committed_date).getTime() - new Date(a.commit?.committed_date).getTime();
10338
- });
10339
- return res.map((branch) => branch.name).slice(0, MAX_BRANCHES_FETCH);
10340
- } catch (e) {
10341
- return [];
10342
- }
10343
- }
10344
- async function createMergeRequest(options) {
10345
- const { projectPath } = parseGitlabOwnerAndRepo(options.repoUrl);
10346
- const api2 = getGitBeaker({
10347
- url: options.repoUrl,
10348
- gitlabAuthToken: options.accessToken
10349
- });
10350
- const res = await api2.MergeRequests.create(
10351
- projectPath,
10352
- options.sourceBranchName,
10353
- options.targetBranchName,
10354
- options.title,
10355
- {
10356
- description: safeBody(options.body, MAX_GITLAB_PR_BODY_LENGTH)
10357
10012
  }
10358
- );
10359
- 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
+ }
10360
10024
  }
10361
- async function getGitlabMergeRequest({
10025
+ async function getGitlabMergeRequestMetrics({
10362
10026
  url,
10363
10027
  prNumber,
10364
10028
  accessToken
@@ -10368,7 +10032,28 @@ async function getGitlabMergeRequest({
10368
10032
  url,
10369
10033
  gitlabAuthToken: accessToken
10370
10034
  });
10371
- 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
+ };
10372
10057
  }
10373
10058
  async function getGitlabCommitUrl({
10374
10059
  url,
@@ -10447,26 +10132,125 @@ function parseGitlabOwnerAndRepo(gitlabUrl) {
10447
10132
  const { organization, repoName, projectPath } = parsingResult;
10448
10133
  return { owner: organization, repo: repoName, projectPath };
10449
10134
  }
10450
- async function getGitlabBlameRanges({ ref, gitlabUrl, path: path25 }, options) {
10451
- const { projectPath } = parseGitlabOwnerAndRepo(gitlabUrl);
10452
- const api2 = getGitBeaker({
10453
- url: gitlabUrl,
10454
- gitlabAuthToken: options?.gitlabAuthToken
10455
- });
10456
- const resp = await api2.RepositoryFiles.allFileBlames(projectPath, path25, ref);
10457
- let lineNumber = 1;
10458
- return resp.filter((range) => range.lines).map((range) => {
10459
- const oldLineNumber = lineNumber;
10460
- if (!range.lines) {
10461
- throw new Error("range.lines should not be undefined");
10462
- }
10463
- 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
+ }
10464
10239
  return {
10465
- startingLine: oldLineNumber,
10466
- endingLine: lineNumber - 1,
10467
- commitSha: range.commit.id
10240
+ remaining: remainingNum,
10241
+ reset: new Date(resetNum * 1e3),
10242
+ limit: limit ? parseInt(limit, 10) : void 0
10468
10243
  };
10469
- });
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
+ }
10470
10254
  }
10471
10255
  async function processBody(response) {
10472
10256
  const headers = response.headers;
@@ -10507,8 +10291,90 @@ async function brokerRequestHandler(endpoint, options) {
10507
10291
  };
10508
10292
  throw new Error(`gitbeaker: ${response.statusText}`);
10509
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
+ }
10510
10375
 
10511
10376
  // src/features/analysis/scm/gitlab/GitlabSCMLib.ts
10377
+ init_client_generates();
10512
10378
  var GitlabSCMLib = class extends SCMLib {
10513
10379
  constructor(url, accessToken, scmOrg) {
10514
10380
  super(url, accessToken, scmOrg);
@@ -10535,7 +10401,7 @@ var GitlabSCMLib = class extends SCMLib {
10535
10401
  }
10536
10402
  async getRepoList(_scmOrg) {
10537
10403
  if (!this.accessToken) {
10538
- console.error("no access token");
10404
+ contextLogger.warn("[GitlabSCMLib.getRepoList] No access token provided");
10539
10405
  throw new Error("no access token");
10540
10406
  }
10541
10407
  return getGitlabRepoList(this.url, this.accessToken);
@@ -10627,16 +10493,6 @@ var GitlabSCMLib = class extends SCMLib {
10627
10493
  markdownComment: comment
10628
10494
  });
10629
10495
  }
10630
- async getRepoBlameRanges(ref, path25) {
10631
- this._validateUrl();
10632
- return await getGitlabBlameRanges(
10633
- { ref, path: path25, gitlabUrl: this.url },
10634
- {
10635
- url: this.url,
10636
- gitlabAuthToken: this.accessToken
10637
- }
10638
- );
10639
- }
10640
10496
  async getReferenceData(ref) {
10641
10497
  this._validateUrl();
10642
10498
  return await getGitlabReferenceData(
@@ -10680,25 +10536,180 @@ var GitlabSCMLib = class extends SCMLib {
10680
10536
  this._validateAccessTokenAndUrl();
10681
10537
  return `${this.url}/-/commits/${branchName}`;
10682
10538
  }
10683
- async getCommitDiff(_commitSha) {
10684
- 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
+ };
10685
10554
  }
10686
- async getSubmitRequestDiff(_submitRequestId) {
10687
- 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
+ };
10688
10630
  }
10689
- async getSubmitRequests(_repoUrl) {
10690
- 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
+ });
10691
10638
  }
10692
- async searchSubmitRequests(_params) {
10693
- 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
+ });
10694
10646
  }
10695
10647
  async searchRepos(_params) {
10696
10648
  throw new Error("searchRepos not implemented for GitLab");
10697
10649
  }
10698
- // TODO: Add comprehensive tests for getPullRequestMetrics (GitLab)
10699
- // See clients/cli/src/features/analysis/scm/__tests__/github.test.ts:589-648 for reference
10700
- async getPullRequestMetrics(_prNumber) {
10701
- 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;
10702
10713
  }
10703
10714
  };
10704
10715
 
@@ -10757,10 +10768,6 @@ var StubSCMLib = class extends SCMLib {
10757
10768
  console.warn("getUserHasAccessToRepo() returning false");
10758
10769
  return false;
10759
10770
  }
10760
- async getRepoBlameRanges(_ref, _path) {
10761
- console.warn("getRepoBlameRanges() returning empty array");
10762
- return [];
10763
- }
10764
10771
  async getReferenceData(_ref) {
10765
10772
  console.warn("getReferenceData() returning null/empty defaults");
10766
10773
  return {
@@ -10825,14 +10832,18 @@ var StubSCMLib = class extends SCMLib {
10825
10832
  diffLines: []
10826
10833
  };
10827
10834
  }
10828
- async getSubmitRequests(_repoUrl) {
10829
- console.warn("getSubmitRequests() returning empty array");
10830
- return [];
10831
- }
10832
10835
  async getPullRequestMetrics(_prNumber) {
10833
10836
  console.warn("getPullRequestMetrics() returning empty object");
10834
10837
  throw new Error("getPullRequestMetrics() not implemented");
10835
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
+ }
10836
10847
  };
10837
10848
 
10838
10849
  // src/features/analysis/scm/scmFactory.ts
@@ -12853,7 +12864,7 @@ import Debug10 from "debug";
12853
12864
 
12854
12865
  // src/features/analysis/add_fix_comments_for_pr/utils/utils.ts
12855
12866
  import Debug9 from "debug";
12856
- import parseDiff from "parse-diff";
12867
+ import parseDiff2 from "parse-diff";
12857
12868
  import { z as z27 } from "zod";
12858
12869
 
12859
12870
  // src/features/analysis/utils/by_key.ts
@@ -13226,7 +13237,7 @@ Refresh the page in order to see the changes.`,
13226
13237
  }
13227
13238
  async function getRelevantVulenrabilitiesFromDiff(params) {
13228
13239
  const { gqlClient, diff, vulnerabilityReportId } = params;
13229
- const parsedDiff = parseDiff(diff);
13240
+ const parsedDiff = parseDiff2(diff);
13230
13241
  const fileHunks = parsedDiff.map((file) => {
13231
13242
  const fileNumbers = file.chunks.flatMap((chunk) => chunk.changes).filter((change) => change.type === "add").map((_change) => {
13232
13243
  const change = _change;
@@ -15327,7 +15338,7 @@ async function getRepositoryUrl() {
15327
15338
  }
15328
15339
  const remoteUrl = await gitService.getRemoteUrl();
15329
15340
  const parsed = parseScmURL(remoteUrl);
15330
- return parsed?.scmType === "GitHub" /* GitHub */ ? remoteUrl : null;
15341
+ return parsed?.scmType === "GitHub" /* GitHub */ || parsed?.scmType === "GitLab" /* GitLab */ ? remoteUrl : null;
15331
15342
  } catch {
15332
15343
  return null;
15333
15344
  }
@@ -21493,7 +21504,7 @@ import {
21493
21504
  writeFileSync
21494
21505
  } from "fs";
21495
21506
  import fs21 from "fs/promises";
21496
- import parseDiff2 from "parse-diff";
21507
+ import parseDiff3 from "parse-diff";
21497
21508
  import path20 from "path";
21498
21509
  var PatchApplicationService = class {
21499
21510
  /**
@@ -22177,7 +22188,7 @@ var PatchApplicationService = class {
22177
22188
  fixId,
22178
22189
  scanContext
22179
22190
  }) {
22180
- const parsedPatch = parseDiff2(patch);
22191
+ const parsedPatch = parseDiff3(patch);
22181
22192
  if (!parsedPatch || parsedPatch.length === 0) {
22182
22193
  throw new Error("Failed to parse patch - no changes found");
22183
22194
  }