@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.
@@ -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
- const args = ['ls-tree', '-z', '--end-of-options', safeRev(rev)];
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\}\}/g, 'README.md')
111
- .replace(/\{\{Description\}\}/g, repo.description ?? '')
112
- .replace(/\{\{Name\}\}/g, repo.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);
@@ -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 === 'pre-receive' || name === 'update') {
6
- // branch protection enforcement goes here; accept by default
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
- const handled = await dispatch(cfg, actor, req.method, rest, req, res, token)
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) })