plugin-git-manager 1.0.3
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/client.d.ts +3 -0
- package/client.js +1 -0
- package/dist/client/100.e08d760e0b01997c.js +10 -0
- package/dist/client/components/CommitHistory.d.ts +2 -0
- package/dist/client/components/FileExplorer.d.ts +2 -0
- package/dist/client/components/GitManagerSettings.d.ts +2 -0
- package/dist/client/components/GitOperations.d.ts +2 -0
- package/dist/client/components/RepositoryConfig.d.ts +2 -0
- package/dist/client/context/GitManagerContext.d.ts +24 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +10 -0
- package/dist/client/locale.d.ts +2 -0
- package/dist/externalVersion.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +48 -0
- package/dist/locale/en-US.json +45 -0
- package/dist/locale/vi-VN.json +45 -0
- package/dist/node_modules/simple-git/dist/cjs/index.js +7399 -0
- package/dist/node_modules/simple-git/dist/esm/index.js +4745 -0
- package/dist/node_modules/simple-git/dist/esm/package.json +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/api.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/args/log-format.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-construct-error.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-error.d.ts +30 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-plugin-error.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-response-error.d.ts +32 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/task-configuration-error.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/git-factory.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/git-logger.d.ts +21 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch-delete.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-commit.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-diff-summary.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-fetch.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-list-log-summary.d.ts +6 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-merge.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-move.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-pull.d.ts +6 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-push.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-messages.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-objects.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/abort-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/block-unsafe-operations-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/command-config-prefixing-plugin.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/completion-detection.plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/custom-binary.plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/error-detection.plugin.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/index.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/plugin-store.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/progress-monitor-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/simple-git-plugin.d.ts +48 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/spawn-options-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/suffix-paths.plugin.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/timout-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/BranchDeleteSummary.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/BranchSummary.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/CheckIgnore.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/CleanSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/ConfigList.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/DiffSummary.d.ts +10 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/FileStatusSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/GetRemoteSummary.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/InitSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/MergeSummary.d.ts +16 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/PullSummary.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/StatusSummary.d.ts +19 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/TagList.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor-chain.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/promise-wrapped.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/scheduler.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/tasks-pending-queue.d.ts +23 -0
- package/dist/node_modules/simple-git/dist/src/lib/simple-git-api.d.ts +20 -0
- package/dist/node_modules/simple-git/dist/src/lib/task-callback.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/apply-patch.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/branch.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/change-working-directory.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/check-ignore.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/check-is-repo.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/checkout.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/clean.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/clone.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/commit.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/config.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/count-objects.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/diff-name-status.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/diff.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/fetch.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/first-commit.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/grep.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/hash-object.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/init.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/log.d.ts +32 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/merge.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/move.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/pull.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/push.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/remote.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/reset.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/show.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/stash-list.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/status.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/sub-module.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/tag.d.ts +18 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/task.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/version.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/handlers.d.ts +21 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/index.d.ts +136 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/tasks.d.ts +19 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/argument-filters.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/exit-codes.d.ts +10 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/git-output-streams.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/index.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/line-parser.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/simple-git-options.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/task-options.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/task-parser.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/util.d.ts +47 -0
- package/dist/node_modules/simple-git/dist/typings/errors.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/typings/index.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/typings/response.d.ts +556 -0
- package/dist/node_modules/simple-git/dist/typings/simple-git.d.ts +1033 -0
- package/dist/node_modules/simple-git/dist/typings/types.d.ts +22 -0
- package/dist/node_modules/simple-git/node_modules/debug/package.json +64 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/browser.js +272 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/common.js +292 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/index.js +10 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/node.js +263 -0
- package/dist/node_modules/simple-git/package.json +1 -0
- package/dist/node_modules/simple-git/promise.js +17 -0
- package/dist/server/actions/git-actions.d.ts +13 -0
- package/dist/server/actions/git-actions.js +354 -0
- package/dist/server/collections/gitRepositories.d.ts +2 -0
- package/dist/server/collections/gitRepositories.js +75 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +48 -0
- package/dist/server/plugin.d.ts +6 -0
- package/dist/server/plugin.js +120 -0
- package/package.json +32 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
- package/src/client/components/CommitHistory.tsx +326 -0
- package/src/client/components/FileExplorer.tsx +329 -0
- package/src/client/components/GitManagerSettings.tsx +123 -0
- package/src/client/components/GitOperations.tsx +195 -0
- package/src/client/components/RepositoryConfig.tsx +188 -0
- package/src/client/context/GitManagerContext.tsx +84 -0
- package/src/client/index.tsx +19 -0
- package/src/client/locale.ts +9 -0
- package/src/index.ts +2 -0
- package/src/locale/en-US.json +45 -0
- package/src/locale/vi-VN.json +45 -0
- package/src/server/actions/git-actions.ts +346 -0
- package/src/server/collections/gitRepositories.ts +45 -0
- package/src/server/index.ts +2 -0
- package/src/server/plugin.ts +81 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import simpleGit, { SimpleGit } from 'simple-git';
|
|
2
|
+
import { Context } from '@nocobase/actions';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const REF_PATTERN = /^[a-zA-Z0-9._\-\/]+$/;
|
|
7
|
+
|
|
8
|
+
// Per-repo mutex to prevent PAT race conditions in withAuth
|
|
9
|
+
const repoLocks = new Map<string, Promise<any>>();
|
|
10
|
+
|
|
11
|
+
function acquireLock(key: string): { promise: Promise<void>; release: () => void } {
|
|
12
|
+
const prev = repoLocks.get(key) || Promise.resolve();
|
|
13
|
+
let release: () => void;
|
|
14
|
+
const next = new Promise<void>((resolve) => {
|
|
15
|
+
release = resolve;
|
|
16
|
+
});
|
|
17
|
+
const promise = prev.then(() => {});
|
|
18
|
+
repoLocks.set(key, next);
|
|
19
|
+
return { promise, release: release! };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateRef(ref: string): string {
|
|
23
|
+
if (!REF_PATTERN.test(ref)) {
|
|
24
|
+
throw new Error(`Invalid ref: ${ref}`);
|
|
25
|
+
}
|
|
26
|
+
return ref;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateBranch(branch: string): string {
|
|
30
|
+
if (!branch || !REF_PATTERN.test(branch)) {
|
|
31
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
32
|
+
}
|
|
33
|
+
return branch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateRepoUrl(repoUrl: string): void {
|
|
37
|
+
let parsed: URL;
|
|
38
|
+
try {
|
|
39
|
+
parsed = new URL(repoUrl);
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error('Invalid repository URL');
|
|
42
|
+
}
|
|
43
|
+
if (parsed.protocol !== 'https:') {
|
|
44
|
+
throw new Error('Only HTTPS repository URLs are allowed');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function withAuth(git: ReturnType<typeof simpleGit>, repoUrl: string, pat: string, fn: () => Promise<any>) {
|
|
49
|
+
const lockKey = repoUrl;
|
|
50
|
+
const lock = acquireLock(lockKey);
|
|
51
|
+
await lock.promise;
|
|
52
|
+
const authUrl = getAuthUrl(repoUrl, pat);
|
|
53
|
+
await git.remote(['set-url', 'origin', authUrl]);
|
|
54
|
+
try {
|
|
55
|
+
return await fn();
|
|
56
|
+
} finally {
|
|
57
|
+
await git.remote(['set-url', 'origin', repoUrl]);
|
|
58
|
+
lock.release();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getAuthUrl(repoUrl: string, pat: string): string {
|
|
63
|
+
const url = new URL(repoUrl);
|
|
64
|
+
url.username = 'oauth2';
|
|
65
|
+
url.password = pat;
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getGit(localPath: string): SimpleGit {
|
|
70
|
+
return simpleGit(localPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function getRepo(ctx: Context) {
|
|
74
|
+
const { repositoryId } = ctx.action.params;
|
|
75
|
+
const repo = await ctx.db.getRepository('gitRepositories').findOne({
|
|
76
|
+
filterByTk: repositoryId,
|
|
77
|
+
});
|
|
78
|
+
if (!repo) {
|
|
79
|
+
ctx.throw(404, 'Repository not found');
|
|
80
|
+
}
|
|
81
|
+
return repo;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate localPath to prevent path traversal
|
|
85
|
+
function validateLocalPath(localPath: string): string {
|
|
86
|
+
const basePath = process.env.GIT_REPOS_BASE_PATH || path.join(process.cwd(), 'storage', 'git-repos');
|
|
87
|
+
const resolved = path.resolve(basePath, localPath);
|
|
88
|
+
|
|
89
|
+
// Ensure the resolved path is strictly inside the basePath.
|
|
90
|
+
// We add path.sep to prevent partial matches like /storage/git-repo-hack matching /storage/git-repo
|
|
91
|
+
const strictBasePath = path.resolve(basePath) + path.sep;
|
|
92
|
+
|
|
93
|
+
if (!resolved.startsWith(strictBasePath) && resolved !== path.resolve(basePath)) {
|
|
94
|
+
throw new Error('Invalid local path: path traversal detected or path is outside the allowed base directory');
|
|
95
|
+
}
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function clone(ctx: Context, next: () => Promise<void>) {
|
|
100
|
+
const repo = await getRepo(ctx);
|
|
101
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
102
|
+
const repoUrl = repo.get('repoUrl') as string;
|
|
103
|
+
const pat = repo.get('pat') as string;
|
|
104
|
+
|
|
105
|
+
validateRepoUrl(repoUrl);
|
|
106
|
+
|
|
107
|
+
// Check if directory already exists
|
|
108
|
+
if (fs.existsSync(localPath)) {
|
|
109
|
+
ctx.throw(400, 'Directory already exists. Remove it before cloning again.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(path.dirname(localPath))) {
|
|
113
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const authUrl = getAuthUrl(repoUrl, pat);
|
|
117
|
+
try {
|
|
118
|
+
await simpleGit().clone(authUrl, localPath, ['--branch', repo.get('defaultBranch') || 'main']);
|
|
119
|
+
// Remove PAT from the cloned repo's remote URL
|
|
120
|
+
await simpleGit(localPath).remote(['set-url', 'origin', repoUrl]);
|
|
121
|
+
await ctx.db.getRepository('gitRepositories').update({
|
|
122
|
+
filterByTk: repo.get('id'),
|
|
123
|
+
values: { status: 'connected' },
|
|
124
|
+
});
|
|
125
|
+
ctx.body = { success: true, message: 'Repository cloned successfully' };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
await ctx.db.getRepository('gitRepositories').update({
|
|
128
|
+
filterByTk: repo.get('id'),
|
|
129
|
+
values: { status: 'error' },
|
|
130
|
+
});
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
await next();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function pull(ctx: Context, next: () => Promise<void>) {
|
|
137
|
+
const repo = await getRepo(ctx);
|
|
138
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
139
|
+
const pat = repo.get('pat') as string;
|
|
140
|
+
const repoUrl = repo.get('repoUrl') as string;
|
|
141
|
+
|
|
142
|
+
const git = getGit(localPath);
|
|
143
|
+
const result = await withAuth(git, repoUrl, pat, () => git.pull());
|
|
144
|
+
|
|
145
|
+
ctx.body = { success: true, data: result };
|
|
146
|
+
await next();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function push(ctx: Context, next: () => Promise<void>) {
|
|
150
|
+
const repo = await getRepo(ctx);
|
|
151
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
152
|
+
const pat = repo.get('pat') as string;
|
|
153
|
+
const repoUrl = repo.get('repoUrl') as string;
|
|
154
|
+
|
|
155
|
+
const git = getGit(localPath);
|
|
156
|
+
const result = await withAuth(git, repoUrl, pat, () => git.push());
|
|
157
|
+
|
|
158
|
+
ctx.body = { success: true, data: result };
|
|
159
|
+
await next();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function fetch(ctx: Context, next: () => Promise<void>) {
|
|
163
|
+
const repo = await getRepo(ctx);
|
|
164
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
165
|
+
const pat = repo.get('pat') as string;
|
|
166
|
+
const repoUrl = repo.get('repoUrl') as string;
|
|
167
|
+
|
|
168
|
+
const git = getGit(localPath);
|
|
169
|
+
const result = await withAuth(git, repoUrl, pat, () => git.fetch());
|
|
170
|
+
|
|
171
|
+
ctx.body = { success: true, data: result };
|
|
172
|
+
await next();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function diff(ctx: Context, next: () => Promise<void>) {
|
|
176
|
+
const repo = await getRepo(ctx);
|
|
177
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
178
|
+
const { file, commitHash, compareHash } = ctx.action.params;
|
|
179
|
+
|
|
180
|
+
const git = getGit(localPath);
|
|
181
|
+
const args: string[] = [];
|
|
182
|
+
if (commitHash && compareHash) {
|
|
183
|
+
args.push(validateRef(commitHash), validateRef(compareHash));
|
|
184
|
+
} else if (commitHash) {
|
|
185
|
+
args.push(validateRef(commitHash) + '^', validateRef(commitHash));
|
|
186
|
+
}
|
|
187
|
+
if (file) {
|
|
188
|
+
if (file.includes('..')) ctx.throw(400, 'Invalid file path');
|
|
189
|
+
args.push('--', file);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await git.diff(args);
|
|
193
|
+
ctx.body = { success: true, data: result };
|
|
194
|
+
await next();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function status(ctx: Context, next: () => Promise<void>) {
|
|
198
|
+
const repo = await getRepo(ctx);
|
|
199
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
200
|
+
const result = await getGit(localPath).status();
|
|
201
|
+
ctx.body = { success: true, data: result };
|
|
202
|
+
await next();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function log(ctx: Context, next: () => Promise<void>) {
|
|
206
|
+
const repo = await getRepo(ctx);
|
|
207
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
208
|
+
const { maxCount = 50, file } = ctx.action.params;
|
|
209
|
+
|
|
210
|
+
const parsed = parseInt(maxCount, 10);
|
|
211
|
+
const options: Record<string, any> = { maxCount: Math.min(Math.max(parsed || 50, 1), 500) };
|
|
212
|
+
if (file) {
|
|
213
|
+
if (file.includes('..')) ctx.throw(400, 'Invalid file path');
|
|
214
|
+
options.file = file;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await getGit(localPath).log(options);
|
|
218
|
+
ctx.body = { success: true, data: result };
|
|
219
|
+
await next();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function branches(ctx: Context, next: () => Promise<void>) {
|
|
223
|
+
const repo = await getRepo(ctx);
|
|
224
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
225
|
+
const result = await getGit(localPath).branch();
|
|
226
|
+
ctx.body = { success: true, data: result };
|
|
227
|
+
await next();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function checkout(ctx: Context, next: () => Promise<void>) {
|
|
231
|
+
const repo = await getRepo(ctx);
|
|
232
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
233
|
+
const { branch } = ctx.action.params;
|
|
234
|
+
validateBranch(branch);
|
|
235
|
+
await getGit(localPath).checkout(branch);
|
|
236
|
+
ctx.body = { success: true, message: `Switched to branch ${branch}` };
|
|
237
|
+
await next();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function fileTree(ctx: Context, next: () => Promise<void>) {
|
|
241
|
+
const repo = await getRepo(ctx);
|
|
242
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
243
|
+
const { ref = 'HEAD', treePath = '' } = ctx.action.params;
|
|
244
|
+
|
|
245
|
+
const git = getGit(localPath);
|
|
246
|
+
validateRef(ref);
|
|
247
|
+
if (treePath && treePath.includes('..')) {
|
|
248
|
+
ctx.throw(400, 'Invalid tree path');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const detailArgs = ['ls-tree', '-l', ref];
|
|
252
|
+
if (treePath) detailArgs.push(treePath + '/');
|
|
253
|
+
const detailedResult = await git.raw(detailArgs);
|
|
254
|
+
const items = detailedResult.trim().split('\n').filter(Boolean).map((line) => {
|
|
255
|
+
// format: <mode> <type> <hash> <size>\t<name>
|
|
256
|
+
const match = line.match(/^(\d+)\s+(blob|tree)\s+([a-f0-9]+)\s+(-|\d+)\t(.+)$/);
|
|
257
|
+
if (!match) return null;
|
|
258
|
+
const fullPath = match[5];
|
|
259
|
+
// Extract just the filename from the full path when using treePath prefix
|
|
260
|
+
const name = fullPath.includes('/') ? fullPath.split('/').pop()! : fullPath;
|
|
261
|
+
return {
|
|
262
|
+
mode: match[1],
|
|
263
|
+
type: match[2] as 'blob' | 'tree',
|
|
264
|
+
hash: match[3],
|
|
265
|
+
size: match[4] === '-' ? 0 : parseInt(match[4], 10),
|
|
266
|
+
name,
|
|
267
|
+
path: treePath ? `${treePath}/${name}` : name,
|
|
268
|
+
};
|
|
269
|
+
}).filter(Boolean);
|
|
270
|
+
|
|
271
|
+
// Sort: directories first, then files, both alphabetical
|
|
272
|
+
items.sort((a, b) => {
|
|
273
|
+
if (a.type !== b.type) return a.type === 'tree' ? -1 : 1;
|
|
274
|
+
return a.name.localeCompare(b.name);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
ctx.body = { success: true, data: items };
|
|
278
|
+
await next();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function fileContent(ctx: Context, next: () => Promise<void>) {
|
|
282
|
+
const repo = await getRepo(ctx);
|
|
283
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
284
|
+
const { ref = 'HEAD', filePath } = ctx.action.params;
|
|
285
|
+
|
|
286
|
+
if (!filePath) {
|
|
287
|
+
ctx.throw(400, 'filePath is required');
|
|
288
|
+
}
|
|
289
|
+
if (filePath.includes('..')) {
|
|
290
|
+
ctx.throw(400, 'Invalid file path');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
validateRef(ref);
|
|
294
|
+
const git = getGit(localPath);
|
|
295
|
+
const content = await git.show([`${ref}:${filePath}`]);
|
|
296
|
+
ctx.body = { success: true, data: { content, filePath, ref } };
|
|
297
|
+
await next();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function commitDetail(ctx: Context, next: () => Promise<void>) {
|
|
301
|
+
const repo = await getRepo(ctx);
|
|
302
|
+
const localPath = validateLocalPath(repo.get('localPath'));
|
|
303
|
+
const { commitHash } = ctx.action.params;
|
|
304
|
+
|
|
305
|
+
if (!commitHash) {
|
|
306
|
+
ctx.throw(400, 'commitHash is required');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const git = getGit(localPath);
|
|
310
|
+
validateRef(commitHash);
|
|
311
|
+
|
|
312
|
+
// Use %x00 in format string to tell git to output null bytes, avoiding null bytes in args
|
|
313
|
+
const DELIM_ARG = '%x00';
|
|
314
|
+
const DELIM_OUT = '\x00';
|
|
315
|
+
const format = `%H${DELIM_ARG}%an${DELIM_ARG}%ae${DELIM_ARG}%aI${DELIM_ARG}%s${DELIM_ARG}%b`;
|
|
316
|
+
|
|
317
|
+
// Run show + diff in parallel for better performance
|
|
318
|
+
const [show, diffResult] = await Promise.all([
|
|
319
|
+
git.show([commitHash, '--stat', `--format=${format}`]),
|
|
320
|
+
git.diff([`${commitHash}^`, commitHash, '--name-status']).catch(() =>
|
|
321
|
+
// Root commit has no parent — use diff-tree --root instead
|
|
322
|
+
git.raw(['diff-tree', '--root', '--name-status', '-r', commitHash]),
|
|
323
|
+
),
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
const parts = show.split(DELIM_OUT);
|
|
327
|
+
const files = diffResult.trim().split('\n').filter(Boolean).map((line) => {
|
|
328
|
+
const [statusCode, ...fileParts] = line.split('\t');
|
|
329
|
+
return { status: statusCode, file: fileParts.join('\t') };
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
ctx.body = {
|
|
333
|
+
success: true,
|
|
334
|
+
data: {
|
|
335
|
+
hash: parts[0] || '',
|
|
336
|
+
author: parts[1] || '',
|
|
337
|
+
email: parts[2] || '',
|
|
338
|
+
date: parts[3] || '',
|
|
339
|
+
subject: parts[4] || '',
|
|
340
|
+
body: (parts[5] || '').split('\n\n')[0].trim(), // body before --stat output
|
|
341
|
+
files,
|
|
342
|
+
raw: show,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
await next();
|
|
346
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineCollection } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default defineCollection({
|
|
4
|
+
name: 'gitRepositories',
|
|
5
|
+
title: 'Git Repositories',
|
|
6
|
+
fields: [
|
|
7
|
+
{
|
|
8
|
+
type: 'string',
|
|
9
|
+
name: 'name',
|
|
10
|
+
interface: 'input',
|
|
11
|
+
uiSchema: { title: 'Repository Name', type: 'string', 'x-component': 'Input' },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: 'string',
|
|
15
|
+
name: 'repoUrl',
|
|
16
|
+
interface: 'input',
|
|
17
|
+
uiSchema: { title: 'Repository URL', type: 'string', 'x-component': 'Input' },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'string',
|
|
21
|
+
name: 'localPath',
|
|
22
|
+
interface: 'input',
|
|
23
|
+
uiSchema: { title: 'Local Path', type: 'string', 'x-component': 'Input' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'password',
|
|
27
|
+
name: 'pat',
|
|
28
|
+
interface: 'password',
|
|
29
|
+
uiSchema: { title: 'Personal Access Token', type: 'string', 'x-component': 'Password' },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'string',
|
|
33
|
+
name: 'defaultBranch',
|
|
34
|
+
defaultValue: 'main',
|
|
35
|
+
interface: 'input',
|
|
36
|
+
uiSchema: { title: 'Default Branch', type: 'string', 'x-component': 'Input' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'string',
|
|
40
|
+
name: 'status',
|
|
41
|
+
defaultValue: 'disconnected',
|
|
42
|
+
interface: 'input',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import * as gitActions from './actions/git-actions';
|
|
4
|
+
|
|
5
|
+
export class PluginGitManagerServer extends Plugin {
|
|
6
|
+
async load() {
|
|
7
|
+
await this.db.import({
|
|
8
|
+
directory: resolve(__dirname, 'collections'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
this.app.resourceManager.define({
|
|
12
|
+
name: 'gitManager',
|
|
13
|
+
actions: {
|
|
14
|
+
clone: gitActions.clone,
|
|
15
|
+
pull: gitActions.pull,
|
|
16
|
+
push: gitActions.push,
|
|
17
|
+
fetch: gitActions.fetch,
|
|
18
|
+
diff: gitActions.diff,
|
|
19
|
+
status: gitActions.status,
|
|
20
|
+
log: gitActions.log,
|
|
21
|
+
branches: gitActions.branches,
|
|
22
|
+
checkout: gitActions.checkout,
|
|
23
|
+
fileTree: gitActions.fileTree,
|
|
24
|
+
fileContent: gitActions.fileContent,
|
|
25
|
+
commitDetail: gitActions.commitDetail,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Read-only operations available to all plugin users
|
|
30
|
+
this.app.acl.registerSnippet({
|
|
31
|
+
name: `pm.${this.name}.read`,
|
|
32
|
+
actions: [
|
|
33
|
+
'gitRepositories:list',
|
|
34
|
+
'gitRepositories:get',
|
|
35
|
+
'gitManager:status',
|
|
36
|
+
'gitManager:log',
|
|
37
|
+
'gitManager:branches',
|
|
38
|
+
'gitManager:diff',
|
|
39
|
+
'gitManager:fileTree',
|
|
40
|
+
'gitManager:fileContent',
|
|
41
|
+
'gitManager:commitDetail',
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Write operations require separate permission
|
|
46
|
+
this.app.acl.registerSnippet({
|
|
47
|
+
name: `pm.${this.name}.write`,
|
|
48
|
+
actions: [
|
|
49
|
+
'gitRepositories:create',
|
|
50
|
+
'gitRepositories:update',
|
|
51
|
+
'gitRepositories:destroy',
|
|
52
|
+
'gitManager:clone',
|
|
53
|
+
'gitManager:pull',
|
|
54
|
+
'gitManager:push',
|
|
55
|
+
'gitManager:fetch',
|
|
56
|
+
'gitManager:checkout',
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Strip PAT from API responses — scoped to gitRepositories only
|
|
61
|
+
this.app.resourceManager.use(async (ctx, next) => {
|
|
62
|
+
if (ctx.action?.resourceName !== 'gitRepositories') {
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
await next();
|
|
66
|
+
if (ctx.body) {
|
|
67
|
+
const items = Array.isArray(ctx.body) ? ctx.body : ctx.body?.data ? (Array.isArray(ctx.body.data) ? ctx.body.data : [ctx.body.data]) : [ctx.body];
|
|
68
|
+
items.forEach((item) => {
|
|
69
|
+
if (item && typeof item === 'object') {
|
|
70
|
+
if (item.pat) item.pat = '••••••••';
|
|
71
|
+
if (item.dataValues?.pat) item.dataValues.pat = '••••••••';
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async install() {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default PluginGitManagerServer;
|