@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,317 @@
|
|
|
1
|
+
// Markup pipeline mirroring gogs internal/markup: markdown render → post
|
|
2
|
+
// process (mentions, #123, sha links, relative URLs) → HTML sanitize
|
|
3
|
+
// (bluemonday UGC-style whitelist).
|
|
4
|
+
import { Marked } from 'marked';
|
|
5
|
+
import { conf } from './conf.js';
|
|
6
|
+
// 原 gotemplate/funcs.js 的 shortSHA1(3 行,内联以解除对模板 funcs 的依赖)
|
|
7
|
+
function shortSHA1(sha) {
|
|
8
|
+
return String(sha).length > 10 ? String(sha).slice(0, 10) : String(sha);
|
|
9
|
+
}
|
|
10
|
+
const marked = new Marked({ gfm: true, breaks: false });
|
|
11
|
+
export function isMarkdownFile(name) {
|
|
12
|
+
const lower = String(name).toLowerCase();
|
|
13
|
+
return conf.markdownFileExtensions.some((ext) => lower.endsWith(ext.toLowerCase()));
|
|
14
|
+
}
|
|
15
|
+
export function isReadmeFile(name) {
|
|
16
|
+
return String(name).toLowerCase().startsWith('readme');
|
|
17
|
+
}
|
|
18
|
+
export function isIPythonNotebook(name) {
|
|
19
|
+
return String(name).toLowerCase().endsWith('.ipynb');
|
|
20
|
+
}
|
|
21
|
+
function cutoutVerbosePrefix(urlPrefix) {
|
|
22
|
+
// gogs cutoutVerbosePrefix: keep 3+subpathDepth leading slashes worth of path
|
|
23
|
+
const count = 3 + conf.subpathDepth;
|
|
24
|
+
const parts = urlPrefix.split('/');
|
|
25
|
+
if (parts.length > count) {
|
|
26
|
+
return parts.slice(0, count).join('/');
|
|
27
|
+
}
|
|
28
|
+
return urlPrefix;
|
|
29
|
+
}
|
|
30
|
+
export function renderIssueIndexPattern(text, urlPrefix, metas) {
|
|
31
|
+
if (!metas || !metas['repoLink'])
|
|
32
|
+
return text;
|
|
33
|
+
const prefix = cutoutVerbosePrefix(urlPrefix);
|
|
34
|
+
if (metas['style'] === 'alphanumeric') {
|
|
35
|
+
return text.replace(/(\s|^|\W)([A-Z]{1,10}-\d+)/g, (m, pre, ref) => {
|
|
36
|
+
if (metas['format']) {
|
|
37
|
+
return `${pre}<a href="${metas['format'].replace('{user}', metas['user'] ?? '').replace('{repo}', metas['repo'] ?? '').replace('{index}', ref)}">${ref}</a>`;
|
|
38
|
+
}
|
|
39
|
+
return `${pre}<a href="${prefix}/issues/${ref}">${ref}</a>`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return text.replace(/(\s|^|\W)#(\d+)\b/g, (m, pre, num) => {
|
|
43
|
+
if (metas['format']) {
|
|
44
|
+
return `${pre}<a href="${metas['format'].replace('{user}', metas['user'] ?? '').replace('{repo}', metas['repo'] ?? '').replace('{index}', num)}">#${num}</a>`;
|
|
45
|
+
}
|
|
46
|
+
return `${pre}<a href="${prefix}/issues/${num}">#${num}</a>`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function renderSpecialLink(text, urlPrefix, metas) {
|
|
50
|
+
let out = text;
|
|
51
|
+
// @mention (only when viewing a repo context or site-wide: gogs uses subpath links)
|
|
52
|
+
out = out.replace(/(\s|^|\W)@([0-9a-zA-Z-_.]+)/g, (m, pre, user) => {
|
|
53
|
+
return `${pre}<a href="${conf.subpath}/${user}">@${user}</a>`;
|
|
54
|
+
});
|
|
55
|
+
out = renderIssueIndexPattern(out, urlPrefix, metas);
|
|
56
|
+
// cross-repo refs owner/repo#123
|
|
57
|
+
out = out.replace(/(\s|^|\W)([0-9a-zA-Z-_.]+)/, (m) => m); // keep (no-op, cross handled by issue pattern with full links)
|
|
58
|
+
// sha1 refs
|
|
59
|
+
if (metas && metas['repoLink']) {
|
|
60
|
+
out = out.replace(/\b[0-9a-f]{7,40}\b/g, (sha) => {
|
|
61
|
+
if (/^\d+$/.test(sha))
|
|
62
|
+
return sha;
|
|
63
|
+
return `<a href="${metas['repoLink']}/commit/${sha}"><code>${shortSHA1(sha)}</code></a>`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
const isAbsoluteURL = (u) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(u) || u.startsWith('mailto:') || u.startsWith('#') || u.startsWith('data:');
|
|
69
|
+
/** Post-process: relative link completion + special links in text nodes only. */
|
|
70
|
+
export function postProcessHTML(html, urlPrefix, metas) {
|
|
71
|
+
urlPrefix = urlPrefix.replaceAll(' ', '%20').replace(/\/+$/, '');
|
|
72
|
+
const prefix = cutoutVerbosePrefix(urlPrefix);
|
|
73
|
+
const toks = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < html.length) {
|
|
76
|
+
const lt = html.indexOf('<', i);
|
|
77
|
+
if (lt < 0) {
|
|
78
|
+
toks.push({ kind: 'text', value: html.slice(i) });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (lt > i)
|
|
82
|
+
toks.push({ kind: 'text', value: html.slice(i, lt) });
|
|
83
|
+
// tag ends at next '>' respecting quoted attrs
|
|
84
|
+
let j = lt + 1;
|
|
85
|
+
let inQuote = null;
|
|
86
|
+
while (j < html.length) {
|
|
87
|
+
const ch = html[j];
|
|
88
|
+
if (inQuote) {
|
|
89
|
+
if (ch === inQuote)
|
|
90
|
+
inQuote = null;
|
|
91
|
+
}
|
|
92
|
+
else if (ch === '"' || ch === "'") {
|
|
93
|
+
inQuote = ch;
|
|
94
|
+
}
|
|
95
|
+
else if (ch === '>')
|
|
96
|
+
break;
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
toks.push({ kind: 'tag', value: html.slice(lt, j + 1) });
|
|
100
|
+
i = j + 1;
|
|
101
|
+
}
|
|
102
|
+
let skipDepth = 0; // inside a/code/pre
|
|
103
|
+
const out = [];
|
|
104
|
+
const stack = [];
|
|
105
|
+
for (const tok of toks) {
|
|
106
|
+
if (tok.kind === 'tag') {
|
|
107
|
+
const m = /^<\s*(\/?)\s*([a-zA-Z0-9]+)/.exec(tok.value);
|
|
108
|
+
if (m) {
|
|
109
|
+
const closing = m[1] === '/';
|
|
110
|
+
const tag = m[2].toLowerCase();
|
|
111
|
+
if (!closing) {
|
|
112
|
+
if (tag === 'a' || tag === 'code' || tag === 'pre')
|
|
113
|
+
skipDepth++;
|
|
114
|
+
// complete relative href/src; image src via /raw/
|
|
115
|
+
if (tag === 'img') {
|
|
116
|
+
tok.value = tok.value.replace(/src="([^"]*)"/g, (mm, src) => {
|
|
117
|
+
if (isAbsoluteURL(src))
|
|
118
|
+
return mm;
|
|
119
|
+
const fixed = urlPrefix.replace('/src/', '/raw/') + '/' + src.replaceAll(' ', '%20');
|
|
120
|
+
return `src="${fixed}"`;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (tag === 'a') {
|
|
124
|
+
tok.value = tok.value.replace(/href="([^"]*)"/g, (mm, href) => {
|
|
125
|
+
if (isAbsoluteURL(href))
|
|
126
|
+
return mm;
|
|
127
|
+
return `href="${prefix}/${href}"`;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (tag === 'a' || tag === 'code' || tag === 'pre')
|
|
133
|
+
skipDepth = Math.max(0, skipDepth - 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
out.push(tok.value);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (skipDepth > 0) {
|
|
140
|
+
out.push(tok.value);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
out.push(renderSpecialLink(tok.value, urlPrefix, metas));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out.join('');
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------- sanitizer
|
|
150
|
+
const VOID_TAGS = new Set(['br', 'hr', 'img', 'input', 'col']);
|
|
151
|
+
const ALLOWED_TAGS = new Set([
|
|
152
|
+
'a', 'abbr', 'acronym', 'b', 'blockquote', 'br', 'code', 'caption', 'col', 'colgroup',
|
|
153
|
+
'dd', 'del', 'details', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr',
|
|
154
|
+
'i', 'img', 'ins', 'kbd', 'li', 'mark', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small',
|
|
155
|
+
'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot',
|
|
156
|
+
'th', 'thead', 'tr', 'u', 'ul', 'var', 'figure', 'figcaption', 'input',
|
|
157
|
+
]);
|
|
158
|
+
const GLOBAL_ATTRS = new Set(['title']);
|
|
159
|
+
const TAG_ATTRS = {
|
|
160
|
+
a: new Set(['href', 'rel']),
|
|
161
|
+
img: new Set(['src', 'alt', 'width', 'height']),
|
|
162
|
+
th: new Set(['align', 'colspan', 'rowspan']),
|
|
163
|
+
td: new Set(['align', 'colspan', 'rowspan']),
|
|
164
|
+
col: new Set(['align', 'span']),
|
|
165
|
+
input: new Set(['type', 'checked', 'disabled']),
|
|
166
|
+
code: new Set(['class']),
|
|
167
|
+
span: new Set(['class']),
|
|
168
|
+
details: new Set(['open']),
|
|
169
|
+
ol: new Set(['start']),
|
|
170
|
+
};
|
|
171
|
+
function escapeHTMLAttr(s) {
|
|
172
|
+
return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
173
|
+
}
|
|
174
|
+
function sanitizeText(s) {
|
|
175
|
+
return s.replaceAll('<', '<').replaceAll('>', '>');
|
|
176
|
+
}
|
|
177
|
+
function allowedDataImage(src) {
|
|
178
|
+
const m = /^data:image\/(png|jpeg|gif|webp|x-icon);/i.exec(src);
|
|
179
|
+
return !!m;
|
|
180
|
+
}
|
|
181
|
+
/** bluemonday UGCPolicy-equivalent whitelist sanitizer. */
|
|
182
|
+
export function sanitizeHTML(html) {
|
|
183
|
+
// remove script/style blocks entirely (tags + content), like bluemonday
|
|
184
|
+
html = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
185
|
+
let out = '';
|
|
186
|
+
let i = 0;
|
|
187
|
+
const stack = [];
|
|
188
|
+
while (i < html.length) {
|
|
189
|
+
const lt = html.indexOf('<', i);
|
|
190
|
+
if (lt < 0) {
|
|
191
|
+
out += sanitizeText(html.slice(i));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
out += sanitizeText(html.slice(i, lt));
|
|
195
|
+
let j = lt + 1;
|
|
196
|
+
let inQuote = null;
|
|
197
|
+
while (j < html.length) {
|
|
198
|
+
const ch = html[j];
|
|
199
|
+
if (inQuote) {
|
|
200
|
+
if (ch === inQuote)
|
|
201
|
+
inQuote = null;
|
|
202
|
+
}
|
|
203
|
+
else if (ch === '"' || ch === "'") {
|
|
204
|
+
inQuote = ch;
|
|
205
|
+
}
|
|
206
|
+
else if (ch === '>')
|
|
207
|
+
break;
|
|
208
|
+
j++;
|
|
209
|
+
}
|
|
210
|
+
if (j >= html.length) {
|
|
211
|
+
// unterminated tag: escape rest
|
|
212
|
+
out += sanitizeText(html.slice(lt));
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
const rawTag = html.slice(lt, j + 1);
|
|
216
|
+
out += sanitizeTag(rawTag, stack);
|
|
217
|
+
i = j + 1;
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
function sanitizeTag(tag, st) {
|
|
221
|
+
const m = /^<\s*(\/?)\s*([a-zA-Z0-9]+)([\s\S]*?)(\/?)\s*>$/.exec(tag);
|
|
222
|
+
if (!m)
|
|
223
|
+
return '';
|
|
224
|
+
const closing = m[1] === '/';
|
|
225
|
+
const name = m[2].toLowerCase();
|
|
226
|
+
if (!ALLOWED_TAGS.has(name))
|
|
227
|
+
return '';
|
|
228
|
+
if (closing) {
|
|
229
|
+
const idx = st.lastIndexOf(name);
|
|
230
|
+
if (idx >= 0) {
|
|
231
|
+
// close intermediate unclosed tags for well-formedness
|
|
232
|
+
let closed = '';
|
|
233
|
+
for (let k = st.length - 1; k >= idx; k--) {
|
|
234
|
+
if (!VOID_TAGS.has(st[k]))
|
|
235
|
+
closed += `</${st[k]}>`;
|
|
236
|
+
}
|
|
237
|
+
st.splice(idx);
|
|
238
|
+
return closed;
|
|
239
|
+
}
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
// attributes
|
|
243
|
+
const attrStr = m[3];
|
|
244
|
+
const allowedAttrs = new Set([...GLOBAL_ATTRS, ...(TAG_ATTRS[name] ?? [])]);
|
|
245
|
+
const attrRe = /([a-zA-Z-]+)\s*=\s*"([^"]*)"|([a-zA-Z-]+)\s*=\s*'([^']*)'|([a-zA-Z-]+)/g;
|
|
246
|
+
let am;
|
|
247
|
+
let attrs = '';
|
|
248
|
+
while ((am = attrRe.exec(attrStr))) {
|
|
249
|
+
const aname = (am[1] ?? am[3] ?? am[5] ?? '').toLowerCase();
|
|
250
|
+
const avalue = am[2] ?? am[4] ?? '';
|
|
251
|
+
if (!allowedAttrs.has(aname))
|
|
252
|
+
continue;
|
|
253
|
+
if (aname === 'href' || aname === 'src') {
|
|
254
|
+
const url = avalue.trim();
|
|
255
|
+
if (aname === 'src' && url.startsWith('data:')) {
|
|
256
|
+
if (!allowedDataImage(url))
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url);
|
|
261
|
+
const allowedSchemes = new Set(['http', 'https', 'mailto', 'ftp', ...conf.customURLSchemes]);
|
|
262
|
+
if (scheme && !allowedSchemes.has(scheme[1].toLowerCase()))
|
|
263
|
+
continue;
|
|
264
|
+
if (!scheme && url.startsWith('//'))
|
|
265
|
+
continue;
|
|
266
|
+
if (url.toLowerCase().startsWith('javascript:'))
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
attrs += ` ${aname}="${escapeHTMLAttr(avalue)}"`;
|
|
270
|
+
}
|
|
271
|
+
else if (aname === 'class') {
|
|
272
|
+
if (!/^language-[a-zA-Z0-9]+$/.test(avalue) && name === 'code')
|
|
273
|
+
continue;
|
|
274
|
+
if (name === 'span' && !/^(language-[a-zA-Z0-9]+|math|mermaid)$/.test(avalue))
|
|
275
|
+
continue;
|
|
276
|
+
attrs += ` class="${escapeHTMLAttr(avalue)}"`;
|
|
277
|
+
}
|
|
278
|
+
else if (aname === 'rel') {
|
|
279
|
+
continue; // rewritten below
|
|
280
|
+
}
|
|
281
|
+
else if (name === 'input') {
|
|
282
|
+
if (aname === 'type' && avalue !== 'checkbox')
|
|
283
|
+
continue;
|
|
284
|
+
attrs += ` ${aname}="${escapeHTMLAttr(avalue)}"`;
|
|
285
|
+
}
|
|
286
|
+
else if (aname === 'checked' || aname === 'disabled' || aname === 'open') {
|
|
287
|
+
attrs += ` ${aname}`;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
attrs += ` ${aname}="${escapeHTMLAttr(avalue)}"`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (name === 'a')
|
|
294
|
+
attrs += ' rel="nofollow"';
|
|
295
|
+
if (VOID_TAGS.has(name)) {
|
|
296
|
+
return `<${name}${attrs}${name === 'input' ? (attrs.includes('checked') ? ' checked' : '') : ''} />`.replace(/ \/>$/, ' />');
|
|
297
|
+
}
|
|
298
|
+
st.push(name);
|
|
299
|
+
return `<${name}${attrs}>`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------- entry points
|
|
303
|
+
export function rawMarkdown(input, urlPrefix, metas) {
|
|
304
|
+
const html = marked.parse(input, { async: false });
|
|
305
|
+
return postProcessHTML(html, urlPrefix, metas);
|
|
306
|
+
}
|
|
307
|
+
export function markdown(input, urlPrefix, metas) {
|
|
308
|
+
const rendered = rawMarkdown(input, urlPrefix, metas);
|
|
309
|
+
return sanitizeHTML(rendered);
|
|
310
|
+
}
|
|
311
|
+
export function sanitizeBytes(input) {
|
|
312
|
+
return sanitizeHTML(input);
|
|
313
|
+
}
|
|
314
|
+
export function htmlEscape(s) {
|
|
315
|
+
return s.replaceAll('&', '&').replaceAll("'", ''').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=markup.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Prometheus metrics endpoint (/-/metrics), text format.
|
|
2
|
+
import * as db from './db/db.js';
|
|
3
|
+
import { conf } from './conf.js';
|
|
4
|
+
export function renderMetrics() {
|
|
5
|
+
const g = (name, help, value) => `# HELP ${name} ${help}\n# TYPE ${name} gauge\n${name} ${value}\n`;
|
|
6
|
+
let out = '';
|
|
7
|
+
out += g('gogs_user_total', 'Number of users', db.db().prepare('SELECT COUNT(*) c FROM user WHERE type = 0').get().c);
|
|
8
|
+
out += g('gogs_org_total', 'Number of organizations', db.db().prepare('SELECT COUNT(*) c FROM user WHERE type = 1').get().c);
|
|
9
|
+
out += g('gogs_repo_total', 'Number of repositories', db.db().prepare('SELECT COUNT(*) c FROM repository').get().c);
|
|
10
|
+
out += g('gogs_issue_total', 'Number of issues', db.db().prepare('SELECT COUNT(*) c FROM issue').get().c);
|
|
11
|
+
out += g('gogs_comment_total', 'Number of comments', db.db().prepare('SELECT COUNT(*) c FROM comment').get().c);
|
|
12
|
+
out += g('gogs_action_total', 'Number of actions', db.db().prepare('SELECT COUNT(*) c FROM action').get().c);
|
|
13
|
+
out += g('gogs_webhook_total', 'Number of webhooks', db.db().prepare('SELECT COUNT(*) c FROM webhook').get().c);
|
|
14
|
+
out += g('gogs_process_uptime_seconds', 'Process uptime', Math.floor(process.uptime()));
|
|
15
|
+
out += `# HELP gogs_info Application info\n# TYPE gogs_info gauge\ngogs_info{version="${conf.version}",goversion="node-${process.version}"} 1\n`;
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Mirror synchronization: periodic loop + on-demand sync, mirroring gogs
|
|
2
|
+
// internal/database/mirror.go (git remote update + ref-change actions).
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { conf } from './conf.js';
|
|
6
|
+
import * as db from './db/db.js';
|
|
7
|
+
import * as git from './gitx/git.js';
|
|
8
|
+
/** Address stored in the bare repo config remote "origin" (credentials stripped for display). */
|
|
9
|
+
export function mirrorAddress(repoDir) {
|
|
10
|
+
try {
|
|
11
|
+
const cfg = fs.readFileSync(path.join(repoDir, 'config'), 'utf8');
|
|
12
|
+
const m = /remote "origin"[\s\S]*?url = (.+)/.exec(cfg);
|
|
13
|
+
if (m) {
|
|
14
|
+
// strip credentials for display
|
|
15
|
+
return m[1].replace(/^(https?:\/\/)([^@/]+)@/, '$1');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// ignore
|
|
20
|
+
}
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
function intervalSeconds(repoID) {
|
|
24
|
+
const row = db.db().prepare('SELECT interval FROM mirror WHERE repo_id = ?').get(repoID);
|
|
25
|
+
const hours = row?.interval > 0 ? row.interval : conf.defaultMirrorInterval;
|
|
26
|
+
return hours * 3600;
|
|
27
|
+
}
|
|
28
|
+
/** Parse `git remote update` stderr into ref changes like gogs parseRemoteUpdateOutput.
|
|
29
|
+
* Real output (space-aligned, no tabs):
|
|
30
|
+
* ` * [new branch] master -> main` (new)
|
|
31
|
+
* ` - [deleted] (none) -> origin/x` (delete)
|
|
32
|
+
* ` cf1685c..4386276 main -> main` (update)
|
|
33
|
+
* For --mirror remotes the ref names stay fully qualified. */
|
|
34
|
+
export function parseRemoteUpdateOutput(output) {
|
|
35
|
+
const refs = [];
|
|
36
|
+
const EMPTY = '0000000000000000000000000000000000000000';
|
|
37
|
+
for (const line of output.split('\n')) {
|
|
38
|
+
let m = /^ \* \[[^\]]+\]\s+(\S+)\s+->\s+(\S+)$/.exec(line);
|
|
39
|
+
if (m) {
|
|
40
|
+
const ref = m[2].startsWith('refs/') ? m[2] : 'refs/heads/' + m[2];
|
|
41
|
+
refs.push({ oldSha: EMPTY, newSha: '', ref });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
m = /^ - \[[^\]]+\]\s+\S+\s+->\s+(\S+)$/.exec(line);
|
|
45
|
+
if (m) {
|
|
46
|
+
const ref = m[1].startsWith('refs/') ? m[1] : 'refs/heads/' + m[1];
|
|
47
|
+
refs.push({ oldSha: '', newSha: EMPTY, ref });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
m = /^ ([0-9a-f]+)\.\.([0-9a-f]+)\s+(\S+)\s+->\s+(\S+)\s*$/.exec(line);
|
|
51
|
+
if (m) {
|
|
52
|
+
const ref = m[4].startsWith('refs/') ? m[4] : 'refs/heads/' + m[4];
|
|
53
|
+
refs.push({ oldSha: m[1], newSha: m[2], ref });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return refs;
|
|
58
|
+
}
|
|
59
|
+
async function fillNewSha(repoDir, change) {
|
|
60
|
+
if (change.newSha === '') {
|
|
61
|
+
const out = await git.gitOK(repoDir, 'rev-parse', '--verify', '--quiet', '--end-of-options', change.ref);
|
|
62
|
+
change.newSha = out?.toString().trim() ?? '';
|
|
63
|
+
}
|
|
64
|
+
if (change.oldSha === '') {
|
|
65
|
+
change.oldSha = '0000000000000000000000000000000000000000';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Sync one mirror repo now; fires mirror-sync actions and webhooks. */
|
|
69
|
+
export async function syncMirror(repoID, doerID) {
|
|
70
|
+
const mirror = db.db().prepare('SELECT * FROM mirror WHERE repo_id = ?').get(repoID);
|
|
71
|
+
const repo = db.getRepoByID(repoID);
|
|
72
|
+
if (!mirror || !repo)
|
|
73
|
+
return;
|
|
74
|
+
const repoDir = repo.RepoPath();
|
|
75
|
+
const remoteArgs = ['remote', 'update'];
|
|
76
|
+
if (mirror.enable_prune)
|
|
77
|
+
remoteArgs.push('--prune');
|
|
78
|
+
const r = await git.runGit(repoDir, remoteArgs, conf.gitTimeoutMirror * 1000);
|
|
79
|
+
const refs = parseRemoteUpdateOutput(r.stderr);
|
|
80
|
+
const doer = db.getUserByID(doerID) ?? db.getUserByID(repo.owner_id);
|
|
81
|
+
if (!doer)
|
|
82
|
+
return;
|
|
83
|
+
const { ActionType } = await import('./db/actions.js');
|
|
84
|
+
for (const change of refs) {
|
|
85
|
+
await fillNewSha(repoDir, change);
|
|
86
|
+
const branch = change.ref.replace('refs/heads/', '');
|
|
87
|
+
const isTag = change.ref.startsWith('refs/tags/');
|
|
88
|
+
const newEmpty = change.newSha === '0000000000000000000000000000000000000000' || change.newSha === '';
|
|
89
|
+
const oldEmpty = change.oldSha === '0000000000000000000000000000000000000000' || change.oldSha === '';
|
|
90
|
+
try {
|
|
91
|
+
if (isTag) {
|
|
92
|
+
if (newEmpty) {
|
|
93
|
+
await fire(doer, repo, ActionType.DELETE_TAG, '', JSON.stringify(branch));
|
|
94
|
+
}
|
|
95
|
+
else if (oldEmpty) {
|
|
96
|
+
await fire(doer, repo, ActionType.PUSH_TAG, branch, '');
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (newEmpty) {
|
|
101
|
+
await fire(doer, repo, ActionType.MIRROR_SYNC_DELETE, branch, '');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
let commits = [];
|
|
105
|
+
if (oldEmpty) {
|
|
106
|
+
const head = await git.catFileCommit(repoDir, change.newSha);
|
|
107
|
+
const ancestors = await git.gitOK(repoDir, 'log', '--max-count=9', '--pretty=format:%H', '--end-of-options', change.newSha, '--');
|
|
108
|
+
const shas = ancestors?.toString().trim().split('\n').filter(Boolean) ?? [];
|
|
109
|
+
commits = [head, ...await Promise.all(shas.map((s) => git.catFileCommit(repoDir, s).catch(() => null)))];
|
|
110
|
+
commits = commits.filter(Boolean);
|
|
111
|
+
await fire(doer, repo, ActionType.MIRROR_SYNC_CREATE, branch, JSON.stringify({ Sha1: head?.id, Name: branch }));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
commits = await git.commitsAfter(repoDir, change.oldSha, change.newSha);
|
|
115
|
+
}
|
|
116
|
+
await fire(doer, repo, ActionType.MIRROR_SYNC_PUSH, branch, JSON.stringify({
|
|
117
|
+
TotalCommits: commits.length,
|
|
118
|
+
Len: commits.length,
|
|
119
|
+
Commits: commits.map((c) => ({ Sha1: c.id, Message: c.message, AuthorEmail: c.author.email })),
|
|
120
|
+
Compares: commits.length > 0 ? [{ numCommits: commits.length }] : [],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.error('[mirror] action error:', e);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await git.updateServerInfo(repoDir);
|
|
129
|
+
const { size } = await git.countObjects(repoDir);
|
|
130
|
+
db.updateRepoColumns(repoID, { size });
|
|
131
|
+
// also sync the wiki mirror when present
|
|
132
|
+
const wikiDir = repo.WikiPath();
|
|
133
|
+
if (fs.existsSync(wikiDir)) {
|
|
134
|
+
const wikiArgs = ['remote', 'update'];
|
|
135
|
+
if (mirror.enable_prune)
|
|
136
|
+
wikiArgs.push('--prune');
|
|
137
|
+
await git.gitOK(wikiDir, ...wikiArgs);
|
|
138
|
+
}
|
|
139
|
+
const now = Math.floor(Date.now() / 1000);
|
|
140
|
+
db.db()
|
|
141
|
+
.prepare('UPDATE mirror SET updated_unix = ?, next_update_unix = ? WHERE repo_id = ?')
|
|
142
|
+
.run(now, now + intervalSeconds(repoID), repoID);
|
|
143
|
+
}
|
|
144
|
+
async function fire(doer, repo, opType, refName, content) {
|
|
145
|
+
db.db()
|
|
146
|
+
.prepare(`INSERT INTO action (user_id, op_type, act_user_id, act_user_name, repo_id, repo_user_name, repo_name, ref_name, is_private, content, created_unix)
|
|
147
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`)
|
|
148
|
+
.run(repo.owner_id, opType, doer.id, doer.name, repo.id, repo.OwnerName(), repo.name, refName, repo.is_private ?? 0, content, Math.floor(Date.now() / 1000));
|
|
149
|
+
// mirror push actions carry a webhook payload like commitRepo
|
|
150
|
+
const actionsMod = await import('./db/actions.js');
|
|
151
|
+
if (opType === 20 && content.includes('Commits')) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(content);
|
|
154
|
+
await actionsMod.mirrorSyncPushAction(doer, repo, 'refs/heads/' + refName, parsed);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// fall through to generic create/delete events below
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (opType === 9)
|
|
162
|
+
await actionsMod.pushTagAction(doer, repo, refName);
|
|
163
|
+
else if (opType === 21)
|
|
164
|
+
await actionsMod.createBranchAction(doer, repo, refName);
|
|
165
|
+
else if (opType === 18 || opType === 22)
|
|
166
|
+
await actionsMod.deleteBranchAction(doer, repo, refName);
|
|
167
|
+
}
|
|
168
|
+
let loopStarted = false;
|
|
169
|
+
/** Periodic mirror update loop (gogs InitSyncMirrors + cron.UPDATE_MIRRORS). */
|
|
170
|
+
export function startMirrorLoop() {
|
|
171
|
+
if (loopStarted)
|
|
172
|
+
return;
|
|
173
|
+
loopStarted = true;
|
|
174
|
+
const tick = async () => {
|
|
175
|
+
try {
|
|
176
|
+
const due = db
|
|
177
|
+
.db()
|
|
178
|
+
.prepare('SELECT repo_id FROM mirror WHERE next_update_unix <= ?')
|
|
179
|
+
.all(Math.floor(Date.now() / 1000));
|
|
180
|
+
for (const row of due) {
|
|
181
|
+
const repo = db.getRepoByID(row.repo_id);
|
|
182
|
+
if (!repo)
|
|
183
|
+
continue;
|
|
184
|
+
console.log('[mirror] syncing', repo.FullName());
|
|
185
|
+
try {
|
|
186
|
+
await syncMirror(row.repo_id, repo.owner_id);
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.error(`[mirror] sync failed for ${repo.FullName()}: ${e?.message ?? e}`);
|
|
190
|
+
// push next attempt back
|
|
191
|
+
const now = Math.floor(Date.now() / 1000);
|
|
192
|
+
db.db().prepare('UPDATE mirror SET updated_unix = ?, next_update_unix = ? WHERE repo_id = ?').run(now, now + intervalSeconds(row.repo_id), row.repo_id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
console.error('[mirror] loop error:', e);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
setInterval(tick, 60 * 1000);
|
|
201
|
+
setTimeout(tick, 10 * 1000);
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=mirror.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// multipart/form-data parsing using busboy.
|
|
2
|
+
import busboy from 'busboy';
|
|
3
|
+
export async function parseMultipart(req, buf) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const bb = busboy({ headers: req.headers });
|
|
6
|
+
const fields = {};
|
|
7
|
+
const files = [];
|
|
8
|
+
bb.on('field', (name, val) => {
|
|
9
|
+
if (fields[name] === undefined)
|
|
10
|
+
fields[name] = val;
|
|
11
|
+
else if (Array.isArray(fields[name]))
|
|
12
|
+
fields[name].push(val);
|
|
13
|
+
else
|
|
14
|
+
fields[name] = [fields[name], val];
|
|
15
|
+
});
|
|
16
|
+
bb.on('file', (name, stream, info) => {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
stream.on('data', (d) => chunks.push(d));
|
|
19
|
+
stream.on('end', () => {
|
|
20
|
+
files.push({
|
|
21
|
+
field: name,
|
|
22
|
+
name: info.filename,
|
|
23
|
+
mime: info.mimeType,
|
|
24
|
+
buffer: Buffer.concat(chunks),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
bb.on('error', reject);
|
|
29
|
+
bb.on('close', () => resolve({ fields, files }));
|
|
30
|
+
bb.end(buf);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=multipart.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 仓库领域的活函数:从已裁撤的 routes/repo.ts(网页路由)中摘出,
|
|
2
|
+
// 供 /api/v1 建仓与 /api/dsh PR 合并性检测使用。routes/ 死网页路由已整体删除。
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { conf } from './conf.js';
|
|
6
|
+
import * as db from './db/db.js';
|
|
7
|
+
import { nowUnix } from './db/db.js';
|
|
8
|
+
import * as git from './gitx/git.js';
|
|
9
|
+
import * as svc from './gitx/service.js';
|
|
10
|
+
import { createRepoAction } from './db/actions.js';
|
|
11
|
+
/** 建仓:写 repository 记录 + 初始化裸仓 + 关注/动作。原 routes/repo.ts createRepositoryRecord。 */
|
|
12
|
+
export async function createRepositoryRecord(doer, owner, opts) {
|
|
13
|
+
const now = nowUnix();
|
|
14
|
+
const info = db
|
|
15
|
+
.db()
|
|
16
|
+
.prepare(`INSERT INTO repository (owner_id, lower_name, name, description, website, default_branch,
|
|
17
|
+
is_private, is_bare, is_mirror, enable_wiki, enable_issues, enable_pulls,
|
|
18
|
+
num_watches, num_stars, num_forks, num_issues, num_closed_issues, num_pulls, num_closed_pulls,
|
|
19
|
+
num_milestones, num_closed_milestones, created_unix, updated_unix, size)
|
|
20
|
+
VALUES (?,?,?,?,?,?,?,?,?,1,1,1,0,0,0,0,0,0,0,0,0,?,?,0)`)
|
|
21
|
+
.run(owner.id, opts.name.toLowerCase(), opts.name, opts.description ?? '', opts.website ?? '', conf.defaultBranch, opts.private ? 1 : 0, opts.autoInit ? 0 : 1, opts.mirror ? 1 : 0, now, now);
|
|
22
|
+
const repo = db.getRepoByID(Number(info.lastInsertRowid));
|
|
23
|
+
db.db().prepare('UPDATE user SET num_repos = num_repos + 1 WHERE id = ?').run(owner.id);
|
|
24
|
+
// owner watches own repo
|
|
25
|
+
db.watchRepo(doer.id, repo.id, true);
|
|
26
|
+
await svc.initRepository(repo, { autoInit: opts.autoInit, doer, readme: opts.readme, gitignores: opts.gitignores, license: opts.license });
|
|
27
|
+
createRepoAction(doer, repo);
|
|
28
|
+
return repo;
|
|
29
|
+
}
|
|
30
|
+
/** PR 合并性检测:clone 基线分支后 `git apply --check` 试套 patch。原 routes/repo.ts testPullRequestMergeable。 */
|
|
31
|
+
export async function testPullRequestMergeable(repo, pr) {
|
|
32
|
+
const patchFile = path.join(conf.appDataPath, 'patches', String(repo.id), `${pr.index}.patch`);
|
|
33
|
+
if (!fs.existsSync(patchFile))
|
|
34
|
+
return pr.status ?? 0;
|
|
35
|
+
const tmpDir = path.join(conf.appDataPath, 'tmp', 'prtest-' + Date.now());
|
|
36
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
37
|
+
try {
|
|
38
|
+
const baseDir = repo.RepoPath();
|
|
39
|
+
await git.git(process.cwd(), 'clone', '-q', '-b', pr.base_branch, baseDir, tmpDir);
|
|
40
|
+
const wsFlag = repo.pulls_ignore_whitespace ? '--ignore-whitespace' : null;
|
|
41
|
+
const args = ['apply', '--check'];
|
|
42
|
+
if (wsFlag)
|
|
43
|
+
args.push(wsFlag);
|
|
44
|
+
args.push(patchFile);
|
|
45
|
+
const r = await git.gitOK(tmpDir, ...args);
|
|
46
|
+
return r !== null ? 2 : 0; // 2=mergeable 0=conflict
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** wiki 写页:clone wiki 仓 → 写/删 .md → commit → push。原 routes/repo.ts writeWikiPage。 */
|
|
56
|
+
export async function writeWikiPage(repo, title, content, doer, oldTitle, isDelete = false) {
|
|
57
|
+
const wikiDir = repo.WikiPath();
|
|
58
|
+
await svc.initWiki(wikiDir);
|
|
59
|
+
const tmpDir = path.join(conf.appDataPath, 'tmp', 'wiki-' + Date.now());
|
|
60
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
61
|
+
try {
|
|
62
|
+
const branch = svc.wikiBranch(wikiDir);
|
|
63
|
+
const hasBranch = await git.refExists(wikiDir, 'refs/heads/' + branch);
|
|
64
|
+
if (hasBranch) {
|
|
65
|
+
await git.git(process.cwd(), 'clone', wikiDir, tmpDir);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await git.git(process.cwd(), 'clone', wikiDir, tmpDir);
|
|
69
|
+
}
|
|
70
|
+
const target = path.join(tmpDir, title + '.md');
|
|
71
|
+
if (isDelete) {
|
|
72
|
+
if (fs.existsSync(target))
|
|
73
|
+
fs.unlinkSync(target);
|
|
74
|
+
const symlink = target.endsWith('.md') ? target.slice(0, -3) : target;
|
|
75
|
+
if (fs.existsSync(symlink))
|
|
76
|
+
fs.unlinkSync(symlink);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
if (oldTitle && oldTitle !== title) {
|
|
80
|
+
const oldPath = path.join(tmpDir, oldTitle + '.md');
|
|
81
|
+
if (fs.existsSync(oldPath))
|
|
82
|
+
fs.unlinkSync(oldPath);
|
|
83
|
+
}
|
|
84
|
+
if (!isDelete && fs.existsSync(target) && oldTitle === null) {
|
|
85
|
+
throw new Error('page already exists');
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(target, content);
|
|
88
|
+
}
|
|
89
|
+
const author = `${doer.name} <${doer.email}>`;
|
|
90
|
+
await git.git(tmpDir, 'add', '--all');
|
|
91
|
+
const msg = isDelete ? `Delete page '${title}'` : oldTitle ? `Update page '${title}'` : `Update page '${title}'`;
|
|
92
|
+
await git.git(tmpDir, 'commit', `--author=${author}`, '-m', msg);
|
|
93
|
+
await git.git(tmpDir, 'push', 'origin', `HEAD:refs/heads/${branch}`);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=repox.js.map
|