plugin-git-manager 1.1.10 → 1.1.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/client/187.d5545b7cc8b90bfc.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/locale/en-US.json +2 -0
- package/dist/locale/vi-VN.json +2 -0
- package/dist/server/actions/git-actions.js +15 -12
- package/dist/server/actions/review.d.ts +5 -2
- package/dist/server/actions/review.js +171 -28
- package/dist/server/collections/gitCodeReviews.d.ts +1 -1
- package/dist/server/collections/gitCodeReviews.js +1 -0
- package/dist/server/collections/gitRepositories.d.ts +1 -1
- package/dist/server/collections/gitReviewFlows.d.ts +1 -1
- package/dist/server/plugin.js +5 -0
- package/package.json +1 -1
- package/src/client/components/CommitHistory.tsx +3 -0
- package/src/client/components/FileExplorer.tsx +29 -24
- package/src/client/components/GitOperations.tsx +3 -0
- package/src/client/components/ReviewFlows.tsx +11 -1
- package/src/client/components/ReviewHistory.tsx +14 -1
- package/src/locale/en-US.json +2 -0
- package/src/locale/vi-VN.json +2 -0
- package/src/server/actions/git-actions.ts +15 -12
- package/src/server/actions/review.ts +219 -38
- package/src/server/collections/gitCodeReviews.ts +1 -0
- package/src/server/plugin.ts +25 -20
- package/dist/client/187.08dd0bf4d0f68036.js +0 -10
|
@@ -26,17 +26,30 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
27
|
var review_exports = {};
|
|
28
28
|
__export(review_exports, {
|
|
29
|
+
WORKER_JOB_GIT_REVIEW_PROCESS: () => WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
29
30
|
branchMatches: () => branchMatches,
|
|
30
31
|
pickFlowMatchingBranch: () => pickFlowMatchingBranch,
|
|
31
32
|
recoverStuckReviews: () => recoverStuckReviews,
|
|
33
|
+
registerReviewQueue: () => registerReviewQueue,
|
|
32
34
|
reviewApprovePost: () => reviewApprovePost,
|
|
33
35
|
reviewReject: () => reviewReject,
|
|
34
36
|
triggerReview: () => triggerReview,
|
|
35
|
-
triggerReviewInternal: () => triggerReviewInternal
|
|
37
|
+
triggerReviewInternal: () => triggerReviewInternal,
|
|
38
|
+
unregisterReviewQueue: () => unregisterReviewQueue
|
|
36
39
|
});
|
|
37
40
|
module.exports = __toCommonJS(review_exports);
|
|
38
41
|
var import_gitlab_url = require("../utils/gitlab-url");
|
|
39
42
|
var import_redact = require("../utils/redact");
|
|
43
|
+
const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
|
|
44
|
+
const REVIEW_QUEUE_CHANNEL = "plugin-git-manager.review";
|
|
45
|
+
const REVIEW_QUEUE_CONCURRENCY = Math.max(
|
|
46
|
+
1,
|
|
47
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_CONCURRENCY || process.env.GIT_REVIEW_MAX_CONCURRENCY || "3", 10) || 3
|
|
48
|
+
);
|
|
49
|
+
const REVIEW_QUEUE_TIMEOUT_MS = Math.max(
|
|
50
|
+
6e4,
|
|
51
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_TIMEOUT_MS || "", 10) || 10 * 60 * 1e3
|
|
52
|
+
);
|
|
40
53
|
function getActionParams(ctx) {
|
|
41
54
|
var _a, _b;
|
|
42
55
|
return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
|
|
@@ -142,13 +155,12 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
142
155
|
latestSha: headSha || existingLatestSha || null,
|
|
143
156
|
triggeredBy: args.triggeredBy || "manual",
|
|
144
157
|
status: "pending",
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
158
|
+
// `startedAt` is stamped by the queue worker when execution actually
|
|
159
|
+
// starts. Pending rows may be legitimately waiting in Redis.
|
|
160
|
+
startedAt: null,
|
|
149
161
|
finishedAt: null,
|
|
150
162
|
durationMs: null,
|
|
151
|
-
postStatus: flow.
|
|
163
|
+
postStatus: getInitialPostStatus(flow, args.targetType),
|
|
152
164
|
error: null
|
|
153
165
|
};
|
|
154
166
|
let reviewId;
|
|
@@ -166,25 +178,129 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
166
178
|
const review = await reviewsRepo.create({ values: baseValues });
|
|
167
179
|
reviewId = review.get("id");
|
|
168
180
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
await enqueueReview(app, {
|
|
182
|
+
reviewId,
|
|
183
|
+
repositoryId: args.repositoryId,
|
|
184
|
+
targetType: args.targetType,
|
|
185
|
+
mrIid: args.targetType === "mr" ? args.mrIid : null,
|
|
186
|
+
commitSha: args.targetType === "commit" ? args.commitSha : null,
|
|
187
|
+
branch: args.branch || void 0,
|
|
188
|
+
headSha,
|
|
189
|
+
aiEmployeeUsername,
|
|
190
|
+
extraInstructions: args.extraInstructions,
|
|
191
|
+
userId: args.userId ?? null,
|
|
192
|
+
flowSnapshot: createFlowSnapshot(flow)
|
|
193
|
+
});
|
|
194
|
+
return reviewId;
|
|
195
|
+
}
|
|
196
|
+
function registerReviewQueue(app) {
|
|
197
|
+
app.eventQueue.subscribe(REVIEW_QUEUE_CHANNEL, {
|
|
198
|
+
concurrency: REVIEW_QUEUE_CONCURRENCY,
|
|
199
|
+
idle: () => isGitReviewWorker(app),
|
|
200
|
+
process: async (message) => {
|
|
201
|
+
await processQueuedReview(app, message);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function unregisterReviewQueue(app) {
|
|
206
|
+
app.eventQueue.unsubscribe(REVIEW_QUEUE_CHANNEL);
|
|
207
|
+
}
|
|
208
|
+
function createFlowSnapshot(flow) {
|
|
209
|
+
return {
|
|
210
|
+
id: Number(flow.get("id")),
|
|
211
|
+
name: flow.get("name"),
|
|
212
|
+
postMode: flow.get("postMode"),
|
|
213
|
+
llmService: flow.get("llmService"),
|
|
214
|
+
model: flow.get("model"),
|
|
215
|
+
instructions: flow.get("instructions")
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createFlowFromSnapshot(snapshot, fallback) {
|
|
219
|
+
return {
|
|
220
|
+
get(name) {
|
|
221
|
+
var _a;
|
|
222
|
+
if (snapshot && Object.prototype.hasOwnProperty.call(snapshot, name)) {
|
|
223
|
+
return snapshot[name];
|
|
224
|
+
}
|
|
225
|
+
return (_a = fallback == null ? void 0 : fallback.get) == null ? void 0 : _a.call(fallback, name);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function isGitReviewWorker(app) {
|
|
230
|
+
const workerMode = process.env.WORKER_MODE || "";
|
|
231
|
+
return app.serving(WORKER_JOB_GIT_REVIEW_PROCESS) || workerMode === "worker" || workerMode === "task" || process.env.APP_ROLE === "worker";
|
|
232
|
+
}
|
|
233
|
+
async function enqueueReview(app, message) {
|
|
234
|
+
try {
|
|
235
|
+
await app.eventQueue.publish(REVIEW_QUEUE_CHANNEL, message, {
|
|
236
|
+
timeout: REVIEW_QUEUE_TIMEOUT_MS,
|
|
237
|
+
maxRetries: 1
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
241
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
242
|
+
filterByTk: message.reviewId,
|
|
243
|
+
values: {
|
|
244
|
+
status: "failed",
|
|
245
|
+
error: `Failed to enqueue review: ${safeMessage}`,
|
|
246
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function failQueuedReview(app, reviewId, err) {
|
|
253
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
254
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
255
|
+
filterByTk: reviewId,
|
|
256
|
+
values: {
|
|
257
|
+
status: "failed",
|
|
258
|
+
error: safeMessage,
|
|
259
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function processQueuedReview(app, message) {
|
|
264
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
265
|
+
const db = app.db;
|
|
266
|
+
const reviewsRepo = db.getRepository("gitCodeReviews");
|
|
267
|
+
const review = await reviewsRepo.findOne({ filterByTk: message.reviewId });
|
|
268
|
+
if (!review) {
|
|
269
|
+
(_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `git review queue: review ${message.reviewId} not found, skipping`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (review.get("status") !== "pending") {
|
|
273
|
+
(_d = (_c = app.log) == null ? void 0 : _c.info) == null ? void 0 : _d.call(_c, `git review queue: review ${message.reviewId} is ${review.get("status")}, skipping`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const repo = await db.getRepository("gitRepositories").findOne({
|
|
278
|
+
filterByTk: message.repositoryId || review.get("repositoryId")
|
|
279
|
+
});
|
|
280
|
+
if (!repo) throw new Error("Repository not found");
|
|
281
|
+
const storedFlow = await db.getRepository("gitReviewFlows").findOne({
|
|
282
|
+
filterByTk: ((_e = message.flowSnapshot) == null ? void 0 : _e.id) || review.get("flowId")
|
|
283
|
+
});
|
|
284
|
+
const flow = createFlowFromSnapshot(message.flowSnapshot, storedFlow);
|
|
285
|
+
const aiEmployeeUsername = message.aiEmployeeUsername || flow.get("aiEmployeeUsername");
|
|
286
|
+
if (!aiEmployeeUsername) throw new Error("Flow has no AI employee configured");
|
|
287
|
+
await runReview(app, {
|
|
288
|
+
reviewId: message.reviewId,
|
|
172
289
|
flow,
|
|
173
290
|
repo,
|
|
174
|
-
targetType:
|
|
175
|
-
mrIid:
|
|
176
|
-
commitSha:
|
|
177
|
-
branch:
|
|
178
|
-
headSha,
|
|
291
|
+
targetType: message.targetType || review.get("targetType"),
|
|
292
|
+
mrIid: message.targetType === "mr" ? message.mrIid ?? Number(review.get("mrIid")) : null,
|
|
293
|
+
commitSha: message.targetType === "commit" ? message.commitSha || review.get("commitSha") : null,
|
|
294
|
+
branch: message.branch || review.get("branch"),
|
|
295
|
+
headSha: message.headSha || review.get("headSha"),
|
|
179
296
|
aiEmployeeUsername,
|
|
180
|
-
extraInstructions:
|
|
181
|
-
userId:
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return reviewId;
|
|
297
|
+
extraInstructions: message.extraInstructions,
|
|
298
|
+
userId: message.userId ?? null
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
(_g = (_f = app.log) == null ? void 0 : _f.error) == null ? void 0 : _g.call(_f, "git review queue: failed before review execution", err);
|
|
302
|
+
await failQueuedReview(app, message.reviewId, err);
|
|
303
|
+
}
|
|
188
304
|
}
|
|
189
305
|
async function reviewApprovePost(ctx, next) {
|
|
190
306
|
var _a, _b;
|
|
@@ -211,7 +327,8 @@ async function reviewApprovePost(ctx, next) {
|
|
|
211
327
|
postStatus: "posted",
|
|
212
328
|
postedNoteId: String(noteId),
|
|
213
329
|
approvedBy: userId ? String(userId) : null,
|
|
214
|
-
approvedAt: /* @__PURE__ */ new Date()
|
|
330
|
+
approvedAt: /* @__PURE__ */ new Date(),
|
|
331
|
+
error: null
|
|
215
332
|
}
|
|
216
333
|
});
|
|
217
334
|
ctx.body = { success: true, data: { reviewId, postedNoteId: noteId } };
|
|
@@ -357,9 +474,10 @@ async function runReview(app, args) {
|
|
|
357
474
|
}
|
|
358
475
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
359
476
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
360
|
-
const postMode = args.flow
|
|
361
|
-
let postStatus =
|
|
477
|
+
const postMode = getFlowPostMode(args.flow);
|
|
478
|
+
let postStatus = getInitialPostStatus(args.flow, args.targetType);
|
|
362
479
|
let postedNoteId = null;
|
|
480
|
+
let autoPostError = null;
|
|
363
481
|
if (postMode === "disabled") {
|
|
364
482
|
postStatus = "skipped";
|
|
365
483
|
} else if (postMode === "auto" && args.targetType === "mr" && args.mrIid) {
|
|
@@ -367,6 +485,8 @@ async function runReview(app, args) {
|
|
|
367
485
|
postedNoteId = String(await postNoteToGitLab(args.repo, args.mrIid, content));
|
|
368
486
|
postStatus = "posted";
|
|
369
487
|
} catch (err) {
|
|
488
|
+
autoPostError = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
489
|
+
postStatus = "post_failed";
|
|
370
490
|
(_c = (_b = app.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, "Auto-post review note failed", err);
|
|
371
491
|
}
|
|
372
492
|
}
|
|
@@ -384,11 +504,14 @@ async function runReview(app, args) {
|
|
|
384
504
|
finishedAt,
|
|
385
505
|
postStatus,
|
|
386
506
|
postedNoteId,
|
|
507
|
+
error: autoPostError ? `Auto-post failed: ${autoPostError}` : null,
|
|
387
508
|
metadata: {
|
|
388
509
|
flowName: args.flow.get("name"),
|
|
389
510
|
aiEmployeeUsername: args.aiEmployeeUsername,
|
|
390
511
|
llmService,
|
|
391
|
-
model
|
|
512
|
+
model,
|
|
513
|
+
postMode,
|
|
514
|
+
autoPostError
|
|
392
515
|
}
|
|
393
516
|
}
|
|
394
517
|
});
|
|
@@ -559,6 +682,23 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
559
682
|
}
|
|
560
683
|
const MAX_BRANCH_FILTER_LENGTH = 200;
|
|
561
684
|
const loggedBadFilters = /* @__PURE__ */ new Set();
|
|
685
|
+
function getFlowPostMode(flow) {
|
|
686
|
+
var _a;
|
|
687
|
+
const rawValue = (_a = flow == null ? void 0 : flow.get) == null ? void 0 : _a.call(flow, "postMode");
|
|
688
|
+
const value = rawValue && typeof rawValue === "object" && "value" in rawValue ? rawValue.value : rawValue;
|
|
689
|
+
const normalized = String(value || "manual").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
690
|
+
if (["auto", "auto_post", "autopost", "auto_post_to_mr"].includes(normalized)) return "auto";
|
|
691
|
+
if (["disabled", "disable", "do_not_post", "dont_post", "none", "skip", "skipped"].includes(normalized)) {
|
|
692
|
+
return "disabled";
|
|
693
|
+
}
|
|
694
|
+
return "manual";
|
|
695
|
+
}
|
|
696
|
+
function getInitialPostStatus(flow, targetType) {
|
|
697
|
+
const postMode = getFlowPostMode(flow);
|
|
698
|
+
if (postMode === "disabled") return "skipped";
|
|
699
|
+
if (postMode === "auto" && targetType !== "mr") return "skipped";
|
|
700
|
+
return "pending_approval";
|
|
701
|
+
}
|
|
562
702
|
function warnInvalidBranchFilter(filter, reason) {
|
|
563
703
|
if (loggedBadFilters.has(filter)) return;
|
|
564
704
|
loggedBadFilters.add(filter);
|
|
@@ -601,7 +741,7 @@ async function recoverStuckReviews(app) {
|
|
|
601
741
|
const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
|
|
602
742
|
const stuck = await reviewsRepo.find({
|
|
603
743
|
filter: {
|
|
604
|
-
status:
|
|
744
|
+
status: "running",
|
|
605
745
|
startedAt: { $lt: cutoff }
|
|
606
746
|
}
|
|
607
747
|
});
|
|
@@ -629,11 +769,14 @@ async function recoverStuckReviews(app) {
|
|
|
629
769
|
}
|
|
630
770
|
// Annotate the CommonJS export names for ESM import in node:
|
|
631
771
|
0 && (module.exports = {
|
|
772
|
+
WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
632
773
|
branchMatches,
|
|
633
774
|
pickFlowMatchingBranch,
|
|
634
775
|
recoverStuckReviews,
|
|
776
|
+
registerReviewQueue,
|
|
635
777
|
reviewApprovePost,
|
|
636
778
|
reviewReject,
|
|
637
779
|
triggerReview,
|
|
638
|
-
triggerReviewInternal
|
|
780
|
+
triggerReviewInternal,
|
|
781
|
+
unregisterReviewQueue
|
|
639
782
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("@nocobase/database").CollectionOptions;
|
|
2
2
|
export default _default;
|
|
@@ -166,6 +166,7 @@ var gitCodeReviews_default = (0, import_database.defineCollection)({
|
|
|
166
166
|
{ value: "pending_approval", label: "Pending Approval" },
|
|
167
167
|
{ value: "approved", label: "Approved" },
|
|
168
168
|
{ value: "posted", label: "Posted" },
|
|
169
|
+
{ value: "post_failed", label: "Post Failed" },
|
|
169
170
|
{ value: "skipped", label: "Skipped" },
|
|
170
171
|
{ value: "rejected", label: "Rejected" }
|
|
171
172
|
]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("@nocobase/database").CollectionOptions;
|
|
2
2
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("@nocobase/database").CollectionOptions;
|
|
2
2
|
export default _default;
|
package/dist/server/plugin.js
CHANGED
|
@@ -105,6 +105,7 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
105
105
|
}
|
|
106
106
|
return next();
|
|
107
107
|
});
|
|
108
|
+
(0, import_review.registerReviewQueue)(this.app);
|
|
108
109
|
(0, import_ai_tools.registerGitReviewAiTools)(this.app);
|
|
109
110
|
this.app.on("afterStart", async () => {
|
|
110
111
|
await ensureAutoReviewFlowSchema(this.app).catch(
|
|
@@ -122,9 +123,11 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
122
123
|
(0, import_poller.startPoller)(this.app);
|
|
123
124
|
});
|
|
124
125
|
this.app.on("beforeStop", () => {
|
|
126
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
125
127
|
(0, import_poller.stopPoller)();
|
|
126
128
|
});
|
|
127
129
|
this.app.on("beforeDestroy", () => {
|
|
130
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
128
131
|
(0, import_poller.stopPoller)();
|
|
129
132
|
});
|
|
130
133
|
this.app.acl.registerSnippet({
|
|
@@ -207,9 +210,11 @@ class PluginGitManagerServer extends import_server.Plugin {
|
|
|
207
210
|
await ((_a = this.app.db.getCollection("gitRepositories")) == null ? void 0 : _a.sync());
|
|
208
211
|
}
|
|
209
212
|
async beforeDisable() {
|
|
213
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
210
214
|
(0, import_poller.stopPoller)();
|
|
211
215
|
}
|
|
212
216
|
async beforeUnload() {
|
|
217
|
+
(0, import_review.unregisterReviewQueue)(this.app);
|
|
213
218
|
(0, import_poller.stopPoller)();
|
|
214
219
|
}
|
|
215
220
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Git Manager",
|
|
4
4
|
"displayName.zh-CN": "Git 管理器",
|
|
5
5
|
"description": "Manage Git repositories with PAT authentication - pull, push, fetch, diff, file browsing",
|
|
6
|
-
"version": "1.1.
|
|
6
|
+
"version": "1.1.12",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"main": "dist/server/index.js",
|
|
9
9
|
"files": [
|
|
@@ -49,6 +49,9 @@ export const CommitHistory: React.FC = () => {
|
|
|
49
49
|
});
|
|
50
50
|
const responseData = data?.data?.data || data?.data;
|
|
51
51
|
setCommits(responseData?.all || []);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('Failed to load commit history:', error);
|
|
54
|
+
setCommits([]);
|
|
52
55
|
} finally {
|
|
53
56
|
setLoading(false);
|
|
54
57
|
}
|
|
@@ -75,30 +75,35 @@ export const FileExplorer: React.FC = () => {
|
|
|
75
75
|
const loadTree = useCallback(
|
|
76
76
|
async (treePath = '', ref = currentRef) => {
|
|
77
77
|
if (!selectedRepo) return [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
try {
|
|
79
|
+
const { data } = await api.request({
|
|
80
|
+
url: 'gitManager:fileTree',
|
|
81
|
+
params: { repositoryId: selectedRepo.id, ref, treePath },
|
|
82
|
+
});
|
|
83
|
+
const responseData = data?.data || data || [];
|
|
84
|
+
const list = Array.isArray(responseData) ? responseData : (Array.isArray(responseData?.data) ? responseData.data : []);
|
|
85
|
+
|
|
86
|
+
return list.map((item: any) => ({
|
|
87
|
+
key: item.path,
|
|
88
|
+
title: (
|
|
89
|
+
<Text style={{ fontSize: 13 }}>
|
|
90
|
+
{item.name}
|
|
91
|
+
{item.type === 'blob' && item.size > 0 && (
|
|
92
|
+
<Text type="secondary" style={{ fontSize: 11, marginLeft: 8 }}>
|
|
93
|
+
{item.size > 1024 ? `${(item.size / 1024).toFixed(1)}KB` : `${item.size}B`}
|
|
94
|
+
</Text>
|
|
95
|
+
)}
|
|
96
|
+
</Text>
|
|
97
|
+
),
|
|
98
|
+
icon: getFileIcon(item.name, item.type),
|
|
99
|
+
isLeaf: item.type === 'blob',
|
|
100
|
+
filePath: item.path,
|
|
101
|
+
fileType: item.type,
|
|
102
|
+
}));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn('Failed to load file tree:', error);
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
102
107
|
},
|
|
103
108
|
[api, selectedRepo, currentRef],
|
|
104
109
|
);
|
|
@@ -50,6 +50,9 @@ export const GitOperations: React.FC = () => {
|
|
|
50
50
|
});
|
|
51
51
|
const responseData = data?.data?.data || data?.data;
|
|
52
52
|
setStatusData(responseData);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('Failed to load git status:', error);
|
|
55
|
+
setStatusData(null);
|
|
53
56
|
} finally {
|
|
54
57
|
setActionLoading(null);
|
|
55
58
|
}
|
|
@@ -22,6 +22,15 @@ const POST_LABELS: Record<string, string> = {
|
|
|
22
22
|
disabled: 'Disabled',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
const DEFAULT_INSTRUCTION = `Please review the code changes with a focus on:
|
|
26
|
+
1. Best Practices & Style:
|
|
27
|
+
- C#/VB.NET: Follow standard naming conventions (PascalCase for methods/properties, camelCase for fields). Ensure proper use of async/await (avoid .Result/.Wait()). Use LINQ efficiently.
|
|
28
|
+
- JS/TS: Use strict equality (===), prefer const/let. Ensure TypeScript types are explicit and avoid 'any'.
|
|
29
|
+
2. Performance: Watch out for N+1 queries, unnecessary loops, and memory leaks.
|
|
30
|
+
3. Security: Identify potential SQL injections, XSS vulnerabilities, and improper data validation.
|
|
31
|
+
4. Maintainability: Check for readability, SOLID principles, and DRY. Ensure meaningful naming.
|
|
32
|
+
5. Error Handling: Verify that exceptions are properly caught, handled, and logged.`;
|
|
33
|
+
|
|
25
34
|
export const ReviewFlows: React.FC = () => {
|
|
26
35
|
const t = useT();
|
|
27
36
|
const api = useAPIClient();
|
|
@@ -92,6 +101,7 @@ export const ReviewFlows: React.FC = () => {
|
|
|
92
101
|
enabled: true,
|
|
93
102
|
triggerMode: 'manual',
|
|
94
103
|
postMode: 'manual',
|
|
104
|
+
instructions: DEFAULT_INSTRUCTION,
|
|
95
105
|
});
|
|
96
106
|
setOpen(true);
|
|
97
107
|
};
|
|
@@ -259,7 +269,7 @@ export const ReviewFlows: React.FC = () => {
|
|
|
259
269
|
<Input placeholder="^(feature|hotfix)/.*$" />
|
|
260
270
|
</Form.Item>
|
|
261
271
|
<Form.Item name="instructions" label={t('Additional Instructions (optional)')}>
|
|
262
|
-
<Input.TextArea rows={
|
|
272
|
+
<Input.TextArea rows={8} placeholder={t('Extra guidance appended to every review prompt')} />
|
|
263
273
|
</Form.Item>
|
|
264
274
|
<Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
|
|
265
275
|
<Switch />
|
|
@@ -25,6 +25,7 @@ const POST_STATUS_COLOR: Record<string, string> = {
|
|
|
25
25
|
pending_approval: 'orange',
|
|
26
26
|
approved: 'cyan',
|
|
27
27
|
posted: 'green',
|
|
28
|
+
post_failed: 'red',
|
|
28
29
|
skipped: 'default',
|
|
29
30
|
rejected: 'red',
|
|
30
31
|
};
|
|
@@ -278,6 +279,7 @@ export const ReviewHistory: React.FC<{ initialFilter?: 'all' | 'pending_approval
|
|
|
278
279
|
{ value: 'pending_approval', label: t('pending_approval') },
|
|
279
280
|
{ value: 'approved', label: t('approved') },
|
|
280
281
|
{ value: 'posted', label: t('posted') },
|
|
282
|
+
{ value: 'post_failed', label: t('post_failed') },
|
|
281
283
|
{ value: 'skipped', label: t('skipped') },
|
|
282
284
|
{ value: 'rejected', label: t('rejected') },
|
|
283
285
|
]}
|
|
@@ -395,7 +397,9 @@ const ReviewDetailView: React.FC<{
|
|
|
395
397
|
const canApprove =
|
|
396
398
|
review.status === 'completed' &&
|
|
397
399
|
review.targetType === 'mr' &&
|
|
398
|
-
(review.postStatus === 'pending_approval' ||
|
|
400
|
+
(review.postStatus === 'pending_approval' ||
|
|
401
|
+
review.postStatus === 'approved' ||
|
|
402
|
+
review.postStatus === 'post_failed');
|
|
399
403
|
|
|
400
404
|
return (
|
|
401
405
|
<div>
|
|
@@ -417,6 +421,15 @@ const ReviewDetailView: React.FC<{
|
|
|
417
421
|
style={{ marginBottom: 16 }}
|
|
418
422
|
/>
|
|
419
423
|
)}
|
|
424
|
+
{review.status === 'completed' && review.postStatus === 'post_failed' && review.error && (
|
|
425
|
+
<Alert
|
|
426
|
+
type="error"
|
|
427
|
+
showIcon
|
|
428
|
+
message={t('Post Failed')}
|
|
429
|
+
description={<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{review.error}</pre>}
|
|
430
|
+
style={{ marginBottom: 16 }}
|
|
431
|
+
/>
|
|
432
|
+
)}
|
|
420
433
|
|
|
421
434
|
{/* Action bar */}
|
|
422
435
|
{canApprove && (
|
package/src/locale/en-US.json
CHANGED
package/src/locale/vi-VN.json
CHANGED
|
@@ -91,7 +91,10 @@ function getAuthUrl(repoUrl: string, pat: string, username?: string): string {
|
|
|
91
91
|
return url.toString();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function getGit(localPath: string): SimpleGit {
|
|
94
|
+
function getGit(ctx: Context, localPath: string): SimpleGit {
|
|
95
|
+
if (!fs.existsSync(localPath)) {
|
|
96
|
+
ctx.throw(400, 'Repository directory does not exist. Please clone the repository first.');
|
|
97
|
+
}
|
|
95
98
|
return simpleGit(localPath);
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -170,7 +173,7 @@ export async function pull(ctx: Context, next: () => Promise<void>) {
|
|
|
170
173
|
const repoUrl = (repo.get('repoUrl') as string || '').trim();
|
|
171
174
|
const username = (repo.get('username') as string || '').trim();
|
|
172
175
|
|
|
173
|
-
const git = getGit(localPath);
|
|
176
|
+
const git = getGit(ctx, localPath);
|
|
174
177
|
const result = await withAuth(git, localPath, repoUrl, pat, () => git.pull(), username);
|
|
175
178
|
|
|
176
179
|
ctx.body = { success: true, data: result };
|
|
@@ -184,7 +187,7 @@ export async function push(ctx: Context, next: () => Promise<void>) {
|
|
|
184
187
|
const repoUrl = (repo.get('repoUrl') as string || '').trim();
|
|
185
188
|
const username = (repo.get('username') as string || '').trim();
|
|
186
189
|
|
|
187
|
-
const git = getGit(localPath);
|
|
190
|
+
const git = getGit(ctx, localPath);
|
|
188
191
|
const result = await withAuth(git, localPath, repoUrl, pat, () => git.push(), username);
|
|
189
192
|
|
|
190
193
|
ctx.body = { success: true, data: result };
|
|
@@ -198,7 +201,7 @@ export async function fetch(ctx: Context, next: () => Promise<void>) {
|
|
|
198
201
|
const repoUrl = (repo.get('repoUrl') as string || '').trim();
|
|
199
202
|
const username = (repo.get('username') as string || '').trim();
|
|
200
203
|
|
|
201
|
-
const git = getGit(localPath);
|
|
204
|
+
const git = getGit(ctx, localPath);
|
|
202
205
|
const result = await withAuth(git, localPath, repoUrl, pat, () => git.fetch(), username);
|
|
203
206
|
|
|
204
207
|
ctx.body = { success: true, data: result };
|
|
@@ -210,7 +213,7 @@ export async function diff(ctx: Context, next: () => Promise<void>) {
|
|
|
210
213
|
const localPath = validateLocalPath(repo.get('localPath'));
|
|
211
214
|
const { file, commitHash, compareHash } = ctx.action.params;
|
|
212
215
|
|
|
213
|
-
const git = getGit(localPath);
|
|
216
|
+
const git = getGit(ctx, localPath);
|
|
214
217
|
const args: string[] = [];
|
|
215
218
|
if (commitHash && compareHash) {
|
|
216
219
|
args.push(validateRef(commitHash), validateRef(compareHash));
|
|
@@ -230,7 +233,7 @@ export async function diff(ctx: Context, next: () => Promise<void>) {
|
|
|
230
233
|
export async function status(ctx: Context, next: () => Promise<void>) {
|
|
231
234
|
const repo = await getRepo(ctx);
|
|
232
235
|
const localPath = validateLocalPath(repo.get('localPath'));
|
|
233
|
-
const result = await getGit(localPath).status();
|
|
236
|
+
const result = await getGit(ctx, localPath).status();
|
|
234
237
|
ctx.body = { success: true, data: result };
|
|
235
238
|
await next();
|
|
236
239
|
}
|
|
@@ -247,7 +250,7 @@ export async function log(ctx: Context, next: () => Promise<void>) {
|
|
|
247
250
|
options.file = file;
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
const result = await getGit(localPath).log(options);
|
|
253
|
+
const result = await getGit(ctx, localPath).log(options);
|
|
251
254
|
ctx.body = { success: true, data: result };
|
|
252
255
|
await next();
|
|
253
256
|
}
|
|
@@ -255,7 +258,7 @@ export async function log(ctx: Context, next: () => Promise<void>) {
|
|
|
255
258
|
export async function branches(ctx: Context, next: () => Promise<void>) {
|
|
256
259
|
const repo = await getRepo(ctx);
|
|
257
260
|
const localPath = validateLocalPath(repo.get('localPath'));
|
|
258
|
-
const result = await getGit(localPath).branch();
|
|
261
|
+
const result = await getGit(ctx, localPath).branch();
|
|
259
262
|
ctx.body = { success: true, data: result };
|
|
260
263
|
await next();
|
|
261
264
|
}
|
|
@@ -265,7 +268,7 @@ export async function checkout(ctx: Context, next: () => Promise<void>) {
|
|
|
265
268
|
const localPath = validateLocalPath(repo.get('localPath'));
|
|
266
269
|
const { branch } = ctx.action.params;
|
|
267
270
|
validateBranch(branch);
|
|
268
|
-
await getGit(localPath).checkout(branch);
|
|
271
|
+
await getGit(ctx, localPath).checkout(branch);
|
|
269
272
|
ctx.body = { success: true, message: `Switched to branch ${branch}` };
|
|
270
273
|
await next();
|
|
271
274
|
}
|
|
@@ -275,7 +278,7 @@ export async function fileTree(ctx: Context, next: () => Promise<void>) {
|
|
|
275
278
|
const localPath = validateLocalPath(repo.get('localPath'));
|
|
276
279
|
const { ref = 'HEAD', treePath = '' } = ctx.action.params;
|
|
277
280
|
|
|
278
|
-
const git = getGit(localPath);
|
|
281
|
+
const git = getGit(ctx, localPath);
|
|
279
282
|
validateRef(ref);
|
|
280
283
|
if (treePath && treePath.includes('..')) {
|
|
281
284
|
ctx.throw(400, 'Invalid tree path');
|
|
@@ -324,7 +327,7 @@ export async function fileContent(ctx: Context, next: () => Promise<void>) {
|
|
|
324
327
|
}
|
|
325
328
|
|
|
326
329
|
validateRef(ref);
|
|
327
|
-
const git = getGit(localPath);
|
|
330
|
+
const git = getGit(ctx, localPath);
|
|
328
331
|
const content = await git.show([`${ref}:${filePath}`]);
|
|
329
332
|
ctx.body = { success: true, data: { content, filePath, ref } };
|
|
330
333
|
await next();
|
|
@@ -339,7 +342,7 @@ export async function commitDetail(ctx: Context, next: () => Promise<void>) {
|
|
|
339
342
|
ctx.throw(400, 'commitHash is required');
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
const git = getGit(localPath);
|
|
345
|
+
const git = getGit(ctx, localPath);
|
|
343
346
|
validateRef(commitHash);
|
|
344
347
|
|
|
345
348
|
// Use %x00 in format string to tell git to output null bytes, avoiding null bytes in args
|