@weibaohui/dsh-git-server 0.1.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 +43 -0
- package/client/bundle.js +2356 -0
- package/client/index.js +2342 -0
- package/cordis.patch.yml +11 -0
- package/package.json +70 -0
- package/server/dist/api/v1.js +2029 -0
- package/server/dist/authx/password.js +43 -0
- package/server/dist/authx/um.js +157 -0
- package/server/dist/conf.js +323 -0
- package/server/dist/context.js +701 -0
- package/server/dist/db/actions.js +202 -0
- package/server/dist/db/db.js +838 -0
- package/server/dist/dshapi.js +1416 -0
- package/server/dist/editorconfig.js +104 -0
- package/server/dist/gitx/git.js +580 -0
- package/server/dist/gitx/http.js +232 -0
- package/server/dist/gitx/paths.js +28 -0
- package/server/dist/gitx/service.js +251 -0
- package/server/dist/gosprintf.js +167 -0
- package/server/dist/hook.js +53 -0
- package/server/dist/index.js +112 -0
- package/server/dist/lfsx.js +265 -0
- package/server/dist/mailer.js +127 -0
- package/server/dist/markup.js +317 -0
- package/server/dist/metrics.js +18 -0
- package/server/dist/mirror.js +203 -0
- package/server/dist/multipart.js +33 -0
- package/server/dist/repox.js +99 -0
- package/server/dist/router.js +185 -0
- package/server/dist/serv.js +43 -0
- package/server/dist/server.js +382 -0
- package/server/dist/sshx/dispatch.js +160 -0
- package/server/dist/sshx/lfs-transfer.js +289 -0
- package/server/dist/sshx/server.js +107 -0
- package/server/dist/toolx.js +108 -0
- package/server/dist/twofactor.js +160 -0
- package/server/dist/webapi.js +377 -0
- package/server/dist/webhook.js +128 -0
- package/server/package.json +34 -0
- package/server/public/img/avatar_default.png +0 -0
- package/server/tsconfig.json +19 -0
- package/server/vendored-conf/app.ini +538 -0
- package/server/vendored-conf/gitignore/Actionscript +11 -0
- package/server/vendored-conf/gitignore/Ada +5 -0
- package/server/vendored-conf/gitignore/Agda +1 -0
- package/server/vendored-conf/gitignore/Android +32 -0
- package/server/vendored-conf/gitignore/Anjuta +3 -0
- package/server/vendored-conf/gitignore/AppEngine +2 -0
- package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
- package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
- package/server/vendored-conf/gitignore/Archives +27 -0
- package/server/vendored-conf/gitignore/Autotools +14 -0
- package/server/vendored-conf/gitignore/BricxCC +4 -0
- package/server/vendored-conf/gitignore/C +32 -0
- package/server/vendored-conf/gitignore/C Sharp +108 -0
- package/server/vendored-conf/gitignore/C++ +28 -0
- package/server/vendored-conf/gitignore/CFWheels +12 -0
- package/server/vendored-conf/gitignore/CMake +6 -0
- package/server/vendored-conf/gitignore/CUDA +6 -0
- package/server/vendored-conf/gitignore/CakePHP +13 -0
- package/server/vendored-conf/gitignore/ChefCookbook +9 -0
- package/server/vendored-conf/gitignore/Cloud9 +3 -0
- package/server/vendored-conf/gitignore/CodeIgniter +6 -0
- package/server/vendored-conf/gitignore/CodeKit +3 -0
- package/server/vendored-conf/gitignore/CommonLisp +3 -0
- package/server/vendored-conf/gitignore/Composer +6 -0
- package/server/vendored-conf/gitignore/Concrete5 +3 -0
- package/server/vendored-conf/gitignore/Coq +3 -0
- package/server/vendored-conf/gitignore/CraftCMS +3 -0
- package/server/vendored-conf/gitignore/DM +5 -0
- package/server/vendored-conf/gitignore/Dart +15 -0
- package/server/vendored-conf/gitignore/DartEditor +2 -0
- package/server/vendored-conf/gitignore/Delphi +57 -0
- package/server/vendored-conf/gitignore/Dreamweaver +3 -0
- package/server/vendored-conf/gitignore/Drupal +36 -0
- package/server/vendored-conf/gitignore/EPiServer +4 -0
- package/server/vendored-conf/gitignore/Eagle +36 -0
- package/server/vendored-conf/gitignore/Eclipse +39 -0
- package/server/vendored-conf/gitignore/EiffelStudio +2 -0
- package/server/vendored-conf/gitignore/Elisp +5 -0
- package/server/vendored-conf/gitignore/Elixir +4 -0
- package/server/vendored-conf/gitignore/Emacs +32 -0
- package/server/vendored-conf/gitignore/Ensime +4 -0
- package/server/vendored-conf/gitignore/Erlang +10 -0
- package/server/vendored-conf/gitignore/Espresso +1 -0
- package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
- package/server/vendored-conf/gitignore/ExtJs +4 -0
- package/server/vendored-conf/gitignore/Fancy +2 -0
- package/server/vendored-conf/gitignore/Finale +13 -0
- package/server/vendored-conf/gitignore/FlexBuilder +3 -0
- package/server/vendored-conf/gitignore/ForceDotCom +4 -0
- package/server/vendored-conf/gitignore/FuelPHP +2 -0
- package/server/vendored-conf/gitignore/GWT +28 -0
- package/server/vendored-conf/gitignore/Gcov +5 -0
- package/server/vendored-conf/gitignore/GitBook +16 -0
- package/server/vendored-conf/gitignore/Go +24 -0
- package/server/vendored-conf/gitignore/Gradle +8 -0
- package/server/vendored-conf/gitignore/Grails +33 -0
- package/server/vendored-conf/gitignore/Haskell +16 -0
- package/server/vendored-conf/gitignore/IGORPro +5 -0
- package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
- package/server/vendored-conf/gitignore/Idris +2 -0
- package/server/vendored-conf/gitignore/JDeveloper +13 -0
- package/server/vendored-conf/gitignore/Java +12 -0
- package/server/vendored-conf/gitignore/Jboss +19 -0
- package/server/vendored-conf/gitignore/Jekyll +3 -0
- package/server/vendored-conf/gitignore/JetBrains +46 -0
- package/server/vendored-conf/gitignore/Joomla +525 -0
- package/server/vendored-conf/gitignore/KDevelop4 +2 -0
- package/server/vendored-conf/gitignore/Kate +3 -0
- package/server/vendored-conf/gitignore/KiCAD +13 -0
- package/server/vendored-conf/gitignore/Kohana +2 -0
- package/server/vendored-conf/gitignore/LabVIEW +16 -0
- package/server/vendored-conf/gitignore/Laravel +4 -0
- package/server/vendored-conf/gitignore/Lazarus +30 -0
- package/server/vendored-conf/gitignore/Leiningen +12 -0
- package/server/vendored-conf/gitignore/LemonStand +21 -0
- package/server/vendored-conf/gitignore/LibreOffice +2 -0
- package/server/vendored-conf/gitignore/Lilypond +6 -0
- package/server/vendored-conf/gitignore/Linux +7 -0
- package/server/vendored-conf/gitignore/Lithium +2 -0
- package/server/vendored-conf/gitignore/Lua +41 -0
- package/server/vendored-conf/gitignore/LyX +4 -0
- package/server/vendored-conf/gitignore/Magento +126 -0
- package/server/vendored-conf/gitignore/Matlab +16 -0
- package/server/vendored-conf/gitignore/Maven +9 -0
- package/server/vendored-conf/gitignore/Mercurial +6 -0
- package/server/vendored-conf/gitignore/Mercury +13 -0
- package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
- package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
- package/server/vendored-conf/gitignore/ModelSim +23 -0
- package/server/vendored-conf/gitignore/Momentics +8 -0
- package/server/vendored-conf/gitignore/MonoDevelop +8 -0
- package/server/vendored-conf/gitignore/Nanoc +10 -0
- package/server/vendored-conf/gitignore/NetBeans +8 -0
- package/server/vendored-conf/gitignore/Nim +1 -0
- package/server/vendored-conf/gitignore/Ninja +2 -0
- package/server/vendored-conf/gitignore/Node +28 -0
- package/server/vendored-conf/gitignore/NotepadPP +2 -0
- package/server/vendored-conf/gitignore/OCaml +20 -0
- package/server/vendored-conf/gitignore/Objective-C +43 -0
- package/server/vendored-conf/gitignore/Opa +13 -0
- package/server/vendored-conf/gitignore/OpenCart +11 -0
- package/server/vendored-conf/gitignore/OracleForms +8 -0
- package/server/vendored-conf/gitignore/Packer +5 -0
- package/server/vendored-conf/gitignore/Perl +20 -0
- package/server/vendored-conf/gitignore/Phalcon +2 -0
- package/server/vendored-conf/gitignore/PhpStorm +63 -0
- package/server/vendored-conf/gitignore/PlayFramework +17 -0
- package/server/vendored-conf/gitignore/Plone +18 -0
- package/server/vendored-conf/gitignore/Prestashop +25 -0
- package/server/vendored-conf/gitignore/Processing +7 -0
- package/server/vendored-conf/gitignore/Python +58 -0
- package/server/vendored-conf/gitignore/Qooxdoo +5 -0
- package/server/vendored-conf/gitignore/Qt +34 -0
- package/server/vendored-conf/gitignore/R +16 -0
- package/server/vendored-conf/gitignore/ROS +47 -0
- package/server/vendored-conf/gitignore/Rails +35 -0
- package/server/vendored-conf/gitignore/Redcar +1 -0
- package/server/vendored-conf/gitignore/Redis +3 -0
- package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
- package/server/vendored-conf/gitignore/Ruby +36 -0
- package/server/vendored-conf/gitignore/Rust +11 -0
- package/server/vendored-conf/gitignore/SBT +9 -0
- package/server/vendored-conf/gitignore/SCons +2 -0
- package/server/vendored-conf/gitignore/SVN +1 -0
- package/server/vendored-conf/gitignore/Sass +2 -0
- package/server/vendored-conf/gitignore/Scala +17 -0
- package/server/vendored-conf/gitignore/Scrivener +7 -0
- package/server/vendored-conf/gitignore/Sdcc +8 -0
- package/server/vendored-conf/gitignore/SeamGen +26 -0
- package/server/vendored-conf/gitignore/SketchUp +1 -0
- package/server/vendored-conf/gitignore/SlickEdit +11 -0
- package/server/vendored-conf/gitignore/Stella +12 -0
- package/server/vendored-conf/gitignore/SublimeText +14 -0
- package/server/vendored-conf/gitignore/SugarCRM +25 -0
- package/server/vendored-conf/gitignore/Swift +43 -0
- package/server/vendored-conf/gitignore/Symfony +37 -0
- package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
- package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
- package/server/vendored-conf/gitignore/Tags +14 -0
- package/server/vendored-conf/gitignore/TeX +136 -0
- package/server/vendored-conf/gitignore/TextMate +3 -0
- package/server/vendored-conf/gitignore/Textpattern +11 -0
- package/server/vendored-conf/gitignore/TortoiseGit +2 -0
- package/server/vendored-conf/gitignore/TurboGears2 +20 -0
- package/server/vendored-conf/gitignore/Typo3 +20 -0
- package/server/vendored-conf/gitignore/Umbraco +17 -0
- package/server/vendored-conf/gitignore/Unity +21 -0
- package/server/vendored-conf/gitignore/UnrealEngine +76 -0
- package/server/vendored-conf/gitignore/VVVV +6 -0
- package/server/vendored-conf/gitignore/Vagrant +1 -0
- package/server/vendored-conf/gitignore/Vim +6 -0
- package/server/vendored-conf/gitignore/VirtualEnv +10 -0
- package/server/vendored-conf/gitignore/VisualStudio +217 -0
- package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
- package/server/vendored-conf/gitignore/Waf +3 -0
- package/server/vendored-conf/gitignore/WebMethods +14 -0
- package/server/vendored-conf/gitignore/WebStorm +63 -0
- package/server/vendored-conf/gitignore/Windows +18 -0
- package/server/vendored-conf/gitignore/WordPress +18 -0
- package/server/vendored-conf/gitignore/Xcode +23 -0
- package/server/vendored-conf/gitignore/XilinxISE +57 -0
- package/server/vendored-conf/gitignore/Xojo +11 -0
- package/server/vendored-conf/gitignore/Yeoman +6 -0
- package/server/vendored-conf/gitignore/Yii +6 -0
- package/server/vendored-conf/gitignore/ZendFramework +20 -0
- package/server/vendored-conf/gitignore/Zephir +26 -0
- package/server/vendored-conf/gitignore/macOS +25 -0
- package/server/vendored-conf/label/Default +7 -0
- package/server/vendored-conf/license/Abstyles License +10 -0
- package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
- package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
- package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
- package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
- package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
- package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
- package/server/vendored-conf/license/Apache License 1.0 +19 -0
- package/server/vendored-conf/license/Apache License 1.1 +20 -0
- package/server/vendored-conf/license/Apache License 2.0 +72 -0
- package/server/vendored-conf/license/Artistic License 1.0 +48 -0
- package/server/vendored-conf/license/Artistic License 2.0 +84 -0
- package/server/vendored-conf/license/BSD 2-clause License +8 -0
- package/server/vendored-conf/license/BSD 3-clause License +10 -0
- package/server/vendored-conf/license/BSD 4-clause License +13 -0
- package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
- package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
- package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
- package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
- package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
- package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
- package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
- package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
- package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
- package/server/vendored-conf/license/ISC license +6 -0
- package/server/vendored-conf/license/MIT License +8 -0
- package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
- package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
- package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
- package/server/vendored-conf/readme/Default +3 -0
- package/src/dsh-host.js +168 -0
- package/src/index.js +682 -0
- package/src/runner.js +364 -0
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
// dsh 原生界面的内核 JSON API(token 认证,按人权限)。
|
|
2
|
+
//
|
|
3
|
+
// 插件宿主把 /dsh-git-server/api/dsh/* 透传到这里(带用户个人令牌),
|
|
4
|
+
// 供 dsh 原生 UI 使用:仓库总览(README 渲染)、markdown 渲染、PR
|
|
5
|
+
// 列表/详情/合并、wiki 列表/读取/保存、发版列表/创建。
|
|
6
|
+
// 这是私有内核的内部接口——不再背负上游 gogs API 的兼容义务。
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
import * as db from './db/db.js';
|
|
11
|
+
import * as git from './gitx/git.js';
|
|
12
|
+
import { conf } from './conf.js';
|
|
13
|
+
import { markdown, sanitizeHTML } from './markup.js';
|
|
14
|
+
import { authenticateUserByToken } from './context.js';
|
|
15
|
+
import * as svc from './gitx/service.js';
|
|
16
|
+
function requireAuth() { return { authenticateUserByToken }; }
|
|
17
|
+
export function registerDshRoutes(m) {
|
|
18
|
+
// ── token 认证 + 仓库解析(:o/:r) ─────────────────────────────
|
|
19
|
+
const authRepo = (c, needWrite = false) => {
|
|
20
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
21
|
+
const m2 = /^token (.+)$/.exec(header);
|
|
22
|
+
const token = m2 ? m2[1] : '';
|
|
23
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
24
|
+
if (!user) {
|
|
25
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const owner = db.getUserByUsername(c.Params(':o'));
|
|
29
|
+
const repo = owner ? db.getRepoByOwnerAndName(owner, c.Params(':r')) : null;
|
|
30
|
+
if (!repo) {
|
|
31
|
+
c.JSON(404, { error: 'repo not found' });
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (needWrite && db.accessMode(user.id, repo) < db.AccessMode.WRITE) {
|
|
35
|
+
c.JSON(403, { error: 'forbidden' });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { repo, user };
|
|
39
|
+
};
|
|
40
|
+
// ── markdown 渲染 ─────────────────────────────────────────────
|
|
41
|
+
m.post('/api/dsh/markdown', async (c) => {
|
|
42
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
43
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
44
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
45
|
+
if (!user) {
|
|
46
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const body = await c.form();
|
|
50
|
+
const html = markdown(String(body.text ?? ''), conf.subpath + '/', {});
|
|
51
|
+
c.JSONSuccess({ html });
|
|
52
|
+
});
|
|
53
|
+
// ── 仓库总览:分支/标签/默认分支/README ────────────────────────
|
|
54
|
+
m.get('/api/dsh/repos/:o/:r/overview', async (c) => {
|
|
55
|
+
const ar = authRepo(c);
|
|
56
|
+
if (!ar)
|
|
57
|
+
return;
|
|
58
|
+
const { repo } = ar;
|
|
59
|
+
const dir = repo.RepoPath();
|
|
60
|
+
const branches = (await git.getBranches(dir)).map((b) => b.name);
|
|
61
|
+
const tags = (await git.getTags(dir)).map((t) => t.name);
|
|
62
|
+
const def = repo.default_branch || conf.defaultBranch;
|
|
63
|
+
// README(默认分支根目录)
|
|
64
|
+
let readmeHtml = '';
|
|
65
|
+
try {
|
|
66
|
+
const commit = await git.getCommit(dir, def);
|
|
67
|
+
if (commit) {
|
|
68
|
+
const tree = await git.lsTree(dir, commit.id, '');
|
|
69
|
+
const entry = (tree?.entries ?? []).find((e) => e.type === 'blob' && /^readme(\.md|\.markdown)?$/i.test(e.name));
|
|
70
|
+
if (entry) {
|
|
71
|
+
const text = (await git.blobBytes(dir, entry.sha)).toString('utf8');
|
|
72
|
+
readmeHtml = sanitizeHTML(markdown(text, conf.subpath + '/', repo.ComposeMetas()));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { /* no readme */ }
|
|
77
|
+
const numCommits = Number((await git.git(dir, 'rev-list', '--count', '--end-of-options', def))?.toString('utf8').trim() || 0);
|
|
78
|
+
const numReleases = db.db().prepare('SELECT COUNT(*) AS c FROM release WHERE repo_id = ?').get(repo.id).c;
|
|
79
|
+
c.JSONSuccess({
|
|
80
|
+
defaultBranch: def, branches, tags, readmeHtml,
|
|
81
|
+
name: repo.name, owner: repo.OwnerName(), description: repo.description || '',
|
|
82
|
+
private: !!repo.is_private, numCommits, numBranches: branches.length, numTags: tags.length, numReleases,
|
|
83
|
+
numStars: repo.num_stars || 0, numForks: repo.num_forks || 0, numWatches: repo.num_watches || 0,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// ── git 标签列表(名称/指向/日期/说明) ──────────────────────
|
|
87
|
+
m.get('/api/dsh/repos/:o/:r/tags', async (c) => {
|
|
88
|
+
const ar = authRepo(c);
|
|
89
|
+
if (!ar)
|
|
90
|
+
return;
|
|
91
|
+
const dir = ar.repo.RepoPath();
|
|
92
|
+
const out = (await git.git(dir, 'for-each-ref', '--sort=-creatordate', '--format=%(refname:short)%01%(objectname:short)%01%(creatordate:unix)%01%(contents:subject)', 'refs/tags/'))?.toString('utf8') ?? '';
|
|
93
|
+
const rows = out.split('\n').filter(Boolean).map((l) => {
|
|
94
|
+
const [name, sha, date, msg] = l.split('\x01');
|
|
95
|
+
return { name, sha: sha || '', date: Number(date) * 1000 || 0, msg: msg || '' };
|
|
96
|
+
});
|
|
97
|
+
c.JSONSuccess(rows);
|
|
98
|
+
});
|
|
99
|
+
// ── 文件列表(带每项最近提交) ────────────────────────────────
|
|
100
|
+
m.get('/api/dsh/repos/:o/:r/tree', async (c) => {
|
|
101
|
+
const ar = authRepo(c);
|
|
102
|
+
if (!ar)
|
|
103
|
+
return;
|
|
104
|
+
const dir = ar.repo.RepoPath();
|
|
105
|
+
const ref = c.Query('ref') || ar.repo.default_branch || conf.defaultBranch;
|
|
106
|
+
const p = c.Query('path');
|
|
107
|
+
const tree = await git.lsTree(dir, ref, p || '');
|
|
108
|
+
if (!tree) {
|
|
109
|
+
c.JSON(404, { error: 'not found' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const e of tree.entries) {
|
|
114
|
+
const epath = p ? p + '/' + e.name : e.name;
|
|
115
|
+
let size = 0;
|
|
116
|
+
if (e.type === 'blob')
|
|
117
|
+
size = Number((await git.git(dir, 'cat-file', '-s', e.sha))?.toString('utf8').trim() || 0);
|
|
118
|
+
const out = (await git.git(dir, 'log', '-1', '--pretty=format:%h%x1f%s%x1f%ct%x1f%an', '--end-of-options', ref, '--', epath))?.toString('utf8') ?? '';
|
|
119
|
+
const [lsha, lmsg, ldate, lauthor] = out.split('\x1f');
|
|
120
|
+
entries.push({
|
|
121
|
+
name: e.name, type: e.type, path: epath, size,
|
|
122
|
+
last: lsha ? { sha: lsha, msg: (lmsg || '').slice(0, 90), date: Number(ldate) * 1000, author: lauthor || '' } : null,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
c.JSONSuccess({ entries });
|
|
126
|
+
});
|
|
127
|
+
// ── PR 列表(issue is_pull) ──────────────────────────────────
|
|
128
|
+
m.get('/api/dsh/repos/:o/:r/pulls', async (c) => {
|
|
129
|
+
const ar = authRepo(c);
|
|
130
|
+
if (!ar)
|
|
131
|
+
return;
|
|
132
|
+
const rows = db.db().prepare(`SELECT i.id, i."index", i.name AS title, i.is_closed, i.num_comments, i.created_unix, i.updated_unix,
|
|
133
|
+
pr.head_branch, pr.base_branch, pr.has_merged, u.name AS author,
|
|
134
|
+
ms.id AS msId, ms.name AS msTitle, au.name AS assignee
|
|
135
|
+
FROM issue i JOIN pull_request pr ON pr.issue_id = i.id
|
|
136
|
+
LEFT JOIN user u ON u.id = i.poster_id
|
|
137
|
+
LEFT JOIN milestone ms ON ms.id = i.milestone_id
|
|
138
|
+
LEFT JOIN user au ON au.id = i.assignee_id
|
|
139
|
+
WHERE i.repo_id = ? ORDER BY i."index" DESC LIMIT 50`).all(ar.repo.id);
|
|
140
|
+
const labelMap = {};
|
|
141
|
+
if (rows.length) {
|
|
142
|
+
const marks = rows.map(() => '?').join(',');
|
|
143
|
+
const ls = db.db().prepare(`SELECT il.issue_id, l.id, l.name, l.color FROM issue_label il JOIN label l ON l.id = il.label_id WHERE il.issue_id IN (${marks})`).all(...rows.map((r) => r.id));
|
|
144
|
+
for (const x of ls)
|
|
145
|
+
(labelMap[x.issue_id] = labelMap[x.issue_id] || []).push({ id: x.id, name: x.name, color: x.color });
|
|
146
|
+
}
|
|
147
|
+
c.JSONSuccess(rows.map((r) => ({
|
|
148
|
+
index: r.index, title: r.title,
|
|
149
|
+
state: r.has_merged ? 'merged' : (r.is_closed ? 'closed' : 'open'),
|
|
150
|
+
head: r.head_branch, base: r.base_branch, author: r.author,
|
|
151
|
+
updatedAt: r.updated_unix, comments: r.num_comments || 0,
|
|
152
|
+
labels: labelMap[r.id] || [],
|
|
153
|
+
milestone: r.msId ? { id: r.msId, title: r.msTitle } : null,
|
|
154
|
+
assignee: r.assignee || '',
|
|
155
|
+
})));
|
|
156
|
+
});
|
|
157
|
+
// ── PR 创建 ───────────────────────────────────────────────────
|
|
158
|
+
m.post('/api/dsh/repos/:o/:r/pulls', async (c) => {
|
|
159
|
+
const ar = authRepo(c, true);
|
|
160
|
+
if (!ar)
|
|
161
|
+
return;
|
|
162
|
+
const { repo, user } = ar;
|
|
163
|
+
const body = await c.form();
|
|
164
|
+
const title = String(body.title ?? '').trim();
|
|
165
|
+
const head = String(body.head ?? '').trim();
|
|
166
|
+
const base = String(body.base ?? '') || repo.default_branch || conf.defaultBranch;
|
|
167
|
+
if (!title || !head) {
|
|
168
|
+
c.JSON(422, { error: 'title and head required' });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const { issueAction, ActionType } = await import('./db/actions.js');
|
|
173
|
+
const index = db.maxIssueIndex(repo.id) + 1;
|
|
174
|
+
const now = Math.floor(Date.now() / 1000);
|
|
175
|
+
const info = db.db().prepare(`INSERT INTO issue (repo_id, "index", poster_id, name, content, is_closed, is_pull, num_comments, created_unix, updated_unix)
|
|
176
|
+
VALUES (?,?,?,?,?,0,1,0,?,?)`).run(repo.id, index, user.id, title, String(body.body ?? ''), now, now);
|
|
177
|
+
const issueID = Number(info.lastInsertRowid);
|
|
178
|
+
db.db().prepare('INSERT INTO issue_user (uid, issue_id, repo_id, is_poster, is_read) VALUES (?,?,?,1,1)').run(user.id, issueID, repo.id);
|
|
179
|
+
if (Array.isArray(body.labels)) {
|
|
180
|
+
for (const lid of body.labels)
|
|
181
|
+
db.db().prepare('INSERT OR IGNORE INTO issue_label (issue_id, label_id) VALUES (?,?)').run(issueID, Number(lid));
|
|
182
|
+
}
|
|
183
|
+
if (body.milestone || body.assignee) {
|
|
184
|
+
const au = body.assignee ? db.getUserByUsername(String(body.assignee)) : null;
|
|
185
|
+
db.db().prepare('UPDATE issue SET milestone_id = ?, assignee_id = ? WHERE id = ?')
|
|
186
|
+
.run(Number(body.milestone) || 0, au ? au.id : 0, issueID);
|
|
187
|
+
}
|
|
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
|
+
const patchDir = path.join(conf.appDataPath, 'patches', String(repo.id));
|
|
192
|
+
fs.mkdirSync(patchDir, { recursive: true });
|
|
193
|
+
fs.writeFileSync(path.join(patchDir, `${index}.patch`), patch);
|
|
194
|
+
const { testPullRequestMergeable } = await import('./repox.js');
|
|
195
|
+
const status = await testPullRequestMergeable(repo, { index, base_branch: base });
|
|
196
|
+
db.db().prepare(`INSERT INTO pull_request (type, status, issue_id, "index", head_repo_id, base_repo_id, head_user_name, head_branch, base_branch, merge_base)
|
|
197
|
+
VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(status, issueID, index, repo.id, repo.id, repo.OwnerName(), head, base, mergeBase);
|
|
198
|
+
db.refreshIssueCounts(repo.id);
|
|
199
|
+
const issue = db.getIssueByID(issueID);
|
|
200
|
+
await issueAction(ActionType.CREATE_PULL_REQUEST, user, repo, issue);
|
|
201
|
+
c.JSONSuccess({ ok: true, index });
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// ── 仓库信息(设置页用) ─────────────────────────────────────
|
|
208
|
+
m.get('/api/dsh/repos/:o/:r', async (c) => {
|
|
209
|
+
const ar = authRepo(c);
|
|
210
|
+
if (!ar)
|
|
211
|
+
return;
|
|
212
|
+
c.JSONSuccess({
|
|
213
|
+
name: ar.repo.name, owner: ar.repo.OwnerName(),
|
|
214
|
+
description: ar.repo.description || '', private: !!ar.repo.is_private,
|
|
215
|
+
defaultBranch: ar.repo.default_branch || conf.defaultBranch,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// ── 单条 issue(详情页编辑用) ───────────────────────────────
|
|
219
|
+
m.get('/api/dsh/repos/:o/:r/issues/:idx', async (c) => {
|
|
220
|
+
const ar = authRepo(c);
|
|
221
|
+
if (!ar)
|
|
222
|
+
return;
|
|
223
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':idx'));
|
|
224
|
+
if (!issue) {
|
|
225
|
+
c.JSON(404, { error: 'not found' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const labels = db.db().prepare('SELECT l.id, l.name, l.color FROM issue_label il JOIN label l ON l.id = il.label_id WHERE il.issue_id = ?').all(issue.id);
|
|
229
|
+
const ms = issue.milestone_id ? db.db().prepare('SELECT id, name FROM milestone WHERE id = ?').get(issue.milestone_id) : null;
|
|
230
|
+
const as = issue.assignee_id ? db.db().prepare('SELECT name FROM user WHERE id = ?').get(issue.assignee_id) : null;
|
|
231
|
+
const poster = issue.poster_id ? db.db().prepare('SELECT name FROM user WHERE id = ?').get(issue.poster_id) : null;
|
|
232
|
+
const participants = db.db().prepare('SELECT COUNT(DISTINCT poster_id) AS c FROM comment WHERE issue_id = ? UNION ALL SELECT poster_id AS c FROM issue WHERE id = ?').all(issue.id, issue.id).length;
|
|
233
|
+
c.JSONSuccess({
|
|
234
|
+
number: Number(issue.index), title: issue.name, body: issue.content || '',
|
|
235
|
+
state: issue.is_closed ? 'closed' : 'open',
|
|
236
|
+
labels, milestone: ms ? { id: ms.id, title: ms.name } : null,
|
|
237
|
+
assignee: as ? as.name : '', author: poster ? poster.name : '',
|
|
238
|
+
createdAt: Number(issue.created_unix) * 1000 || 0, participants,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ── issue 列表(富字段 + 标签/里程碑/负责人过滤) ─────────────
|
|
242
|
+
m.get('/api/dsh/repos/:o/:r/issues', async (c) => {
|
|
243
|
+
const ar = authRepo(c);
|
|
244
|
+
if (!ar)
|
|
245
|
+
return;
|
|
246
|
+
const state = c.Query('state') || 'open';
|
|
247
|
+
const labelF = c.QueryInt('label');
|
|
248
|
+
const msF = c.QueryInt('milestone');
|
|
249
|
+
const asg = c.Query('assignee');
|
|
250
|
+
const where = ['i.repo_id = ?', 'i.is_pull = 0'];
|
|
251
|
+
const args = [ar.repo.id];
|
|
252
|
+
if (state === 'open' || state === 'closed') {
|
|
253
|
+
where.push('i.is_closed = ?');
|
|
254
|
+
args.push(state === 'closed' ? 1 : 0);
|
|
255
|
+
}
|
|
256
|
+
if (msF) {
|
|
257
|
+
where.push('i.milestone_id = ?');
|
|
258
|
+
args.push(msF);
|
|
259
|
+
}
|
|
260
|
+
if (asg) {
|
|
261
|
+
const au = db.getUserByUsername(asg);
|
|
262
|
+
where.push('i.assignee_id = ?');
|
|
263
|
+
args.push(au ? au.id : -1);
|
|
264
|
+
}
|
|
265
|
+
if (labelF) {
|
|
266
|
+
where.push('i.id IN (SELECT issue_id FROM issue_label WHERE label_id = ?)');
|
|
267
|
+
args.push(labelF);
|
|
268
|
+
}
|
|
269
|
+
const search = c.Query('search').trim().toLowerCase();
|
|
270
|
+
if (search) {
|
|
271
|
+
where.push('(lower(i.name) LIKE ? OR lower(i.content) LIKE ?)');
|
|
272
|
+
args.push('%' + search + '%', '%' + search + '%');
|
|
273
|
+
}
|
|
274
|
+
const sortKey = { latest: 'i.created_unix DESC', oldest: 'i.created_unix ASC', recentupdate: 'i.updated_unix DESC', leastupdate: 'i.updated_unix ASC', mostcomment: 'i.num_comments DESC', leastcomment: 'i.num_comments ASC' }[c.Query('sort') || 'latest'] || 'i.updated_unix DESC';
|
|
275
|
+
const rows = db.db().prepare(`SELECT i.id, i."index" AS number, i.name AS title, i.is_closed, i.num_comments AS comments, i.updated_unix AS updated,
|
|
276
|
+
u.name AS user, ms.id AS msId, ms.name AS msTitle, au.name AS assignee
|
|
277
|
+
FROM issue i LEFT JOIN user u ON u.id = i.poster_id
|
|
278
|
+
LEFT JOIN milestone ms ON ms.id = i.milestone_id
|
|
279
|
+
LEFT JOIN user au ON au.id = i.assignee_id
|
|
280
|
+
WHERE ${where.join(' AND ')} ORDER BY ${sortKey} LIMIT 200`).all(...args);
|
|
281
|
+
const labelMap = {};
|
|
282
|
+
if (rows.length) {
|
|
283
|
+
const marks = rows.map(() => '?').join(',');
|
|
284
|
+
const ls = db.db().prepare(`SELECT il.issue_id, l.id, l.name, l.color FROM issue_label il JOIN label l ON l.id = il.label_id WHERE il.issue_id IN (${marks})`).all(...rows.map((r) => r.id));
|
|
285
|
+
for (const x of ls)
|
|
286
|
+
(labelMap[x.issue_id] = labelMap[x.issue_id] || []).push({ id: x.id, name: x.name, color: x.color });
|
|
287
|
+
}
|
|
288
|
+
c.JSONSuccess(rows.map((r) => ({
|
|
289
|
+
number: r.number, title: r.title, state: r.is_closed ? 'closed' : 'open',
|
|
290
|
+
user: r.user, comments: r.comments, updatedAt: r.updated * 1000,
|
|
291
|
+
labels: labelMap[r.id] || [],
|
|
292
|
+
milestone: r.msId ? { id: r.msId, title: r.msTitle } : null,
|
|
293
|
+
assignee: r.assignee || '',
|
|
294
|
+
})));
|
|
295
|
+
});
|
|
296
|
+
// ── 提交详情(diff) ─────────────────────────────────────────
|
|
297
|
+
m.get('/api/dsh/repos/:o/:r/commits/:sha', async (c) => {
|
|
298
|
+
const ar = authRepo(c);
|
|
299
|
+
if (!ar)
|
|
300
|
+
return;
|
|
301
|
+
const dir = ar.repo.RepoPath();
|
|
302
|
+
const sha = c.Params(':sha');
|
|
303
|
+
const meta = await git.getCommit(dir, sha);
|
|
304
|
+
if (!meta) {
|
|
305
|
+
c.JSON(404, { error: 'commit not found' });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const parent = meta.parents[0] || '';
|
|
309
|
+
const diff = parent
|
|
310
|
+
? await git.git(dir, 'diff', '--unified=3', '--end-of-options', parent, sha)
|
|
311
|
+
: await git.git(dir, 'show', '--unified=3', '--format=', sha);
|
|
312
|
+
c.JSONSuccess({
|
|
313
|
+
sha, message: meta.message, author: meta.author, committer: meta.committer,
|
|
314
|
+
files: (diff || '').toString(),
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// ── blame ─────────────────────────────────────────────────────
|
|
318
|
+
m.get('/api/dsh/repos/:o/:r/blame', async (c) => {
|
|
319
|
+
const ar = authRepo(c);
|
|
320
|
+
if (!ar)
|
|
321
|
+
return;
|
|
322
|
+
const dir = ar.repo.RepoPath();
|
|
323
|
+
const ref = c.Query('ref') || ar.repo.default_branch || conf.defaultBranch;
|
|
324
|
+
const p = c.Query('path');
|
|
325
|
+
if (!p) {
|
|
326
|
+
c.JSON(422, { error: 'path required' });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const out = (await git.git(dir, 'blame', '--line-porcelain', ref, '--', p))?.toString('utf8') ?? '';
|
|
330
|
+
const lines = [];
|
|
331
|
+
let cur = null;
|
|
332
|
+
for (const line of out.split('\n')) {
|
|
333
|
+
const bm = /^([0-9a-f]{40}) \d+ (\d+)/.exec(line);
|
|
334
|
+
if (bm) {
|
|
335
|
+
if (cur && cur.sha)
|
|
336
|
+
lines.push(cur);
|
|
337
|
+
cur = { sha: bm[1], line: Number(bm[2]) };
|
|
338
|
+
}
|
|
339
|
+
else if (line.startsWith('author ')) {
|
|
340
|
+
if (cur)
|
|
341
|
+
cur.author = line.slice(7);
|
|
342
|
+
}
|
|
343
|
+
else if (line.startsWith('author-mail ')) {
|
|
344
|
+
if (cur)
|
|
345
|
+
cur.email = line.slice(12).replace(/[<>]/g, '');
|
|
346
|
+
}
|
|
347
|
+
else if (line.startsWith('summary ')) {
|
|
348
|
+
if (cur)
|
|
349
|
+
cur.summary = line.slice(8);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (cur && cur.sha)
|
|
353
|
+
lines.push(cur);
|
|
354
|
+
c.JSONSuccess({ lines });
|
|
355
|
+
});
|
|
356
|
+
// ── compare(分支对比:commits + diff) ───────────────────────
|
|
357
|
+
m.get('/api/dsh/repos/:o/:r/compare', async (c) => {
|
|
358
|
+
const ar = authRepo(c);
|
|
359
|
+
if (!ar)
|
|
360
|
+
return;
|
|
361
|
+
const dir = ar.repo.RepoPath();
|
|
362
|
+
const base = c.Query('base') || ar.repo.default_branch || conf.defaultBranch;
|
|
363
|
+
const head = c.Query('head') || base;
|
|
364
|
+
const commits = (await git.git(dir, 'log', '--pretty=format:%H%x1f%s%x1f%an%x1f%aI', '--end-of-options', `${base}..${head}`, '--'))
|
|
365
|
+
?.toString('utf8').split('\n').filter(Boolean).map((l) => { const [sha, message, author, date] = l.split('\x1f'); return { sha, message, author, date }; }) ?? [];
|
|
366
|
+
const diff = (await git.git(dir, 'diff', '--stat', '--end-of-options', `${base}...${head}`))?.toString('utf8') ?? '';
|
|
367
|
+
const diffFull = (await git.git(dir, 'diff', '--unified=3', '--end-of-options', `${base}...${head}`))?.toString('utf8') ?? '';
|
|
368
|
+
c.JSONSuccess({ base, head, commits, diffStat: diff, diff: diffFull });
|
|
369
|
+
});
|
|
370
|
+
// ── 文件历史(git log -- path) ───────────────────────────────
|
|
371
|
+
m.get('/api/dsh/repos/:o/:r/file-history', async (c) => {
|
|
372
|
+
const ar = authRepo(c);
|
|
373
|
+
if (!ar)
|
|
374
|
+
return;
|
|
375
|
+
const dir = ar.repo.RepoPath();
|
|
376
|
+
const ref = c.Query('ref') || ar.repo.default_branch || conf.defaultBranch;
|
|
377
|
+
const p = c.Query('path');
|
|
378
|
+
if (!p) {
|
|
379
|
+
c.JSON(422, { error: 'path required' });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const out = (await git.git(dir, 'log', '--pretty=format:%h%x1f%s%x1f%an%x1f%ct', '--end-of-options', ref, '--', p))?.toString('utf8') ?? '';
|
|
383
|
+
const rows = out.split('\n').filter(Boolean).map((l) => {
|
|
384
|
+
const [sha, msg, author, date] = l.split('\x1f');
|
|
385
|
+
return { sha, msg, author, date: Number(date) * 1000 };
|
|
386
|
+
});
|
|
387
|
+
c.JSONSuccess({ commits: rows });
|
|
388
|
+
});
|
|
389
|
+
// ── 删除文件(网页删文件提交) ──────────────────────────────────
|
|
390
|
+
m.delete('/api/dsh/repos/:o/:r/files', async (c) => {
|
|
391
|
+
const ar = authRepo(c, true);
|
|
392
|
+
if (!ar)
|
|
393
|
+
return;
|
|
394
|
+
const body = await c.form();
|
|
395
|
+
const fileName = String(body.path ?? '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
|
|
396
|
+
if (!fileName || fileName.includes('..')) {
|
|
397
|
+
c.JSON(422, { error: '路径非法' });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const branch = String(body.branch ?? '') || ar.repo.default_branch || conf.defaultBranch;
|
|
401
|
+
const message = String(body.message ?? '') || `Delete ${fileName}`;
|
|
402
|
+
const dir = ar.repo.RepoPath();
|
|
403
|
+
const tmp = path.join(conf.appDataPath, 'tmp', 'delfile-' + Date.now());
|
|
404
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
405
|
+
try {
|
|
406
|
+
await git.git(process.cwd(), 'clone', '-q', '-b', branch, dir, tmp);
|
|
407
|
+
if (!fs.existsSync(path.join(tmp, fileName))) {
|
|
408
|
+
c.JSON(404, { error: '文件不存在' });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await git.git(tmp, 'rm', '-q', '--', fileName);
|
|
412
|
+
await git.git(tmp, 'commit', '-q', `--author=${ar.user.name} <${ar.user.email}>`, '-m', message);
|
|
413
|
+
await git.git(tmp, 'push', '-q', 'origin', `HEAD:refs/heads/${branch}`);
|
|
414
|
+
c.JSONSuccess({ ok: true });
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
// ── 网页新建/上传文件(新的文件 / 上传文件) ────────────────────
|
|
424
|
+
m.post('/api/dsh/repos/:o/:r/files', async (c) => {
|
|
425
|
+
const ar = authRepo(c, true);
|
|
426
|
+
if (!ar)
|
|
427
|
+
return;
|
|
428
|
+
const body = await c.form();
|
|
429
|
+
const fileName = String(body.path ?? '').replace(/\\/g, '/').replace(/^\/+/, '').trim();
|
|
430
|
+
if (!fileName || fileName.includes('..')) {
|
|
431
|
+
c.JSON(422, { error: '路径非法' });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const branch = String(body.branch ?? '') || ar.repo.default_branch || conf.defaultBranch;
|
|
435
|
+
const message = String(body.message ?? '') || (body.overwrite ? `Update ${fileName}` : `Create ${fileName}`);
|
|
436
|
+
const base64 = String(body.contentBase64 ?? '');
|
|
437
|
+
const content = base64 ? Buffer.from(base64, 'base64') : Buffer.from(String(body.content ?? ''), 'utf8');
|
|
438
|
+
const dir = ar.repo.RepoPath();
|
|
439
|
+
const tmp = path.join(conf.appDataPath, 'tmp', 'newfile-' + Date.now());
|
|
440
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
441
|
+
try {
|
|
442
|
+
await git.git(process.cwd(), 'clone', '-q', '-b', branch, dir, tmp);
|
|
443
|
+
const fp = path.join(tmp, fileName);
|
|
444
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
445
|
+
const exists = fs.existsSync(fp);
|
|
446
|
+
if (exists && !body.overwrite) {
|
|
447
|
+
c.JSON(409, { error: '同名文件已存在(勾选覆盖可更新)' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
fs.writeFileSync(fp, content);
|
|
451
|
+
await git.git(tmp, 'add', '--', fileName);
|
|
452
|
+
await git.git(tmp, 'commit', '-q', `--author=${ar.user.name} <${ar.user.email}>`, '-m', message);
|
|
453
|
+
await git.git(tmp, 'push', '-q', 'origin', `HEAD:refs/heads/${branch}`);
|
|
454
|
+
c.JSONSuccess({ ok: true, path: fileName });
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
// ── 源码下载(archive zip/tar.gz,二进制流) ──────────────────
|
|
464
|
+
m.get('/api/dsh/repos/:o/:r/archive/*', async (c) => {
|
|
465
|
+
const ar = authRepo(c);
|
|
466
|
+
if (!ar)
|
|
467
|
+
return;
|
|
468
|
+
const wild = c.Params(':*');
|
|
469
|
+
const ext = wild.endsWith('.zip') ? 'zip' : wild.endsWith('.tar.gz') ? 'tar.gz' : null;
|
|
470
|
+
if (!ext) {
|
|
471
|
+
c.JSON(404, { error: 'bad format' });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const ref = wild.slice(0, wild.length - (ext === 'zip' ? 4 : 7));
|
|
475
|
+
const dir = ar.repo.RepoPath();
|
|
476
|
+
const sha = (await git.gitOK(dir, 'rev-parse', '--verify', '--end-of-options', ref))?.toString().trim();
|
|
477
|
+
if (!sha) {
|
|
478
|
+
c.JSON(404, { error: 'ref not found' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const dst = path.join(os.tmpdir(), `dgs-archive-${Date.now()}.${ext}`);
|
|
482
|
+
try {
|
|
483
|
+
await git.archive(dir, sha, ext, dst, ar.repo.name + '/');
|
|
484
|
+
const buf = fs.readFileSync(dst);
|
|
485
|
+
c.res.setHeader('Content-Type', ext === 'zip' ? 'application/zip' : 'application/gzip');
|
|
486
|
+
c.res.setHeader('Content-Disposition', `attachment; filename=${ar.repo.name}-${ref.replace(/\//g, '-')}.${ext}`);
|
|
487
|
+
c.res.setHeader('Content-Length', String(buf.length));
|
|
488
|
+
c.res.statusCode = 200;
|
|
489
|
+
c.res.end(buf);
|
|
490
|
+
c.rendered = true;
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 160) });
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
fs.rmSync(dst, { force: true });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
// ── 标签管理 CRUD ─────────────────────────────────────────────
|
|
500
|
+
m.get('/api/dsh/repos/:o/:r/labels', async (c) => {
|
|
501
|
+
const ar = authRepo(c);
|
|
502
|
+
if (!ar)
|
|
503
|
+
return;
|
|
504
|
+
const rows = db.listLabels(ar.repo.id);
|
|
505
|
+
const counts = db.db().prepare('SELECT il.label_id, COUNT(*) AS c FROM issue_label il JOIN issue i ON i.id = il.issue_id WHERE i.repo_id = ? AND i.is_closed = 0 AND i.is_pull = 0 GROUP BY il.label_id').all(ar.repo.id);
|
|
506
|
+
const cmap = {};
|
|
507
|
+
for (const x of counts)
|
|
508
|
+
cmap[x.label_id] = x.c;
|
|
509
|
+
c.JSONSuccess(rows.map((l) => ({ id: l.id, name: l.name, color: l.color, openIssues: cmap[l.id] || 0 })));
|
|
510
|
+
});
|
|
511
|
+
m.post('/api/dsh/repos/:o/:r/labels', async (c) => {
|
|
512
|
+
const ar = authRepo(c, true);
|
|
513
|
+
if (!ar)
|
|
514
|
+
return;
|
|
515
|
+
const body = await c.form();
|
|
516
|
+
const name = String(body.name ?? '').trim();
|
|
517
|
+
if (!name) {
|
|
518
|
+
c.JSON(422, { error: 'name required' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const info = db.db().prepare('INSERT INTO label (repo_id, name, color, num_issues, num_closed_issues) VALUES (?,?,?,?,0)').run(ar.repo.id, name, String(body.color ?? '#70c24a'), 0);
|
|
522
|
+
c.JSONSuccess({ id: Number(info.lastInsertRowid), name });
|
|
523
|
+
});
|
|
524
|
+
m.patch('/api/dsh/repos/:o/:r/labels/:id', async (c) => {
|
|
525
|
+
const ar = authRepo(c, true);
|
|
526
|
+
if (!ar)
|
|
527
|
+
return;
|
|
528
|
+
const body = await c.form();
|
|
529
|
+
const id = c.ParamsInt64(':id');
|
|
530
|
+
const label = db.getLabelByID(ar.repo.id, id);
|
|
531
|
+
if (!label) {
|
|
532
|
+
c.JSON(404, { error: 'not found' });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const sets = [];
|
|
536
|
+
const args = [];
|
|
537
|
+
if (body.name !== undefined) {
|
|
538
|
+
sets.push('name = ?');
|
|
539
|
+
args.push(String(body.name));
|
|
540
|
+
}
|
|
541
|
+
if (body.color !== undefined) {
|
|
542
|
+
sets.push('color = ?');
|
|
543
|
+
args.push(String(body.color));
|
|
544
|
+
}
|
|
545
|
+
if (sets.length)
|
|
546
|
+
db.db().prepare(`UPDATE label SET ${sets.join(', ')} WHERE id = ? AND repo_id = ?`).run(...args, id, ar.repo.id);
|
|
547
|
+
c.JSONSuccess({ ok: true });
|
|
548
|
+
});
|
|
549
|
+
m.delete('/api/dsh/repos/:o/:r/labels/:id', async (c) => {
|
|
550
|
+
const ar = authRepo(c, true);
|
|
551
|
+
if (!ar)
|
|
552
|
+
return;
|
|
553
|
+
db.db().prepare('DELETE FROM label WHERE id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
554
|
+
c.JSONSuccess({ ok: true });
|
|
555
|
+
});
|
|
556
|
+
// ── 里程碑 CRUD ───────────────────────────────────────────────
|
|
557
|
+
m.get('/api/dsh/repos/:o/:r/milestones', async (c) => {
|
|
558
|
+
const ar = authRepo(c);
|
|
559
|
+
if (!ar)
|
|
560
|
+
return;
|
|
561
|
+
// 计数实时计算:milestone 行的 num_issues 存列在 dsh 通道下从不刷新
|
|
562
|
+
const rows = db.db().prepare(`SELECT m.id, m.name, m.content, m.deadline_unix, m.is_closed,
|
|
563
|
+
(SELECT COUNT(*) FROM issue WHERE milestone_id = m.id) AS total,
|
|
564
|
+
(SELECT COUNT(*) FROM issue WHERE milestone_id = m.id AND is_closed = 1) AS done
|
|
565
|
+
FROM milestone m WHERE m.repo_id = ? ORDER BY m.deadline_unix ASC, m.id ASC`).all(ar.repo.id);
|
|
566
|
+
c.JSONSuccess(rows.map((m) => ({
|
|
567
|
+
id: m.id, title: m.name, description: m.content,
|
|
568
|
+
due: m.deadline_unix, closed: !!m.is_closed,
|
|
569
|
+
open: m.total - m.done, closedIssues: m.done,
|
|
570
|
+
})));
|
|
571
|
+
});
|
|
572
|
+
m.post('/api/dsh/repos/:o/:r/milestones', async (c) => {
|
|
573
|
+
const ar = authRepo(c, true);
|
|
574
|
+
if (!ar)
|
|
575
|
+
return;
|
|
576
|
+
const body = await c.form();
|
|
577
|
+
const title = String(body.title ?? '').trim();
|
|
578
|
+
if (!title) {
|
|
579
|
+
c.JSON(422, { error: 'title required' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
let deadline = 0;
|
|
583
|
+
if (body.due) {
|
|
584
|
+
const t = Date.parse(String(body.due));
|
|
585
|
+
if (!Number.isNaN(t))
|
|
586
|
+
deadline = Math.floor(t / 1000);
|
|
587
|
+
}
|
|
588
|
+
const info = db.db().prepare('INSERT INTO milestone (repo_id, name, content, is_closed, num_issues, num_closed_issues, completeness, deadline_unix) VALUES (?,?,?,?,0,0,0,?)')
|
|
589
|
+
.run(ar.repo.id, title, String(body.description ?? ''), 0, deadline);
|
|
590
|
+
c.JSONSuccess({ id: Number(info.lastInsertRowid), title });
|
|
591
|
+
});
|
|
592
|
+
m.patch('/api/dsh/repos/:o/:r/milestones/:id', async (c) => {
|
|
593
|
+
const ar = authRepo(c, true);
|
|
594
|
+
if (!ar)
|
|
595
|
+
return;
|
|
596
|
+
const id = c.ParamsInt64(':id');
|
|
597
|
+
const body = await c.form();
|
|
598
|
+
const sets = [];
|
|
599
|
+
const args = [];
|
|
600
|
+
if (body.title !== undefined) {
|
|
601
|
+
sets.push('name = ?');
|
|
602
|
+
args.push(String(body.title));
|
|
603
|
+
}
|
|
604
|
+
if (body.description !== undefined) {
|
|
605
|
+
sets.push('content = ?');
|
|
606
|
+
args.push(String(body.description));
|
|
607
|
+
}
|
|
608
|
+
if (body.due !== undefined) {
|
|
609
|
+
const t2 = Date.parse(String(body.due));
|
|
610
|
+
sets.push('deadline_unix = ?');
|
|
611
|
+
args.push(Number.isNaN(t2) ? 0 : Math.floor(t2 / 1000));
|
|
612
|
+
}
|
|
613
|
+
if (body.closed !== undefined) {
|
|
614
|
+
sets.push('is_closed = ?');
|
|
615
|
+
args.push(body.closed ? 1 : 0);
|
|
616
|
+
}
|
|
617
|
+
if (sets.length)
|
|
618
|
+
db.db().prepare(`UPDATE milestone SET ${sets.join(', ')} WHERE id = ? AND repo_id = ?`).run(...args, id, ar.repo.id);
|
|
619
|
+
c.JSONSuccess({ ok: true });
|
|
620
|
+
});
|
|
621
|
+
m.delete('/api/dsh/repos/:o/:r/milestones/:id', async (c) => {
|
|
622
|
+
const ar = authRepo(c, true);
|
|
623
|
+
if (!ar)
|
|
624
|
+
return;
|
|
625
|
+
db.db().prepare('DELETE FROM milestone WHERE id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
626
|
+
c.JSONSuccess({ ok: true });
|
|
627
|
+
});
|
|
628
|
+
// ── issue 增强:编辑/标签/里程碑/负责人 ────────────────────────
|
|
629
|
+
m.patch('/api/dsh/repos/:o/:r/issues/:idx', async (c) => {
|
|
630
|
+
const ar = authRepo(c);
|
|
631
|
+
if (!ar)
|
|
632
|
+
return;
|
|
633
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':idx'));
|
|
634
|
+
if (!issue) {
|
|
635
|
+
c.JSON(404, { error: 'not found' });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const body = await c.form();
|
|
639
|
+
const sets = [];
|
|
640
|
+
const args = [];
|
|
641
|
+
if (body.title !== undefined) {
|
|
642
|
+
sets.push('name = ?');
|
|
643
|
+
args.push(String(body.title));
|
|
644
|
+
}
|
|
645
|
+
if (body.body !== undefined) {
|
|
646
|
+
sets.push('content = ?');
|
|
647
|
+
args.push(String(body.body));
|
|
648
|
+
}
|
|
649
|
+
if (body.milestone !== undefined) {
|
|
650
|
+
sets.push('milestone_id = ?');
|
|
651
|
+
args.push(Number(body.milestone) || 0);
|
|
652
|
+
}
|
|
653
|
+
if (body.assignee !== undefined) {
|
|
654
|
+
const au = body.assignee ? db.getUserByUsername(String(body.assignee)) : null;
|
|
655
|
+
sets.push('assignee_id = ?');
|
|
656
|
+
args.push(au ? au.id : 0);
|
|
657
|
+
}
|
|
658
|
+
if (sets.length) {
|
|
659
|
+
sets.push('updated_unix = ?');
|
|
660
|
+
args.push(Math.floor(Date.now() / 1000), issue.id);
|
|
661
|
+
db.db().prepare(`UPDATE issue SET ${sets.join(', ')} WHERE id = ?`).run(...args);
|
|
662
|
+
}
|
|
663
|
+
if (body.labels !== undefined && Array.isArray(body.labels)) {
|
|
664
|
+
db.db().prepare('DELETE FROM issue_label WHERE issue_id = ?').run(issue.id);
|
|
665
|
+
for (const lid of body.labels) {
|
|
666
|
+
db.db().prepare('INSERT INTO issue_label (issue_id, label_id) VALUES (?,?)').run(issue.id, Number(lid));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
db.refreshIssueCounts(ar.repo.id);
|
|
670
|
+
c.JSONSuccess({ ok: true });
|
|
671
|
+
});
|
|
672
|
+
// ── 关注 / 星标(toggle + 列表,对齐原版 action/watch|star) ──
|
|
673
|
+
const starWatch = (table, countCol) => {
|
|
674
|
+
const ucol = table === 'star' ? 'uid' : 'user_id';
|
|
675
|
+
return ({
|
|
676
|
+
get: async (c) => {
|
|
677
|
+
const ar = authRepo(c);
|
|
678
|
+
if (!ar)
|
|
679
|
+
return;
|
|
680
|
+
const on = !!db.db().prepare(`SELECT 1 FROM ${table} WHERE ${ucol} = ? AND repo_id = ?`).get(ar.user.id, ar.repo.id);
|
|
681
|
+
const users = db.db().prepare(`SELECT u.name FROM ${table} x JOIN user u ON u.id = x.${ucol} WHERE x.repo_id = ? ORDER BY x.id DESC LIMIT 50`).all(ar.repo.id).map((r) => r.name);
|
|
682
|
+
c.JSONSuccess({ on, count: Number(ar.repo[countCol] || 0), users });
|
|
683
|
+
},
|
|
684
|
+
post: async (c) => {
|
|
685
|
+
const ar = authRepo(c);
|
|
686
|
+
if (!ar)
|
|
687
|
+
return;
|
|
688
|
+
const on = !!db.db().prepare(`SELECT 1 FROM ${table} WHERE ${ucol} = ? AND repo_id = ?`).get(ar.user.id, ar.repo.id);
|
|
689
|
+
if (on)
|
|
690
|
+
db.db().prepare(`DELETE FROM ${table} WHERE ${ucol} = ? AND repo_id = ?`).run(ar.user.id, ar.repo.id);
|
|
691
|
+
else
|
|
692
|
+
db.db().prepare(`INSERT INTO ${table} (${ucol}, repo_id) VALUES (?,?)`).run(ar.user.id, ar.repo.id);
|
|
693
|
+
const n = db.db().prepare(`SELECT COUNT(*) AS c FROM ${table} WHERE repo_id = ?`).get(ar.repo.id).c;
|
|
694
|
+
db.db().prepare(`UPDATE repository SET ${countCol} = ? WHERE id = ?`).run(n, ar.repo.id);
|
|
695
|
+
c.JSONSuccess({ on: !on, count: n });
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
const star = starWatch('star', 'num_stars');
|
|
700
|
+
const watch = starWatch('watch', 'num_watches');
|
|
701
|
+
m.get('/api/dsh/repos/:o/:r/star', (c) => star.get(c));
|
|
702
|
+
m.post('/api/dsh/repos/:o/:r/star', (c) => star.post(c));
|
|
703
|
+
m.get('/api/dsh/repos/:o/:r/watch', (c) => watch.get(c));
|
|
704
|
+
m.post('/api/dsh/repos/:o/:r/watch', (c) => watch.post(c));
|
|
705
|
+
// ── fork ────────────────────────────────────────────────────────
|
|
706
|
+
m.post('/api/dsh/repos/:o/:r/fork', async (c) => {
|
|
707
|
+
const ar = authRepo(c);
|
|
708
|
+
if (!ar)
|
|
709
|
+
return;
|
|
710
|
+
const src = ar.repo;
|
|
711
|
+
const body = await c.form();
|
|
712
|
+
const orgName = String(body.org ?? '');
|
|
713
|
+
const owner = orgName ? db.getUserByUsername(orgName) : ar.user;
|
|
714
|
+
if (!owner) {
|
|
715
|
+
c.JSON(404, { error: 'org not found' });
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (orgName) {
|
|
719
|
+
const mem = db.db().prepare('SELECT 1 FROM org_user WHERE org_id = ? AND uid = ? AND is_owner = 1').get(owner.id, ar.user.id);
|
|
720
|
+
if (!mem && ar.user.is_admin !== 1) {
|
|
721
|
+
c.JSON(403, { error: '需要组织管理员' });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (db.getRepoByOwnerAndName(owner, src.name)) {
|
|
726
|
+
c.JSON(422, { error: '同名仓库已存在' });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const now = Math.floor(Date.now() / 1000);
|
|
730
|
+
const info = db.db().prepare('INSERT INTO repository (owner_id, lower_name, name, description, is_private, is_fork, fork_id, num_watches, num_stars, num_forks, num_issues, num_closed_issues, num_pulls, num_closed_pulls, created_unix) VALUES (?,?,?,?,?,?,?,?,?,?,0,0,0,0,?)').run(owner.id, src.lower_name, src.name, src.description, src.is_private, 1, src.id, 1, 0, 0, now);
|
|
731
|
+
const fork = db.getRepoByID(Number(info.lastInsertRowid));
|
|
732
|
+
if (!fork) {
|
|
733
|
+
c.JSON(500, { error: 'fork insert failed' });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
await svc.forkRepository(src, fork);
|
|
738
|
+
db.db().prepare('UPDATE repository SET num_forks = num_forks + 1 WHERE id = ?').run(src.id);
|
|
739
|
+
const { forkRepoAction } = await import('./db/actions.js');
|
|
740
|
+
await forkRepoAction(ar.user, fork);
|
|
741
|
+
c.JSONSuccess({ ok: true, owner: owner.name, name: fork.name });
|
|
742
|
+
}
|
|
743
|
+
catch (e) {
|
|
744
|
+
db.db().prepare('DELETE FROM repository WHERE id = ?').run(fork.id);
|
|
745
|
+
c.JSON(500, { error: 'fork failed: ' + String(e?.message ?? e).slice(0, 160) });
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
// ── 转移仓库所有权 / 清除 Wiki(危险区) ────────────────────────
|
|
749
|
+
m.post('/api/dsh/repos/:o/:r/transfer', async (c) => {
|
|
750
|
+
const ar = authRepo(c, true);
|
|
751
|
+
if (!ar)
|
|
752
|
+
return;
|
|
753
|
+
if (ar.repo.OwnerName() !== ar.user.name && ar.user.is_admin !== 1) {
|
|
754
|
+
c.JSON(403, { error: '仅仓库所有者可转移' });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const body = await c.form();
|
|
758
|
+
const orgName = String(body.org ?? '');
|
|
759
|
+
const owner = orgName ? db.getUserByUsername(orgName) : ar.user;
|
|
760
|
+
if (!owner) {
|
|
761
|
+
c.JSON(404, { error: 'org not found' });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (orgName && owner.type !== 1) {
|
|
765
|
+
c.JSON(422, { error: '目标必须是组织' });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (orgName) {
|
|
769
|
+
const mem = db.db().prepare('SELECT 1 FROM org_user WHERE org_id = ? AND uid = ? AND is_owner = 1').get(owner.id, ar.user.id);
|
|
770
|
+
if (!mem && ar.user.is_admin !== 1) {
|
|
771
|
+
c.JSON(403, { error: '需要组织管理员' });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
db.db().prepare('UPDATE repository SET owner_id = ? WHERE id = ?').run(owner.id, ar.repo.id);
|
|
776
|
+
c.JSONSuccess({ ok: true, owner: owner.name });
|
|
777
|
+
});
|
|
778
|
+
m.post('/api/dsh/repos/:o/:r/wiki-wipe', async (c) => {
|
|
779
|
+
const ar = authRepo(c, true);
|
|
780
|
+
if (!ar)
|
|
781
|
+
return;
|
|
782
|
+
const dir = ar.repo.WikiPath();
|
|
783
|
+
if (fs.existsSync(dir))
|
|
784
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
785
|
+
c.JSONSuccess({ ok: true });
|
|
786
|
+
});
|
|
787
|
+
// ── webhook CRUD ────────────────────────────────────────────────
|
|
788
|
+
m.get('/api/dsh/repos/:o/:r/hooks', async (c) => {
|
|
789
|
+
const ar = authRepo(c);
|
|
790
|
+
if (!ar)
|
|
791
|
+
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);
|
|
793
|
+
c.JSONSuccess(rows);
|
|
794
|
+
});
|
|
795
|
+
m.post('/api/dsh/repos/:o/:r/hooks', async (c) => {
|
|
796
|
+
const ar = authRepo(c, true);
|
|
797
|
+
if (!ar)
|
|
798
|
+
return;
|
|
799
|
+
const body = await c.form();
|
|
800
|
+
const url = String(body.url ?? '').trim();
|
|
801
|
+
if (!url.startsWith('http')) {
|
|
802
|
+
c.JSON(422, { error: 'url required' });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const events = JSON.stringify(body.events || { push: true });
|
|
806
|
+
const info = db.db().prepare('INSERT INTO webhook (repo_id, url, content_type, is_active, events, created_unix, updated_unix) VALUES (?,?,1,1,?,?,?)')
|
|
807
|
+
.run(ar.repo.id, url, events, Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000));
|
|
808
|
+
c.JSONSuccess({ id: Number(info.lastInsertRowid), url });
|
|
809
|
+
});
|
|
810
|
+
m.patch('/api/dsh/repos/:o/:r/hooks/:id', async (c) => {
|
|
811
|
+
const ar = authRepo(c, true);
|
|
812
|
+
if (!ar)
|
|
813
|
+
return;
|
|
814
|
+
const body = await c.form();
|
|
815
|
+
const sets = [];
|
|
816
|
+
const args = [];
|
|
817
|
+
if (body.url !== undefined) {
|
|
818
|
+
sets.push('url = ?');
|
|
819
|
+
args.push(String(body.url));
|
|
820
|
+
}
|
|
821
|
+
if (body.active !== undefined) {
|
|
822
|
+
sets.push('is_active = ?');
|
|
823
|
+
args.push(body.active ? 1 : 0);
|
|
824
|
+
}
|
|
825
|
+
if (sets.length)
|
|
826
|
+
db.db().prepare(`UPDATE webhook SET ${sets.join(', ')}, updated_unix = ? WHERE id = ? AND repo_id = ?`)
|
|
827
|
+
.run(...args, Math.floor(Date.now() / 1000), c.ParamsInt64(':id'), ar.repo.id);
|
|
828
|
+
c.JSONSuccess({ ok: true });
|
|
829
|
+
});
|
|
830
|
+
m.delete('/api/dsh/repos/:o/:r/hooks/:id', async (c) => {
|
|
831
|
+
const ar = authRepo(c, true);
|
|
832
|
+
if (!ar)
|
|
833
|
+
return;
|
|
834
|
+
db.db().prepare('DELETE FROM webhook WHERE id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
835
|
+
c.JSONSuccess({ ok: true });
|
|
836
|
+
});
|
|
837
|
+
// ── 部署密钥(settings/keys) ─────────────────────────────────
|
|
838
|
+
m.get('/api/dsh/repos/:o/:r/keys', async (c) => {
|
|
839
|
+
const ar = authRepo(c);
|
|
840
|
+
if (!ar)
|
|
841
|
+
return;
|
|
842
|
+
const rows = db.db().prepare('SELECT k.id, k.name, k.fingerprint, k.created_unix FROM deploy_key dk JOIN public_key k ON k.id = dk.key_id WHERE dk.repo_id = ? ORDER BY k.id').all(ar.repo.id);
|
|
843
|
+
c.JSONSuccess(rows);
|
|
844
|
+
});
|
|
845
|
+
m.post('/api/dsh/repos/:o/:r/keys', async (c) => {
|
|
846
|
+
const ar = authRepo(c, true);
|
|
847
|
+
if (!ar)
|
|
848
|
+
return;
|
|
849
|
+
const body = await c.form();
|
|
850
|
+
const name = String(body.name ?? '').trim();
|
|
851
|
+
const content = String(body.content ?? '').trim();
|
|
852
|
+
if (!name || !content) {
|
|
853
|
+
c.JSON(422, { error: 'name and content required' });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
let fingerprint = '';
|
|
857
|
+
try {
|
|
858
|
+
const tmp = path.join(os.tmpdir(), 'dgs-key-' + Date.now());
|
|
859
|
+
fs.writeFileSync(tmp, content);
|
|
860
|
+
fingerprint = String((await git.git(os.tmpdir(), 'ssh-keygen', '-lf', tmp))?.toString('utf8') || '').trim();
|
|
861
|
+
fs.rmSync(tmp, { force: true });
|
|
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);
|
|
878
|
+
c.JSONSuccess({ ok: true });
|
|
879
|
+
});
|
|
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) => {
|
|
889
|
+
const ar = authRepo(c, true);
|
|
890
|
+
if (!ar)
|
|
891
|
+
return;
|
|
892
|
+
const u = db.getUserByUsername(c.Params(':name'));
|
|
893
|
+
if (!u) {
|
|
894
|
+
c.JSON(404, { error: 'user not found' });
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const exists = db.db().prepare('SELECT 1 FROM collaboration WHERE user_id = ? AND repo_id = ?').get(u.id, ar.repo.id);
|
|
898
|
+
if (!exists)
|
|
899
|
+
db.db().prepare('INSERT INTO collaboration (user_id, repo_id, mode) VALUES (?,?,2)').run(u.id, ar.repo.id);
|
|
900
|
+
c.JSONSuccess({ ok: true });
|
|
901
|
+
});
|
|
902
|
+
m.delete('/api/dsh/repos/:o/:r/collaborators/:name', async (c) => {
|
|
903
|
+
const ar = authRepo(c, true);
|
|
904
|
+
if (!ar)
|
|
905
|
+
return;
|
|
906
|
+
const u = db.getUserByUsername(c.Params(':name'));
|
|
907
|
+
if (u)
|
|
908
|
+
db.db().prepare('DELETE FROM collaboration WHERE user_id = ? AND repo_id = ?').run(u.id, ar.repo.id);
|
|
909
|
+
c.JSONSuccess({ ok: true });
|
|
910
|
+
});
|
|
911
|
+
// ── 仓库设置:改名/描述/私有 + 删除 ────────────────────────────
|
|
912
|
+
m.patch('/api/dsh/repos/:o/:r', async (c) => {
|
|
913
|
+
const ar = authRepo(c);
|
|
914
|
+
if (!ar)
|
|
915
|
+
return;
|
|
916
|
+
if (ar.repo.OwnerName() !== ar.user.name && ar.user.is_admin !== 1) {
|
|
917
|
+
c.JSON(403, { error: 'forbidden' });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const body = await c.form();
|
|
921
|
+
const sets = [];
|
|
922
|
+
const args = [];
|
|
923
|
+
if (body.description !== undefined) {
|
|
924
|
+
sets.push('description = ?');
|
|
925
|
+
args.push(String(body.description));
|
|
926
|
+
}
|
|
927
|
+
if (body.private !== undefined) {
|
|
928
|
+
sets.push('is_private = ?');
|
|
929
|
+
args.push(body.private ? 1 : 0);
|
|
930
|
+
}
|
|
931
|
+
if (sets.length)
|
|
932
|
+
db.db().prepare(`UPDATE repository SET ${sets.join(', ')} WHERE id = ?`).run(...args, ar.repo.id);
|
|
933
|
+
c.JSONSuccess({ ok: true });
|
|
934
|
+
});
|
|
935
|
+
// ── 用户 profile(资料+仓库+活动) ─────────────────────────────
|
|
936
|
+
m.get('/api/dsh/users/:name', async (c) => {
|
|
937
|
+
const u = db.getUserByUsername(c.Params(':name'));
|
|
938
|
+
if (!u) {
|
|
939
|
+
c.JSON(404, { error: 'user not found' });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const repos = db.listReposByOwner(u.id).filter((r) => !r.is_private || c.IsLogged).map((r) => ({
|
|
943
|
+
name: r.name, owner: r.OwnerName(), description: r.description, stars: r.num_stars,
|
|
944
|
+
updated: r.updated_unix,
|
|
945
|
+
}));
|
|
946
|
+
const activity = db.db().prepare('SELECT a.op_type, a.ref_name, a.content, a.created_unix, r.lower_name AS repo, ow.name AS owner FROM action a JOIN repository r ON r.id = a.repo_id JOIN user ow ON ow.id = r.owner_id WHERE a.act_user_id = ? ORDER BY a.id DESC LIMIT 20').all(u.id);
|
|
947
|
+
const followers = db.db().prepare('SELECT u.name FROM follow f JOIN user u ON u.id = f.user_id WHERE f.follow_id = ?').all(u.id).map((x) => x.name);
|
|
948
|
+
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
|
+
c.JSONSuccess({
|
|
950
|
+
name: u.name, fullName: u.full_name || '', email: u.email, isAdmin: u.is_admin === 1,
|
|
951
|
+
followers, following, repos, activity: activity.map((a) => ({
|
|
952
|
+
type: a.op_type, repo: a.owner + '/' + a.repo, ref: a.ref_name, content: a.content, date: a.created_unix,
|
|
953
|
+
})),
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
// ── 组织:我的组织列表 / 组织详情 / 创建 ────────────────────────
|
|
957
|
+
m.get('/api/dsh/orgs', async (c) => {
|
|
958
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
959
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
960
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
961
|
+
if (!user) {
|
|
962
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const rows = db.db().prepare('SELECT u.id, u.name, u.full_name FROM user u JOIN org_user ou ON ou.org_id = u.id WHERE u.type = 1 AND ou.uid = ?').all(user.id);
|
|
966
|
+
c.JSONSuccess(rows.map((r) => ({ id: r.id, name: r.name, fullName: r.full_name })));
|
|
967
|
+
});
|
|
968
|
+
m.get('/api/dsh/orgs/:name', async (c) => {
|
|
969
|
+
const o = db.getUserByUsername(c.Params(':name'));
|
|
970
|
+
if (!o || o.type !== 1) {
|
|
971
|
+
c.JSON(404, { error: 'org not found' });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const members = db.db().prepare('SELECT u.name, u.full_name, ou.is_owner FROM org_user ou JOIN user u ON u.id = ou.uid WHERE ou.org_id = ?').all(o.id);
|
|
975
|
+
const repos = db.db().prepare('SELECT name, lower_name, description FROM repository WHERE owner_id = ?').all(o.id);
|
|
976
|
+
c.JSONSuccess({ name: o.name, fullName: o.full_name, description: o.description,
|
|
977
|
+
members: members.map((m) => ({ name: m.name, owner: !!m.is_owner })),
|
|
978
|
+
repos: repos.map((r) => ({ name: r.name, owner: o.name })) });
|
|
979
|
+
});
|
|
980
|
+
m.post('/api/dsh/orgs', async (c) => {
|
|
981
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
982
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
983
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
984
|
+
if (!user) {
|
|
985
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const body = await c.form();
|
|
989
|
+
const name = String(body.name ?? '').trim();
|
|
990
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
|
|
991
|
+
c.JSON(422, { error: 'bad org name' });
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
db.createOrganization(user, name, { fullName: String(body.fullName ?? ''), description: String(body.description ?? '') });
|
|
996
|
+
c.JSONSuccess({ ok: true });
|
|
997
|
+
}
|
|
998
|
+
catch (e) {
|
|
999
|
+
c.JSON(422, { error: String(e?.message ?? e).slice(0, 120) });
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
m.patch('/api/dsh/orgs/:name', async (c) => {
|
|
1003
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1004
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1005
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1006
|
+
if (!user) {
|
|
1007
|
+
c.JSON(401, { error: 'unauthorized' });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const o = db.getUserByUsername(c.Params(':name'));
|
|
1011
|
+
if (!o || o.type !== 1) {
|
|
1012
|
+
c.JSON(404, { error: 'org not found' });
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const ownerRow = db.db().prepare('SELECT is_owner FROM org_user WHERE org_id = ? AND uid = ?').get(o.id, user.id);
|
|
1016
|
+
if ((!ownerRow || !ownerRow.is_owner) && user.is_admin !== 1) {
|
|
1017
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const body = await c.form();
|
|
1021
|
+
const sets = [];
|
|
1022
|
+
const args = [];
|
|
1023
|
+
if (body.fullName !== undefined) {
|
|
1024
|
+
sets.push('full_name = ?');
|
|
1025
|
+
args.push(String(body.fullName));
|
|
1026
|
+
}
|
|
1027
|
+
if (body.description !== undefined) {
|
|
1028
|
+
sets.push('description = ?');
|
|
1029
|
+
args.push(String(body.description));
|
|
1030
|
+
}
|
|
1031
|
+
if (sets.length)
|
|
1032
|
+
db.db().prepare(`UPDATE user SET ${sets.join(', ')} WHERE id = ? AND type = 1`).run(...args, o.id);
|
|
1033
|
+
c.JSONSuccess({ ok: true });
|
|
1034
|
+
});
|
|
1035
|
+
// ── 探索:仓库/用户搜索 ───────────────────────────────────────
|
|
1036
|
+
m.get('/api/dsh/explore/repos', async (c) => {
|
|
1037
|
+
const q = '%' + c.Query('q').toLowerCase() + '%';
|
|
1038
|
+
const rows = db.db().prepare('SELECT r.name, r.lower_name, r.description, r.is_private, r.num_stars, u.name AS owner FROM repository r JOIN user u ON u.id = r.owner_id WHERE (r.lower_name LIKE ? OR lower(r.description) LIKE ?) AND r.is_private = 0 ORDER BY r.num_stars DESC LIMIT 50').all(q, q);
|
|
1039
|
+
c.JSONSuccess(rows.map((r) => ({ name: r.name, owner: r.owner, description: r.description, stars: r.num_stars })));
|
|
1040
|
+
});
|
|
1041
|
+
m.get('/api/dsh/explore/users', async (c) => {
|
|
1042
|
+
const q = '%' + c.Query('q').toLowerCase() + '%';
|
|
1043
|
+
const rows = db.db().prepare("SELECT name, full_name FROM user WHERE (type = 0 OR type = 1) AND (lower_name LIKE ? OR lower(full_name) LIKE ?) LIMIT 50").all(q, q);
|
|
1044
|
+
c.JSONSuccess(rows);
|
|
1045
|
+
});
|
|
1046
|
+
// ── 管理面:统计 + 用户/仓库列表(admin token) ────────────────
|
|
1047
|
+
m.get('/api/dsh/admin/sysinfo', async (c) => {
|
|
1048
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1049
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1050
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1051
|
+
if (!user || user.is_admin !== 1) {
|
|
1052
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const mem = process.memoryUsage();
|
|
1056
|
+
let gitVersion = '';
|
|
1057
|
+
try {
|
|
1058
|
+
gitVersion = (await git.git(process.cwd(), '--version'))?.toString('utf8').trim() || '';
|
|
1059
|
+
}
|
|
1060
|
+
catch { }
|
|
1061
|
+
c.JSONSuccess({
|
|
1062
|
+
version: '0.1.0-dsh', gitVersion: gitVersion.replace('git version ', ''),
|
|
1063
|
+
nodeVersion: process.version, uptimeSec: Math.floor(process.uptime()),
|
|
1064
|
+
mem: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
m.get('/api/dsh/admin/orgs', async (c) => {
|
|
1068
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1069
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1070
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1071
|
+
if (!user || user.is_admin !== 1) {
|
|
1072
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const rows = db.db().prepare("SELECT name, full_name, created_unix FROM user WHERE type = 1 ORDER BY id").all();
|
|
1076
|
+
c.JSONSuccess(rows.map((r) => ({ name: r.name, fullName: r.full_name || '', created: r.created_unix })));
|
|
1077
|
+
});
|
|
1078
|
+
m.post('/api/dsh/admin/ops', async (c) => {
|
|
1079
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1080
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1081
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1082
|
+
if (!user || user.is_admin !== 1) {
|
|
1083
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const body = await c.form();
|
|
1087
|
+
const op = String(body.op ?? '');
|
|
1088
|
+
const repos = db.db().prepare('SELECT id FROM repository').all();
|
|
1089
|
+
if (op === 'sync_hooks') {
|
|
1090
|
+
for (const r of repos) {
|
|
1091
|
+
try {
|
|
1092
|
+
const repo = db.getRepoByID(r.id);
|
|
1093
|
+
if (repo)
|
|
1094
|
+
svc.createDelegateHooks(repo.RepoPath());
|
|
1095
|
+
}
|
|
1096
|
+
catch { }
|
|
1097
|
+
}
|
|
1098
|
+
c.JSONSuccess({ ok: true, n: repos.length });
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (op === 'gc') {
|
|
1102
|
+
for (const r of repos) {
|
|
1103
|
+
try {
|
|
1104
|
+
const repo = db.getRepoByID(r.id);
|
|
1105
|
+
if (repo)
|
|
1106
|
+
await git.git(repo.RepoPath(), 'gc', '--quiet');
|
|
1107
|
+
}
|
|
1108
|
+
catch { }
|
|
1109
|
+
}
|
|
1110
|
+
c.JSONSuccess({ ok: true, n: repos.length });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
c.JSON(422, { error: 'unknown op: ' + op });
|
|
1114
|
+
});
|
|
1115
|
+
m.get('/api/dsh/admin/summary', async (c) => {
|
|
1116
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1117
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1118
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1119
|
+
if (!user || user.is_admin !== 1) {
|
|
1120
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const q = (t) => db.db().prepare(`SELECT COUNT(*) AS c FROM ${t}`).get().c;
|
|
1124
|
+
c.JSONSuccess({
|
|
1125
|
+
users: q('user'), orgs: db.db().prepare('SELECT COUNT(*) AS c FROM user WHERE type = 1').get().c,
|
|
1126
|
+
repos: q('repository'), issues: q('issue'), pulls: db.db().prepare('SELECT COUNT(*) AS c FROM issue WHERE is_pull = 1').get().c,
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
m.get('/api/dsh/admin/users', async (c) => {
|
|
1130
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1131
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1132
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1133
|
+
if (!user || user.is_admin !== 1) {
|
|
1134
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const rows = db.db().prepare('SELECT name, full_name, email, is_admin, created_unix FROM user ORDER BY id').all();
|
|
1138
|
+
c.JSONSuccess(rows.map((r) => ({ name: r.name, fullName: r.full_name, email: r.email, isAdmin: r.is_admin === 1, created: r.created_unix })));
|
|
1139
|
+
});
|
|
1140
|
+
m.get('/api/dsh/admin/repos', async (c) => {
|
|
1141
|
+
const header = String(c.req.headers.authorization ?? '');
|
|
1142
|
+
const token = /^token (.+)$/.exec(header)?.[1] ?? '';
|
|
1143
|
+
const user = token ? authenticateUserByToken(token) : null;
|
|
1144
|
+
if (!user || user.is_admin !== 1) {
|
|
1145
|
+
c.JSON(403, { error: 'forbidden' });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const rows = db.db().prepare('SELECT r.name, r.lower_name, r.is_private, r.num_stars, u.name AS owner FROM repository r JOIN user u ON u.id = r.owner_id ORDER BY r.id').all();
|
|
1149
|
+
c.JSONSuccess(rows.map((r) => ({ name: r.owner + '/' + r.name, owner: r.owner, private: !!r.is_private, stars: r.num_stars })));
|
|
1150
|
+
});
|
|
1151
|
+
// ── PR 合并 ───────────────────────────────────────────────────
|
|
1152
|
+
m.post('/api/dsh/repos/:o/:r/pulls/:index/merge', async (c) => {
|
|
1153
|
+
const ar = authRepo(c, true);
|
|
1154
|
+
if (!ar)
|
|
1155
|
+
return;
|
|
1156
|
+
const { repo, user } = ar;
|
|
1157
|
+
const index = c.ParamsInt64(':index');
|
|
1158
|
+
const issue = db.getIssueByIndex(repo.id, index);
|
|
1159
|
+
const pr = issue ? db.db().prepare('SELECT * FROM pull_request WHERE issue_id = ?').get(issue.id) : null;
|
|
1160
|
+
if (!issue || !pr || pr.has_merged || issue.is_closed) {
|
|
1161
|
+
c.JSON(404, { error: 'pull not found or already closed' });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const baseDir = repo.RepoPath();
|
|
1165
|
+
const headRepo = db.getRepoByID(pr.head_repo_id) ?? repo;
|
|
1166
|
+
const tmpDir = path.join(conf.appDataPath, 'tmp', 'merge-' + Date.now());
|
|
1167
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1168
|
+
try {
|
|
1169
|
+
await git.git(process.cwd(), 'clone', '-b', pr.base_branch, baseDir, tmpDir);
|
|
1170
|
+
await git.git(tmpDir, 'remote', 'add', 'head_repo', headRepo.RepoPath());
|
|
1171
|
+
await git.git(tmpDir, 'fetch', 'head_repo');
|
|
1172
|
+
const style = (await c.form()).style || 'merge';
|
|
1173
|
+
if (style === 'rebase') {
|
|
1174
|
+
await git.git(tmpDir, 'rebase', '--end-of-options', `head_repo/${pr.head_branch}`);
|
|
1175
|
+
}
|
|
1176
|
+
else if (style === 'squash') {
|
|
1177
|
+
await git.git(tmpDir, 'merge', '--squash', '--end-of-options', `head_repo/${pr.head_branch}`);
|
|
1178
|
+
const msg = `${pr.title} (#${issue.index})\n\n${issue.content || ''}`;
|
|
1179
|
+
await git.git(tmpDir, 'commit', `--author=${user.name} <${user.email}>`, '-m', msg);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
await git.git(tmpDir, 'merge', '--no-ff', '--no-commit', '--end-of-options', `head_repo/${pr.head_branch}`);
|
|
1183
|
+
const msg = `Merge branch '${pr.head_branch}' of ${pr.head_user_name}/${headRepo.name} into ${pr.base_branch}`;
|
|
1184
|
+
await git.git(tmpDir, 'commit', `--author=${user.name} <${user.email}>`, '-m', msg);
|
|
1185
|
+
}
|
|
1186
|
+
await git.git(tmpDir, 'push', 'origin', `HEAD:refs/heads/${pr.base_branch}`);
|
|
1187
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1188
|
+
db.db().prepare('UPDATE pull_request SET has_merged = 1, merger_id = ?, merged_unix = ? WHERE id = ?').run(user.id, now, pr.id);
|
|
1189
|
+
db.updateIssueColumns(issue.id, { is_closed: 1 });
|
|
1190
|
+
db.refreshIssueCounts(repo.id);
|
|
1191
|
+
const { issueAction, ActionType } = await import('./db/actions.js');
|
|
1192
|
+
await issueAction(ActionType.MERGE_PULL_REQUEST, user, repo, issue);
|
|
1193
|
+
c.JSONSuccess({ ok: true });
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
console.error('[dsh merge]', e);
|
|
1197
|
+
c.JSON(500, { error: 'merge failed: ' + String(e?.message ?? e).slice(0, 200) });
|
|
1198
|
+
}
|
|
1199
|
+
finally {
|
|
1200
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
// ── PR 不合并关闭 ─────────────────────────────────────────────
|
|
1204
|
+
m.post('/api/dsh/repos/:o/:r/pulls/:index/close', async (c) => {
|
|
1205
|
+
const ar = authRepo(c, true);
|
|
1206
|
+
if (!ar)
|
|
1207
|
+
return;
|
|
1208
|
+
const issue = db.getIssueByIndex(ar.repo.id, c.ParamsInt64(':index'));
|
|
1209
|
+
if (!issue || !issue.is_pull) {
|
|
1210
|
+
c.JSON(404, { error: 'pull not found' });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
db.updateIssueColumns(issue.id, { is_closed: 1 });
|
|
1214
|
+
db.refreshIssueCounts(ar.repo.id);
|
|
1215
|
+
c.JSONSuccess({ ok: true });
|
|
1216
|
+
});
|
|
1217
|
+
// ── wiki:列表 / 读取 / 保存 ──────────────────────────────────
|
|
1218
|
+
const wikiDirOf = (repo) => repo.WikiPath();
|
|
1219
|
+
m.get('/api/dsh/repos/:o/:r/wiki', async (c) => {
|
|
1220
|
+
const ar = authRepo(c);
|
|
1221
|
+
if (!ar)
|
|
1222
|
+
return;
|
|
1223
|
+
const dir = wikiDirOf(ar.repo);
|
|
1224
|
+
if (!fs.existsSync(dir)) {
|
|
1225
|
+
c.JSONSuccess({ pages: [], initialized: false });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const svc = await import('./gitx/service.js');
|
|
1229
|
+
const branch = svc.wikiBranch(dir);
|
|
1230
|
+
const ref = 'refs/heads/' + branch;
|
|
1231
|
+
if (!(await git.refExists(dir, ref))) {
|
|
1232
|
+
c.JSONSuccess({ pages: [], initialized: true });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const commit = await git.getCommit(dir, ref);
|
|
1236
|
+
if (!commit) {
|
|
1237
|
+
c.JSONSuccess({ pages: [], initialized: true });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const tree = await git.lsTree(dir, commit.id, '');
|
|
1241
|
+
const pages = [];
|
|
1242
|
+
for (const e of tree?.entries ?? []) {
|
|
1243
|
+
if (e.type !== 'blob' || !e.name.endsWith('.md'))
|
|
1244
|
+
continue;
|
|
1245
|
+
const last = await git.commitByPath(dir, ref, e.name);
|
|
1246
|
+
pages.push({ name: e.name.slice(0, -3), updatedAt: last?.committer?.when ?? null, author: last?.committer?.name ?? '' });
|
|
1247
|
+
}
|
|
1248
|
+
c.JSONSuccess({ pages, initialized: true });
|
|
1249
|
+
});
|
|
1250
|
+
m.get('/api/dsh/repos/:o/:r/wiki/:page', async (c) => {
|
|
1251
|
+
const ar = authRepo(c);
|
|
1252
|
+
if (!ar)
|
|
1253
|
+
return;
|
|
1254
|
+
const svc = await import('./gitx/service.js');
|
|
1255
|
+
const dir = wikiDirOf(ar.repo);
|
|
1256
|
+
const name = svc.toWikiPageName(decodeURIComponent(c.Params(':page')));
|
|
1257
|
+
const branch = svc.wikiBranch(dir);
|
|
1258
|
+
const ref = 'refs/heads/' + branch;
|
|
1259
|
+
let content = '';
|
|
1260
|
+
try {
|
|
1261
|
+
const commit = await git.getCommit(dir, ref);
|
|
1262
|
+
if (commit) {
|
|
1263
|
+
const tree = await git.lsTree(dir, commit.id, name + '.md');
|
|
1264
|
+
const entry = tree?.entries[0];
|
|
1265
|
+
if (entry)
|
|
1266
|
+
content = (await git.blobBytes(dir, entry.sha)).toString('utf8');
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
catch { /* empty */ }
|
|
1270
|
+
if (!content) {
|
|
1271
|
+
c.JSON(404, { error: 'page not found' });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const html = sanitizeHTML(markdown(content, conf.subpath + '/', ar.repo.ComposeMetas()));
|
|
1275
|
+
c.JSONSuccess({ name, content, html });
|
|
1276
|
+
});
|
|
1277
|
+
m.get('/api/dsh/repos/:o/:r/wiki/:page/commits', async (c) => {
|
|
1278
|
+
const ar = authRepo(c);
|
|
1279
|
+
if (!ar)
|
|
1280
|
+
return;
|
|
1281
|
+
const dir = wikiDirOf(ar.repo);
|
|
1282
|
+
if (!fs.existsSync(dir)) {
|
|
1283
|
+
c.JSONSuccess([]);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const pageName = c.Params(':page');
|
|
1287
|
+
const file = pageName.endsWith('.md') ? pageName : pageName + '.md';
|
|
1288
|
+
const out = (await git.git(dir, 'log', '--pretty=format:%H%x1f%s%x1f%an%x1f%ai', '--', file))?.toString('utf8') ?? '';
|
|
1289
|
+
const rows = out.split('\n').filter(Boolean).map((l) => { const [sha, message, author, date] = l.split('\x1f'); return { sha, message, author, date }; });
|
|
1290
|
+
c.JSONSuccess(rows);
|
|
1291
|
+
});
|
|
1292
|
+
m.delete('/api/dsh/repos/:o/:r/wiki/:page', async (c) => {
|
|
1293
|
+
const ar = authRepo(c, true);
|
|
1294
|
+
if (!ar)
|
|
1295
|
+
return;
|
|
1296
|
+
const dir = wikiDirOf(ar.repo);
|
|
1297
|
+
if (!fs.existsSync(dir)) {
|
|
1298
|
+
c.JSON(404, { error: 'no wiki' });
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const pageName = c.Params(':page');
|
|
1302
|
+
const file = pageName.endsWith('.md') ? pageName : pageName + '.md';
|
|
1303
|
+
try {
|
|
1304
|
+
await git.git(dir, 'rm', '-q', '--', file);
|
|
1305
|
+
await git.git(dir, 'commit', '-q', '-m', `Delete ${pageName}`);
|
|
1306
|
+
c.JSONSuccess({ ok: true });
|
|
1307
|
+
}
|
|
1308
|
+
catch (e) {
|
|
1309
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 120) });
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
m.post('/api/dsh/repos/:o/:r/wiki/:page', async (c) => {
|
|
1313
|
+
const ar = authRepo(c, true);
|
|
1314
|
+
if (!ar)
|
|
1315
|
+
return;
|
|
1316
|
+
const svc = await import('./gitx/service.js');
|
|
1317
|
+
const name = svc.toWikiPageName(decodeURIComponent(c.Params(':page')));
|
|
1318
|
+
const body = await c.form();
|
|
1319
|
+
// 复用 repox.ts 的写页实现(clone→写文件→commit→push→delegate hook)
|
|
1320
|
+
try {
|
|
1321
|
+
const __repo = await import('./repox.js');
|
|
1322
|
+
await __repo.writeWikiPage(ar.repo, name, String(body.content ?? ''), ar.user, null);
|
|
1323
|
+
c.JSONSuccess({ ok: true });
|
|
1324
|
+
}
|
|
1325
|
+
catch (e) {
|
|
1326
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
// ── 发版:列表 / 创建 ────────────────────────────────────────
|
|
1330
|
+
m.get('/api/dsh/repos/:o/:r/releases', async (c) => {
|
|
1331
|
+
const ar = authRepo(c);
|
|
1332
|
+
if (!ar)
|
|
1333
|
+
return;
|
|
1334
|
+
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
|
+
const dir = ar.repo.RepoPath();
|
|
1336
|
+
const out = [];
|
|
1337
|
+
for (const r of rows) {
|
|
1338
|
+
let behind = 0;
|
|
1339
|
+
try {
|
|
1340
|
+
behind = Number((await git.git(dir, 'rev-list', '--count', '--end-of-options', `${r.tag_name}..${r.target || ar.repo.default_branch || conf.defaultBranch}`))?.toString('utf8').trim() || 0);
|
|
1341
|
+
}
|
|
1342
|
+
catch {
|
|
1343
|
+
behind = 0;
|
|
1344
|
+
}
|
|
1345
|
+
out.push({
|
|
1346
|
+
id: r.id, tag: r.tag_name, title: r.title, draft: !!r.is_draft, prerelease: !!r.is_prerelease,
|
|
1347
|
+
noteHtml: r.note ? sanitizeHTML(markdown(r.note, conf.subpath + '/', {})) : '',
|
|
1348
|
+
noteRaw: r.note || '',
|
|
1349
|
+
author: r.author, createdAt: r.created_unix, target: r.target || '', behind,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
c.JSONSuccess(out);
|
|
1353
|
+
});
|
|
1354
|
+
m.patch('/api/dsh/repos/:o/:r/releases/:id', async (c) => {
|
|
1355
|
+
const ar = authRepo(c, true);
|
|
1356
|
+
if (!ar)
|
|
1357
|
+
return;
|
|
1358
|
+
const body = await c.form();
|
|
1359
|
+
const sets = [];
|
|
1360
|
+
const args = [];
|
|
1361
|
+
if (body.title !== undefined) {
|
|
1362
|
+
sets.push('title = ?');
|
|
1363
|
+
args.push(String(body.title));
|
|
1364
|
+
}
|
|
1365
|
+
if (body.note !== undefined) {
|
|
1366
|
+
sets.push('note = ?');
|
|
1367
|
+
args.push(String(body.note));
|
|
1368
|
+
}
|
|
1369
|
+
if (sets.length)
|
|
1370
|
+
db.db().prepare(`UPDATE release SET ${sets.join(', ')} WHERE id = ? AND repo_id = ?`).run(...args, c.ParamsInt64(':id'), ar.repo.id);
|
|
1371
|
+
c.JSONSuccess({ ok: true });
|
|
1372
|
+
});
|
|
1373
|
+
m.delete('/api/dsh/repos/:o/:r/releases/:id', async (c) => {
|
|
1374
|
+
const ar = authRepo(c, true);
|
|
1375
|
+
if (!ar)
|
|
1376
|
+
return;
|
|
1377
|
+
db.db().prepare('DELETE FROM release WHERE id = ? AND repo_id = ?').run(c.ParamsInt64(':id'), ar.repo.id);
|
|
1378
|
+
c.JSONSuccess({ ok: true });
|
|
1379
|
+
});
|
|
1380
|
+
m.post('/api/dsh/repos/:o/:r/releases', async (c) => {
|
|
1381
|
+
const ar = authRepo(c, true);
|
|
1382
|
+
if (!ar)
|
|
1383
|
+
return;
|
|
1384
|
+
const { repo, user } = ar;
|
|
1385
|
+
const body = await c.form();
|
|
1386
|
+
const tag = String(body.tag ?? '').trim();
|
|
1387
|
+
const title = String(body.title ?? '').trim() || tag;
|
|
1388
|
+
const note = String(body.note ?? '');
|
|
1389
|
+
const target = String(body.target ?? '') || repo.default_branch || conf.defaultBranch;
|
|
1390
|
+
if (!tag) {
|
|
1391
|
+
c.JSON(422, { error: 'tag required' });
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (db.db().prepare('SELECT 1 FROM `release` WHERE repo_id = ? AND lower_tag_name = ?').get(repo.id, tag.toLowerCase())) {
|
|
1395
|
+
c.JSON(422, { error: 'tag already released' });
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const repoDir = repo.RepoPath();
|
|
1399
|
+
try {
|
|
1400
|
+
const sha = (await git.gitOK(repoDir, 'rev-parse', '--verify', '--end-of-options', target))?.toString().trim() ?? '';
|
|
1401
|
+
if (!sha) {
|
|
1402
|
+
c.JSON(422, { error: 'target not found' });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
await git.gitOK(repoDir, 'tag', tag, sha);
|
|
1406
|
+
const numCommits = await git.commitsCount(repoDir, sha);
|
|
1407
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1408
|
+
db.db().prepare('INSERT INTO `release` (repo_id, publisher_id, tag_name, lower_tag_name, target, title, sha1, num_commits, note, is_draft, is_prerelease, created_unix) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)').run(repo.id, user.id, tag, tag.toLowerCase(), target, title, sha, numCommits, note, body.draft ? 1 : 0, body.prerelease ? 1 : 0, now);
|
|
1409
|
+
c.JSONSuccess({ ok: true });
|
|
1410
|
+
}
|
|
1411
|
+
catch (e) {
|
|
1412
|
+
c.JSON(500, { error: String(e?.message ?? e).slice(0, 200) });
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
//# sourceMappingURL=dshapi.js.map
|