plugin-git-manager 1.0.10 → 1.0.11
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/externalVersion.js +4 -4
- package/dist/server/actions/git-actions.js +30 -25
- package/dist/server/actions/gitlab-api.js +23 -10
- package/dist/server/actions/review.d.ts +1 -0
- package/dist/server/actions/review.js +105 -14
- package/dist/server/ai-tools.js +57 -9
- package/dist/server/collections/gitCodeReviews.d.ts +1 -1
- package/dist/server/collections/gitRepositories.d.ts +1 -1
- package/dist/server/collections/gitRepositories.js +1 -1
- package/dist/server/collections/gitReviewFlows.d.ts +1 -1
- package/dist/server/plugin.js +7 -0
- package/dist/server/poller.js +103 -27
- package/dist/server/utils/redact.d.ts +13 -0
- package/dist/server/utils/redact.js +49 -0
- package/package.json +1 -1
- package/src/server/actions/git-actions.ts +39 -25
- package/src/server/actions/gitlab-api.ts +34 -11
- package/src/server/actions/review.ts +161 -15
- package/src/server/ai-tools.ts +67 -8
- package/src/server/collections/gitRepositories.ts +1 -1
- package/src/server/plugin.ts +5 -0
- package/src/server/poller.ts +121 -35
- package/src/server/utils/redact.ts +24 -0
package/dist/externalVersion.js
CHANGED
|
@@ -11,8 +11,8 @@ module.exports = {
|
|
|
11
11
|
"react": "18.2.0",
|
|
12
12
|
"antd": "5.24.2",
|
|
13
13
|
"@ant-design/icons": "5.6.1",
|
|
14
|
-
"@nocobase/client": "2.0.
|
|
15
|
-
"@nocobase/server": "2.0.
|
|
16
|
-
"@nocobase/actions": "2.0.
|
|
17
|
-
"@nocobase/database": "2.0.
|
|
14
|
+
"@nocobase/client": "2.0.47",
|
|
15
|
+
"@nocobase/server": "2.0.47",
|
|
16
|
+
"@nocobase/actions": "2.0.47",
|
|
17
|
+
"@nocobase/database": "2.0.47"
|
|
18
18
|
};
|
|
@@ -53,7 +53,8 @@ module.exports = __toCommonJS(git_actions_exports);
|
|
|
53
53
|
var import_simple_git = __toESM(require("simple-git"));
|
|
54
54
|
var path = __toESM(require("path"));
|
|
55
55
|
var fs = __toESM(require("fs"));
|
|
56
|
-
|
|
56
|
+
var import_redact = require("../utils/redact");
|
|
57
|
+
const REF_PATTERN = /^(?!-)[a-zA-Z0-9._\-\/]+$/;
|
|
57
58
|
const repoLocks = /* @__PURE__ */ new Map();
|
|
58
59
|
function acquireLock(key) {
|
|
59
60
|
const prev = repoLocks.get(key) || Promise.resolve();
|
|
@@ -89,14 +90,16 @@ function validateRepoUrl(repoUrl) {
|
|
|
89
90
|
throw new Error("Only HTTPS repository URLs are allowed");
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
async function withAuth(git, repoUrl, pat, fn, username) {
|
|
93
|
-
const lockKey =
|
|
93
|
+
async function withAuth(git, localPath, repoUrl, pat, fn, username) {
|
|
94
|
+
const lockKey = localPath;
|
|
94
95
|
const lock = acquireLock(lockKey);
|
|
95
96
|
await lock.promise;
|
|
96
97
|
const authUrl = getAuthUrl(repoUrl, pat, username);
|
|
97
98
|
await git.remote(["set-url", "origin", authUrl]);
|
|
98
99
|
try {
|
|
99
100
|
return await fn();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw (0, import_redact.redactError)(err);
|
|
100
103
|
} finally {
|
|
101
104
|
try {
|
|
102
105
|
await git.remote(["set-url", "origin", repoUrl]);
|
|
@@ -105,8 +108,8 @@ async function withAuth(git, repoUrl, pat, fn, username) {
|
|
|
105
108
|
await git.remote(["set-url", "origin", repoUrl]);
|
|
106
109
|
} catch {
|
|
107
110
|
console.error(
|
|
108
|
-
`[plugin-git-manager] CRITICAL: failed to remove PAT from remote URL for ${repoUrl}. Manual cleanup of .git/config may be required.`,
|
|
109
|
-
cleanupErr
|
|
111
|
+
`[plugin-git-manager] CRITICAL: failed to remove PAT from remote URL for ${(0, import_redact.redactPat)(repoUrl)}. Manual cleanup of .git/config may be required.`,
|
|
112
|
+
(0, import_redact.redactError)(cleanupErr)
|
|
110
113
|
);
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -114,9 +117,9 @@ async function withAuth(git, repoUrl, pat, fn, username) {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
function getAuthUrl(repoUrl, pat, username) {
|
|
117
|
-
const url = new URL(repoUrl);
|
|
118
|
-
url.username = username || "oauth2";
|
|
119
|
-
url.password = pat;
|
|
120
|
+
const url = new URL(repoUrl.trim());
|
|
121
|
+
url.username = (username || "oauth2").trim();
|
|
122
|
+
url.password = pat.trim();
|
|
120
123
|
return url.toString();
|
|
121
124
|
}
|
|
122
125
|
function getGit(localPath) {
|
|
@@ -144,10 +147,12 @@ function validateLocalPath(localPath) {
|
|
|
144
147
|
async function clone(ctx, next) {
|
|
145
148
|
const repo = await getRepo(ctx);
|
|
146
149
|
const localPath = validateLocalPath(repo.get("localPath"));
|
|
147
|
-
const repoUrl = repo.get("repoUrl");
|
|
148
|
-
const pat = repo.get("pat");
|
|
149
|
-
const username = repo.get("username");
|
|
150
|
+
const repoUrl = (repo.get("repoUrl") || "").trim();
|
|
151
|
+
const pat = (repo.get("pat") || "").trim();
|
|
152
|
+
const username = (repo.get("username") || "").trim();
|
|
153
|
+
const defaultBranch = (repo.get("defaultBranch") || "main").trim() || "main";
|
|
150
154
|
validateRepoUrl(repoUrl);
|
|
155
|
+
validateBranch(defaultBranch);
|
|
151
156
|
if (fs.existsSync(localPath)) {
|
|
152
157
|
ctx.throw(400, "Directory already exists. Remove it before cloning again.");
|
|
153
158
|
}
|
|
@@ -156,7 +161,7 @@ async function clone(ctx, next) {
|
|
|
156
161
|
}
|
|
157
162
|
const authUrl = getAuthUrl(repoUrl, pat, username);
|
|
158
163
|
try {
|
|
159
|
-
await (0, import_simple_git.default)().clone(authUrl, localPath, ["--branch",
|
|
164
|
+
await (0, import_simple_git.default)().clone(authUrl, localPath, ["--branch", defaultBranch]);
|
|
160
165
|
await (0, import_simple_git.default)(localPath).remote(["set-url", "origin", repoUrl]);
|
|
161
166
|
await ctx.db.getRepository("gitRepositories").update({
|
|
162
167
|
filterByTk: repo.get("id"),
|
|
@@ -168,40 +173,40 @@ async function clone(ctx, next) {
|
|
|
168
173
|
filterByTk: repo.get("id"),
|
|
169
174
|
values: { status: "error" }
|
|
170
175
|
});
|
|
171
|
-
throw err;
|
|
176
|
+
throw (0, import_redact.redactError)(err);
|
|
172
177
|
}
|
|
173
178
|
await next();
|
|
174
179
|
}
|
|
175
180
|
async function pull(ctx, next) {
|
|
176
181
|
const repo = await getRepo(ctx);
|
|
177
182
|
const localPath = validateLocalPath(repo.get("localPath"));
|
|
178
|
-
const pat = repo.get("pat");
|
|
179
|
-
const repoUrl = repo.get("repoUrl");
|
|
180
|
-
const username = repo.get("username");
|
|
183
|
+
const pat = (repo.get("pat") || "").trim();
|
|
184
|
+
const repoUrl = (repo.get("repoUrl") || "").trim();
|
|
185
|
+
const username = (repo.get("username") || "").trim();
|
|
181
186
|
const git = getGit(localPath);
|
|
182
|
-
const result = await withAuth(git, repoUrl, pat, () => git.pull(), username);
|
|
187
|
+
const result = await withAuth(git, localPath, repoUrl, pat, () => git.pull(), username);
|
|
183
188
|
ctx.body = { success: true, data: result };
|
|
184
189
|
await next();
|
|
185
190
|
}
|
|
186
191
|
async function push(ctx, next) {
|
|
187
192
|
const repo = await getRepo(ctx);
|
|
188
193
|
const localPath = validateLocalPath(repo.get("localPath"));
|
|
189
|
-
const pat = repo.get("pat");
|
|
190
|
-
const repoUrl = repo.get("repoUrl");
|
|
191
|
-
const username = repo.get("username");
|
|
194
|
+
const pat = (repo.get("pat") || "").trim();
|
|
195
|
+
const repoUrl = (repo.get("repoUrl") || "").trim();
|
|
196
|
+
const username = (repo.get("username") || "").trim();
|
|
192
197
|
const git = getGit(localPath);
|
|
193
|
-
const result = await withAuth(git, repoUrl, pat, () => git.push(), username);
|
|
198
|
+
const result = await withAuth(git, localPath, repoUrl, pat, () => git.push(), username);
|
|
194
199
|
ctx.body = { success: true, data: result };
|
|
195
200
|
await next();
|
|
196
201
|
}
|
|
197
202
|
async function fetch(ctx, next) {
|
|
198
203
|
const repo = await getRepo(ctx);
|
|
199
204
|
const localPath = validateLocalPath(repo.get("localPath"));
|
|
200
|
-
const pat = repo.get("pat");
|
|
201
|
-
const repoUrl = repo.get("repoUrl");
|
|
202
|
-
const username = repo.get("username");
|
|
205
|
+
const pat = (repo.get("pat") || "").trim();
|
|
206
|
+
const repoUrl = (repo.get("repoUrl") || "").trim();
|
|
207
|
+
const username = (repo.get("username") || "").trim();
|
|
203
208
|
const git = getGit(localPath);
|
|
204
|
-
const result = await withAuth(git, repoUrl, pat, () => git.fetch(), username);
|
|
209
|
+
const result = await withAuth(git, localPath, repoUrl, pat, () => git.fetch(), username);
|
|
205
210
|
ctx.body = { success: true, data: result };
|
|
206
211
|
await next();
|
|
207
212
|
}
|
|
@@ -66,8 +66,8 @@ async function getRepoApiContext(ctx) {
|
|
|
66
66
|
if (!repo) {
|
|
67
67
|
ctx.throw(404, "Repository not found");
|
|
68
68
|
}
|
|
69
|
-
const pat = repo.get("pat");
|
|
70
|
-
const repoUrl = repo.get("repoUrl");
|
|
69
|
+
const pat = (repo.get("pat") || "").trim();
|
|
70
|
+
const repoUrl = (repo.get("repoUrl") || "").trim();
|
|
71
71
|
const isGitHub = repoUrl.includes("github.com");
|
|
72
72
|
const { apiBase, encodedProject, projectPath } = (0, import_gitlab_url.parseGitLabProject)(repoUrl);
|
|
73
73
|
return { repo, pat, apiBase, encodedProject, projectPath, isGitHub };
|
|
@@ -128,8 +128,14 @@ async function mergeRequests(ctx, next) {
|
|
|
128
128
|
direction: sort
|
|
129
129
|
});
|
|
130
130
|
let pullRequests = result.data || [];
|
|
131
|
+
let mergedFilterApplied = false;
|
|
131
132
|
if (state === "merged") {
|
|
132
133
|
pullRequests = pullRequests.filter((pr) => pr.merged_at);
|
|
134
|
+
mergedFilterApplied = true;
|
|
135
|
+
}
|
|
136
|
+
if (mergedFilterApplied) {
|
|
137
|
+
result.totalPages = null;
|
|
138
|
+
result.total = null;
|
|
133
139
|
}
|
|
134
140
|
items = pullRequests.map((pr) => {
|
|
135
141
|
var _a2, _b;
|
|
@@ -147,18 +153,18 @@ async function mergeRequests(ctx, next) {
|
|
|
147
153
|
labels: (pr.labels || []).map((l) => l.name),
|
|
148
154
|
draft: pr.draft || false,
|
|
149
155
|
mergedBy: null,
|
|
150
|
-
// Not returned
|
|
156
|
+
// Not returned by the PR list endpoint
|
|
151
157
|
mergedAt: pr.merged_at,
|
|
152
158
|
createdAt: pr.created_at,
|
|
153
159
|
updatedAt: pr.updated_at,
|
|
154
|
-
userNotesCount: 0,
|
|
155
|
-
// Not returned in pull list API
|
|
160
|
+
userNotesCount: typeof pr.comments === "number" ? pr.comments : 0,
|
|
156
161
|
upvotes: 0,
|
|
157
162
|
downvotes: 0,
|
|
158
163
|
webUrl: pr.html_url,
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
164
|
+
// GitHub's PR list does not include mergeability info — surface as
|
|
165
|
+
// `null` (unknown) so the UI doesn't render a misleading "no conflicts".
|
|
166
|
+
hasConflicts: null,
|
|
167
|
+
changesCount: typeof pr.changed_files === "number" ? pr.changed_files : null
|
|
162
168
|
};
|
|
163
169
|
});
|
|
164
170
|
} else {
|
|
@@ -252,8 +258,15 @@ async function mergeRequestDetail(ctx, next) {
|
|
|
252
258
|
// Not always readily available on GitHub without extra call
|
|
253
259
|
closedAt: pr.closed_at,
|
|
254
260
|
webUrl: pr.html_url,
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
// `mergeable_state === 'unknown'` when GitHub is still computing — surface
|
|
262
|
+
// `null` instead of `false` so the UI can distinguish "no conflicts" from
|
|
263
|
+
// "not yet known".
|
|
264
|
+
hasConflicts: pr.mergeable_state === "dirty" ? true : pr.mergeable_state === "unknown" || pr.mergeable === null ? null : false,
|
|
265
|
+
diffStats: {
|
|
266
|
+
additions: typeof pr.additions === "number" ? pr.additions : null,
|
|
267
|
+
deletions: typeof pr.deletions === "number" ? pr.deletions : null,
|
|
268
|
+
changedFiles: typeof pr.changed_files === "number" ? pr.changed_files : null
|
|
269
|
+
},
|
|
257
270
|
changes
|
|
258
271
|
};
|
|
259
272
|
} else {
|
|
@@ -33,4 +33,5 @@ export declare function reviewApprovePost(ctx: Context, next: () => Promise<void
|
|
|
33
33
|
export declare function reviewReject(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
34
34
|
export declare function branchMatches(flow: any, branch: string): boolean;
|
|
35
35
|
export declare function pickFlowMatchingBranch(flows: any[], branch?: string): any | null;
|
|
36
|
+
export declare function recoverStuckReviews(app: Application): Promise<number>;
|
|
36
37
|
export {};
|
|
@@ -28,6 +28,7 @@ var review_exports = {};
|
|
|
28
28
|
__export(review_exports, {
|
|
29
29
|
branchMatches: () => branchMatches,
|
|
30
30
|
pickFlowMatchingBranch: () => pickFlowMatchingBranch,
|
|
31
|
+
recoverStuckReviews: () => recoverStuckReviews,
|
|
31
32
|
reviewApprovePost: () => reviewApprovePost,
|
|
32
33
|
reviewReject: () => reviewReject,
|
|
33
34
|
triggerReview: () => triggerReview,
|
|
@@ -35,6 +36,17 @@ __export(review_exports, {
|
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(review_exports);
|
|
37
38
|
var import_gitlab_url = require("../utils/gitlab-url");
|
|
39
|
+
var import_redact = require("../utils/redact");
|
|
40
|
+
function targetKey(args) {
|
|
41
|
+
if (args.targetType === "mr") return `${args.repositoryId}:mr:${args.mrIid}`;
|
|
42
|
+
if (args.targetType === "commit") return `${args.repositoryId}:commit:${args.commitSha}`;
|
|
43
|
+
return `${args.repositoryId}:branch:${args.branch}`;
|
|
44
|
+
}
|
|
45
|
+
const TRIGGER_LOCK_TTL_MS = 3e4;
|
|
46
|
+
async function withTriggerLock(app, key, fn) {
|
|
47
|
+
const lockKey = `git-review:trigger:${key}`;
|
|
48
|
+
return app.lockManager.runExclusive(lockKey, fn, TRIGGER_LOCK_TTL_MS);
|
|
49
|
+
}
|
|
38
50
|
async function triggerReview(ctx, next) {
|
|
39
51
|
var _a, _b, _c;
|
|
40
52
|
const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
|
|
@@ -73,6 +85,9 @@ async function triggerReview(ctx, next) {
|
|
|
73
85
|
await next();
|
|
74
86
|
}
|
|
75
87
|
async function triggerReviewInternal(app, args) {
|
|
88
|
+
return withTriggerLock(app, targetKey(args), () => triggerReviewInternalLocked(app, args));
|
|
89
|
+
}
|
|
90
|
+
async function triggerReviewInternalLocked(app, args) {
|
|
76
91
|
const db = app.db;
|
|
77
92
|
const flowsRepo = db.getRepository("gitReviewFlows");
|
|
78
93
|
let flow = null;
|
|
@@ -123,12 +138,19 @@ async function triggerReviewInternal(app, args) {
|
|
|
123
138
|
latestSha: headSha || existingLatestSha || null,
|
|
124
139
|
triggeredBy: args.triggeredBy || "manual",
|
|
125
140
|
status: "pending",
|
|
141
|
+
// Stamp startedAt synchronously so `recoverStuckReviews` can sweep rows
|
|
142
|
+
// that get stuck in `pending` (process died before runReview ran).
|
|
143
|
+
// runReview's own update will refresh this on actual start.
|
|
144
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
145
|
+
finishedAt: null,
|
|
146
|
+
durationMs: null,
|
|
126
147
|
postStatus: flow.get("postMode") === "disabled" ? "skipped" : "pending_approval",
|
|
127
148
|
error: null
|
|
128
149
|
};
|
|
129
150
|
let reviewId;
|
|
130
151
|
if (existing) {
|
|
131
|
-
|
|
152
|
+
const st = existing.get("status");
|
|
153
|
+
if (st === "running" || st === "pending") {
|
|
132
154
|
return existing.get("id");
|
|
133
155
|
}
|
|
134
156
|
await reviewsRepo.update({
|
|
@@ -356,11 +378,12 @@ async function runReview(app, args) {
|
|
|
356
378
|
}
|
|
357
379
|
} catch (err) {
|
|
358
380
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
381
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
359
382
|
await reviewsRepo.update({
|
|
360
383
|
filterByTk: args.reviewId,
|
|
361
384
|
values: {
|
|
362
385
|
status: "failed",
|
|
363
|
-
error:
|
|
386
|
+
error: safeMessage,
|
|
364
387
|
finishedAt,
|
|
365
388
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
366
389
|
}
|
|
@@ -409,18 +432,39 @@ function buildReviewPrompt(args) {
|
|
|
409
432
|
return lines.join("\n");
|
|
410
433
|
}
|
|
411
434
|
function extractLastAiMessageContent(result) {
|
|
412
|
-
|
|
413
|
-
if (!
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!msg)
|
|
417
|
-
|
|
418
|
-
|
|
435
|
+
const messages = result == null ? void 0 : result.messages;
|
|
436
|
+
if (!Array.isArray(messages)) return "";
|
|
437
|
+
const isAiMessage = (msg) => {
|
|
438
|
+
var _a;
|
|
439
|
+
if (!msg) return false;
|
|
440
|
+
if (typeof msg._getType === "function") {
|
|
441
|
+
try {
|
|
442
|
+
return msg._getType() === "ai";
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (typeof msg.role === "string") {
|
|
447
|
+
const r = msg.role.toLowerCase();
|
|
448
|
+
return r === "assistant" || r === "ai";
|
|
449
|
+
}
|
|
450
|
+
if (typeof msg.type === "string" && msg.type.toLowerCase() === "ai") return true;
|
|
451
|
+
const name = (_a = msg == null ? void 0 : msg.constructor) == null ? void 0 : _a.name;
|
|
452
|
+
if (name === "AIMessage" || name === "AIMessageChunk") return true;
|
|
453
|
+
return false;
|
|
454
|
+
};
|
|
455
|
+
const getContent = (msg) => {
|
|
419
456
|
if (typeof msg.content === "string") return msg.content;
|
|
420
457
|
if (Array.isArray(msg.content)) {
|
|
421
|
-
const textBlock = msg.content.find((c) => c.type === "text");
|
|
422
|
-
if (textBlock == null ? void 0 : textBlock.text) return textBlock.text;
|
|
458
|
+
const textBlock = msg.content.find((c) => (c == null ? void 0 : c.type) === "text");
|
|
459
|
+
if (typeof (textBlock == null ? void 0 : textBlock.text) === "string") return textBlock.text;
|
|
423
460
|
}
|
|
461
|
+
return "";
|
|
462
|
+
};
|
|
463
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
464
|
+
const msg = messages[i];
|
|
465
|
+
if (!isAiMessage(msg)) continue;
|
|
466
|
+
const content = getContent(msg);
|
|
467
|
+
if (content) return content;
|
|
424
468
|
}
|
|
425
469
|
return "";
|
|
426
470
|
}
|
|
@@ -495,14 +539,26 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
495
539
|
}
|
|
496
540
|
}
|
|
497
541
|
const MAX_BRANCH_FILTER_LENGTH = 200;
|
|
542
|
+
const loggedBadFilters = /* @__PURE__ */ new Set();
|
|
543
|
+
function warnInvalidBranchFilter(filter, reason) {
|
|
544
|
+
if (loggedBadFilters.has(filter)) return;
|
|
545
|
+
loggedBadFilters.add(filter);
|
|
546
|
+
console.warn(
|
|
547
|
+
`[plugin-git-manager] branchFilter rejected (${reason}): ${JSON.stringify(filter)}. Flow will not match any branch.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
498
550
|
function branchMatches(flow, branch) {
|
|
499
551
|
const filter = flow.get("branchFilter");
|
|
500
552
|
if (!filter) return true;
|
|
501
|
-
if (filter.length > MAX_BRANCH_FILTER_LENGTH)
|
|
553
|
+
if (filter.length > MAX_BRANCH_FILTER_LENGTH) {
|
|
554
|
+
warnInvalidBranchFilter(filter, `too long (>${MAX_BRANCH_FILTER_LENGTH} chars)`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
502
557
|
try {
|
|
503
558
|
return new RegExp(filter).test(branch);
|
|
504
|
-
} catch {
|
|
505
|
-
|
|
559
|
+
} catch (err) {
|
|
560
|
+
warnInvalidBranchFilter(filter, `invalid regex: ${(err == null ? void 0 : err.message) || err}`);
|
|
561
|
+
return false;
|
|
506
562
|
}
|
|
507
563
|
}
|
|
508
564
|
function pickFlowMatchingBranch(flows, branch) {
|
|
@@ -518,10 +574,45 @@ function throwHttp(status, message) {
|
|
|
518
574
|
err.status = status;
|
|
519
575
|
throw err;
|
|
520
576
|
}
|
|
577
|
+
const STUCK_REVIEW_CUTOFF_MS = 10 * 60 * 1e3;
|
|
578
|
+
async function recoverStuckReviews(app) {
|
|
579
|
+
var _a, _b, _c, _d;
|
|
580
|
+
try {
|
|
581
|
+
const reviewsRepo = app.db.getRepository("gitCodeReviews");
|
|
582
|
+
const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
|
|
583
|
+
const stuck = await reviewsRepo.find({
|
|
584
|
+
filter: {
|
|
585
|
+
status: { $in: ["running", "pending"] },
|
|
586
|
+
startedAt: { $lt: cutoff }
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
if (!(stuck == null ? void 0 : stuck.length)) return 0;
|
|
590
|
+
const finishedAt = /* @__PURE__ */ new Date();
|
|
591
|
+
for (const review of stuck) {
|
|
592
|
+
const startedAt = review.get("startedAt");
|
|
593
|
+
const durationMs = startedAt ? finishedAt.getTime() - new Date(startedAt).getTime() : null;
|
|
594
|
+
await reviewsRepo.update({
|
|
595
|
+
filterByTk: review.get("id"),
|
|
596
|
+
values: {
|
|
597
|
+
status: "failed",
|
|
598
|
+
error: "Review interrupted by application restart",
|
|
599
|
+
finishedAt,
|
|
600
|
+
durationMs
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
(_b = (_a = app.log) == null ? void 0 : _a.info) == null ? void 0 : _b.call(_a, `plugin-git-manager: marked ${stuck.length} stuck review(s) as failed after restart`);
|
|
605
|
+
return stuck.length;
|
|
606
|
+
} catch (err) {
|
|
607
|
+
(_d = (_c = app.log) == null ? void 0 : _c.error) == null ? void 0 : _d.call(_c, `plugin-git-manager: recoverStuckReviews failed: ${err == null ? void 0 : err.message}`);
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
521
611
|
// Annotate the CommonJS export names for ESM import in node:
|
|
522
612
|
0 && (module.exports = {
|
|
523
613
|
branchMatches,
|
|
524
614
|
pickFlowMatchingBranch,
|
|
615
|
+
recoverStuckReviews,
|
|
525
616
|
reviewApprovePost,
|
|
526
617
|
reviewReject,
|
|
527
618
|
triggerReview,
|
package/dist/server/ai-tools.js
CHANGED
|
@@ -49,7 +49,31 @@ function registerGitReviewAiTools(app) {
|
|
|
49
49
|
(_c = (_b = app.log) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, "plugin-git-manager: AIManager.toolsManager not available; skipping AI tool registration");
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const enforceAcl = (ctx, resource, action) => {
|
|
53
|
+
var _a2, _b2, _c2, _d2;
|
|
54
|
+
const user = (_a2 = ctx == null ? void 0 : ctx.state) == null ? void 0 : _a2.currentUser;
|
|
55
|
+
if (!(user == null ? void 0 : user.id)) {
|
|
56
|
+
const err = new Error("AI tool requires an authenticated user context");
|
|
57
|
+
err.status = 401;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
const acl = (_b2 = ctx.app) == null ? void 0 : _b2.acl;
|
|
61
|
+
if (!(acl == null ? void 0 : acl.can)) return;
|
|
62
|
+
const role = ((_c2 = ctx == null ? void 0 : ctx.state) == null ? void 0 : _c2.currentRole) || Array.isArray(user.roles) && ((_d2 = user.roles[0]) == null ? void 0 : _d2.name) || null;
|
|
63
|
+
if (!role) {
|
|
64
|
+
const err = new Error("AI tool requires a resolvable role on the current user");
|
|
65
|
+
err.status = 403;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const allowed = acl.can({ role, resource, action });
|
|
69
|
+
if (!allowed) {
|
|
70
|
+
const err = new Error(`Permission denied: ${resource}:${action}`);
|
|
71
|
+
err.status = 403;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const runResourceAction = async (ctx, handler, params, gate) => {
|
|
76
|
+
enforceAcl(ctx, gate.resource, gate.action);
|
|
53
77
|
const synthCtx = {
|
|
54
78
|
...ctx,
|
|
55
79
|
app: ctx.app,
|
|
@@ -80,7 +104,10 @@ function registerGitReviewAiTools(app) {
|
|
|
80
104
|
})
|
|
81
105
|
},
|
|
82
106
|
invoke: async (ctx, args) => {
|
|
83
|
-
const body = await runResourceAction(ctx, gitlabApi.mergeRequestDetail, args
|
|
107
|
+
const body = await runResourceAction(ctx, gitlabApi.mergeRequestDetail, args, {
|
|
108
|
+
resource: "gitManager",
|
|
109
|
+
action: "mergeRequestDetail"
|
|
110
|
+
});
|
|
84
111
|
return (body == null ? void 0 : body.data) ?? body;
|
|
85
112
|
}
|
|
86
113
|
},
|
|
@@ -102,7 +129,10 @@ function registerGitReviewAiTools(app) {
|
|
|
102
129
|
})
|
|
103
130
|
},
|
|
104
131
|
invoke: async (ctx, args) => {
|
|
105
|
-
const body = await runResourceAction(ctx, gitlabApi.mergeRequests, args
|
|
132
|
+
const body = await runResourceAction(ctx, gitlabApi.mergeRequests, args, {
|
|
133
|
+
resource: "gitManager",
|
|
134
|
+
action: "mergeRequests"
|
|
135
|
+
});
|
|
106
136
|
return (body == null ? void 0 : body.data) ?? body;
|
|
107
137
|
}
|
|
108
138
|
},
|
|
@@ -121,7 +151,10 @@ function registerGitReviewAiTools(app) {
|
|
|
121
151
|
})
|
|
122
152
|
},
|
|
123
153
|
invoke: async (ctx, args) => {
|
|
124
|
-
const body = await runResourceAction(ctx, gitlabApi.mergeRequestNotes, args
|
|
154
|
+
const body = await runResourceAction(ctx, gitlabApi.mergeRequestNotes, args, {
|
|
155
|
+
resource: "gitManager",
|
|
156
|
+
action: "mergeRequestNotes"
|
|
157
|
+
});
|
|
125
158
|
return (body == null ? void 0 : body.data) ?? body;
|
|
126
159
|
}
|
|
127
160
|
},
|
|
@@ -140,7 +173,10 @@ function registerGitReviewAiTools(app) {
|
|
|
140
173
|
})
|
|
141
174
|
},
|
|
142
175
|
invoke: async (ctx, args) => {
|
|
143
|
-
const body = await runResourceAction(ctx, gitActions.commitDetail, args
|
|
176
|
+
const body = await runResourceAction(ctx, gitActions.commitDetail, args, {
|
|
177
|
+
resource: "gitManager",
|
|
178
|
+
action: "commitDetail"
|
|
179
|
+
});
|
|
144
180
|
return (body == null ? void 0 : body.data) ?? body;
|
|
145
181
|
}
|
|
146
182
|
},
|
|
@@ -161,7 +197,10 @@ function registerGitReviewAiTools(app) {
|
|
|
161
197
|
})
|
|
162
198
|
},
|
|
163
199
|
invoke: async (ctx, args) => {
|
|
164
|
-
const body = await runResourceAction(ctx, gitActions.diff, args
|
|
200
|
+
const body = await runResourceAction(ctx, gitActions.diff, args, {
|
|
201
|
+
resource: "gitManager",
|
|
202
|
+
action: "diff"
|
|
203
|
+
});
|
|
165
204
|
return (body == null ? void 0 : body.data) ?? body;
|
|
166
205
|
}
|
|
167
206
|
},
|
|
@@ -181,7 +220,10 @@ function registerGitReviewAiTools(app) {
|
|
|
181
220
|
})
|
|
182
221
|
},
|
|
183
222
|
invoke: async (ctx, args) => {
|
|
184
|
-
const body = await runResourceAction(ctx, gitActions.fileContent, args
|
|
223
|
+
const body = await runResourceAction(ctx, gitActions.fileContent, args, {
|
|
224
|
+
resource: "gitManager",
|
|
225
|
+
action: "fileContent"
|
|
226
|
+
});
|
|
185
227
|
return (body == null ? void 0 : body.data) ?? body;
|
|
186
228
|
}
|
|
187
229
|
},
|
|
@@ -197,7 +239,10 @@ function registerGitReviewAiTools(app) {
|
|
|
197
239
|
schema: import_zod.z.object({ repositoryId: import_zod.z.number() })
|
|
198
240
|
},
|
|
199
241
|
invoke: async (ctx, args) => {
|
|
200
|
-
const body = await runResourceAction(ctx, gitActions.branches, args
|
|
242
|
+
const body = await runResourceAction(ctx, gitActions.branches, args, {
|
|
243
|
+
resource: "gitManager",
|
|
244
|
+
action: "branches"
|
|
245
|
+
});
|
|
201
246
|
return (body == null ? void 0 : body.data) ?? body;
|
|
202
247
|
}
|
|
203
248
|
},
|
|
@@ -217,7 +262,10 @@ function registerGitReviewAiTools(app) {
|
|
|
217
262
|
})
|
|
218
263
|
},
|
|
219
264
|
invoke: async (ctx, args) => {
|
|
220
|
-
const body = await runResourceAction(ctx, gitActions.log, args
|
|
265
|
+
const body = await runResourceAction(ctx, gitActions.log, args, {
|
|
266
|
+
resource: "gitManager",
|
|
267
|
+
action: "log"
|
|
268
|
+
});
|
|
221
269
|
return (body == null ? void 0 : body.data) ?? body;
|
|
222
270
|
}
|
|
223
271
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -62,7 +62,7 @@ var gitRepositories_default = (0, import_database.defineCollection)({
|
|
|
62
62
|
uiSchema: { title: "Local Path", type: "string", "x-component": "Input" }
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
|
-
type: "
|
|
65
|
+
type: "string",
|
|
66
66
|
name: "pat",
|
|
67
67
|
interface: "password",
|
|
68
68
|
uiSchema: { title: "Personal Access Token", type: "string", "x-component": "Password" }
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
package/dist/server/plugin.js
CHANGED
|
@@ -46,6 +46,7 @@ var gitActions = __toESM(require("./actions/git-actions"));
|
|
|
46
46
|
var gitlabApi = __toESM(require("./actions/gitlab-api"));
|
|
47
47
|
var reviewActions = __toESM(require("./actions/review"));
|
|
48
48
|
var pollerActions = __toESM(require("./actions/poller"));
|
|
49
|
+
var import_review = require("./actions/review");
|
|
49
50
|
var import_ai_tools = require("./ai-tools");
|
|
50
51
|
var import_poller = require("./poller");
|
|
51
52
|
class PluginGitManagerServer extends import_server.Plugin {
|
|
@@ -80,6 +81,12 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
80
81
|
});
|
|
81
82
|
(0, import_ai_tools.registerGitReviewAiTools)(this.app);
|
|
82
83
|
this.app.on("afterStart", () => {
|
|
84
|
+
(0, import_review.recoverStuckReviews)(this.app).catch(
|
|
85
|
+
(err) => {
|
|
86
|
+
var _a, _b;
|
|
87
|
+
return (_b = (_a = this.app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "plugin-git-manager: recoverStuckReviews error", err);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
83
90
|
(0, import_poller.startPoller)(this.app);
|
|
84
91
|
});
|
|
85
92
|
this.app.on("beforeStop", () => {
|