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