@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,232 @@
|
|
|
1
|
+
import * as fsMod from 'node:fs';
|
|
2
|
+
import * as zlib from 'node:zlib';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { conf } from '../conf.js';
|
|
5
|
+
import * as db from '../db/db.js';
|
|
6
|
+
import * as git from './git.js';
|
|
7
|
+
import { authenticateUserByBasic } from '../context.js';
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
function require_md5hex(s) {
|
|
10
|
+
return crypto.createHash('md5').update(s).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
const NO_CACHE_HEADERS = {
|
|
13
|
+
Expires: 'Fri, 01 Jan 1980 00:00:00 GMT',
|
|
14
|
+
Pragma: 'no-cache',
|
|
15
|
+
'Cache-Control': 'no-cache, max-age=0, must-revalidate',
|
|
16
|
+
};
|
|
17
|
+
/** Try to handle a git smart-HTTP request; returns false when not a git path. */
|
|
18
|
+
export function handleGitHTTP(req, res, pathname) {
|
|
19
|
+
const m = /^\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/(.*))?$/.exec(pathname);
|
|
20
|
+
if (!m)
|
|
21
|
+
return false;
|
|
22
|
+
const [, username, rawRepo, rest] = m;
|
|
23
|
+
if (!rest)
|
|
24
|
+
return false;
|
|
25
|
+
// repoName may carry .git / .wiki suffixes
|
|
26
|
+
let repoName = rawRepo;
|
|
27
|
+
let isWiki = false;
|
|
28
|
+
if (repoName.endsWith('.wiki')) {
|
|
29
|
+
isWiki = true;
|
|
30
|
+
repoName = repoName.slice(0, -5);
|
|
31
|
+
}
|
|
32
|
+
else if (repoName.endsWith('.git')) {
|
|
33
|
+
repoName = repoName.slice(0, -4);
|
|
34
|
+
if (repoName.endsWith('.wiki')) {
|
|
35
|
+
isWiki = true;
|
|
36
|
+
repoName = repoName.slice(0, -5);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const action = rest.split('?')[0];
|
|
40
|
+
// Git LFS: /info/lfs/...
|
|
41
|
+
const lfsMatch = /^info\/lfs(\/.*)?$/.exec(action);
|
|
42
|
+
if (lfsMatch) {
|
|
43
|
+
(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const owner = db.getUserByUsername(username);
|
|
46
|
+
const repo = owner ? db.getRepoByOwnerAndName(owner, repoName) : null;
|
|
47
|
+
const fsMod = await import('node:fs');
|
|
48
|
+
if (!owner || !repo || !fsMod.existsSync(isWiki ? repo.WikiPath() : repo.RepoPath())) {
|
|
49
|
+
res.statusCode = 404;
|
|
50
|
+
res.end('repository does not exist');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const { handleLFS } = await import('../lfsx.js');
|
|
54
|
+
await handleLFS(req, res, owner, repo, lfsMatch[1] ?? '/');
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error('[lfs]', e);
|
|
58
|
+
if (!res.headersSent)
|
|
59
|
+
res.statusCode = 500;
|
|
60
|
+
res.end();
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const GIT_ACTIONS = new Set([
|
|
66
|
+
'info/refs', 'HEAD', 'git-upload-pack', 'git-receive-pack',
|
|
67
|
+
'objects/info/alternates', 'objects/info/http-alternates', 'objects/info/packs',
|
|
68
|
+
]);
|
|
69
|
+
const isObjectReq = /^objects\/info\/.*/.test(action) ||
|
|
70
|
+
/^objects\/[0-9a-f]{2}\/[0-9a-f]{38}$/.test(action) ||
|
|
71
|
+
/^objects\/pack\/pack-[0-9a-f]{40}\.(pack|idx)$/.test(action);
|
|
72
|
+
if (!GIT_ACTIONS.has(action) && !isObjectReq)
|
|
73
|
+
return false;
|
|
74
|
+
const lowerPath = pathname.toLowerCase();
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const owner = db.getUserByUsername(username);
|
|
78
|
+
const repo = owner ? db.getRepoByOwnerAndName(owner, repoName) : null;
|
|
79
|
+
if (!repo) {
|
|
80
|
+
res.statusCode = 404;
|
|
81
|
+
res.end('repository does not exist');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const repoDir = isWiki ? repo.WikiPath() : repo.RepoPath();
|
|
85
|
+
if (!fsMod.existsSync(repoDir)) {
|
|
86
|
+
res.statusCode = 404;
|
|
87
|
+
res.end('repository does not exist');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// service detection
|
|
91
|
+
const service = new URL(req.url ?? '/', 'http://internal').searchParams.get('service') ?? '';
|
|
92
|
+
const isReceive = action === 'git-receive-pack' || service === 'git-receive-pack';
|
|
93
|
+
const isPull = action === 'info/refs' ? service !== 'git-receive-pack' : action !== 'git-receive-pack';
|
|
94
|
+
if (conf.disableHTTPGit) {
|
|
95
|
+
res.statusCode = 403;
|
|
96
|
+
res.end('The repository\'s HTTP git is disabled');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// public repo pull requires no auth (unless REQUIRE_SIGNIN_VIEW)
|
|
100
|
+
if (!(isPull && !repo.is_private && !conf.requireSigninView)) {
|
|
101
|
+
const authHeader = String(req.headers.authorization ?? '');
|
|
102
|
+
const authed = await authenticateUserByBasic(authHeader);
|
|
103
|
+
if (!authed) {
|
|
104
|
+
res.statusCode = 401;
|
|
105
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="."');
|
|
106
|
+
res.end('Requires authentication');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const mode = db.accessMode(authed.user.id, repo);
|
|
110
|
+
const need = isPull ? db.AccessMode.READ : db.AccessMode.WRITE;
|
|
111
|
+
if (mode < need) {
|
|
112
|
+
res.statusCode = 403;
|
|
113
|
+
res.end('User does not have access to repository');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (isReceive && repo.is_mirror) {
|
|
117
|
+
res.statusCode = 403;
|
|
118
|
+
res.end('Mirror repository is read-only');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// stash authed user for post-processing
|
|
122
|
+
req.__authedUser = authed.user;
|
|
123
|
+
}
|
|
124
|
+
for (const [k, v] of Object.entries(NO_CACHE_HEADERS))
|
|
125
|
+
res.setHeader(k, v);
|
|
126
|
+
if (action === 'info/refs') {
|
|
127
|
+
if (service !== 'git-upload-pack' && service !== 'git-receive-pack') {
|
|
128
|
+
// dumb protocol: run update-server-info and serve the static file
|
|
129
|
+
await git.git(repoDir, 'update-server-info');
|
|
130
|
+
const file = path.join(repoDir, 'info', 'refs');
|
|
131
|
+
if (fsMod.existsSync(file)) {
|
|
132
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
133
|
+
res.end(fsMod.readFileSync(file));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
res.statusCode = 404;
|
|
137
|
+
res.end();
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const out = await git.statelessRPC(repoDir, service === 'git-receive-pack' ? 'receive-pack' : 'upload-pack', true, Buffer.alloc(0));
|
|
142
|
+
res.setHeader('Content-Type', `application/x-${service}-advertisement`);
|
|
143
|
+
const head = git.pktLine(`# service=${service}\n`) + '0000';
|
|
144
|
+
res.end(Buffer.concat([Buffer.from(head), out]));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (action === 'git-upload-pack' || action === 'git-receive-pack') {
|
|
148
|
+
const svc = action === 'git-upload-pack' ? 'upload-pack' : 'receive-pack';
|
|
149
|
+
const reqContentType = `application/x-git-${svc}-request`;
|
|
150
|
+
if (String(req.headers['content-type'] ?? '') !== reqContentType) {
|
|
151
|
+
res.statusCode = 401;
|
|
152
|
+
res.end();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let body = await readBody(req);
|
|
156
|
+
if (String(req.headers['content-encoding'] ?? '') === 'gzip') {
|
|
157
|
+
body = zlib.gunzipSync(body);
|
|
158
|
+
}
|
|
159
|
+
res.setHeader('Content-Type', `application/x-${svc}-result`);
|
|
160
|
+
if (svc === 'receive-pack') {
|
|
161
|
+
// post-processing runs via delegate hooks (env flows into them)
|
|
162
|
+
const out = await git.statelessRPC(repoDir, svc, false, body, {
|
|
163
|
+
GOGS_AUTH_USER_ID: String(req.__authedUser?.id ?? 0),
|
|
164
|
+
GOGS_AUTH_USER_NAME: String(req.__authedUser?.name ?? ''),
|
|
165
|
+
GOGS_AUTH_USER_EMAIL: String(req.__authedUser?.email ?? ''),
|
|
166
|
+
GOGS_REPO_OWNER_NAME: String(owner.name),
|
|
167
|
+
GOGS_REPO_OWNER_SALT_MD5: require_md5hex(`${owner.name}${owner.salt ?? ''}`),
|
|
168
|
+
GOGS_REPO_ID: String(repo.id),
|
|
169
|
+
GOGS_REPO_NAME: String(repo.name),
|
|
170
|
+
GOGS_REPO_CUSTOM_HOOKS_PATH: path.join(repoDir, 'custom_hooks'),
|
|
171
|
+
});
|
|
172
|
+
res.end(out);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const out = await git.statelessRPC(repoDir, svc, false, body);
|
|
176
|
+
res.end(out);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// static object serving (dumb protocol)
|
|
181
|
+
const safe = path.normalize(action).replace(/^(\.\.[/\\])+/, '');
|
|
182
|
+
const file = path.join(repoDir, safe);
|
|
183
|
+
if (fsMod.existsSync(file) && fsMod.statSync(file).isFile()) {
|
|
184
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
185
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
186
|
+
res.end(fsMod.readFileSync(file));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
res.statusCode = 404;
|
|
190
|
+
res.end();
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error('[git http]', e);
|
|
194
|
+
if (!res.headersSent)
|
|
195
|
+
res.statusCode = 500;
|
|
196
|
+
res.end();
|
|
197
|
+
}
|
|
198
|
+
})();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
function readBody(req) {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const chunks = [];
|
|
204
|
+
req.on('data', (d) => chunks.push(d));
|
|
205
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
206
|
+
req.on('error', reject);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function parseRefUpdates(body) {
|
|
210
|
+
// pkt-line stream: 4-hex len lines "old new ref\0capabilities" until 0000
|
|
211
|
+
const updates = [];
|
|
212
|
+
let pos = 0;
|
|
213
|
+
while (pos + 4 <= body.length) {
|
|
214
|
+
const lenStr = body.toString('utf8', pos, pos + 4);
|
|
215
|
+
if (!/^[0-9a-f]{4}$/.test(lenStr))
|
|
216
|
+
break;
|
|
217
|
+
const len = parseInt(lenStr, 16);
|
|
218
|
+
if (len === 0)
|
|
219
|
+
break;
|
|
220
|
+
let line = body.toString('utf8', pos + 4, pos + len);
|
|
221
|
+
pos += len;
|
|
222
|
+
const nul = line.indexOf('\0');
|
|
223
|
+
if (nul >= 0)
|
|
224
|
+
line = line.slice(0, nul);
|
|
225
|
+
const parts = line.trim().split(/\s+/);
|
|
226
|
+
if (parts.length >= 3 && /^[0-9a-f]{40}$/.test(parts[0]) && /^[0-9a-f]{40}$/.test(parts[1])) {
|
|
227
|
+
updates.push({ old: parts[0], new: parts[1], ref: parts[2] });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return updates;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Repository disk layout, byte-compatible with gogs:
|
|
2
|
+
// {ROOT}/{owner lowercase}/{repo lowercase}.git
|
|
3
|
+
// {ROOT}/{owner lowercase}/{repo lowercase}.wiki.git
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import { conf } from '../conf.js';
|
|
7
|
+
/** pathx.Clean: prevent path traversal, collapse separators */
|
|
8
|
+
export function cleanPath(name) {
|
|
9
|
+
let n = name.replace(/\\/g, '/');
|
|
10
|
+
n = path.posix.normalize(n);
|
|
11
|
+
n = n.replace(/^(\.\.(\/|$))+/, '');
|
|
12
|
+
if (n.startsWith('/'))
|
|
13
|
+
n = n.slice(1);
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
export function userPath(username) {
|
|
17
|
+
return path.join(conf.repositoryRoot, cleanPath(username.toLowerCase()));
|
|
18
|
+
}
|
|
19
|
+
export function repoPath(username, repoName) {
|
|
20
|
+
return path.join(userPath(username), cleanPath(repoName.toLowerCase()) + '.git');
|
|
21
|
+
}
|
|
22
|
+
export function wikiPath(username, repoName) {
|
|
23
|
+
return path.join(userPath(username), cleanPath(repoName.toLowerCase()) + '.wiki.git');
|
|
24
|
+
}
|
|
25
|
+
export function isRepositoryExist(username, repoName) {
|
|
26
|
+
return fs.existsSync(repoPath(username, repoName));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Repository lifecycle service: create (bare init), fork, wiki init, and the
|
|
2
|
+
// push-update pipeline that gogs runs via delegate hooks + tasks/trigger.
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { conf } from '../conf.js';
|
|
7
|
+
import * as db from '../db/db.js';
|
|
8
|
+
import * as git from './git.js';
|
|
9
|
+
import { userPath } from './paths.js';
|
|
10
|
+
const EMPTY_SHA = '0000000000000000000000000000000000000000';
|
|
11
|
+
/** ComposeHookEnvs equivalent for local (file-remote) pushes. */
|
|
12
|
+
export function composeHookEnv(doer, repo, ownerOverride) {
|
|
13
|
+
const ownerName = ownerOverride ?? repo.OwnerName();
|
|
14
|
+
const salt = repo.owner?.salt ?? '';
|
|
15
|
+
return {
|
|
16
|
+
SSH_ORIGINAL_COMMAND: '1',
|
|
17
|
+
GOGS_AUTH_USER_ID: String(doer?.id ?? 0),
|
|
18
|
+
GOGS_AUTH_USER_NAME: String(doer?.name ?? ''),
|
|
19
|
+
GOGS_AUTH_USER_EMAIL: String(doer?.email ?? ''),
|
|
20
|
+
GOGS_REPO_OWNER_NAME: ownerName,
|
|
21
|
+
GOGS_REPO_OWNER_SALT_MD5: crypto.createHash('md5').update(`${ownerName}${salt}`).digest('hex'),
|
|
22
|
+
GOGS_REPO_ID: String(repo.id),
|
|
23
|
+
GOGS_REPO_NAME: String(repo.name),
|
|
24
|
+
GOGS_REPO_CUSTOM_HOOKS_PATH: path.join(repo.RepoPath(), 'custom_hooks'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** createDelegateHooks equivalent: since push handling runs in-process after
|
|
28
|
+
* receive-pack, delegate hooks are only written for SSH/custom compat. */
|
|
29
|
+
export function createDelegateHooks(repoPath) {
|
|
30
|
+
const hooksDir = path.join(repoPath, 'hooks');
|
|
31
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
32
|
+
const dist = path.join(conf.workDir, 'dist', 'index.js');
|
|
33
|
+
const srcCli = path.join(conf.workDir, 'node_modules', '.bin', 'tsx');
|
|
34
|
+
const srcEntry = path.join(conf.workDir, 'src', 'index.ts');
|
|
35
|
+
const script = (name, args) => {
|
|
36
|
+
let runner;
|
|
37
|
+
if (fs.existsSync(dist)) {
|
|
38
|
+
runner = `${process.execPath} "${dist}"`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// NOTE: absolute node + tsx CLI entry (ssh sessions have a minimal PATH);
|
|
42
|
+
// config passes via env — `tsx` consumes --config as its own flag
|
|
43
|
+
const tsxCli = path.join(conf.workDir, 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
|
44
|
+
runner = `GOGS_CUSTOM_CONF='${conf.customConf}' "${process.execPath}" "${tsxCli}" "${srcEntry}"`;
|
|
45
|
+
}
|
|
46
|
+
return `#!/usr/bin/env bash\n${runner} hook ${name} ${args}\n`;
|
|
47
|
+
};
|
|
48
|
+
fs.writeFileSync(path.join(hooksDir, 'pre-receive'), script('pre-receive', ''), { mode: 0o755 });
|
|
49
|
+
fs.writeFileSync(path.join(hooksDir, 'update'), script('update', '$1 $2 $3'), { mode: 0o755 });
|
|
50
|
+
fs.writeFileSync(path.join(hooksDir, 'post-receive'), script('post-receive', ''), { mode: 0o755 });
|
|
51
|
+
}
|
|
52
|
+
export async function initRepository(repo, opts = {}) {
|
|
53
|
+
const repoDir = repo.RepoPath();
|
|
54
|
+
fs.mkdirSync(userPath(repo.OwnerName()), { recursive: true });
|
|
55
|
+
await git.initBare(repoDir, conf.defaultBranch);
|
|
56
|
+
createDelegateHooks(repoDir);
|
|
57
|
+
if (opts.autoInit) {
|
|
58
|
+
// clone to temp dir, seed files, push
|
|
59
|
+
const tmpDir = path.join(conf.appDataPath, 'tmp', 'init-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
|
60
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
61
|
+
try {
|
|
62
|
+
await git.git(process.cwd(), 'clone', repoDir, tmpDir);
|
|
63
|
+
let hasContent = false;
|
|
64
|
+
const files = [];
|
|
65
|
+
if (opts.readme) {
|
|
66
|
+
files.push(['README.md', renderReadmeTemplate(opts.readme, repo)]);
|
|
67
|
+
hasContent = true;
|
|
68
|
+
}
|
|
69
|
+
if (opts.gitignores) {
|
|
70
|
+
const gi = readVendorTemplate('gitignore', opts.gitignores);
|
|
71
|
+
if (gi) {
|
|
72
|
+
files.push(['.gitignore', gi]);
|
|
73
|
+
hasContent = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (opts.license) {
|
|
77
|
+
const lic = readVendorTemplate('license', opts.license);
|
|
78
|
+
if (lic) {
|
|
79
|
+
files.push(['LICENSE', lic]);
|
|
80
|
+
hasContent = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [name, content] of files) {
|
|
84
|
+
fs.writeFileSync(path.join(tmpDir, name), content);
|
|
85
|
+
}
|
|
86
|
+
if (hasContent) {
|
|
87
|
+
const author = opts.doer ? `${opts.doer.name} <${opts.doer.email}>` : 'Gogs <gogs@fake.local>';
|
|
88
|
+
await git.git(tmpDir, 'add', '--all');
|
|
89
|
+
await git.git(tmpDir, 'commit', `--author=${author}`, '-m', 'Initial commit');
|
|
90
|
+
// local remotes inherit the push process env — hooks receive the context
|
|
91
|
+
const hookEnv = composeHookEnv(opts.doer, repo);
|
|
92
|
+
await git.runGit(tmpDir, ['push', 'origin', `master:${conf.defaultBranch}`], 120000, hookEnv);
|
|
93
|
+
if (conf.defaultBranch !== 'master') {
|
|
94
|
+
await git.git(repoDir, 'symbolic-ref', 'HEAD', `refs/heads/${conf.defaultBranch}`);
|
|
95
|
+
}
|
|
96
|
+
await git.updateServerInfo(repoDir);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await git.updateServerInfo(repoDir);
|
|
104
|
+
}
|
|
105
|
+
function renderReadmeTemplate(name, repo) {
|
|
106
|
+
const tpl = readVendorTemplate('readme', name);
|
|
107
|
+
if (!tpl)
|
|
108
|
+
return `# ${repo.name}\n\n${repo.description ?? ''}\n`;
|
|
109
|
+
return tpl
|
|
110
|
+
.replace(/\{\{Filename\}\}/g, 'README.md')
|
|
111
|
+
.replace(/\{\{Description\}\}/g, repo.description ?? '')
|
|
112
|
+
.replace(/\{\{Name\}\}/g, repo.name);
|
|
113
|
+
}
|
|
114
|
+
export function readVendorTemplate(kind, name) {
|
|
115
|
+
const file = path.join(conf.workDir, 'vendored-conf', kind, name);
|
|
116
|
+
if (!fs.existsSync(file))
|
|
117
|
+
return null;
|
|
118
|
+
return fs.readFileSync(file, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
export function listVendorTemplates(kind) {
|
|
121
|
+
const dir = path.join(conf.workDir, 'vendored-conf', kind);
|
|
122
|
+
if (!fs.existsSync(dir))
|
|
123
|
+
return [];
|
|
124
|
+
return fs.readdirSync(dir).filter((f) => !f.startsWith('.'));
|
|
125
|
+
}
|
|
126
|
+
/** git clone --bare fork. */
|
|
127
|
+
export async function forkRepository(base, fork) {
|
|
128
|
+
fs.mkdirSync(userPath(fork.OwnerName()), { recursive: true });
|
|
129
|
+
await git.git(process.cwd(), 'clone', '--bare', base.RepoPath(), fork.RepoPath());
|
|
130
|
+
await git.updateServerInfo(fork.RepoPath());
|
|
131
|
+
createDelegateHooks(fork.RepoPath());
|
|
132
|
+
}
|
|
133
|
+
/** gogs PushUpdate equivalent, invoked in-process after receive-pack. */
|
|
134
|
+
export async function processPushUpdate(opts) {
|
|
135
|
+
const repoDir = opts.repo.RepoPath();
|
|
136
|
+
const { commitRepoAction, pushTagAction, ActionType } = await import('../db/actions.js');
|
|
137
|
+
await git.updateServerInfo(repoDir);
|
|
138
|
+
const { size } = await git.countObjects(repoDir);
|
|
139
|
+
// upstream RepositoriesStore.Touch: any push clears the bare flag
|
|
140
|
+
db.updateRepoColumns(opts.repo.id, { size, is_bare: 0 });
|
|
141
|
+
if (opts.refName.startsWith('refs/tags/')) {
|
|
142
|
+
const tagName = opts.refName.slice('refs/tags/'.length);
|
|
143
|
+
if (opts.newSha === EMPTY_SHA) {
|
|
144
|
+
const { deleteBranchAction } = await import('../db/actions.js');
|
|
145
|
+
// tag deletion action (op 18)
|
|
146
|
+
await deleteBranchAction(opts.doer, opts.repo, tagName);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await pushTagAction(opts.doer, opts.repo, tagName);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const branch = opts.refName.replace('refs/heads/', '');
|
|
153
|
+
if (opts.newSha === EMPTY_SHA) {
|
|
154
|
+
const { deleteBranchAction } = await import('../db/actions.js');
|
|
155
|
+
deleteBranchAction(opts.doer, opts.repo, branch);
|
|
156
|
+
db.db().prepare('UPDATE repository SET is_bare = CASE WHEN ? = default_branch THEN 1 ELSE is_bare END WHERE id = ?').run(opts.repo.default_branch, opts.repo.id);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let commits = [];
|
|
160
|
+
if (opts.oldSha === EMPTY_SHA) {
|
|
161
|
+
// new branch: take up to 10 commits (new + 9 ancestors).
|
|
162
|
+
// upstream fires only the commit-push action here — no separate "create" event.
|
|
163
|
+
const head = await git.catFileCommit(repoDir, opts.newSha);
|
|
164
|
+
const ancestors = await commitsList(repoDir, opts.newSha, 9);
|
|
165
|
+
commits = [head, ...ancestors];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
commits = await git.commitsAfter(repoDir, opts.oldSha, opts.newSha);
|
|
169
|
+
}
|
|
170
|
+
await commitRepoAction(opts.doer, opts.repo, opts.refName, {
|
|
171
|
+
TotalCommits: commits.length,
|
|
172
|
+
Len: commits.length,
|
|
173
|
+
Commits: commits.map((c) => ({
|
|
174
|
+
Sha1: c.id,
|
|
175
|
+
Message: c.message,
|
|
176
|
+
AuthorEmail: c.author.email,
|
|
177
|
+
})),
|
|
178
|
+
Compares: commits.length > 0 ? [{ numCommits: commits.length }] : [],
|
|
179
|
+
});
|
|
180
|
+
// issue reference from commit messages (#123)
|
|
181
|
+
try {
|
|
182
|
+
await processCommitIssueRefs(opts.repo, commits, opts.doer);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
console.error('[push update] issue refs:', e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function commitsList(repoDir, rev, maxCount) {
|
|
189
|
+
const out = await git.gitOK(repoDir, 'log', `--max-count=${maxCount}`, '--pretty=format:%H', '--end-of-options', rev, '--');
|
|
190
|
+
if (!out)
|
|
191
|
+
return [];
|
|
192
|
+
const shas = out.toString().trim().split('\n').filter(Boolean);
|
|
193
|
+
const commits = [];
|
|
194
|
+
for (const sha of shas) {
|
|
195
|
+
try {
|
|
196
|
+
commits.push(await git.catFileCommit(repoDir, sha));
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return commits;
|
|
203
|
+
}
|
|
204
|
+
/** Commit messages referencing #123 close/mention issues like gogs update.go. */
|
|
205
|
+
async function processCommitIssueRefs(repo, commits, doer) {
|
|
206
|
+
for (const commit of commits) {
|
|
207
|
+
const matches = [...commit.message.matchAll(/#(\d+)/g)];
|
|
208
|
+
for (const m of matches) {
|
|
209
|
+
const index = Number(m[1]);
|
|
210
|
+
const issue = db.getIssueByIndex(repo.id, index);
|
|
211
|
+
if (!issue || issue.is_closed)
|
|
212
|
+
continue;
|
|
213
|
+
// upstream matches the keyword anywhere in the message (no word-start anchor)
|
|
214
|
+
const closes = /(close[sd]?|fix(?:e[sd]?)?|resolve[sd]?)[:\s]+#\d+/i.test(commit.message);
|
|
215
|
+
if (closes) {
|
|
216
|
+
db.updateIssueColumns(issue.id, { is_closed: 1 });
|
|
217
|
+
db.db().prepare('INSERT INTO comment (type, poster_id, issue_id, content, created_unix, updated_unix, commit_sha) VALUES (2,?,?,?,?,?,?)')
|
|
218
|
+
.run(doer.id, issue.id, '', Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000), commit.id);
|
|
219
|
+
db.refreshIssueCounts(repo.id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** wiki repository helpers */
|
|
225
|
+
export async function initWiki(repoDir) {
|
|
226
|
+
if (fs.existsSync(repoDir))
|
|
227
|
+
return;
|
|
228
|
+
fs.mkdirSync(path.dirname(repoDir), { recursive: true });
|
|
229
|
+
await git.initBare(repoDir, 'master');
|
|
230
|
+
createDelegateHooks(repoDir);
|
|
231
|
+
}
|
|
232
|
+
export function wikiBranch(repoDir) {
|
|
233
|
+
// main if exists, else master
|
|
234
|
+
const main = fs.existsSync(path.join(repoDir, 'refs', 'heads', 'main')) ||
|
|
235
|
+
fs.existsSync(path.join(repoDir, 'packed-refs')) &&
|
|
236
|
+
fs.readFileSync(path.join(repoDir, 'packed-refs'), 'utf8').includes('refs/heads/main');
|
|
237
|
+
return main ? 'main' : 'master';
|
|
238
|
+
}
|
|
239
|
+
export function toWikiPageName(name) {
|
|
240
|
+
let n = name;
|
|
241
|
+
try {
|
|
242
|
+
n = decodeURIComponent(name);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// keep raw
|
|
246
|
+
}
|
|
247
|
+
n = n.replace(/\\/g, '/').replace(/\/+/g, ' ').replace(/^\/|\/$/g, '');
|
|
248
|
+
n = n.replaceAll('/', ' ').trim();
|
|
249
|
+
return n || 'Home';
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// goSprintf:Go fmt.Sprintf 子集,仅供 i18n 翻译插值使用。
|
|
2
|
+
// 从 gotemplate/engine.ts 抽出的自包含实现(模板引擎本体已随网页 UI 一并裁撤)。
|
|
3
|
+
// SafeHTML 兼容:不 import engine 的 SafeHTML 类,改为结构判断({ html: string }),
|
|
4
|
+
// 避免反向依赖整个模板引擎。
|
|
5
|
+
export function isTrue(v) {
|
|
6
|
+
if (v == null || v === false)
|
|
7
|
+
return false;
|
|
8
|
+
if (typeof v === 'number')
|
|
9
|
+
return v !== 0 && !Number.isNaN(v) ? true : false;
|
|
10
|
+
if (typeof v === 'string' || v instanceof String)
|
|
11
|
+
return String(v).length > 0;
|
|
12
|
+
if (Array.isArray(v))
|
|
13
|
+
return v.length > 0;
|
|
14
|
+
if (v instanceof Map)
|
|
15
|
+
return v.size > 0;
|
|
16
|
+
if (typeof v === 'object')
|
|
17
|
+
return Object.keys(v).length > 0 || Object.getPrototypeOf(v) !== Object.prototype;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function printPlain(v) {
|
|
21
|
+
// 用户拍板:嵌套 nil(容器整体打印时)也渲染空串,偏离 Go fmt 的 '<nil>'
|
|
22
|
+
if (v == null)
|
|
23
|
+
return '';
|
|
24
|
+
// SafeHTML 结构判断:{ html: string } 直接取可信 HTML,不转义
|
|
25
|
+
if (typeof v === 'object' && typeof v.html === 'string')
|
|
26
|
+
return v.html;
|
|
27
|
+
if (typeof v === 'string' || v instanceof String)
|
|
28
|
+
return String(v);
|
|
29
|
+
if (typeof v === 'number')
|
|
30
|
+
return fmtNumber(v);
|
|
31
|
+
if (typeof v === 'boolean')
|
|
32
|
+
return v ? 'true' : 'false';
|
|
33
|
+
if (Array.isArray(v))
|
|
34
|
+
return '[' + v.map((x) => printPlain(x)).join(' ') + ']';
|
|
35
|
+
if (v instanceof Map) {
|
|
36
|
+
const keys = Array.from(v.keys()).map(String).sort();
|
|
37
|
+
return 'map[' + keys.map((k) => `${k}:${printPlain(v.get(k))}`).join(' ') + ']';
|
|
38
|
+
}
|
|
39
|
+
if (typeof v === 'object') {
|
|
40
|
+
// Go struct: {v1 v2}
|
|
41
|
+
return '{' + Object.values(v).map((x) => printPlain(x)).join(' ') + '}';
|
|
42
|
+
}
|
|
43
|
+
return String(v);
|
|
44
|
+
}
|
|
45
|
+
function fmtNumber(n) {
|
|
46
|
+
if (Number.isInteger(n))
|
|
47
|
+
return String(n);
|
|
48
|
+
return String(n);
|
|
49
|
+
}
|
|
50
|
+
export function goSprintf(format, args) {
|
|
51
|
+
let argIdx = 0;
|
|
52
|
+
let out = '';
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < format.length) {
|
|
55
|
+
const c = format[i];
|
|
56
|
+
if (c !== '%') {
|
|
57
|
+
out += c;
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// positional form: %[N]<verb> — sets the argument index for this verb only
|
|
62
|
+
const posMatch = /^%\[(\d+)\]/.exec(format.slice(i));
|
|
63
|
+
let positional = false;
|
|
64
|
+
if (posMatch) {
|
|
65
|
+
argIdx = Number(posMatch[1]) - 1;
|
|
66
|
+
i += posMatch[0].length;
|
|
67
|
+
positional = true;
|
|
68
|
+
}
|
|
69
|
+
// parse verb: %[-+ #0]*[0-9]*(\.[0-9]+)?[bdeEfFgGoOqxXscvU%]
|
|
70
|
+
let j = positional ? i : i + 1;
|
|
71
|
+
let flags = '';
|
|
72
|
+
while (j < format.length && /[-+ #0]/.test(format[j])) {
|
|
73
|
+
flags += format[j];
|
|
74
|
+
j++;
|
|
75
|
+
}
|
|
76
|
+
let width = '';
|
|
77
|
+
while (j < format.length && /[0-9]/.test(format[j])) {
|
|
78
|
+
width += format[j];
|
|
79
|
+
j++;
|
|
80
|
+
}
|
|
81
|
+
let prec = '';
|
|
82
|
+
if (format[j] === '.') {
|
|
83
|
+
j++;
|
|
84
|
+
while (j < format.length && /[0-9]/.test(format[j])) {
|
|
85
|
+
prec += format[j];
|
|
86
|
+
j++;
|
|
87
|
+
}
|
|
88
|
+
if (prec === '')
|
|
89
|
+
prec = '0';
|
|
90
|
+
}
|
|
91
|
+
const verb = format[j];
|
|
92
|
+
if (verb === undefined) {
|
|
93
|
+
out += '%';
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (verb === '%') {
|
|
97
|
+
out += '%';
|
|
98
|
+
i = j + 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const arg = args[argIdx];
|
|
102
|
+
argIdx = argIdx + 1;
|
|
103
|
+
out += formatVerb(verb, flags, width, prec, arg);
|
|
104
|
+
i = j + 1;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function formatVerb(verb, flags, width, prec, arg) {
|
|
109
|
+
let s;
|
|
110
|
+
switch (verb) {
|
|
111
|
+
case 's':
|
|
112
|
+
s = arg == null ? '%!s(<nil>)' : printPlain(arg);
|
|
113
|
+
if (prec !== '')
|
|
114
|
+
s = s.slice(0, Number(prec));
|
|
115
|
+
break;
|
|
116
|
+
case 'd': {
|
|
117
|
+
const n = Math.trunc(Number(arg));
|
|
118
|
+
if (Number.isNaN(n))
|
|
119
|
+
s = `%!d(${printPlain(arg)})`;
|
|
120
|
+
else
|
|
121
|
+
s = String(n);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'x':
|
|
125
|
+
s = typeof arg === 'string' ? Buffer.from(arg, 'utf8').toString('hex') : (Number(arg) >>> 0).toString(16);
|
|
126
|
+
break;
|
|
127
|
+
case 'X':
|
|
128
|
+
s = typeof arg === 'string' ? Buffer.from(arg, 'utf8').toString('hex').toUpperCase() : (Number(arg) >>> 0).toString(16).toUpperCase();
|
|
129
|
+
break;
|
|
130
|
+
case 'f':
|
|
131
|
+
case 'F': {
|
|
132
|
+
const n = Number(arg);
|
|
133
|
+
s = n.toFixed(prec === '' ? 6 : Number(prec));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'g':
|
|
137
|
+
s = String(Number(arg));
|
|
138
|
+
break;
|
|
139
|
+
case 'q':
|
|
140
|
+
s = JSON.stringify(printPlain(arg)) ?? '""';
|
|
141
|
+
break;
|
|
142
|
+
case 'v':
|
|
143
|
+
s = printPlain(arg);
|
|
144
|
+
break;
|
|
145
|
+
case 't':
|
|
146
|
+
s = String(isTrue(arg));
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
s = `%!${verb}(${printPlain(arg)})`;
|
|
150
|
+
}
|
|
151
|
+
// width padding
|
|
152
|
+
if (width) {
|
|
153
|
+
const w = Number(width);
|
|
154
|
+
if (s.length < w) {
|
|
155
|
+
if (flags.includes('-'))
|
|
156
|
+
s = s.padEnd(w);
|
|
157
|
+
else if (flags.includes('0') && (verb === 'd' || verb === 'f')) {
|
|
158
|
+
const neg = s.startsWith('-');
|
|
159
|
+
s = neg ? '-' + s.slice(1).padStart(w - (neg ? 1 : 0), '0') : s.padStart(w, '0');
|
|
160
|
+
}
|
|
161
|
+
else
|
|
162
|
+
s = s.padStart(w);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return s;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=gosprintf.js.map
|