plugin-git-manager 1.0.10 → 1.0.12
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 +106 -14
- package/dist/server/ai-tools.js +57 -9
- package/dist/server/collections/gitRepositories.js +1 -1
- package/dist/server/plugin.js +19 -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 +162 -15
- package/src/server/ai-tools.ts +67 -8
- package/src/server/collections/gitRepositories.ts +1 -1
- package/src/server/plugin.ts +24 -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({
|
|
@@ -251,6 +273,7 @@ async function runReview(app, args) {
|
|
|
251
273
|
db,
|
|
252
274
|
state: { currentUser: args.userId ? { id: args.userId } : null },
|
|
253
275
|
req: { headers: { "x-timezone": "UTC", "x-locale": "en-US" } },
|
|
276
|
+
log: app.logger || console,
|
|
254
277
|
get(name) {
|
|
255
278
|
return this.req.headers[String(name).toLowerCase()];
|
|
256
279
|
},
|
|
@@ -356,11 +379,12 @@ async function runReview(app, args) {
|
|
|
356
379
|
}
|
|
357
380
|
} catch (err) {
|
|
358
381
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
382
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
359
383
|
await reviewsRepo.update({
|
|
360
384
|
filterByTk: args.reviewId,
|
|
361
385
|
values: {
|
|
362
386
|
status: "failed",
|
|
363
|
-
error:
|
|
387
|
+
error: safeMessage,
|
|
364
388
|
finishedAt,
|
|
365
389
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
366
390
|
}
|
|
@@ -409,18 +433,39 @@ function buildReviewPrompt(args) {
|
|
|
409
433
|
return lines.join("\n");
|
|
410
434
|
}
|
|
411
435
|
function extractLastAiMessageContent(result) {
|
|
412
|
-
|
|
413
|
-
if (!
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!msg)
|
|
417
|
-
|
|
418
|
-
|
|
436
|
+
const messages = result == null ? void 0 : result.messages;
|
|
437
|
+
if (!Array.isArray(messages)) return "";
|
|
438
|
+
const isAiMessage = (msg) => {
|
|
439
|
+
var _a;
|
|
440
|
+
if (!msg) return false;
|
|
441
|
+
if (typeof msg._getType === "function") {
|
|
442
|
+
try {
|
|
443
|
+
return msg._getType() === "ai";
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof msg.role === "string") {
|
|
448
|
+
const r = msg.role.toLowerCase();
|
|
449
|
+
return r === "assistant" || r === "ai";
|
|
450
|
+
}
|
|
451
|
+
if (typeof msg.type === "string" && msg.type.toLowerCase() === "ai") return true;
|
|
452
|
+
const name = (_a = msg == null ? void 0 : msg.constructor) == null ? void 0 : _a.name;
|
|
453
|
+
if (name === "AIMessage" || name === "AIMessageChunk") return true;
|
|
454
|
+
return false;
|
|
455
|
+
};
|
|
456
|
+
const getContent = (msg) => {
|
|
419
457
|
if (typeof msg.content === "string") return msg.content;
|
|
420
458
|
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;
|
|
459
|
+
const textBlock = msg.content.find((c) => (c == null ? void 0 : c.type) === "text");
|
|
460
|
+
if (typeof (textBlock == null ? void 0 : textBlock.text) === "string") return textBlock.text;
|
|
423
461
|
}
|
|
462
|
+
return "";
|
|
463
|
+
};
|
|
464
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
465
|
+
const msg = messages[i];
|
|
466
|
+
if (!isAiMessage(msg)) continue;
|
|
467
|
+
const content = getContent(msg);
|
|
468
|
+
if (content) return content;
|
|
424
469
|
}
|
|
425
470
|
return "";
|
|
426
471
|
}
|
|
@@ -495,14 +540,26 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
495
540
|
}
|
|
496
541
|
}
|
|
497
542
|
const MAX_BRANCH_FILTER_LENGTH = 200;
|
|
543
|
+
const loggedBadFilters = /* @__PURE__ */ new Set();
|
|
544
|
+
function warnInvalidBranchFilter(filter, reason) {
|
|
545
|
+
if (loggedBadFilters.has(filter)) return;
|
|
546
|
+
loggedBadFilters.add(filter);
|
|
547
|
+
console.warn(
|
|
548
|
+
`[plugin-git-manager] branchFilter rejected (${reason}): ${JSON.stringify(filter)}. Flow will not match any branch.`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
498
551
|
function branchMatches(flow, branch) {
|
|
499
552
|
const filter = flow.get("branchFilter");
|
|
500
553
|
if (!filter) return true;
|
|
501
|
-
if (filter.length > MAX_BRANCH_FILTER_LENGTH)
|
|
554
|
+
if (filter.length > MAX_BRANCH_FILTER_LENGTH) {
|
|
555
|
+
warnInvalidBranchFilter(filter, `too long (>${MAX_BRANCH_FILTER_LENGTH} chars)`);
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
502
558
|
try {
|
|
503
559
|
return new RegExp(filter).test(branch);
|
|
504
|
-
} catch {
|
|
505
|
-
|
|
560
|
+
} catch (err) {
|
|
561
|
+
warnInvalidBranchFilter(filter, `invalid regex: ${(err == null ? void 0 : err.message) || err}`);
|
|
562
|
+
return false;
|
|
506
563
|
}
|
|
507
564
|
}
|
|
508
565
|
function pickFlowMatchingBranch(flows, branch) {
|
|
@@ -518,10 +575,45 @@ function throwHttp(status, message) {
|
|
|
518
575
|
err.status = status;
|
|
519
576
|
throw err;
|
|
520
577
|
}
|
|
578
|
+
const STUCK_REVIEW_CUTOFF_MS = 10 * 60 * 1e3;
|
|
579
|
+
async function recoverStuckReviews(app) {
|
|
580
|
+
var _a, _b, _c, _d;
|
|
581
|
+
try {
|
|
582
|
+
const reviewsRepo = app.db.getRepository("gitCodeReviews");
|
|
583
|
+
const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
|
|
584
|
+
const stuck = await reviewsRepo.find({
|
|
585
|
+
filter: {
|
|
586
|
+
status: { $in: ["running", "pending"] },
|
|
587
|
+
startedAt: { $lt: cutoff }
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
if (!(stuck == null ? void 0 : stuck.length)) return 0;
|
|
591
|
+
const finishedAt = /* @__PURE__ */ new Date();
|
|
592
|
+
for (const review of stuck) {
|
|
593
|
+
const startedAt = review.get("startedAt");
|
|
594
|
+
const durationMs = startedAt ? finishedAt.getTime() - new Date(startedAt).getTime() : null;
|
|
595
|
+
await reviewsRepo.update({
|
|
596
|
+
filterByTk: review.get("id"),
|
|
597
|
+
values: {
|
|
598
|
+
status: "failed",
|
|
599
|
+
error: "Review interrupted by application restart",
|
|
600
|
+
finishedAt,
|
|
601
|
+
durationMs
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
(_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`);
|
|
606
|
+
return stuck.length;
|
|
607
|
+
} catch (err) {
|
|
608
|
+
(_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}`);
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
521
612
|
// Annotate the CommonJS export names for ESM import in node:
|
|
522
613
|
0 && (module.exports = {
|
|
523
614
|
branchMatches,
|
|
524
615
|
pickFlowMatchingBranch,
|
|
616
|
+
recoverStuckReviews,
|
|
525
617
|
reviewApprovePost,
|
|
526
618
|
reviewReject,
|
|
527
619
|
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
|
}
|
|
@@ -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" }
|
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 {
|
|
@@ -78,8 +79,26 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
78
79
|
pollerStatus: pollerActions.pollerStatus
|
|
79
80
|
}
|
|
80
81
|
});
|
|
82
|
+
this.app.use(async (ctx, next) => {
|
|
83
|
+
if (ctx.logger && ctx.logger.warn) {
|
|
84
|
+
const originalWarn = ctx.logger.warn.bind(ctx.logger);
|
|
85
|
+
ctx.logger.warn = (message, ...args) => {
|
|
86
|
+
if (typeof message === "string" && message.includes("[Workflow") && message.includes("collection") && message.includes("not found")) {
|
|
87
|
+
return ctx.logger;
|
|
88
|
+
}
|
|
89
|
+
return originalWarn(message, ...args);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return next();
|
|
93
|
+
});
|
|
81
94
|
(0, import_ai_tools.registerGitReviewAiTools)(this.app);
|
|
82
95
|
this.app.on("afterStart", () => {
|
|
96
|
+
(0, import_review.recoverStuckReviews)(this.app).catch(
|
|
97
|
+
(err) => {
|
|
98
|
+
var _a, _b;
|
|
99
|
+
return (_b = (_a = this.app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "plugin-git-manager: recoverStuckReviews error", err);
|
|
100
|
+
}
|
|
101
|
+
);
|
|
83
102
|
(0, import_poller.startPoller)(this.app);
|
|
84
103
|
});
|
|
85
104
|
this.app.on("beforeStop", () => {
|