@weibaohui/dsh-git-server 0.1.4 → 0.2.1
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 +70 -2
- package/client/bundle.js +597 -69
- package/client/index.js +597 -69
- 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/gitx/git.js
CHANGED
|
@@ -273,7 +273,9 @@ export async function lsTree(repoDir, treeRev, treePath) {
|
|
|
273
273
|
return { entries: entries.entries, sha: rev };
|
|
274
274
|
}
|
|
275
275
|
async function lsTreeOnce(repoDir, rev, subPath) {
|
|
276
|
-
|
|
276
|
+
// -l: blob sizes come inline ("<mode> <type> <sha> <size>\t<name>"), saving a
|
|
277
|
+
// cat-file subprocess per file at every call site
|
|
278
|
+
const args = ['ls-tree', '-l', '-z', '--end-of-options', safeRev(rev)];
|
|
277
279
|
if (subPath)
|
|
278
280
|
args.push('--', subPath);
|
|
279
281
|
const out = await gitOK(repoDir, ...args);
|
|
@@ -282,7 +284,6 @@ async function lsTreeOnce(repoDir, rev, subPath) {
|
|
|
282
284
|
const entries = [];
|
|
283
285
|
const records = out.toString('utf8').split('\0').filter(Boolean);
|
|
284
286
|
for (const rec of records) {
|
|
285
|
-
// "<mode> <type> <sha>\t<name>"
|
|
286
287
|
const tab = rec.indexOf('\t');
|
|
287
288
|
if (tab < 0)
|
|
288
289
|
continue;
|
|
@@ -293,7 +294,7 @@ async function lsTreeOnce(repoDir, rev, subPath) {
|
|
|
293
294
|
type: meta[1],
|
|
294
295
|
sha: meta[2],
|
|
295
296
|
name,
|
|
296
|
-
size: 0,
|
|
297
|
+
size: meta[3] && meta[3] !== '-' ? Number(meta[3]) || 0 : 0,
|
|
297
298
|
});
|
|
298
299
|
}
|
|
299
300
|
return { entries, sha: rev };
|
|
@@ -302,6 +303,41 @@ export async function entrySize(repoDir, sha) {
|
|
|
302
303
|
const out = await gitOK(repoDir, 'cat-file', '-s', sha);
|
|
303
304
|
return out ? Number(out.toString().trim()) : 0;
|
|
304
305
|
}
|
|
306
|
+
/** Last commit touching each path, in ONE `git log` walk (first hit per path
|
|
307
|
+
* wins; -m --first-parent linearizes merges so a merge commit counts as the
|
|
308
|
+
* last change). Paths not resolved within the walk are simply absent — callers
|
|
309
|
+
* may fall back to a per-path `git log -1` for those (rare). */
|
|
310
|
+
export async function lastCommitsForPaths(repoDir, rev, paths, maxCount = 4000) {
|
|
311
|
+
const result = new Map();
|
|
312
|
+
if (paths.length === 0)
|
|
313
|
+
return result;
|
|
314
|
+
const out = await gitOK(repoDir, '-c', 'core.quotePath=false', 'log', '--first-parent', '-m', `--max-count=${maxCount}`, '--pretty=tformat:%x01%h%x1f%s%x1f%ct%x1f%an', '--name-only', '--no-renames', '--end-of-options', safeRev(rev), '--', ...paths);
|
|
315
|
+
if (!out)
|
|
316
|
+
return result;
|
|
317
|
+
let cur = null;
|
|
318
|
+
const pending = new Set(paths);
|
|
319
|
+
for (const line of out.toString('utf8').split('\n')) {
|
|
320
|
+
if (line.startsWith('\x01')) {
|
|
321
|
+
const [sha, msg, ct, author] = line.slice(1).split('\x1f');
|
|
322
|
+
cur = { sha: sha || '', msg: (msg || '').slice(0, 90), date: Number(ct) * 1000 || 0, author: author || '' };
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const name = line.trim();
|
|
326
|
+
if (!name || !cur)
|
|
327
|
+
continue;
|
|
328
|
+
// a name-only record may be the path itself or (for dir pathspecs) a file under it
|
|
329
|
+
for (const p of pending) {
|
|
330
|
+
if (name === p || name.startsWith(p + '/')) {
|
|
331
|
+
if (!result.has(p))
|
|
332
|
+
result.set(p, cur);
|
|
333
|
+
pending.delete(p);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (pending.size === 0)
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
305
341
|
export async function blobBytes(repoDir, sha, maxBytes = 0) {
|
|
306
342
|
if (maxBytes > 0) {
|
|
307
343
|
// read at most maxBytes by streaming through cat-file with size check
|
|
@@ -106,10 +106,11 @@ function renderReadmeTemplate(name, repo) {
|
|
|
106
106
|
const tpl = readVendorTemplate('readme', name);
|
|
107
107
|
if (!tpl)
|
|
108
108
|
return `# ${repo.name}\n\n${repo.description ?? ''}\n`;
|
|
109
|
+
// vendored 模板用单花括号({Name}),上游 gogs 用双花括号({{Name}})——两种都替换
|
|
109
110
|
return tpl
|
|
110
|
-
.replace(/\{\{Filename\}
|
|
111
|
-
.replace(/\{\{Description\}
|
|
112
|
-
.replace(/\{\{Name\}
|
|
111
|
+
.replace(/\{\{?\s*Filename\s*\}?\}/g, 'README.md')
|
|
112
|
+
.replace(/\{\{?\s*Description\s*\}?\}/g, repo.description ?? '')
|
|
113
|
+
.replace(/\{\{?\s*Name\s*\}?\}/g, repo.name);
|
|
113
114
|
}
|
|
114
115
|
export function readVendorTemplate(kind, name) {
|
|
115
116
|
const file = path.join(conf.workDir, 'vendored-conf', kind, name);
|
package/server/dist/hook.js
CHANGED
|
@@ -2,8 +2,37 @@
|
|
|
2
2
|
// Environment carries the auth/repo context (ComposeHookEnvs).
|
|
3
3
|
import * as db from './db/db.js';
|
|
4
4
|
export async function runHook(name) {
|
|
5
|
-
if (name === '
|
|
6
|
-
//
|
|
5
|
+
if (name === 'update') {
|
|
6
|
+
// 分支保护:受保护分支拒绝删除与强推(非快进)。merge/网页编辑都是快进,不受影响。
|
|
7
|
+
const [ref, oldSha, newSha] = process.argv.slice(4);
|
|
8
|
+
if (ref?.startsWith('refs/heads/') && newSha) {
|
|
9
|
+
const branch = ref.slice('refs/heads/'.length);
|
|
10
|
+
const ownerName = process.env.GOGS_REPO_OWNER_NAME ?? '';
|
|
11
|
+
const repoName = process.env.GOGS_REPO_NAME ?? '';
|
|
12
|
+
const owner = db.getUserByUsername(ownerName);
|
|
13
|
+
const repo = owner ? db.getRepoByOwnerAndName(owner, repoName) : null;
|
|
14
|
+
if (repo) {
|
|
15
|
+
const prot = db.db().prepare('SELECT 1 FROM protect_branch WHERE repo_id = ? AND name = ? AND protected = 1').get(repo.id, branch);
|
|
16
|
+
if (prot) {
|
|
17
|
+
const EMPTY = '0000000000000000000000000000000000000000';
|
|
18
|
+
if (newSha === EMPTY) {
|
|
19
|
+
console.error(`[hook] rejected: branch ${branch} is protected (deletion)`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (oldSha && oldSha !== EMPTY && /^[0-9a-f]{40}$/.test(oldSha) && /^[0-9a-f]{40}$/.test(newSha)) {
|
|
23
|
+
const { gitOK } = await import('./gitx/git.js');
|
|
24
|
+
const ok = await gitOK(repo.RepoPath(), 'merge-base', '--is-ancestor', oldSha, newSha);
|
|
25
|
+
if (ok === null) {
|
|
26
|
+
console.error(`[hook] rejected: branch ${branch} is protected (non-fast-forward)`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
if (name === 'pre-receive') {
|
|
7
36
|
process.exit(0);
|
|
8
37
|
}
|
|
9
38
|
if (name !== 'post-receive') {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 站内通知:基于 issue_user 表(is_read/is_assigned/is_mentioned/is_poster)的
|
|
2
|
+
// 轻量实现——评论/指派/提及/状态变更时把相关人的行标记为未读,面板轮询未读数。
|
|
3
|
+
import * as db from './db/db.js';
|
|
4
|
+
function ensureRow(uid, issue) {
|
|
5
|
+
const row = db.db().prepare('SELECT 1 FROM issue_user WHERE uid = ? AND issue_id = ?').get(uid, issue.id);
|
|
6
|
+
if (row)
|
|
7
|
+
return;
|
|
8
|
+
db.db()
|
|
9
|
+
.prepare('INSERT INTO issue_user (uid, issue_id, repo_id, is_read, is_assigned, is_mentioned, is_poster, is_closed) VALUES (?,?,?,1,0,0,0,?)')
|
|
10
|
+
.run(uid, issue.id, issue.repo_id, issue.is_closed ?? 0);
|
|
11
|
+
}
|
|
12
|
+
/** 文本里 @到的已存在用户。 */
|
|
13
|
+
export function mentionedUserIDs(content) {
|
|
14
|
+
const out = new Set();
|
|
15
|
+
for (const m of content.matchAll(/@([a-zA-Z0-9_.-]+)/g)) {
|
|
16
|
+
const u = db.getUserByUsername(m[1]);
|
|
17
|
+
if (u)
|
|
18
|
+
out.add(u.id);
|
|
19
|
+
}
|
|
20
|
+
return [...out];
|
|
21
|
+
}
|
|
22
|
+
/** 把工单相关人(作者/负责人/既往评论者/@提及)标记为未读;actor 本人除外。 */
|
|
23
|
+
export function markIssueUnread(issue, actorID, opts = {}) {
|
|
24
|
+
const uids = new Set();
|
|
25
|
+
if (issue.poster_id)
|
|
26
|
+
uids.add(issue.poster_id);
|
|
27
|
+
if (issue.assignee_id)
|
|
28
|
+
uids.add(issue.assignee_id);
|
|
29
|
+
const commenters = db.db().prepare('SELECT DISTINCT poster_id FROM comment WHERE issue_id = ? AND type = 0').all(issue.id);
|
|
30
|
+
for (const cm of commenters)
|
|
31
|
+
if (cm.poster_id)
|
|
32
|
+
uids.add(cm.poster_id);
|
|
33
|
+
const mentioned = new Set(opts.mentionContent ? mentionedUserIDs(opts.mentionContent) : []);
|
|
34
|
+
for (const uid of mentioned)
|
|
35
|
+
uids.add(uid);
|
|
36
|
+
for (const uid of uids) {
|
|
37
|
+
if (!uid || uid === actorID)
|
|
38
|
+
continue;
|
|
39
|
+
ensureRow(uid, issue);
|
|
40
|
+
db.db()
|
|
41
|
+
.prepare(`UPDATE issue_user SET is_read = 0, is_closed = ?,
|
|
42
|
+
is_mentioned = MAX(COALESCE(is_mentioned, 0), ?), is_assigned = MAX(COALESCE(is_assigned, 0), ?),
|
|
43
|
+
milestone_id = COALESCE(?, milestone_id)
|
|
44
|
+
WHERE uid = ? AND issue_id = ?`)
|
|
45
|
+
.run(issue.is_closed ?? 0, mentioned.has(uid) ? 1 : 0, opts.assigned && issue.assignee_id === uid ? 1 : 0, issue.milestone_id ?? null, uid, issue.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** 查看工单详情后标记已读。 */
|
|
49
|
+
export function markIssueRead(uid, issueID) {
|
|
50
|
+
db.db().prepare('UPDATE issue_user SET is_read = 1 WHERE uid = ? AND issue_id = ?').run(uid, issueID);
|
|
51
|
+
}
|
|
52
|
+
export function unreadCount(uid) {
|
|
53
|
+
return db.db().prepare('SELECT COUNT(*) AS c FROM issue_user WHERE uid = ? AND is_read = 0').get(uid).c;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=notify.js.map
|
package/src/index.js
CHANGED
|
@@ -577,6 +577,7 @@ module.exports = {
|
|
|
577
577
|
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/issues/${idx}/comments`)
|
|
578
578
|
sendJson(res, r.status, { ok: Array.isArray(r.json), comments: (Array.isArray(r.json) ? r.json : []).map((c) => ({
|
|
579
579
|
id: c.id, body: c.body, user: c.user && c.user.login, created: c.created_at,
|
|
580
|
+
type: c.type || 0, commit_sha: c.commit_sha || '',
|
|
580
581
|
})) })
|
|
581
582
|
return
|
|
582
583
|
}
|
|
@@ -603,13 +604,23 @@ module.exports = {
|
|
|
603
604
|
sendJson(res, r.status === 201 ? 200 : r.status, r.status === 201 ? { ok: true } : { ok: false, error: errText(r) })
|
|
604
605
|
return
|
|
605
606
|
}
|
|
607
|
+
// PATCH/DELETE /repos/:o/:r/issues/comments/:id — 评论编辑/删除
|
|
608
|
+
if ((req.method === 'PATCH' || req.method === 'DELETE') && parts[0] === 'repos' && parts[3] === 'issues' && parts[4] === 'comments' && parts[5]) {
|
|
609
|
+
const [, owner, repo, , , cid] = parts
|
|
610
|
+
const body = req.method === 'PATCH' ? JSON.parse((await readBody(req)) || '{}') : undefined
|
|
611
|
+
const r = await kernelApi(cfg, actor, req.method, `/repos/${owner}/${repo}/issues/comments/${cid}`, body)
|
|
612
|
+
sendJson(res, [200, 201, 204].includes(r.status) ? 200 : r.status,
|
|
613
|
+
[200, 201, 204].includes(r.status) ? { ok: true } : { ok: false, error: errText(r) })
|
|
614
|
+
return
|
|
615
|
+
}
|
|
606
616
|
// /dsh/*:宿主进程内直接执行内核 dshapi(省铸令牌+HTTP 跳转)。
|
|
607
617
|
// 异常/未匹配路由回退到原来的 HTTP 转发,保证任何内核端点仍可到达。
|
|
608
618
|
if (parts && parts[0] === 'dsh') {
|
|
609
619
|
try {
|
|
610
620
|
const token = await kernelTokenFor(cfg, actor)
|
|
611
621
|
const { dispatch } = require('./dsh-host')
|
|
612
|
-
|
|
622
|
+
// rest 只是路径部分;进程内 dispatch 需要连同 query 一起(blame/tree 等带参端点)
|
|
623
|
+
const handled = await dispatch(cfg, actor, req.method, rest + (url.search || ''), req, res, token)
|
|
613
624
|
if (handled) return
|
|
614
625
|
} catch (e) {
|
|
615
626
|
sendJson(res, 502, { ok: false, error: 'in-process dispatch failed: ' + String((e && e.message) || e) })
|