plugin-git-manager 1.1.7 → 1.1.10

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.
Files changed (38) hide show
  1. package/dist/client/187.08dd0bf4d0f68036.js +10 -0
  2. package/dist/client/components/LLMModelSelect.d.ts +10 -0
  3. package/dist/client/components/RunReviewButton.d.ts +1 -1
  4. package/dist/client/context/GitManagerContext.d.ts +2 -0
  5. package/dist/client/index.js +1 -1
  6. package/dist/externalVersion.js +6 -4
  7. package/dist/locale/en-US.json +8 -1
  8. package/dist/server/actions/gitlab-api.js +14 -13
  9. package/dist/server/actions/review.js +15 -9
  10. package/dist/server/ai-tools.js +29 -3
  11. package/dist/server/collections/gitCodeReviews.d.ts +1 -1
  12. package/dist/server/collections/gitRepositories.d.ts +1 -1
  13. package/dist/server/collections/gitRepositories.js +12 -0
  14. package/dist/server/collections/gitReviewFlows.d.ts +1 -1
  15. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
  16. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
  17. package/dist/server/plugin.d.ts +4 -0
  18. package/dist/server/plugin.js +38 -6
  19. package/dist/server/poller.js +3 -1
  20. package/package.json +1 -1
  21. package/src/client/components/CommitHistory.tsx +18 -3
  22. package/src/client/components/GitOperations.tsx +29 -16
  23. package/src/client/components/LLMModelSelect.tsx +63 -0
  24. package/src/client/components/PollingStatus.tsx +27 -1
  25. package/src/client/components/RepositoryConfig.tsx +76 -3
  26. package/src/client/components/ReviewFlows.tsx +17 -2
  27. package/src/client/components/RunReviewButton.tsx +375 -278
  28. package/src/client/context/GitManagerContext.tsx +2 -0
  29. package/src/client/index.tsx +31 -31
  30. package/src/locale/en-US.json +8 -1
  31. package/src/server/actions/gitlab-api.ts +8 -4
  32. package/src/server/actions/review.ts +9 -3
  33. package/src/server/ai-tools.ts +31 -3
  34. package/src/server/collections/gitRepositories.ts +12 -0
  35. package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
  36. package/src/server/plugin.ts +216 -180
  37. package/src/server/poller.ts +11 -2
  38. package/dist/client/322.c934c7c0e25a75b0.js +0 -10
@@ -8,6 +8,8 @@ interface Repository {
8
8
  localPath: string;
9
9
  defaultBranch: string;
10
10
  status: string;
11
+ autoReview?: boolean;
12
+ autoReviewFlowId?: number | null;
11
13
  }
12
14
 
13
15
  interface GitManagerContextType {
@@ -1,31 +1,31 @@
1
- import { Plugin } from '@nocobase/client';
2
- import React from 'react';
3
- import {
4
- GitRepositoryWorkContext,
5
- GitMergeRequestWorkContext,
6
- GitCommitWorkContext,
7
- } from './ai-context';
8
-
9
- const GitManagerSettings = React.lazy(() =>
10
- import('./components/GitManagerSettings').then((m) => ({ default: m.GitManagerSettings })),
11
- );
12
-
13
- export class PluginGitManagerClient extends Plugin {
14
- async load() {
15
- this.app.pluginSettingsManager.add('git-manager', {
16
- title: this.t('Git Manager'),
17
- icon: 'BranchesOutlined',
18
- Component: GitManagerSettings,
19
- aclSnippet: 'pm.plugin-git-manager',
20
- });
21
-
22
- const aiManager = (this.app as any).aiManager;
23
- if (aiManager?.registerWorkContext) {
24
- aiManager.registerWorkContext('git-repository', GitRepositoryWorkContext);
25
- aiManager.registerWorkContext('git-merge-request', GitMergeRequestWorkContext);
26
- aiManager.registerWorkContext('git-commit', GitCommitWorkContext);
27
- }
28
- }
29
- }
30
-
31
- export default PluginGitManagerClient;
1
+ import { Plugin } from '@nocobase/client';
2
+ import React from 'react';
3
+ import {
4
+ GitRepositoryWorkContext,
5
+ GitMergeRequestWorkContext,
6
+ GitCommitWorkContext,
7
+ } from './ai-context';
8
+
9
+ const GitManagerSettings = React.lazy(() =>
10
+ import('./components/GitManagerSettings').then((m) => ({ default: m.GitManagerSettings })),
11
+ );
12
+
13
+ export class PluginGitManagerClient extends Plugin {
14
+ async load() {
15
+ (this as any).app.pluginSettingsManager.add('git-manager', {
16
+ title: (this as any).t('Git Manager'),
17
+ icon: 'BranchesOutlined',
18
+ Component: GitManagerSettings,
19
+ aclSnippet: 'pm.plugin-git-manager',
20
+ });
21
+
22
+ const aiManager = ((this as any).app as any).aiManager;
23
+ if (aiManager?.registerWorkContext) {
24
+ aiManager.registerWorkContext('git-repository', GitRepositoryWorkContext);
25
+ aiManager.registerWorkContext('git-merge-request', GitMergeRequestWorkContext);
26
+ aiManager.registerWorkContext('git-commit', GitCommitWorkContext);
27
+ }
28
+ }
29
+ }
30
+
31
+ export default PluginGitManagerClient;
@@ -177,5 +177,12 @@
177
177
  "new": "new",
178
178
  "Reviewed SHA": "Reviewed SHA",
179
179
  "Latest SHA": "Latest SHA",
180
- "All triggers": "All triggers"
180
+ "All triggers": "All triggers",
181
+ "Search commit title or message": "Search commit title or message",
182
+ "Primary Auto Flow": "Primary Auto Flow",
183
+ "Primary Auto Review Flow": "Primary Auto Review Flow",
184
+ "Only flows with automatic trigger modes are shown": "Only flows with automatic trigger modes are shown",
185
+ "Fallback": "Fallback",
186
+ "AI employee not found": "AI employee not found",
187
+ "Failed to open AI chat": "Failed to open AI chat"
181
188
  }
@@ -1,6 +1,10 @@
1
1
  import { Context } from '@nocobase/actions';
2
2
  import { parseGitLabProject } from '../utils/gitlab-url';
3
3
 
4
+ function getActionParams(ctx: Context) {
5
+ return { ...ctx.action.params, ...ctx.action.params?.values, ...((ctx as any).request?.body || {}) };
6
+ }
7
+
4
8
  async function gitlabFetch(apiBase: string, endpoint: string, pat: string, params?: Record<string, any>) {
5
9
  const url = new URL(`${apiBase}${endpoint}`);
6
10
  if (params) {
@@ -33,7 +37,7 @@ async function gitlabFetch(apiBase: string, endpoint: string, pat: string, param
33
37
 
34
38
  async function getRepoApiContext(ctx: Context) {
35
39
  // Fix for POST requests where data might be in ctx.request.body
36
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
40
+ const params = getActionParams(ctx);
37
41
  const { repositoryId } = params;
38
42
 
39
43
  const repo = await ctx.db.getRepository('gitRepositories').findOne({
@@ -90,7 +94,7 @@ async function githubFetch(endpoint: string, pat: string, params?: Record<string
90
94
  export async function mergeRequests(ctx: Context, next: () => Promise<void>) {
91
95
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
92
96
  // Merge params from query and body
93
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
97
+ const params = getActionParams(ctx);
94
98
  const {
95
99
  state = 'opened',
96
100
  search,
@@ -207,7 +211,7 @@ export async function mergeRequests(ctx: Context, next: () => Promise<void>) {
207
211
 
208
212
  export async function mergeRequestDetail(ctx: Context, next: () => Promise<void>) {
209
213
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
210
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
214
+ const params = getActionParams(ctx);
211
215
  const { mrIid } = params;
212
216
 
213
217
  if (!mrIid) {
@@ -321,7 +325,7 @@ export async function mergeRequestDetail(ctx: Context, next: () => Promise<void>
321
325
 
322
326
  export async function mergeRequestNotes(ctx: Context, next: () => Promise<void>) {
323
327
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
324
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
328
+ const params = getActionParams(ctx);
325
329
  const { mrIid, page = 1, perPage = 50 } = params;
326
330
 
327
331
  if (!mrIid) {
@@ -16,6 +16,10 @@ interface TriggerArgs {
16
16
  userId?: number | string | null;
17
17
  }
18
18
 
19
+ function getActionParams(ctx: Context) {
20
+ return { ...ctx.action.params, ...ctx.action.params?.values, ...((ctx as any).request?.body || {}) };
21
+ }
22
+
19
23
  /**
20
24
  * Per-target mutex to prevent two concurrent calls to
21
25
  * `triggerReviewInternal` for the same MR / commit / branch from racing
@@ -47,7 +51,7 @@ async function withTriggerLock<T>(app: Application, key: string, fn: () => Promi
47
51
  * the background. The action returns immediately with the reviewId.
48
52
  */
49
53
  export async function triggerReview(ctx: Context, next: () => Promise<void>) {
50
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
54
+ const params = getActionParams(ctx);
51
55
  const {
52
56
  flowId,
53
57
  repositoryId,
@@ -215,7 +219,7 @@ async function triggerReviewInternalLocked(app: Application, args: TriggerArgs):
215
219
  * Mark a review as approved and post its content to GitLab as an MR note.
216
220
  */
217
221
  export async function reviewApprovePost(ctx: Context, next: () => Promise<void>) {
218
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
222
+ const params = getActionParams(ctx);
219
223
  const { reviewId, editedMarkdown } = params;
220
224
  if (!reviewId) ctx.throw(400, 'reviewId is required');
221
225
 
@@ -255,7 +259,7 @@ export async function reviewApprovePost(ctx: Context, next: () => Promise<void>)
255
259
  * Reject a pending review (do not post to GitLab).
256
260
  */
257
261
  export async function reviewReject(ctx: Context, next: () => Promise<void>) {
258
- const params = { ...ctx.action.params, ...ctx.action.params?.values, ...( (ctx.request.body as any) || {} ) };
262
+ const params = getActionParams(ctx);
259
263
  const { reviewId, reason } = params;
260
264
  if (!reviewId) ctx.throw(400, 'reviewId is required');
261
265
 
@@ -341,6 +345,8 @@ async function runReview(app: Application, args: RunReviewArgs) {
341
345
  const syntheticCtx: any = {
342
346
  app,
343
347
  db,
348
+ isBackgroundReview: true,
349
+ reviewTargetRepositoryId: args.repo.get('id'),
344
350
  state: { currentUser: args.userId ? { id: args.userId } : null },
345
351
  auth: { user: args.userId ? { id: args.userId } : { id: null } },
346
352
  req: { headers: { 'x-timezone': '+00:00', 'x-locale': 'en-US' } },
@@ -25,7 +25,15 @@ export function registerGitReviewAiTools(app: Application) {
25
25
  * and read any repository's MRs / commits / file content regardless of the
26
26
  * caller's ACL.
27
27
  */
28
- const enforceAcl = (ctx: any, resource: string, action: string) => {
28
+ const enforceAcl = (ctx: any, resource: string, action: string, params: any) => {
29
+ if (ctx.isBackgroundReview) {
30
+ if (params.repositoryId && params.repositoryId !== ctx.reviewTargetRepositoryId) {
31
+ const err: any = new Error('Permission denied: AI cannot access other repositories');
32
+ err.status = 403;
33
+ throw err;
34
+ }
35
+ return; // Authorized for this specific repository in background context
36
+ }
29
37
  const user = ctx?.state?.currentUser;
30
38
  if (!user?.id) {
31
39
  const err: any = new Error('AI tool requires an authenticated user context');
@@ -51,18 +59,38 @@ export function registerGitReviewAiTools(app: Application) {
51
59
  }
52
60
  };
53
61
 
62
+ const sanitizeForDb = (obj: any): any => {
63
+ if (typeof obj === 'string') {
64
+ // Remove null bytes \u0000 which crash Postgres jsonb/text fields
65
+ // eslint-disable-next-line no-control-regex
66
+ return obj.replace(/\u0000/g, '');
67
+ }
68
+ if (Array.isArray(obj)) {
69
+ return obj.map(sanitizeForDb);
70
+ }
71
+ if (obj && typeof obj === 'object') {
72
+ const sanitized: any = {};
73
+ for (const [key, value] of Object.entries(obj)) {
74
+ sanitized[key] = sanitizeForDb(value);
75
+ }
76
+ return sanitized;
77
+ }
78
+ return obj;
79
+ };
80
+
54
81
  const runResourceAction = async (
55
82
  ctx: any,
56
83
  handler: (ctx: any, next: () => Promise<void>) => Promise<void>,
57
84
  params: Record<string, any>,
58
85
  gate: { resource: string; action: string },
59
86
  ) => {
60
- enforceAcl(ctx, gate.resource, gate.action);
87
+ enforceAcl(ctx, gate.resource, gate.action, params);
61
88
  const synthCtx: any = {
62
89
  ...ctx,
63
90
  app: ctx.app,
64
91
  db: ctx.db,
65
92
  action: { params },
93
+ request: { ...(ctx.request || {}), body: ctx.request?.body || {} },
66
94
  throw: (status: number, message: string) => {
67
95
  const err: any = new Error(message);
68
96
  err.status = status;
@@ -70,7 +98,7 @@ export function registerGitReviewAiTools(app: Application) {
70
98
  },
71
99
  };
72
100
  await handler(synthCtx, async () => undefined);
73
- return synthCtx.body;
101
+ return sanitizeForDb(synthCtx.body);
74
102
  };
75
103
 
76
104
  aiManager.toolsManager.registerTools([
@@ -57,6 +57,18 @@ export default defineCollection({
57
57
  interface: 'checkbox',
58
58
  uiSchema: { title: 'Auto Review', type: 'boolean', 'x-component': 'Checkbox' },
59
59
  },
60
+ {
61
+ type: 'belongsTo',
62
+ name: 'autoReviewFlow',
63
+ target: 'gitReviewFlows',
64
+ foreignKey: 'autoReviewFlowId',
65
+ interface: 'm2o',
66
+ uiSchema: {
67
+ title: 'Primary Auto Review Flow',
68
+ 'x-component': 'AssociationField',
69
+ 'x-component-props': { fieldNames: { label: 'name', value: 'id' } },
70
+ },
71
+ },
60
72
  {
61
73
  type: 'date',
62
74
  name: 'lastPolledAt',
@@ -0,0 +1,29 @@
1
+ import { Migration } from '@nocobase/server';
2
+ import { DataTypes } from 'sequelize';
3
+
4
+ export default class AddAutoReviewFlowIdMigration extends Migration {
5
+ on = 'afterLoad';
6
+
7
+ async up() {
8
+ const queryInterface = (this as any).db.sequelize.getQueryInterface();
9
+ const tablePrefix = (this as any).db.options?.tablePrefix || '';
10
+ const tableName = `${tablePrefix}gitRepositories`;
11
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
12
+ if (!tableInfo || tableInfo.autoReviewFlowId) return;
13
+
14
+ await queryInterface.addColumn(tableName, 'autoReviewFlowId', {
15
+ type: DataTypes.INTEGER,
16
+ allowNull: true,
17
+ });
18
+ }
19
+
20
+ async down() {
21
+ const queryInterface = (this as any).db.sequelize.getQueryInterface();
22
+ const tablePrefix = (this as any).db.options?.tablePrefix || '';
23
+ const tableName = `${tablePrefix}gitRepositories`;
24
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
25
+ if (!tableInfo?.autoReviewFlowId) return;
26
+
27
+ await queryInterface.removeColumn(tableName, 'autoReviewFlowId');
28
+ }
29
+ }