@weibaohui/dsh-git-server 0.1.3 → 0.2.0
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/README.md +71 -2
- package/client/bundle.js +657 -75
- package/client/index.js +657 -75
- package/package.json +1 -1
- package/server/dist/api/v1.js +71 -3
- package/server/dist/dshapi.js +558 -66
- package/server/dist/gitx/git.js +39 -3
- package/server/dist/gitx/service.js +4 -3
- package/server/dist/hook.js +31 -2
- package/server/dist/notify.js +55 -0
- package/src/index.js +12 -1
package/server/dist/dshapi.js
CHANGED
|
@@ -14,6 +14,17 @@ import { markdown, sanitizeHTML } from './markup.js';
|
|
|
14
14
|
import { authenticateUserByToken } from './context.js';
|
|
15
15
|
import * as svc from './gitx/service.js';
|
|
16
16
|
function requireAuth() { return { authenticateUserByToken }; }
|
|
17
|
+
/** token 认证(非仓库作用域端点用,如通知/我的工单/导入)。 */
|
|
18
|
+
function authUser(c) {
|
|
19
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
20
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
21
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
22
|
+
if (!user) {
|
|
23
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return user;
|
|
27
|
+
}
|
|
17
28
|
export function registerDshRoutes(m) {
|
|
18
29
|
// ── token 认证 + 仓库解析(:o/:r) ─────────────────────────────
|
|
19
30
|
const authRepo = (c, needWrite = false) => {
|
|
@@ -74,7 +85,11 @@ export function registerDshRoutes(m) {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
catch { /* no readme */ }
|
|
77
|
-
|
|
88
|
+
let numCommits = 0;
|
|
89
|
+
try {
|
|
90
|
+
numCommits = Number((await git.git(dir, 'rev-list', '--count', '--end-of-options', def))?.toString('utf8').trim() || 0);
|
|
91
|
+
}
|
|
92
|
+
catch { /* 空仓库没有任何提交 */ }
|
|
78
93
|
const numReleases = db.db().prepare('SELECT COUNT(*) AS c FROM release WHERE repo_id = ?').get(repo.id).c;
|
|
79
94
|
c.JSONSuccess({
|
|
80
95
|
defaultBranch: def, branches, tags, readmeHtml,
|
|
@@ -96,7 +111,7 @@ export function registerDshRoutes(m) {
|
|
|
96
111
|
});
|
|
97
112
|
c.JSONSuccess(rows);
|
|
98
113
|
});
|
|
99
|
-
// ──
|
|
114
|
+
// ── 文件列表(带每项最近提交;一次 git log 批量解析,不再每条目起子进程) ──
|
|
100
115
|
m.get('/api/dsh/repos/:o/:r/tree', async (c) => {
|
|
101
116
|
const ar = authRepo(c);
|
|
102
117
|
if (!ar)
|
|
@@ -106,21 +121,31 @@ export function registerDshRoutes(m) {
|
|
|
106
121
|
const p = c.Query('path');
|
|
107
122
|
const tree = await git.lsTree(dir, ref, p || '');
|
|
108
123
|
if (!tree) {
|
|
124
|
+
// 空仓库(默认分支尚无提交)按空目录返回,而不是 404
|
|
125
|
+
const anyCommit = await git.getCommit(dir, ar.repo.default_branch || conf.defaultBranch);
|
|
126
|
+
if (!anyCommit) {
|
|
127
|
+
c.JSONSuccess({ entries: [] });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
109
130
|
c.JSON(404, { error: 'not found' });
|
|
110
131
|
return;
|
|
111
132
|
}
|
|
112
|
-
const entries =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
const entries = tree.entries.map((e) => ({
|
|
134
|
+
name: e.name, type: e.type, path: p ? p + '/' + e.name : e.name,
|
|
135
|
+
size: e.type === 'blob' ? e.size : 0,
|
|
136
|
+
last: null,
|
|
137
|
+
}));
|
|
138
|
+
const lastMap = await git.lastCommitsForPaths(dir, ref, entries.map((e) => e.path));
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
let last = lastMap.get(e.path) ?? null;
|
|
141
|
+
if (!last) {
|
|
142
|
+
// 批量遍历上限之外的陈旧条目:单条兜底(很少见)
|
|
143
|
+
const out = (await git.gitOK(dir, 'log', '-1', '--pretty=format:%h%x1f%s%x1f%ct%x1f%an', '--end-of-options', ref, '--', e.path))?.toString('utf8') ?? '';
|
|
144
|
+
const [lsha, lmsg, ldate, lauthor] = out.split('\x1f');
|
|
145
|
+
if (lsha)
|
|
146
|
+
last = { sha: lsha, msg: (lmsg || '').slice(0, 90), date: Number(ldate) * 1000, author: lauthor || '' };
|
|
147
|
+
}
|
|
148
|
+
e.last = last;
|
|
124
149
|
}
|
|
125
150
|
c.JSONSuccess({ entries });
|
|
126
151
|
});
|
|
@@ -169,6 +194,10 @@ export function registerDshRoutes(m) {
|
|
|
169
194
|
return;
|
|
170
195
|
}
|
|
171
196
|
try {
|
|
197
|
+
// 先算 diff/patch(分支不存在等失败不落任何行,避免幽灵工单)
|
|
198
|
+
const headDir = repo.RepoPath();
|
|
199
|
+
const mergeBase = (await git.mergeBase(headDir, base, head)) ?? '';
|
|
200
|
+
const patch = await git.git(headDir, 'diff', '--full-index', '--binary', '--end-of-options', mergeBase || base, head);
|
|
172
201
|
const { issueAction, ActionType } = await import('./db/actions.js');
|
|
173
202
|
const index = db.maxIssueIndex(repo.id) + 1;
|
|
174
203
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -185,9 +214,6 @@ export function registerDshRoutes(m) {
|
|
|
185
214
|
db.db().prepare('UPDATE issue SET milestone_id = ?, assignee_id = ? WHERE id = ?')
|
|
186
215
|
.run(Number(body.milestone) || 0, au ? au.id : 0, issueID);
|
|
187
216
|
}
|
|
188
|
-
const headDir = repo.RepoPath();
|
|
189
|
-
const mergeBase = (await git.mergeBase(headDir, base, head)) ?? '';
|
|
190
|
-
const patch = await git.git(headDir, 'diff', '--full-index', '--binary', '--end-of-options', mergeBase || base, head);
|
|
191
217
|
const patchDir = path.join(conf.appDataPath, 'patches', String(repo.id));
|
|
192
218
|
fs.mkdirSync(patchDir, { recursive: true });
|
|
193
219
|
fs.writeFileSync(path.join(patchDir, `${index}.patch`), patch);
|
|
@@ -198,23 +224,63 @@ export function registerDshRoutes(m) {
|
|
|
198
224
|
db.refreshIssueCounts(repo.id);
|
|
199
225
|
const issue = db.getIssueByID(issueID);
|
|
200
226
|
await issueAction(ActionType.CREATE_PULL_REQUEST, user, repo, issue);
|
|
227
|
+
const { markIssueUnread } = await import('./notify.js');
|
|
228
|
+
markIssueUnread(issue, user.id, { mentionContent: String(body.body ?? ''), assigned: true });
|
|
201
229
|
c.JSONSuccess({ ok: true, index });
|
|
202
230
|
}
|
|
203
231
|
catch (e) {
|
|
204
232
|
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
205
233
|
}
|
|
206
234
|
});
|
|
207
|
-
// ──
|
|
235
|
+
// ── 仓库信息(设置页用;镜像仓带上游地址与同步时间) ────────────
|
|
208
236
|
m.get('/api/dsh/repos/:o/:r', async (c) => {
|
|
209
237
|
const ar = authRepo(c);
|
|
210
238
|
if (!ar)
|
|
211
239
|
return;
|
|
240
|
+
let mirror = null;
|
|
241
|
+
if (ar.repo.is_mirror) {
|
|
242
|
+
const { mirrorAddress } = await import('./mirror.js');
|
|
243
|
+
const row = db.db().prepare('SELECT updated_unix, next_update_unix FROM mirror WHERE repo_id = ?').get(ar.repo.id);
|
|
244
|
+
mirror = {
|
|
245
|
+
address: mirrorAddress(ar.repo.RepoPath()),
|
|
246
|
+
updatedAt: (row?.updated_unix ?? 0) * 1000,
|
|
247
|
+
nextAt: (row?.next_update_unix ?? 0) * 1000,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
212
250
|
c.JSONSuccess({
|
|
213
251
|
name: ar.repo.name, owner: ar.repo.OwnerName(),
|
|
214
252
|
description: ar.repo.description || '', private: !!ar.repo.is_private,
|
|
215
253
|
defaultBranch: ar.repo.default_branch || conf.defaultBranch,
|
|
254
|
+
isMirror: !!ar.repo.is_mirror, mirror,
|
|
216
255
|
});
|
|
217
256
|
});
|
|
257
|
+
// ── 镜像:立即同步(拉取上游更新) ──────────────────────────────
|
|
258
|
+
m.post('/api/dsh/repos/:o/:r/mirror-sync', async (c) => {
|
|
259
|
+
const ar = authRepo(c, true);
|
|
260
|
+
if (!ar)
|
|
261
|
+
return;
|
|
262
|
+
if (!ar.repo.is_mirror) {
|
|
263
|
+
c.JSON(422, { error: '不是镜像仓库' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const { syncMirror } = await import('./mirror.js');
|
|
268
|
+
await syncMirror(ar.repo.id, ar.user.id);
|
|
269
|
+
const row = db.db().prepare('SELECT updated_unix, next_update_unix FROM mirror WHERE repo_id = ?').get(ar.repo.id);
|
|
270
|
+
c.JSONSuccess({ ok: true, updatedAt: (row?.updated_unix ?? 0) * 1000, nextAt: (row?.next_update_unix ?? 0) * 1000 });
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
c.JSON(500, { error: '同步失败:' + String(e?.message ?? e).slice(0, 160) });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
// ── fork 列表(谁复刻了本仓库) ─────────────────────────────────
|
|
277
|
+
m.get('/api/dsh/repos/:o/:r/forks', async (c) => {
|
|
278
|
+
const ar = authRepo(c);
|
|
279
|
+
if (!ar)
|
|
280
|
+
return;
|
|
281
|
+
const rows = db.db().prepare('SELECT r.name, r.num_stars, r.updated_unix, u.name AS owner FROM repository r JOIN user u ON u.id = r.owner_id WHERE r.fork_id = ? ORDER BY r.id DESC LIMIT 50').all(ar.repo.id);
|
|
282
|
+
c.JSONSuccess(rows.map((r) => ({ name: r.name, owner: r.owner, stars: r.num_stars || 0, updatedAt: (r.updated_unix ?? 0) * 1000 })));
|
|
283
|
+
});
|
|
218
284
|
// ── 单条 issue(详情页编辑用) ───────────────────────────────
|
|
219
285
|
m.get('/api/dsh/repos/:o/:r/issues/:idx', async (c) => {
|
|
220
286
|
const ar = authRepo(c);
|
|
@@ -650,10 +716,13 @@ export function registerDshRoutes(m) {
|
|
|
650
716
|
sets.push('milestone_id = ?');
|
|
651
717
|
args.push(Number(body.milestone) || 0);
|
|
652
718
|
}
|
|
719
|
+
let assigneeChanged = false;
|
|
653
720
|
if (body.assignee !== undefined) {
|
|
654
721
|
const au = body.assignee ? db.getUserByUsername(String(body.assignee)) : null;
|
|
722
|
+
const nextID = au ? au.id : 0;
|
|
723
|
+
assigneeChanged = nextID !== (issue.assignee_id ?? 0) && nextID !== 0;
|
|
655
724
|
sets.push('assignee_id = ?');
|
|
656
|
-
args.push(
|
|
725
|
+
args.push(nextID);
|
|
657
726
|
}
|
|
658
727
|
if (sets.length) {
|
|
659
728
|
sets.push('updated_unix = ?');
|
|
@@ -667,6 +736,10 @@ export function registerDshRoutes(m) {
|
|
|
667
736
|
}
|
|
668
737
|
}
|
|
669
738
|
db.refreshIssueCounts(ar.repo.id);
|
|
739
|
+
if (assigneeChanged || body.body !== undefined) {
|
|
740
|
+
const { markIssueUnread } = await import('./notify.js');
|
|
741
|
+
markIssueUnread(db.getIssueByID(issue.id), ar.user.id, { mentionContent: String(body.body ?? ''), assigned: assigneeChanged });
|
|
742
|
+
}
|
|
670
743
|
c.JSONSuccess({ ok: true });
|
|
671
744
|
});
|
|
672
745
|
// ── 关注 / 星标(toggle + 列表,对齐原版 action/watch|star) ──
|
|
@@ -789,7 +862,7 @@ export function registerDshRoutes(m) {
|
|
|
789
862
|
const ar = authRepo(c);
|
|
790
863
|
if (!ar)
|
|
791
864
|
return;
|
|
792
|
-
const rows = db.db().prepare('SELECT id, url, content_type, is_active, events, created_unix FROM webhook WHERE repo_id = ? ORDER BY id').all(ar.repo.id);
|
|
865
|
+
const rows = db.db().prepare('SELECT id, url, content_type, is_active, events, last_status, created_unix FROM webhook WHERE repo_id = ? ORDER BY id').all(ar.repo.id);
|
|
793
866
|
c.JSONSuccess(rows);
|
|
794
867
|
});
|
|
795
868
|
m.post('/api/dsh/repos/:o/:r/hooks', async (c) => {
|
|
@@ -834,58 +907,36 @@ export function registerDshRoutes(m) {
|
|
|
834
907
|
db.db().prepare('DELETE FROM webhook WHERE id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
835
908
|
c.JSONSuccess({ ok: true });
|
|
836
909
|
});
|
|
837
|
-
//
|
|
838
|
-
|
|
910
|
+
// 注:部署密钥(deploy_key)是 SSH 专属概念——本插件有意只走 HTTP(不写宿主
|
|
911
|
+
// ~/.ssh),git 凭据走 user-management,故不提供部署密钥端点/界面。
|
|
912
|
+
// ── 协作者 ────────────────────────────────────────────────────
|
|
913
|
+
m.get('/api/dsh/repos/:o/:r/collaborators', async (c) => {
|
|
839
914
|
const ar = authRepo(c);
|
|
840
915
|
if (!ar)
|
|
841
916
|
return;
|
|
842
|
-
const rows = db.db().prepare('SELECT
|
|
917
|
+
const rows = db.db().prepare('SELECT u.name, u.full_name, col.mode FROM collaboration col JOIN user u ON u.id = col.user_id WHERE col.repo_id = ?').all(ar.repo.id);
|
|
843
918
|
c.JSONSuccess(rows);
|
|
844
919
|
});
|
|
845
|
-
|
|
920
|
+
const COLLAB_MODES = { read: 1, write: 2, admin: 3 };
|
|
921
|
+
m.post('/api/dsh/repos/:o/:r/collaborators/:name', async (c) => {
|
|
846
922
|
const ar = authRepo(c, true);
|
|
847
923
|
if (!ar)
|
|
848
924
|
return;
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (!name || !content) {
|
|
853
|
-
c.JSON(422, { error: 'name and content required' });
|
|
925
|
+
const u = db.getUserByUsername(c.Params(':name'));
|
|
926
|
+
if (!u) {
|
|
927
|
+
c.JSON(404, { error: 'user not found' });
|
|
854
928
|
return;
|
|
855
929
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
catch {
|
|
864
|
-
fingerprint = 'ssh-key';
|
|
865
|
-
}
|
|
866
|
-
const now = Math.floor(Date.now() / 1000);
|
|
867
|
-
const info = db.db().prepare('INSERT INTO public_key (owner_id, name, fingerprint, content, created_unix, updated_unix) VALUES (?,?,?,?,?,?)')
|
|
868
|
-
.run(ar.user.id, name, fingerprint, content, now, now);
|
|
869
|
-
db.db().prepare('INSERT INTO deploy_key (key_id, repo_id, name, fingerprint, mode) VALUES (?,?,?,?,1)')
|
|
870
|
-
.run(Number(info.lastInsertRowid), ar.repo.id, name, fingerprint);
|
|
871
|
-
c.JSONSuccess({ id: Number(info.lastInsertRowid), name, fingerprint });
|
|
872
|
-
});
|
|
873
|
-
m.delete('/api/dsh/repos/:o/:r/keys/:id', async (c) => {
|
|
874
|
-
const ar = authRepo(c, true);
|
|
875
|
-
if (!ar)
|
|
876
|
-
return;
|
|
877
|
-
db.db().prepare('DELETE FROM deploy_key WHERE key_id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
930
|
+
const body = await c.form().catch(() => ({}));
|
|
931
|
+
const mode = COLLAB_MODES[String(body.mode ?? 'write')] ?? 2;
|
|
932
|
+
const exists = db.db().prepare('SELECT 1 FROM collaboration WHERE user_id = ? AND repo_id = ?').get(u.id, ar.repo.id);
|
|
933
|
+
if (!exists)
|
|
934
|
+
db.db().prepare('INSERT INTO collaboration (user_id, repo_id, mode) VALUES (?,?,?)').run(u.id, ar.repo.id, mode);
|
|
935
|
+
else
|
|
936
|
+
db.db().prepare('UPDATE collaboration SET mode = ? WHERE user_id = ? AND repo_id = ?').run(mode, u.id, ar.repo.id);
|
|
878
937
|
c.JSONSuccess({ ok: true });
|
|
879
938
|
});
|
|
880
|
-
|
|
881
|
-
m.get('/api/dsh/repos/:o/:r/collaborators', async (c) => {
|
|
882
|
-
const ar = authRepo(c);
|
|
883
|
-
if (!ar)
|
|
884
|
-
return;
|
|
885
|
-
const rows = db.db().prepare('SELECT u.name, u.full_name, col.mode FROM collaboration col JOIN user u ON u.id = col.user_id WHERE col.repo_id = ?').all(ar.repo.id);
|
|
886
|
-
c.JSONSuccess(rows);
|
|
887
|
-
});
|
|
888
|
-
m.post('/api/dsh/repos/:o/:r/collaborators/:name', async (c) => {
|
|
939
|
+
m.patch('/api/dsh/repos/:o/:r/collaborators/:name', async (c) => {
|
|
889
940
|
const ar = authRepo(c, true);
|
|
890
941
|
if (!ar)
|
|
891
942
|
return;
|
|
@@ -894,9 +945,13 @@ export function registerDshRoutes(m) {
|
|
|
894
945
|
c.JSON(404, { error: 'user not found' });
|
|
895
946
|
return;
|
|
896
947
|
}
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
948
|
+
const body = await c.form();
|
|
949
|
+
const mode = COLLAB_MODES[String(body.mode ?? '')];
|
|
950
|
+
if (!mode) {
|
|
951
|
+
c.JSON(422, { error: 'mode must be read|write|admin' });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
db.db().prepare('UPDATE collaboration SET mode = ? WHERE user_id = ? AND repo_id = ?').run(mode, u.id, ar.repo.id);
|
|
900
955
|
c.JSONSuccess({ ok: true });
|
|
901
956
|
});
|
|
902
957
|
m.delete('/api/dsh/repos/:o/:r/collaborators/:name', async (c) => {
|
|
@@ -932,14 +987,17 @@ export function registerDshRoutes(m) {
|
|
|
932
987
|
db.db().prepare(`UPDATE repository SET ${sets.join(', ')} WHERE id = ?`).run(...args, ar.repo.id);
|
|
933
988
|
c.JSONSuccess({ ok: true });
|
|
934
989
|
});
|
|
935
|
-
// ── 用户 profile
|
|
990
|
+
// ── 用户 profile(资料+仓库+活动+关注关系) ─────────────────────
|
|
936
991
|
m.get('/api/dsh/users/:name', async (c) => {
|
|
937
992
|
const u = db.getUserByUsername(c.Params(':name'));
|
|
938
993
|
if (!u) {
|
|
939
994
|
c.JSON(404, { error: 'user not found' });
|
|
940
995
|
return;
|
|
941
996
|
}
|
|
942
|
-
|
|
997
|
+
// 可选登录:带了有效 token 才计算「我是否关注了他」
|
|
998
|
+
const token = /^token (.+)$/.exec(String(c.req.headers.authorization ?? ''))?.[1] ?? '';
|
|
999
|
+
const viewer = token ? authenticateUserByToken(token) : null;
|
|
1000
|
+
const repos = db.listReposByOwner(u.id).filter((r) => !r.is_private || c.IsLogged || viewer).map((r) => ({
|
|
943
1001
|
name: r.name, owner: r.OwnerName(), description: r.description, stars: r.num_stars,
|
|
944
1002
|
updated: r.updated_unix,
|
|
945
1003
|
}));
|
|
@@ -948,11 +1006,36 @@ export function registerDshRoutes(m) {
|
|
|
948
1006
|
const following = db.db().prepare('SELECT u.name FROM follow f JOIN user u ON u.id = f.follow_id WHERE f.user_id = ?').all(u.id).map((x) => x.name);
|
|
949
1007
|
c.JSONSuccess({
|
|
950
1008
|
name: u.name, fullName: u.full_name || '', email: u.email, isAdmin: u.is_admin === 1,
|
|
951
|
-
|
|
1009
|
+
created: u.created_unix ?? 0,
|
|
1010
|
+
followers, following,
|
|
1011
|
+
isFollowing: viewer ? db.isFollowing(viewer.id, u.id) : false,
|
|
1012
|
+
isSelf: viewer ? viewer.id === u.id : false,
|
|
1013
|
+
repos, activity: activity.map((a) => ({
|
|
952
1014
|
type: a.op_type, repo: a.owner + '/' + a.repo, ref: a.ref_name, content: a.content, date: a.created_unix,
|
|
953
1015
|
})),
|
|
954
1016
|
});
|
|
955
1017
|
});
|
|
1018
|
+
// ── 关注/取关(toggle) ────────────────────────────────────────
|
|
1019
|
+
m.post('/api/dsh/users/:name/follow', async (c) => {
|
|
1020
|
+
const user = authUser(c);
|
|
1021
|
+
if (!user)
|
|
1022
|
+
return;
|
|
1023
|
+
const target = db.getUserByUsername(c.Params(':name'));
|
|
1024
|
+
if (!target) {
|
|
1025
|
+
c.JSON(404, { error: 'user not found' });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (target.id === user.id) {
|
|
1029
|
+
c.JSON(422, { error: '不能关注自己' });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const on = !db.isFollowing(user.id, target.id);
|
|
1033
|
+
if (on)
|
|
1034
|
+
db.followUser(user.id, target.id);
|
|
1035
|
+
else
|
|
1036
|
+
db.unfollowUser(user.id, target.id);
|
|
1037
|
+
c.JSONSuccess({ on, followers: db.db().prepare('SELECT COUNT(*) AS c FROM follow WHERE follow_id = ?').get(target.id).c });
|
|
1038
|
+
});
|
|
956
1039
|
// ── 组织:我的组织列表 / 组织详情 / 创建 ────────────────────────
|
|
957
1040
|
m.get('/api/dsh/orgs', async (c) => {
|
|
958
1041
|
const header = String(c.req.headers.authorization ?? '');
|
|
@@ -1190,6 +1273,8 @@ export function registerDshRoutes(m) {
|
|
|
1190
1273
|
db.refreshIssueCounts(repo.id);
|
|
1191
1274
|
const { issueAction, ActionType } = await import('./db/actions.js');
|
|
1192
1275
|
await issueAction(ActionType.MERGE_PULL_REQUEST, user, repo, issue);
|
|
1276
|
+
const { markIssueUnread } = await import('./notify.js');
|
|
1277
|
+
markIssueUnread(issue, user.id);
|
|
1193
1278
|
c.JSONSuccess({ ok: true });
|
|
1194
1279
|
}
|
|
1195
1280
|
catch (e) {
|
|
@@ -1333,6 +1418,7 @@ export function registerDshRoutes(m) {
|
|
|
1333
1418
|
return;
|
|
1334
1419
|
const rows = db.db().prepare('SELECT r.id, r.tag_name, r.title, r.note, r.is_draft, r.is_prerelease, r.created_unix, u.name AS author FROM `release` r LEFT JOIN user u ON u.id = r.publisher_id WHERE r.repo_id = ? ORDER BY r.created_unix DESC').all(ar.repo.id);
|
|
1335
1420
|
const dir = ar.repo.RepoPath();
|
|
1421
|
+
const attachDir = path.join(conf.appDataPath, 'attachments');
|
|
1336
1422
|
const out = [];
|
|
1337
1423
|
for (const r of rows) {
|
|
1338
1424
|
let behind = 0;
|
|
@@ -1342,11 +1428,21 @@ export function registerDshRoutes(m) {
|
|
|
1342
1428
|
catch {
|
|
1343
1429
|
behind = 0;
|
|
1344
1430
|
}
|
|
1431
|
+
const assets = db.db().prepare('SELECT id, uuid, name, created_unix FROM attachment WHERE release_id = ? ORDER BY id').all(r.id)
|
|
1432
|
+
.map((a) => {
|
|
1433
|
+
let size = 0;
|
|
1434
|
+
try {
|
|
1435
|
+
size = fs.statSync(path.join(attachDir, a.uuid)).size;
|
|
1436
|
+
}
|
|
1437
|
+
catch { /* 文件缺失按 0 */ }
|
|
1438
|
+
return { id: a.id, name: a.name, size, createdAt: a.created_unix };
|
|
1439
|
+
});
|
|
1345
1440
|
out.push({
|
|
1346
1441
|
id: r.id, tag: r.tag_name, title: r.title, draft: !!r.is_draft, prerelease: !!r.is_prerelease,
|
|
1347
1442
|
noteHtml: r.note ? sanitizeHTML(markdown(r.note, conf.subpath + '/', {})) : '',
|
|
1348
1443
|
noteRaw: r.note || '',
|
|
1349
1444
|
author: r.author, createdAt: r.created_unix, target: r.target || '', behind,
|
|
1445
|
+
assets,
|
|
1350
1446
|
});
|
|
1351
1447
|
}
|
|
1352
1448
|
c.JSONSuccess(out);
|
|
@@ -1412,5 +1508,401 @@ export function registerDshRoutes(m) {
|
|
|
1412
1508
|
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
1413
1509
|
}
|
|
1414
1510
|
});
|
|
1511
|
+
// ── 通知(issue_user 未读模型) ─────────────────────────────────
|
|
1512
|
+
m.get('/api/dsh/notifications/count', async (c) => {
|
|
1513
|
+
const user = authUser(c);
|
|
1514
|
+
if (!user)
|
|
1515
|
+
return;
|
|
1516
|
+
const { unreadCount } = await import('./notify.js');
|
|
1517
|
+
c.JSONSuccess({ unread: unreadCount(user.id) });
|
|
1518
|
+
});
|
|
1519
|
+
m.get('/api/dsh/notifications', async (c) => {
|
|
1520
|
+
const user = authUser(c);
|
|
1521
|
+
if (!user)
|
|
1522
|
+
return;
|
|
1523
|
+
const { unreadCount } = await import('./notify.js');
|
|
1524
|
+
const rows = db.db().prepare(`SELECT iu.issue_id, iu.is_read, iu.is_assigned, iu.is_mentioned, iu.is_poster,
|
|
1525
|
+
i."index", i.name AS title, i.is_pull, i.is_closed, i.updated_unix,
|
|
1526
|
+
r.name AS repo_name, ow.name AS owner_name
|
|
1527
|
+
FROM issue_user iu
|
|
1528
|
+
JOIN issue i ON i.id = iu.issue_id
|
|
1529
|
+
JOIN repository r ON r.id = i.repo_id
|
|
1530
|
+
JOIN user ow ON ow.id = r.owner_id
|
|
1531
|
+
WHERE iu.uid = ? AND iu.is_read = 0
|
|
1532
|
+
ORDER BY i.updated_unix DESC LIMIT 100`).all(user.id);
|
|
1533
|
+
c.JSONSuccess({
|
|
1534
|
+
unread: unreadCount(user.id),
|
|
1535
|
+
items: rows.map((r) => ({
|
|
1536
|
+
repoOwner: r.owner_name, repoName: r.repo_name, index: Number(r.index),
|
|
1537
|
+
title: r.title, isPull: !!r.is_pull, isClosed: !!r.is_closed,
|
|
1538
|
+
updatedAt: (r.updated_unix ?? 0) * 1000,
|
|
1539
|
+
reason: r.is_mentioned ? 'mentioned' : r.is_assigned ? 'assigned' : r.is_poster ? 'poster' : 'comment',
|
|
1540
|
+
})),
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
m.post('/api/dsh/notifications/read-all', async (c) => {
|
|
1544
|
+
const user = authUser(c);
|
|
1545
|
+
if (!user)
|
|
1546
|
+
return;
|
|
1547
|
+
db.db().prepare('UPDATE issue_user SET is_read = 1 WHERE uid = ?').run(user.id);
|
|
1548
|
+
c.JSONSuccess({ ok: true });
|
|
1549
|
+
});
|
|
1550
|
+
m.post('/api/dsh/repos/:o/:r/issues/:idx/read', async (c) => {
|
|
1551
|
+
const ar = authRepo(c);
|
|
1552
|
+
if (!ar)
|
|
1553
|
+
return;
|
|
1554
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':idx'));
|
|
1555
|
+
if (!issue) {
|
|
1556
|
+
c.JSON(404, { error: 'not found' });
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const { markIssueRead } = await import('./notify.js');
|
|
1560
|
+
markIssueRead(ar.user.id, issue.id);
|
|
1561
|
+
c.JSONSuccess({ ok: true });
|
|
1562
|
+
});
|
|
1563
|
+
// ── 我的工单/PR 聚合(跨仓库) ──────────────────────────────────
|
|
1564
|
+
m.get('/api/dsh/my/issues', async (c) => {
|
|
1565
|
+
const user = authUser(c);
|
|
1566
|
+
if (!user)
|
|
1567
|
+
return;
|
|
1568
|
+
const isPull = c.Query('type') === 'pulls' ? 1 : 0;
|
|
1569
|
+
const filter = c.Query('filter') || 'all';
|
|
1570
|
+
const where = ['i.is_pull = ?'];
|
|
1571
|
+
const args = [isPull];
|
|
1572
|
+
if (filter === 'created') {
|
|
1573
|
+
where.push('i.poster_id = ?');
|
|
1574
|
+
args.push(user.id);
|
|
1575
|
+
}
|
|
1576
|
+
else if (filter === 'assigned') {
|
|
1577
|
+
where.push('i.assignee_id = ?');
|
|
1578
|
+
args.push(user.id);
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
where.push('(i.poster_id = ? OR i.assignee_id = ?)');
|
|
1582
|
+
args.push(user.id, user.id);
|
|
1583
|
+
}
|
|
1584
|
+
const state = c.Query('state');
|
|
1585
|
+
if (state === 'open' || state === 'closed') {
|
|
1586
|
+
where.push('i.is_closed = ?');
|
|
1587
|
+
args.push(state === 'closed' ? 1 : 0);
|
|
1588
|
+
}
|
|
1589
|
+
const rows = db.db().prepare(`SELECT i.id, i."index", i.name AS title, i.is_closed, i.num_comments, i.updated_unix,
|
|
1590
|
+
r.name AS repo_name, ow.name AS owner_name, pu.name AS poster_name, au.name AS assignee_name
|
|
1591
|
+
FROM issue i
|
|
1592
|
+
JOIN repository r ON r.id = i.repo_id
|
|
1593
|
+
JOIN user ow ON ow.id = r.owner_id
|
|
1594
|
+
LEFT JOIN user pu ON pu.id = i.poster_id
|
|
1595
|
+
LEFT JOIN user au ON au.id = i.assignee_id
|
|
1596
|
+
WHERE ${where.join(' AND ')} ORDER BY i.updated_unix DESC LIMIT 100`).all(...args);
|
|
1597
|
+
c.JSONSuccess(rows.map((r) => ({
|
|
1598
|
+
repoOwner: r.owner_name, repoName: r.repo_name, index: Number(r.index),
|
|
1599
|
+
title: r.title, state: r.is_closed ? 'closed' : 'open', comments: r.num_comments || 0,
|
|
1600
|
+
updatedAt: (r.updated_unix ?? 0) * 1000, author: r.poster_name || '', assignee: r.assignee_name || '',
|
|
1601
|
+
})));
|
|
1602
|
+
});
|
|
1603
|
+
// ── 分支删除(此前 UI 调用的宿主路由不存在,一直 404) ────────────
|
|
1604
|
+
m.delete('/api/dsh/repos/:o/:r/branches/*', async (c) => {
|
|
1605
|
+
const ar = authRepo(c, true);
|
|
1606
|
+
if (!ar)
|
|
1607
|
+
return;
|
|
1608
|
+
const branch = c.Params(':*');
|
|
1609
|
+
if (!branch) {
|
|
1610
|
+
c.JSON(422, { error: 'branch required' });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (branch === (ar.repo.default_branch || conf.defaultBranch)) {
|
|
1614
|
+
c.JSON(422, { error: '默认分支不可删除' });
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const prot = db.db().prepare('SELECT 1 FROM protect_branch WHERE repo_id = ? AND name = ? AND protected = 1').get(ar.repo.id, branch);
|
|
1618
|
+
if (prot) {
|
|
1619
|
+
c.JSON(403, { error: '分支受保护,不可删除' });
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const dir = ar.repo.RepoPath();
|
|
1623
|
+
const r = await git.gitOK(dir, 'update-ref', '-d', '--end-of-options', 'refs/heads/' + branch);
|
|
1624
|
+
if (r === null) {
|
|
1625
|
+
c.JSON(500, { error: 'delete failed' });
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const { deleteBranchAction } = await import('./db/actions.js');
|
|
1629
|
+
await deleteBranchAction(ar.user, ar.repo, branch);
|
|
1630
|
+
c.JSONSuccess({ ok: true });
|
|
1631
|
+
});
|
|
1632
|
+
// ── 分支保护(禁删/禁强推;推送侧由 update 钩子强制) ──────────────
|
|
1633
|
+
m.get('/api/dsh/repos/:o/:r/protections', async (c) => {
|
|
1634
|
+
const ar = authRepo(c);
|
|
1635
|
+
if (!ar)
|
|
1636
|
+
return;
|
|
1637
|
+
const rows = db.db().prepare('SELECT name FROM protect_branch WHERE repo_id = ? AND protected = 1 ORDER BY name').all(ar.repo.id);
|
|
1638
|
+
c.JSONSuccess(rows.map((r) => ({ branch: r.name })));
|
|
1639
|
+
});
|
|
1640
|
+
m.post('/api/dsh/repos/:o/:r/protections', async (c) => {
|
|
1641
|
+
const ar = authRepo(c, true);
|
|
1642
|
+
if (!ar)
|
|
1643
|
+
return;
|
|
1644
|
+
const body = await c.form();
|
|
1645
|
+
const branch = String(body.branch ?? '').trim();
|
|
1646
|
+
if (!branch) {
|
|
1647
|
+
c.JSON(422, { error: 'branch required' });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const on = body.protected !== false;
|
|
1651
|
+
db.db().prepare('INSERT INTO protect_branch (repo_id, name, protected) VALUES (?,?,?) ON CONFLICT(repo_id, name) DO UPDATE SET protected = excluded.protected').run(ar.repo.id, branch, on ? 1 : 0);
|
|
1652
|
+
c.JSONSuccess({ ok: true });
|
|
1653
|
+
});
|
|
1654
|
+
// ── PR 评审(最简版:通过/请求修改 + 可附评论) ────────────────────
|
|
1655
|
+
m.get('/api/dsh/repos/:o/:r/pulls/:idx/reviews', async (c) => {
|
|
1656
|
+
const ar = authRepo(c);
|
|
1657
|
+
if (!ar)
|
|
1658
|
+
return;
|
|
1659
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':idx'));
|
|
1660
|
+
if (!issue || !issue.is_pull) {
|
|
1661
|
+
c.JSON(404, { error: 'pull not found' });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const rows = db.db().prepare('SELECT pr.reviewer_id, pr.approved, pr.content, pr.created_unix, u.name AS reviewer FROM pr_review pr JOIN user u ON u.id = pr.reviewer_id WHERE pr.issue_id = ? ORDER BY pr.id DESC').all(issue.id);
|
|
1665
|
+
const seen = new Set();
|
|
1666
|
+
const out = [];
|
|
1667
|
+
for (const r of rows) {
|
|
1668
|
+
if (seen.has(r.reviewer_id))
|
|
1669
|
+
continue; // 每人只取最新一条
|
|
1670
|
+
seen.add(r.reviewer_id);
|
|
1671
|
+
out.push({ user: r.reviewer, approved: !!r.approved, content: r.content || '', createdAt: (r.created_unix ?? 0) * 1000 });
|
|
1672
|
+
}
|
|
1673
|
+
c.JSONSuccess(out);
|
|
1674
|
+
});
|
|
1675
|
+
m.post('/api/dsh/repos/:o/:r/pulls/:idx/reviews', async (c) => {
|
|
1676
|
+
const ar = authRepo(c);
|
|
1677
|
+
if (!ar)
|
|
1678
|
+
return;
|
|
1679
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':idx'));
|
|
1680
|
+
if (!issue || !issue.is_pull) {
|
|
1681
|
+
c.JSON(404, { error: 'pull not found' });
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const body = await c.form();
|
|
1685
|
+
const content = String(body.content ?? '').trim();
|
|
1686
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1687
|
+
db.db().prepare('DELETE FROM pr_review WHERE issue_id = ? AND reviewer_id = ?').run(issue.id, ar.user.id);
|
|
1688
|
+
db.db().prepare('INSERT INTO pr_review (repo_id, issue_id, reviewer_id, approved, content, created_unix) VALUES (?,?,?,?,?,?)')
|
|
1689
|
+
.run(ar.repo.id, issue.id, ar.user.id, body.approved ? 1 : 0, content, now);
|
|
1690
|
+
if (content) {
|
|
1691
|
+
db.db().prepare('INSERT INTO comment (type, poster_id, issue_id, content, created_unix, updated_unix) VALUES (0,?,?,?,?,?)').run(ar.user.id, issue.id, content, now, now);
|
|
1692
|
+
db.db().prepare('UPDATE issue SET num_comments = num_comments + 1 WHERE id = ?').run(issue.id);
|
|
1693
|
+
}
|
|
1694
|
+
const { markIssueUnread } = await import('./notify.js');
|
|
1695
|
+
markIssueUnread(issue, ar.user.id, { mentionContent: content });
|
|
1696
|
+
c.JSONSuccess({ ok: true });
|
|
1697
|
+
});
|
|
1698
|
+
// ── 发版二进制附件 ─────────────────────────────────────────────
|
|
1699
|
+
const attachDir = () => path.join(conf.appDataPath, 'attachments');
|
|
1700
|
+
const MAX_ASSET_BYTES = 25 * 1024 * 1024;
|
|
1701
|
+
m.post('/api/dsh/repos/:o/:r/releases/:id/assets', async (c) => {
|
|
1702
|
+
const ar = authRepo(c, true);
|
|
1703
|
+
if (!ar)
|
|
1704
|
+
return;
|
|
1705
|
+
const rel = db.getReleaseByID(ar.repo.id, c.ParamsInt64(':id'));
|
|
1706
|
+
if (!rel) {
|
|
1707
|
+
c.JSON(404, { error: 'release not found' });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
const body = await c.form();
|
|
1711
|
+
const name = String(body.name ?? '').trim().replace(/[\\/]/g, '_');
|
|
1712
|
+
if (!name) {
|
|
1713
|
+
c.JSON(422, { error: 'name required' });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const buf = Buffer.from(String(body.contentBase64 ?? ''), 'base64');
|
|
1717
|
+
if (buf.length === 0) {
|
|
1718
|
+
c.JSON(422, { error: 'content required' });
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (buf.length > MAX_ASSET_BYTES) {
|
|
1722
|
+
c.JSON(413, { error: '附件超过 25MB 上限' });
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
const uuid = db.newUUID();
|
|
1726
|
+
fs.mkdirSync(attachDir(), { recursive: true });
|
|
1727
|
+
fs.writeFileSync(path.join(attachDir(), uuid), buf);
|
|
1728
|
+
const info = db.db().prepare('INSERT INTO attachment (uuid, release_id, name, created_unix) VALUES (?,?,?,?)')
|
|
1729
|
+
.run(uuid, rel.id, name, Math.floor(Date.now() / 1000));
|
|
1730
|
+
c.JSONSuccess({ ok: true, asset: { id: Number(info.lastInsertRowid), name, size: buf.length } });
|
|
1731
|
+
});
|
|
1732
|
+
m.get('/api/dsh/repos/:o/:r/attachments/:assetId', async (c) => {
|
|
1733
|
+
const ar = authRepo(c);
|
|
1734
|
+
if (!ar)
|
|
1735
|
+
return;
|
|
1736
|
+
const a = db.db().prepare('SELECT at.id, at.uuid, at.name FROM attachment at JOIN `release` r ON r.id = at.release_id WHERE at.id = ? AND r.repo_id = ?').get(c.ParamsInt64(':assetId'), ar.repo.id);
|
|
1737
|
+
if (!a) {
|
|
1738
|
+
c.JSON(404, { error: 'not found' });
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const file = path.join(attachDir(), a.uuid);
|
|
1742
|
+
if (!fs.existsSync(file)) {
|
|
1743
|
+
c.JSON(404, { error: 'file gone' });
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const buf = fs.readFileSync(file);
|
|
1747
|
+
c.res.setHeader('Content-Type', 'application/octet-stream');
|
|
1748
|
+
c.res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(a.name)}`);
|
|
1749
|
+
c.res.setHeader('Content-Length', String(buf.length));
|
|
1750
|
+
c.res.statusCode = 200;
|
|
1751
|
+
c.res.end(buf);
|
|
1752
|
+
c.rendered = true;
|
|
1753
|
+
});
|
|
1754
|
+
m.delete('/api/dsh/repos/:o/:r/attachments/:assetId', async (c) => {
|
|
1755
|
+
const ar = authRepo(c, true);
|
|
1756
|
+
if (!ar)
|
|
1757
|
+
return;
|
|
1758
|
+
const a = db.db().prepare('SELECT at.id, at.uuid FROM attachment at JOIN `release` r ON r.id = at.release_id WHERE at.id = ? AND r.repo_id = ?').get(c.ParamsInt64(':assetId'), ar.repo.id);
|
|
1759
|
+
if (!a) {
|
|
1760
|
+
c.JSON(404, { error: 'not found' });
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
db.db().prepare('DELETE FROM attachment WHERE id = ?').run(a.id);
|
|
1764
|
+
fs.rmSync(path.join(attachDir(), a.uuid), { force: true });
|
|
1765
|
+
c.JSONSuccess({ ok: true });
|
|
1766
|
+
});
|
|
1767
|
+
// ── 从 URL 导入仓库(可选镜像同步) ──────────────────────────────
|
|
1768
|
+
m.post('/api/dsh/migrate', async (c) => {
|
|
1769
|
+
const user = authUser(c);
|
|
1770
|
+
if (!user)
|
|
1771
|
+
return;
|
|
1772
|
+
const body = await c.form();
|
|
1773
|
+
const cloneAddr = String(body.cloneAddr ?? '').trim();
|
|
1774
|
+
const name = String(body.name ?? '').trim();
|
|
1775
|
+
if (!/^https?:\/\//.test(cloneAddr)) {
|
|
1776
|
+
c.JSON(422, { error: '仅支持 http(s) 克隆地址' });
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (!name || !/^[a-zA-Z0-9_.-]+$/.test(name) || name.length > 100) {
|
|
1780
|
+
c.JSON(422, { error: '仓库名非法(字母数字 _.-)' });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const orgName = String(body.org ?? '');
|
|
1784
|
+
const owner = orgName ? db.getUserByUsername(orgName) : user;
|
|
1785
|
+
if (!owner) {
|
|
1786
|
+
c.JSON(404, { error: 'org not found' });
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (orgName) {
|
|
1790
|
+
const mem = db.db().prepare('SELECT 1 FROM org_user WHERE org_id = ? AND uid = ? AND is_owner = 1').get(owner.id, user.id);
|
|
1791
|
+
if (!mem && user.is_admin !== 1) {
|
|
1792
|
+
c.JSON(403, { error: '需要组织管理员' });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (db.getRepoByOwnerAndName(owner, name)) {
|
|
1797
|
+
c.JSON(422, { error: '同名仓库已存在' });
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const isMirror = !!body.mirror;
|
|
1801
|
+
const { createRepositoryRecord } = await import('./repox.js');
|
|
1802
|
+
const repo = await createRepositoryRecord(user, owner, {
|
|
1803
|
+
name, description: String(body.description ?? ''), private: !!body.private, autoInit: false, mirror: isMirror,
|
|
1804
|
+
});
|
|
1805
|
+
const dir = repo.RepoPath();
|
|
1806
|
+
try {
|
|
1807
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1808
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
1809
|
+
await git.git(process.cwd(), 'clone', '--mirror', cloneAddr, dir);
|
|
1810
|
+
svc.createDelegateHooks(dir);
|
|
1811
|
+
await git.updateServerInfo(dir);
|
|
1812
|
+
const def = await git.getDefaultBranch(dir);
|
|
1813
|
+
const { size } = await git.countObjects(dir);
|
|
1814
|
+
db.updateRepoColumns(repo.id, { is_bare: 0, ...(def ? { default_branch: def } : {}), size });
|
|
1815
|
+
if (isMirror) {
|
|
1816
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1817
|
+
db.db().prepare('INSERT INTO mirror (repo_id, interval, enable_prune, updated_unix, next_update_unix) VALUES (?,?,0,?,?)').run(repo.id, 0, now, now);
|
|
1818
|
+
}
|
|
1819
|
+
c.JSONSuccess({ ok: true, owner: owner.name, name: repo.name });
|
|
1820
|
+
}
|
|
1821
|
+
catch (e) {
|
|
1822
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1823
|
+
db.db().prepare('DELETE FROM repository WHERE id = ?').run(repo.id);
|
|
1824
|
+
db.db().prepare('UPDATE user SET num_repos = MAX(num_repos - 1, 0) WHERE id = ?').run(owner.id);
|
|
1825
|
+
c.JSON(500, { error: '克隆失败:' + String(e?.message ?? e).slice(0, 160) });
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
// ── 代码搜索(git grep 包装,每文件最多 5 条、总共 100 条封顶) ─────
|
|
1829
|
+
m.get('/api/dsh/repos/:o/:r/search', async (c) => {
|
|
1830
|
+
const ar = authRepo(c);
|
|
1831
|
+
if (!ar)
|
|
1832
|
+
return;
|
|
1833
|
+
const q = c.Query('q').trim();
|
|
1834
|
+
if (!q) {
|
|
1835
|
+
c.JSON(422, { error: 'q required' });
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const dir = ar.repo.RepoPath();
|
|
1839
|
+
const ref = c.Query('ref') || ar.repo.default_branch || conf.defaultBranch;
|
|
1840
|
+
const resolved = await git.resolveRef(dir, ref);
|
|
1841
|
+
if (!resolved) {
|
|
1842
|
+
c.JSON(404, { error: 'ref not found' });
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const out = await git.gitOK(dir, 'grep', '-n', '-I', '-i', '-m', '5', '-e', q, '--end-of-options', resolved);
|
|
1846
|
+
const matches = [];
|
|
1847
|
+
const prefix = resolved + ':';
|
|
1848
|
+
if (out) {
|
|
1849
|
+
for (const line of out.toString('utf8').split('\n')) {
|
|
1850
|
+
if (!line.startsWith(prefix))
|
|
1851
|
+
continue;
|
|
1852
|
+
const m2 = /^(.*?):(\d+):(.*)$/.exec(line.slice(prefix.length));
|
|
1853
|
+
if (!m2)
|
|
1854
|
+
continue;
|
|
1855
|
+
matches.push({ path: m2[1], line: Number(m2[2]), text: m2[3].slice(0, 200) });
|
|
1856
|
+
if (matches.length >= 100)
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
c.JSONSuccess({ matches, truncated: matches.length >= 100 });
|
|
1861
|
+
});
|
|
1862
|
+
// ── webhook 测试投递 + 最近投递记录 ──────────────────────────────
|
|
1863
|
+
m.post('/api/dsh/repos/:o/:r/hooks/:id/test', async (c) => {
|
|
1864
|
+
const ar = authRepo(c, true);
|
|
1865
|
+
if (!ar)
|
|
1866
|
+
return;
|
|
1867
|
+
const hook = db.getWebhookByID(ar.repo.id, c.ParamsInt64(':id'));
|
|
1868
|
+
if (!hook) {
|
|
1869
|
+
c.JSON(404, { error: 'hook not found' });
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const dir = ar.repo.RepoPath();
|
|
1873
|
+
const def = ar.repo.default_branch || conf.defaultBranch;
|
|
1874
|
+
const commits = await git.commitsByPage(dir, def, 1, 5).catch(() => []);
|
|
1875
|
+
const { deliverWebhook } = await import('./webhook.js');
|
|
1876
|
+
deliverWebhook(hook, 'push', {
|
|
1877
|
+
ref: 'refs/heads/' + def,
|
|
1878
|
+
before: '',
|
|
1879
|
+
after: commits[0]?.id ?? '',
|
|
1880
|
+
commits: commits.map((cm) => ({
|
|
1881
|
+
id: cm.id, message: cm.message, timestamp: cm.committer.when.toISOString(),
|
|
1882
|
+
author: { name: cm.author.name, email: cm.author.email, username: cm.author.name },
|
|
1883
|
+
})),
|
|
1884
|
+
repository: { full_name: ar.repo.FullName(), html_url: ar.repo.HTMLURL(), name: ar.repo.name },
|
|
1885
|
+
pusher: { username: ar.user.name },
|
|
1886
|
+
sender: { username: ar.user.name },
|
|
1887
|
+
}, ar.repo);
|
|
1888
|
+
c.JSONSuccess({ ok: true });
|
|
1889
|
+
});
|
|
1890
|
+
m.get('/api/dsh/repos/:o/:r/hooks/:id/deliveries', async (c) => {
|
|
1891
|
+
const ar = authRepo(c);
|
|
1892
|
+
if (!ar)
|
|
1893
|
+
return;
|
|
1894
|
+
const rows = db.db().prepare('SELECT id, event_type, is_delivered, is_succeed, response_content FROM hook_task WHERE hook_id = ? AND repo_id = ? ORDER BY id DESC LIMIT 10').all(c.ParamsInt64(':id'), ar.repo.id);
|
|
1895
|
+
c.JSONSuccess(rows.map((r) => {
|
|
1896
|
+
let status = 0;
|
|
1897
|
+
let err = '';
|
|
1898
|
+
try {
|
|
1899
|
+
const rc = JSON.parse(r.response_content ?? '{}');
|
|
1900
|
+
status = rc.status ?? 0;
|
|
1901
|
+
err = rc.err ?? '';
|
|
1902
|
+
}
|
|
1903
|
+
catch { /* ignore */ }
|
|
1904
|
+
return { id: r.id, event: r.event_type, delivered: !!r.is_delivered, ok: !!r.is_succeed, status, err };
|
|
1905
|
+
}));
|
|
1906
|
+
});
|
|
1415
1907
|
}
|
|
1416
1908
|
//# sourceMappingURL=dshapi.js.map
|