plugin-git-manager 1.1.9 → 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.
- package/dist/client/187.08dd0bf4d0f68036.js +10 -0
- package/dist/client/components/RunReviewButton.d.ts +1 -1
- package/dist/client/context/GitManagerContext.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -4
- package/dist/locale/en-US.json +8 -1
- package/dist/server/actions/gitlab-api.js +14 -13
- package/dist/server/actions/review.js +13 -9
- package/dist/server/ai-tools.js +2 -0
- package/dist/server/collections/gitCodeReviews.d.ts +1 -1
- package/dist/server/collections/gitRepositories.d.ts +1 -1
- package/dist/server/collections/gitRepositories.js +12 -0
- package/dist/server/collections/gitReviewFlows.d.ts +1 -1
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
- package/dist/server/plugin.d.ts +4 -0
- package/dist/server/plugin.js +38 -6
- package/dist/server/poller.js +3 -1
- package/package.json +1 -1
- package/src/client/components/CommitHistory.tsx +18 -3
- package/src/client/components/GitOperations.tsx +29 -16
- package/src/client/components/PollingStatus.tsx +27 -1
- package/src/client/components/RepositoryConfig.tsx +76 -3
- package/src/client/components/RunReviewButton.tsx +375 -278
- package/src/client/context/GitManagerContext.tsx +2 -0
- package/src/client/index.tsx +31 -31
- package/src/locale/en-US.json +8 -1
- package/src/server/actions/gitlab-api.ts +8 -4
- package/src/server/actions/review.ts +7 -3
- package/src/server/ai-tools.ts +1 -0
- package/src/server/collections/gitRepositories.ts +12 -0
- package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
- package/src/server/plugin.ts +216 -180
- package/src/server/poller.ts +11 -2
- package/dist/client/187.eec7be93247463d7.js +0 -10
package/src/client/index.tsx
CHANGED
|
@@ -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;
|
package/src/locale/en-US.json
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
262
|
+
const params = getActionParams(ctx);
|
|
259
263
|
const { reviewId, reason } = params;
|
|
260
264
|
if (!reviewId) ctx.throw(400, 'reviewId is required');
|
|
261
265
|
|
package/src/server/ai-tools.ts
CHANGED
|
@@ -90,6 +90,7 @@ export function registerGitReviewAiTools(app: Application) {
|
|
|
90
90
|
app: ctx.app,
|
|
91
91
|
db: ctx.db,
|
|
92
92
|
action: { params },
|
|
93
|
+
request: { ...(ctx.request || {}), body: ctx.request?.body || {} },
|
|
93
94
|
throw: (status: number, message: string) => {
|
|
94
95
|
const err: any = new Error(message);
|
|
95
96
|
err.status = status;
|
|
@@ -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
|
+
}
|
package/src/server/plugin.ts
CHANGED
|
@@ -1,180 +1,216 @@
|
|
|
1
|
-
import { Plugin } from '@nocobase/server';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export class PluginGitManagerServer extends Plugin {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.app
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { DataTypes } from 'sequelize';
|
|
4
|
+
import * as gitActions from './actions/git-actions';
|
|
5
|
+
import * as gitlabApi from './actions/gitlab-api';
|
|
6
|
+
import * as reviewActions from './actions/review';
|
|
7
|
+
import * as pollerActions from './actions/poller';
|
|
8
|
+
import { recoverStuckReviews } from './actions/review';
|
|
9
|
+
import { registerGitReviewAiTools } from './ai-tools';
|
|
10
|
+
import { startPoller, stopPoller } from './poller';
|
|
11
|
+
|
|
12
|
+
export class PluginGitManagerServer extends Plugin {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
declare app: any;
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
declare db: any;
|
|
17
|
+
async beforeLoad() {
|
|
18
|
+
await (this as any).app.db.import({
|
|
19
|
+
directory: resolve(__dirname, 'collections'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
(this as any).app.db.addMigrations({
|
|
23
|
+
namespace: (this as any).name,
|
|
24
|
+
directory: resolve(__dirname, 'migrations'),
|
|
25
|
+
context: { plugin: this },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async load() {
|
|
30
|
+
// Ensure dayjs timezone + utc plugins are loaded globally to prevent 'm.startOf is not a function' errors
|
|
31
|
+
const dayjsLib = require('dayjs');
|
|
32
|
+
const utcPlugin = require('dayjs/plugin/utc');
|
|
33
|
+
const timezonePlugin = require('dayjs/plugin/timezone');
|
|
34
|
+
dayjsLib.extend(utcPlugin);
|
|
35
|
+
dayjsLib.extend(timezonePlugin);
|
|
36
|
+
|
|
37
|
+
(this as any).app.resourceManager.define({
|
|
38
|
+
name: 'gitManager',
|
|
39
|
+
actions: {
|
|
40
|
+
clone: gitActions.clone,
|
|
41
|
+
pull: gitActions.pull,
|
|
42
|
+
push: gitActions.push,
|
|
43
|
+
fetch: gitActions.fetch,
|
|
44
|
+
diff: gitActions.diff,
|
|
45
|
+
status: gitActions.status,
|
|
46
|
+
log: gitActions.log,
|
|
47
|
+
branches: gitActions.branches,
|
|
48
|
+
checkout: gitActions.checkout,
|
|
49
|
+
fileTree: gitActions.fileTree,
|
|
50
|
+
fileContent: gitActions.fileContent,
|
|
51
|
+
commitDetail: gitActions.commitDetail,
|
|
52
|
+
mergeRequests: gitlabApi.mergeRequests,
|
|
53
|
+
mergeRequestDetail: gitlabApi.mergeRequestDetail,
|
|
54
|
+
mergeRequestNotes: gitlabApi.mergeRequestNotes,
|
|
55
|
+
triggerReview: reviewActions.triggerReview,
|
|
56
|
+
reviewApprovePost: reviewActions.reviewApprovePost,
|
|
57
|
+
reviewReject: reviewActions.reviewReject,
|
|
58
|
+
pollNow: pollerActions.pollNow,
|
|
59
|
+
pollerStatus: pollerActions.pollerStatus,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Suppress noisy workflow pre-action/post-action warnings for custom resources
|
|
64
|
+
(this as any).app.use(async (ctx, next) => {
|
|
65
|
+
if (ctx.logger && ctx.logger.warn) {
|
|
66
|
+
const originalWarn = ctx.logger.warn.bind(ctx.logger);
|
|
67
|
+
ctx.logger.warn = (message: any, ...args: any[]) => {
|
|
68
|
+
if (
|
|
69
|
+
typeof message === 'string' &&
|
|
70
|
+
message.includes('[Workflow') &&
|
|
71
|
+
message.includes('collection') &&
|
|
72
|
+
message.includes('not found')
|
|
73
|
+
) {
|
|
74
|
+
return ctx.logger;
|
|
75
|
+
}
|
|
76
|
+
return originalWarn(message, ...args);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return next();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
registerGitReviewAiTools((this as any).app);
|
|
83
|
+
|
|
84
|
+
(this as any).app.on('afterStart', async () => {
|
|
85
|
+
await ensureAutoReviewFlowSchema((this as any).app).catch(
|
|
86
|
+
(err) => (this as any).app.log?.error?.('plugin-git-manager: ensure schema error', err),
|
|
87
|
+
);
|
|
88
|
+
// Sweep any review left in `running` state from a previous process.
|
|
89
|
+
recoverStuckReviews((this as any).app).catch(
|
|
90
|
+
(err) => (this as any).app.log?.error?.('plugin-git-manager: recoverStuckReviews error', err),
|
|
91
|
+
);
|
|
92
|
+
startPoller((this as any).app);
|
|
93
|
+
});
|
|
94
|
+
(this as any).app.on('beforeStop', () => {
|
|
95
|
+
stopPoller();
|
|
96
|
+
});
|
|
97
|
+
(this as any).app.on('beforeDestroy', () => {
|
|
98
|
+
stopPoller();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Read-only operations available to all plugin users
|
|
102
|
+
(this as any).app.acl.registerSnippet({
|
|
103
|
+
name: `pm.${(this as any).name}.read`,
|
|
104
|
+
actions: [
|
|
105
|
+
'gitRepositories:list',
|
|
106
|
+
'gitRepositories:get',
|
|
107
|
+
'gitReviewFlows:list',
|
|
108
|
+
'gitReviewFlows:get',
|
|
109
|
+
'gitCodeReviews:list',
|
|
110
|
+
'gitCodeReviews:get',
|
|
111
|
+
'gitManager:status',
|
|
112
|
+
'gitManager:log',
|
|
113
|
+
'gitManager:branches',
|
|
114
|
+
'gitManager:diff',
|
|
115
|
+
'gitManager:fileTree',
|
|
116
|
+
'gitManager:fileContent',
|
|
117
|
+
'gitManager:commitDetail',
|
|
118
|
+
'gitManager:mergeRequests',
|
|
119
|
+
'gitManager:mergeRequestDetail',
|
|
120
|
+
'gitManager:mergeRequestNotes',
|
|
121
|
+
'gitManager:pollerStatus',
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Write operations require separate permission
|
|
126
|
+
(this as any).app.acl.registerSnippet({
|
|
127
|
+
name: `pm.${(this as any).name}.write`,
|
|
128
|
+
actions: [
|
|
129
|
+
'gitRepositories:create',
|
|
130
|
+
'gitRepositories:update',
|
|
131
|
+
'gitRepositories:destroy',
|
|
132
|
+
'gitReviewFlows:create',
|
|
133
|
+
'gitReviewFlows:update',
|
|
134
|
+
'gitReviewFlows:destroy',
|
|
135
|
+
'gitCodeReviews:create',
|
|
136
|
+
'gitCodeReviews:update',
|
|
137
|
+
'gitCodeReviews:destroy',
|
|
138
|
+
'gitManager:clone',
|
|
139
|
+
'gitManager:pull',
|
|
140
|
+
'gitManager:push',
|
|
141
|
+
'gitManager:fetch',
|
|
142
|
+
'gitManager:checkout',
|
|
143
|
+
'gitManager:triggerReview',
|
|
144
|
+
'gitManager:reviewApprovePost',
|
|
145
|
+
'gitManager:reviewReject',
|
|
146
|
+
'gitManager:pollNow',
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Prevent overwriting PAT with obfuscated value on updates
|
|
151
|
+
(this as any).app.resourceManager.use(async (ctx, next) => {
|
|
152
|
+
if (ctx.action?.resourceName === 'gitRepositories' && ['create', 'update'].includes(ctx.action?.actionName)) {
|
|
153
|
+
if (ctx.action.params?.values?.pat === '••••••••') {
|
|
154
|
+
delete ctx.action.params.values.pat;
|
|
155
|
+
}
|
|
156
|
+
if (ctx.request.body?.pat === '••••••••') {
|
|
157
|
+
delete ctx.request.body.pat;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return next();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Strip PAT from API responses — scoped to gitRepositories only
|
|
164
|
+
(this as any).app.resourceManager.use(async (ctx, next) => {
|
|
165
|
+
if (ctx.action?.resourceName !== 'gitRepositories') {
|
|
166
|
+
return next();
|
|
167
|
+
}
|
|
168
|
+
await next();
|
|
169
|
+
if (ctx.body) {
|
|
170
|
+
const items = Array.isArray(ctx.body)
|
|
171
|
+
? ctx.body
|
|
172
|
+
: ctx.body?.data
|
|
173
|
+
? Array.isArray(ctx.body.data)
|
|
174
|
+
? ctx.body.data
|
|
175
|
+
: [ctx.body.data]
|
|
176
|
+
: [ctx.body];
|
|
177
|
+
items.forEach((item) => {
|
|
178
|
+
if (item && typeof item === 'object') {
|
|
179
|
+
if (item.pat) item.pat = '••••••••';
|
|
180
|
+
if (item.dataValues?.pat) item.dataValues.pat = '••••••••';
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async install() {
|
|
188
|
+
await (this as any).app.db.getCollection('gitRepositories')?.sync();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async beforeDisable() {
|
|
192
|
+
stopPoller();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async beforeUnload() {
|
|
196
|
+
stopPoller();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function ensureAutoReviewFlowSchema(app: any) {
|
|
201
|
+
const sequelize = app.db?.sequelize;
|
|
202
|
+
const queryInterface = sequelize?.getQueryInterface?.();
|
|
203
|
+
if (!queryInterface) return;
|
|
204
|
+
|
|
205
|
+
const tablePrefix = app.db.options?.tablePrefix || '';
|
|
206
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
207
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
208
|
+
if (!tableInfo || tableInfo.autoReviewFlowId) return;
|
|
209
|
+
|
|
210
|
+
await queryInterface.addColumn(tableName, 'autoReviewFlowId', {
|
|
211
|
+
type: DataTypes.INTEGER,
|
|
212
|
+
allowNull: true,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default PluginGitManagerServer;
|