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