@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.
Files changed (248) hide show
  1. package/README.md +43 -0
  2. package/client/bundle.js +2356 -0
  3. package/client/index.js +2342 -0
  4. package/cordis.patch.yml +11 -0
  5. package/package.json +70 -0
  6. package/server/dist/api/v1.js +2029 -0
  7. package/server/dist/authx/password.js +43 -0
  8. package/server/dist/authx/um.js +157 -0
  9. package/server/dist/conf.js +323 -0
  10. package/server/dist/context.js +701 -0
  11. package/server/dist/db/actions.js +202 -0
  12. package/server/dist/db/db.js +838 -0
  13. package/server/dist/dshapi.js +1416 -0
  14. package/server/dist/editorconfig.js +104 -0
  15. package/server/dist/gitx/git.js +580 -0
  16. package/server/dist/gitx/http.js +232 -0
  17. package/server/dist/gitx/paths.js +28 -0
  18. package/server/dist/gitx/service.js +251 -0
  19. package/server/dist/gosprintf.js +167 -0
  20. package/server/dist/hook.js +53 -0
  21. package/server/dist/index.js +112 -0
  22. package/server/dist/lfsx.js +265 -0
  23. package/server/dist/mailer.js +127 -0
  24. package/server/dist/markup.js +317 -0
  25. package/server/dist/metrics.js +18 -0
  26. package/server/dist/mirror.js +203 -0
  27. package/server/dist/multipart.js +33 -0
  28. package/server/dist/repox.js +99 -0
  29. package/server/dist/router.js +185 -0
  30. package/server/dist/serv.js +43 -0
  31. package/server/dist/server.js +382 -0
  32. package/server/dist/sshx/dispatch.js +160 -0
  33. package/server/dist/sshx/lfs-transfer.js +289 -0
  34. package/server/dist/sshx/server.js +107 -0
  35. package/server/dist/toolx.js +108 -0
  36. package/server/dist/twofactor.js +160 -0
  37. package/server/dist/webapi.js +377 -0
  38. package/server/dist/webhook.js +128 -0
  39. package/server/package.json +34 -0
  40. package/server/public/img/avatar_default.png +0 -0
  41. package/server/tsconfig.json +19 -0
  42. package/server/vendored-conf/app.ini +538 -0
  43. package/server/vendored-conf/gitignore/Actionscript +11 -0
  44. package/server/vendored-conf/gitignore/Ada +5 -0
  45. package/server/vendored-conf/gitignore/Agda +1 -0
  46. package/server/vendored-conf/gitignore/Android +32 -0
  47. package/server/vendored-conf/gitignore/Anjuta +3 -0
  48. package/server/vendored-conf/gitignore/AppEngine +2 -0
  49. package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
  50. package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
  51. package/server/vendored-conf/gitignore/Archives +27 -0
  52. package/server/vendored-conf/gitignore/Autotools +14 -0
  53. package/server/vendored-conf/gitignore/BricxCC +4 -0
  54. package/server/vendored-conf/gitignore/C +32 -0
  55. package/server/vendored-conf/gitignore/C Sharp +108 -0
  56. package/server/vendored-conf/gitignore/C++ +28 -0
  57. package/server/vendored-conf/gitignore/CFWheels +12 -0
  58. package/server/vendored-conf/gitignore/CMake +6 -0
  59. package/server/vendored-conf/gitignore/CUDA +6 -0
  60. package/server/vendored-conf/gitignore/CakePHP +13 -0
  61. package/server/vendored-conf/gitignore/ChefCookbook +9 -0
  62. package/server/vendored-conf/gitignore/Cloud9 +3 -0
  63. package/server/vendored-conf/gitignore/CodeIgniter +6 -0
  64. package/server/vendored-conf/gitignore/CodeKit +3 -0
  65. package/server/vendored-conf/gitignore/CommonLisp +3 -0
  66. package/server/vendored-conf/gitignore/Composer +6 -0
  67. package/server/vendored-conf/gitignore/Concrete5 +3 -0
  68. package/server/vendored-conf/gitignore/Coq +3 -0
  69. package/server/vendored-conf/gitignore/CraftCMS +3 -0
  70. package/server/vendored-conf/gitignore/DM +5 -0
  71. package/server/vendored-conf/gitignore/Dart +15 -0
  72. package/server/vendored-conf/gitignore/DartEditor +2 -0
  73. package/server/vendored-conf/gitignore/Delphi +57 -0
  74. package/server/vendored-conf/gitignore/Dreamweaver +3 -0
  75. package/server/vendored-conf/gitignore/Drupal +36 -0
  76. package/server/vendored-conf/gitignore/EPiServer +4 -0
  77. package/server/vendored-conf/gitignore/Eagle +36 -0
  78. package/server/vendored-conf/gitignore/Eclipse +39 -0
  79. package/server/vendored-conf/gitignore/EiffelStudio +2 -0
  80. package/server/vendored-conf/gitignore/Elisp +5 -0
  81. package/server/vendored-conf/gitignore/Elixir +4 -0
  82. package/server/vendored-conf/gitignore/Emacs +32 -0
  83. package/server/vendored-conf/gitignore/Ensime +4 -0
  84. package/server/vendored-conf/gitignore/Erlang +10 -0
  85. package/server/vendored-conf/gitignore/Espresso +1 -0
  86. package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
  87. package/server/vendored-conf/gitignore/ExtJs +4 -0
  88. package/server/vendored-conf/gitignore/Fancy +2 -0
  89. package/server/vendored-conf/gitignore/Finale +13 -0
  90. package/server/vendored-conf/gitignore/FlexBuilder +3 -0
  91. package/server/vendored-conf/gitignore/ForceDotCom +4 -0
  92. package/server/vendored-conf/gitignore/FuelPHP +2 -0
  93. package/server/vendored-conf/gitignore/GWT +28 -0
  94. package/server/vendored-conf/gitignore/Gcov +5 -0
  95. package/server/vendored-conf/gitignore/GitBook +16 -0
  96. package/server/vendored-conf/gitignore/Go +24 -0
  97. package/server/vendored-conf/gitignore/Gradle +8 -0
  98. package/server/vendored-conf/gitignore/Grails +33 -0
  99. package/server/vendored-conf/gitignore/Haskell +16 -0
  100. package/server/vendored-conf/gitignore/IGORPro +5 -0
  101. package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
  102. package/server/vendored-conf/gitignore/Idris +2 -0
  103. package/server/vendored-conf/gitignore/JDeveloper +13 -0
  104. package/server/vendored-conf/gitignore/Java +12 -0
  105. package/server/vendored-conf/gitignore/Jboss +19 -0
  106. package/server/vendored-conf/gitignore/Jekyll +3 -0
  107. package/server/vendored-conf/gitignore/JetBrains +46 -0
  108. package/server/vendored-conf/gitignore/Joomla +525 -0
  109. package/server/vendored-conf/gitignore/KDevelop4 +2 -0
  110. package/server/vendored-conf/gitignore/Kate +3 -0
  111. package/server/vendored-conf/gitignore/KiCAD +13 -0
  112. package/server/vendored-conf/gitignore/Kohana +2 -0
  113. package/server/vendored-conf/gitignore/LabVIEW +16 -0
  114. package/server/vendored-conf/gitignore/Laravel +4 -0
  115. package/server/vendored-conf/gitignore/Lazarus +30 -0
  116. package/server/vendored-conf/gitignore/Leiningen +12 -0
  117. package/server/vendored-conf/gitignore/LemonStand +21 -0
  118. package/server/vendored-conf/gitignore/LibreOffice +2 -0
  119. package/server/vendored-conf/gitignore/Lilypond +6 -0
  120. package/server/vendored-conf/gitignore/Linux +7 -0
  121. package/server/vendored-conf/gitignore/Lithium +2 -0
  122. package/server/vendored-conf/gitignore/Lua +41 -0
  123. package/server/vendored-conf/gitignore/LyX +4 -0
  124. package/server/vendored-conf/gitignore/Magento +126 -0
  125. package/server/vendored-conf/gitignore/Matlab +16 -0
  126. package/server/vendored-conf/gitignore/Maven +9 -0
  127. package/server/vendored-conf/gitignore/Mercurial +6 -0
  128. package/server/vendored-conf/gitignore/Mercury +13 -0
  129. package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
  130. package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
  131. package/server/vendored-conf/gitignore/ModelSim +23 -0
  132. package/server/vendored-conf/gitignore/Momentics +8 -0
  133. package/server/vendored-conf/gitignore/MonoDevelop +8 -0
  134. package/server/vendored-conf/gitignore/Nanoc +10 -0
  135. package/server/vendored-conf/gitignore/NetBeans +8 -0
  136. package/server/vendored-conf/gitignore/Nim +1 -0
  137. package/server/vendored-conf/gitignore/Ninja +2 -0
  138. package/server/vendored-conf/gitignore/Node +28 -0
  139. package/server/vendored-conf/gitignore/NotepadPP +2 -0
  140. package/server/vendored-conf/gitignore/OCaml +20 -0
  141. package/server/vendored-conf/gitignore/Objective-C +43 -0
  142. package/server/vendored-conf/gitignore/Opa +13 -0
  143. package/server/vendored-conf/gitignore/OpenCart +11 -0
  144. package/server/vendored-conf/gitignore/OracleForms +8 -0
  145. package/server/vendored-conf/gitignore/Packer +5 -0
  146. package/server/vendored-conf/gitignore/Perl +20 -0
  147. package/server/vendored-conf/gitignore/Phalcon +2 -0
  148. package/server/vendored-conf/gitignore/PhpStorm +63 -0
  149. package/server/vendored-conf/gitignore/PlayFramework +17 -0
  150. package/server/vendored-conf/gitignore/Plone +18 -0
  151. package/server/vendored-conf/gitignore/Prestashop +25 -0
  152. package/server/vendored-conf/gitignore/Processing +7 -0
  153. package/server/vendored-conf/gitignore/Python +58 -0
  154. package/server/vendored-conf/gitignore/Qooxdoo +5 -0
  155. package/server/vendored-conf/gitignore/Qt +34 -0
  156. package/server/vendored-conf/gitignore/R +16 -0
  157. package/server/vendored-conf/gitignore/ROS +47 -0
  158. package/server/vendored-conf/gitignore/Rails +35 -0
  159. package/server/vendored-conf/gitignore/Redcar +1 -0
  160. package/server/vendored-conf/gitignore/Redis +3 -0
  161. package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
  162. package/server/vendored-conf/gitignore/Ruby +36 -0
  163. package/server/vendored-conf/gitignore/Rust +11 -0
  164. package/server/vendored-conf/gitignore/SBT +9 -0
  165. package/server/vendored-conf/gitignore/SCons +2 -0
  166. package/server/vendored-conf/gitignore/SVN +1 -0
  167. package/server/vendored-conf/gitignore/Sass +2 -0
  168. package/server/vendored-conf/gitignore/Scala +17 -0
  169. package/server/vendored-conf/gitignore/Scrivener +7 -0
  170. package/server/vendored-conf/gitignore/Sdcc +8 -0
  171. package/server/vendored-conf/gitignore/SeamGen +26 -0
  172. package/server/vendored-conf/gitignore/SketchUp +1 -0
  173. package/server/vendored-conf/gitignore/SlickEdit +11 -0
  174. package/server/vendored-conf/gitignore/Stella +12 -0
  175. package/server/vendored-conf/gitignore/SublimeText +14 -0
  176. package/server/vendored-conf/gitignore/SugarCRM +25 -0
  177. package/server/vendored-conf/gitignore/Swift +43 -0
  178. package/server/vendored-conf/gitignore/Symfony +37 -0
  179. package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
  180. package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
  181. package/server/vendored-conf/gitignore/Tags +14 -0
  182. package/server/vendored-conf/gitignore/TeX +136 -0
  183. package/server/vendored-conf/gitignore/TextMate +3 -0
  184. package/server/vendored-conf/gitignore/Textpattern +11 -0
  185. package/server/vendored-conf/gitignore/TortoiseGit +2 -0
  186. package/server/vendored-conf/gitignore/TurboGears2 +20 -0
  187. package/server/vendored-conf/gitignore/Typo3 +20 -0
  188. package/server/vendored-conf/gitignore/Umbraco +17 -0
  189. package/server/vendored-conf/gitignore/Unity +21 -0
  190. package/server/vendored-conf/gitignore/UnrealEngine +76 -0
  191. package/server/vendored-conf/gitignore/VVVV +6 -0
  192. package/server/vendored-conf/gitignore/Vagrant +1 -0
  193. package/server/vendored-conf/gitignore/Vim +6 -0
  194. package/server/vendored-conf/gitignore/VirtualEnv +10 -0
  195. package/server/vendored-conf/gitignore/VisualStudio +217 -0
  196. package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
  197. package/server/vendored-conf/gitignore/Waf +3 -0
  198. package/server/vendored-conf/gitignore/WebMethods +14 -0
  199. package/server/vendored-conf/gitignore/WebStorm +63 -0
  200. package/server/vendored-conf/gitignore/Windows +18 -0
  201. package/server/vendored-conf/gitignore/WordPress +18 -0
  202. package/server/vendored-conf/gitignore/Xcode +23 -0
  203. package/server/vendored-conf/gitignore/XilinxISE +57 -0
  204. package/server/vendored-conf/gitignore/Xojo +11 -0
  205. package/server/vendored-conf/gitignore/Yeoman +6 -0
  206. package/server/vendored-conf/gitignore/Yii +6 -0
  207. package/server/vendored-conf/gitignore/ZendFramework +20 -0
  208. package/server/vendored-conf/gitignore/Zephir +26 -0
  209. package/server/vendored-conf/gitignore/macOS +25 -0
  210. package/server/vendored-conf/label/Default +7 -0
  211. package/server/vendored-conf/license/Abstyles License +10 -0
  212. package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
  213. package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
  214. package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
  215. package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
  216. package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
  217. package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
  218. package/server/vendored-conf/license/Apache License 1.0 +19 -0
  219. package/server/vendored-conf/license/Apache License 1.1 +20 -0
  220. package/server/vendored-conf/license/Apache License 2.0 +72 -0
  221. package/server/vendored-conf/license/Artistic License 1.0 +48 -0
  222. package/server/vendored-conf/license/Artistic License 2.0 +84 -0
  223. package/server/vendored-conf/license/BSD 2-clause License +8 -0
  224. package/server/vendored-conf/license/BSD 3-clause License +10 -0
  225. package/server/vendored-conf/license/BSD 4-clause License +13 -0
  226. package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
  227. package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
  228. package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
  229. package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
  230. package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
  231. package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
  232. package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
  233. package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
  234. package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
  235. package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
  236. package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
  237. package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
  238. package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
  239. package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
  240. package/server/vendored-conf/license/ISC license +6 -0
  241. package/server/vendored-conf/license/MIT License +8 -0
  242. package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
  243. package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
  244. package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
  245. package/server/vendored-conf/readme/Default +3 -0
  246. package/src/dsh-host.js +168 -0
  247. package/src/index.js +682 -0
  248. package/src/runner.js +364 -0
@@ -0,0 +1,838 @@
1
+ // Database layer: SQLite via better-sqlite3, schema mirroring gogs.
2
+ import Database from 'better-sqlite3';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import * as crypto from 'node:crypto';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { conf } from '../conf.js';
8
+ import { encodePassword, randomSalt, md5 } from '../authx/password.js';
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ export const EmptyID = '0000000000000000000000000000000000000000';
11
+ export function nowUnix() {
12
+ return Math.floor(Date.now() / 1000);
13
+ }
14
+ export function randomChars(n) {
15
+ // strx.RandomChars: alphanumeric
16
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
17
+ let out = '';
18
+ const bytes = crypto.randomBytes(n);
19
+ for (let i = 0; i < n; i++)
20
+ out += alphabet[bytes[i] % alphabet.length];
21
+ return out;
22
+ }
23
+ export function newUUID() {
24
+ return crypto.randomUUID().replaceAll('-', '');
25
+ }
26
+ let sqlite = null;
27
+ export function openDB() {
28
+ if (sqlite)
29
+ return sqlite;
30
+ fs.mkdirSync(path.dirname(conf.dbPath), { recursive: true });
31
+ sqlite = new Database(conf.dbPath);
32
+ sqlite.pragma('journal_mode = WAL');
33
+ let schemaFile = path.join(__dirname, 'schema.sql');
34
+ if (!fs.existsSync(schemaFile)) {
35
+ schemaFile = path.join(conf.workDir, 'src', 'db', 'schema.sql');
36
+ }
37
+ const schema = fs.readFileSync(schemaFile, 'utf8');
38
+ sqlite.exec(schema);
39
+ // version table single row (gogs minDBVersion 19 + 3 migrations = 22)
40
+ const row = sqlite.prepare('SELECT version FROM version WHERE id = 1').get();
41
+ if (!row)
42
+ sqlite.prepare('INSERT INTO version (id, version) VALUES (1, 22)').run();
43
+ return sqlite;
44
+ }
45
+ export function db() {
46
+ return openDB();
47
+ }
48
+ // ---------------------------------------------------------------- errors
49
+ export class NotFoundError extends Error {
50
+ constructor(msg = 'not found') {
51
+ super(msg);
52
+ }
53
+ }
54
+ export class AlreadyExistError extends Error {
55
+ constructor(msg = 'already exist') {
56
+ super(msg);
57
+ }
58
+ }
59
+ export class NameNotAllowedError extends Error {
60
+ reason;
61
+ constructor(reason) {
62
+ super(`name is not allowed: ${reason}`);
63
+ this.reason = reason;
64
+ }
65
+ }
66
+ // ---------------------------------------------------------------- reserved names
67
+ const reservedUsernames = new Set([
68
+ '-', 'explore', 'create', 'assets', 'css', 'img', 'js', 'less', 'plugins', 'debug',
69
+ 'raw', 'install', 'api', 'avatar', 'user', 'org', 'help', 'stars', 'issues', 'pulls',
70
+ 'commits', 'repo', 'template', 'admin', 'new', '.', '..',
71
+ ]);
72
+ const reservedUsernamePatterns = ['*.keys'];
73
+ const reservedRepoNames = new Set([
74
+ '-', '.', '..', 'explore', 'create', 'assets', 'css', 'img', 'js', 'less', 'plugins',
75
+ 'debug', 'raw', 'install', 'api', 'avatar', 'user', 'org', 'help', 'stars', 'issues',
76
+ 'pulls', 'commits', 'repo', 'template', 'admin', 'new', 'search', 'serviceworker.js',
77
+ 'manifest.json', 'api', 'metrics',
78
+ ]);
79
+ export function isUsernameAllowed(name) {
80
+ const n = name.trim().toLowerCase();
81
+ if (!n)
82
+ return false;
83
+ if (reservedUsernames.has(n))
84
+ return false;
85
+ for (const p of reservedUsernamePatterns) {
86
+ if ((p[0] === '*' && n.endsWith(p.slice(1))) || (p.endsWith('*') && n.startsWith(p.slice(0, -1))))
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ export function isRepoNameAllowed(name) {
92
+ const n = name.trim().toLowerCase();
93
+ if (!n)
94
+ return false;
95
+ return !reservedRepoNames.has(n);
96
+ }
97
+ // ---------------------------------------------------------------- model wrappers
98
+ // Provide the methods that gogs templates and handlers call on models.
99
+ export class User {
100
+ constructor(row) {
101
+ Object.assign(this, row);
102
+ goAlias(this);
103
+ }
104
+ id;
105
+ lower_name;
106
+ name;
107
+ full_name;
108
+ email;
109
+ passwd;
110
+ salt;
111
+ rands;
112
+ is_admin;
113
+ is_active;
114
+ type; // 0=individual 1=organization
115
+ avatar;
116
+ avatar_email;
117
+ use_custom_avatar;
118
+ num_repos;
119
+ num_followers;
120
+ num_following;
121
+ num_stars;
122
+ created_unix;
123
+ updated_unix;
124
+ prohibit_login;
125
+ max_repo_creation;
126
+ description;
127
+ location;
128
+ website;
129
+ get IsOrganization() {
130
+ return this.type === 1;
131
+ }
132
+ DisplayName() {
133
+ return this.full_name || this.name;
134
+ }
135
+ ShortName(length) {
136
+ const name = this.DisplayName();
137
+ return name.length > length ? name.slice(0, length - 2) + '…' : name;
138
+ }
139
+ HomeURLPath() {
140
+ return conf.subpath + '/' + this.name;
141
+ }
142
+ /** gogs User.Link / HomeLink */
143
+ Link() {
144
+ return this.HomeURLPath();
145
+ }
146
+ HomeLink() {
147
+ return this.HomeURLPath();
148
+ }
149
+ HTMLURL() {
150
+ return conf.externalURL + this.name;
151
+ }
152
+ get NumOpenIssues() {
153
+ return 0;
154
+ }
155
+ /** gogs User.Created / Updated (time.Time views over unix columns) */
156
+ get Created() {
157
+ return new Date((this.created_unix ?? 0) * 1000);
158
+ }
159
+ /** gogs User.GetOrganizationCount */
160
+ GetOrganizationCount() {
161
+ const r = db().prepare('SELECT COUNT(*) AS c FROM org_user WHERE uid = ?').get(this.id);
162
+ return r?.c ?? 0;
163
+ }
164
+ NumRepos() {
165
+ return this.num_repos;
166
+ }
167
+ get Updated() {
168
+ return new Date((this.updated_unix ?? 0) * 1000);
169
+ }
170
+ AvatarURLPath() {
171
+ if (this.id <= 0)
172
+ return conf.subpath + '/img/avatar_default.png';
173
+ if (this.use_custom_avatar) {
174
+ return `${conf.subpath}/user/avatars/${this.id}`;
175
+ }
176
+ if (conf.disableGravatar) {
177
+ return conf.subpath + '/img/avatar_default.png';
178
+ }
179
+ return `${conf.subpath}/user/avatar/${this.avatar}`;
180
+ }
181
+ AvatarURL() {
182
+ const link = this.AvatarURLPath();
183
+ if (link[0] === '/' && link[1] !== '/') {
184
+ return conf.externalURL + link.slice(conf.subpath ? conf.subpath.length + 1 : 1);
185
+ }
186
+ return link;
187
+ }
188
+ CanCreateOrganization() {
189
+ return this.is_admin === 1;
190
+ }
191
+ }
192
+ export class Repository {
193
+ constructor(row) {
194
+ Object.assign(this, row);
195
+ goAlias(this);
196
+ }
197
+ id;
198
+ owner_id;
199
+ lower_name;
200
+ name;
201
+ description;
202
+ website;
203
+ default_branch;
204
+ is_private;
205
+ is_bare;
206
+ is_mirror;
207
+ is_fork;
208
+ fork_id;
209
+ is_unlisted;
210
+ size;
211
+ num_watches;
212
+ num_stars;
213
+ num_forks;
214
+ num_issues;
215
+ num_closed_issues;
216
+ num_pulls;
217
+ num_closed_pulls;
218
+ num_milestones;
219
+ num_closed_milestones;
220
+ enable_issues;
221
+ enable_wiki;
222
+ allow_public_wiki;
223
+ enable_pulls;
224
+ pulls_ignore_whitespace;
225
+ pulls_allow_rebase;
226
+ allow_public_issues;
227
+ enable_external_tracker;
228
+ external_tracker_url;
229
+ external_tracker_format;
230
+ external_tracker_style;
231
+ created_unix;
232
+ updated_unix;
233
+ owner;
234
+ RepoPath() {
235
+ return repoPath(this.OwnerName(), this.name);
236
+ }
237
+ /** gogs Repository.Link */
238
+ Link() {
239
+ return conf.subpath + '/' + this.FullName();
240
+ }
241
+ get NumOpenIssues() {
242
+ return (this.num_issues ?? 0) - (this.num_closed_issues ?? 0);
243
+ }
244
+ /** gogs Repository.Created / Updated (time.Time views over unix columns) */
245
+ get Created() {
246
+ return new Date((this.created_unix ?? 0) * 1000);
247
+ }
248
+ get Updated() {
249
+ return new Date((this.updated_unix ?? 0) * 1000);
250
+ }
251
+ get NumOpenPulls() {
252
+ return (this.num_pulls ?? 0) - (this.num_closed_pulls ?? 0);
253
+ }
254
+ get NumClosedIssues() {
255
+ return this.num_closed_issues ?? 0;
256
+ }
257
+ get NumClosedPulls() {
258
+ return this.num_closed_pulls ?? 0;
259
+ }
260
+ AllowsPulls() {
261
+ return this.enable_pulls === 1;
262
+ }
263
+ AllowsIssues() {
264
+ return this.enable_issues === 1;
265
+ }
266
+ IsPartialPublic() {
267
+ return !this.is_private;
268
+ }
269
+ CanGuestViewIssues() {
270
+ return this.enable_issues === 1 && this.allow_public_issues === 1;
271
+ }
272
+ CanGuestViewWiki() {
273
+ return this.enable_wiki === 1 && this.allow_public_wiki === 1;
274
+ }
275
+ OwnerName() {
276
+ return this.owner ? this.owner.name : this.lower_name;
277
+ }
278
+ FullName() {
279
+ return `${this.OwnerName()}/${this.name}`;
280
+ }
281
+ HTMLURL() {
282
+ return conf.externalURL + this.FullName();
283
+ }
284
+ CloneURL() {
285
+ return conf.externalURL + this.FullName() + '.git';
286
+ }
287
+ WikiPath() {
288
+ return wikiPath(this.OwnerName(), this.name);
289
+ }
290
+ ComposeMetas() {
291
+ const metas = { repoLink: conf.subpath + '/' + this.FullName() };
292
+ if (this.enable_external_tracker) {
293
+ metas['format'] = this.external_tracker_format;
294
+ metas['user'] = this.OwnerName();
295
+ metas['repo'] = this.name;
296
+ metas['style'] = this.external_tracker_style ?? '';
297
+ metas['externalTrackerURL'] = this.external_tracker_url ?? '';
298
+ }
299
+ return metas;
300
+ }
301
+ }
302
+ // ---------------------------------------------------------------- disk layout
303
+ import { repoPath, wikiPath, userPath } from '../gitx/paths.js';
304
+ export { repoPath, wikiPath, userPath };
305
+ // ---------------------------------------------------------------- users
306
+ export function getUserByID(id) {
307
+ const row = db().prepare('SELECT * FROM user WHERE id = ?').get(id);
308
+ return row ? new User(row) : null;
309
+ }
310
+ export function getUserByUsername(name) {
311
+ const row = db().prepare('SELECT * FROM user WHERE lower_name = ?').get(name.toLowerCase());
312
+ return row ? new User(row) : null;
313
+ }
314
+ export function getUserByEmail(email) {
315
+ email = email.toLowerCase().trim();
316
+ const row = db().prepare('SELECT * FROM user WHERE lower(email) = ?').get(email);
317
+ if (row)
318
+ return new User(row);
319
+ const ea = db().prepare('SELECT uid FROM email_address WHERE lower(email) = ? AND is_activated = 1').get(email);
320
+ if (ea)
321
+ return getUserByID(ea.uid);
322
+ return null;
323
+ }
324
+ export function createUser(username, email, opts = {}) {
325
+ username = username.trim();
326
+ if (!isUsernameAllowed(username))
327
+ throw new NameNotAllowedError('reserved');
328
+ if (getUserByUsername(username))
329
+ throw new AlreadyExistError(`user already exists [name: ${username}]`);
330
+ email = email.toLowerCase().trim();
331
+ if (email && getUserByEmail(email))
332
+ throw new AlreadyExistError(`email has been used [email: ${email}]`);
333
+ const salt = randomSalt();
334
+ const now = nowUnix();
335
+ const info = db()
336
+ .prepare(`INSERT INTO user (lower_name, name, full_name, email, passwd, login_source, login_name,
337
+ type, location, website, rands, salt, created_unix, updated_unix, max_repo_creation,
338
+ is_active, is_admin, avatar, avatar_email, use_custom_avatar,
339
+ num_followers, num_following, num_stars, num_repos, num_teams, num_members)
340
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
341
+ .run(username.toLowerCase(), username, opts.fullName ?? '', email, encodePassword(opts.password ?? '', salt), opts.loginSource ?? 0, opts.loginName ?? '', 0, opts.location ?? '', opts.website ?? '', randomSalt(), salt, now, now, -1, opts.activated ? 1 : 0, opts.admin ? 1 : 0, md5(email), email, 0, 0, 0, 0, 0, 0, 0);
342
+ const user = getUserByID(Number(info.lastInsertRowid));
343
+ if (email) {
344
+ db()
345
+ .prepare('INSERT INTO email_address (uid, email, is_activated) VALUES (?,?,?)')
346
+ .run(user.id, email, opts.activated ? 1 : 0);
347
+ }
348
+ return user;
349
+ }
350
+ export function createOrganization(doer, username, opts) {
351
+ if (!isUsernameAllowed(username))
352
+ throw new NameNotAllowedError('reserved');
353
+ if (getUserByUsername(username))
354
+ throw new AlreadyExistError(`user already exists [name: ${username}]`);
355
+ const now = nowUnix();
356
+ const salt = randomSalt();
357
+ const info = db()
358
+ .prepare(`INSERT INTO user (lower_name, name, full_name, email, passwd, type, description, location,
359
+ website, rands, salt, created_unix, updated_unix, max_repo_creation, is_active,
360
+ avatar, avatar_email, num_teams, num_members)
361
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
362
+ .run(username.toLowerCase(), username, opts.fullName ?? '', doer.email, // orgs reuse creator email as placeholder like gogs
363
+ encodePassword(randomSalt(), salt), 1, opts.description ?? '', opts.location ?? '', opts.website ?? '', randomSalt(), salt, now, now, -1, 1, md5(username + '@localhost'), username + '@localhost', 1, // owners team
364
+ 1);
365
+ const org = getUserByID(Number(info.lastInsertRowid));
366
+ // owners team
367
+ const team = db()
368
+ .prepare('INSERT INTO team (org_id, lower_name, name, description, authorize, num_repos, num_members) VALUES (?,?,?,?,?,0,1)')
369
+ .run(org.id, 'owners', 'Owners', '', 4);
370
+ db().prepare('INSERT INTO team_user (org_id, team_id, uid) VALUES (?,?,?)').run(org.id, Number(team.lastInsertRowid), doer.id);
371
+ db().prepare('INSERT INTO org_user (uid, org_id, is_public, is_owner, num_teams) VALUES (?,?,0,1,1)').run(doer.id, org.id);
372
+ return org;
373
+ }
374
+ export function updateUserColumns(id, cols) {
375
+ const keys = Object.keys(cols);
376
+ if (!keys.length)
377
+ return;
378
+ const setSql = keys.map((k) => `${k} = ?`).join(', ');
379
+ db()
380
+ .prepare(`UPDATE user SET ${setSql}, updated_unix = ? WHERE id = ?`)
381
+ .run(...keys.map((k) => cols[k]), nowUnix(), id);
382
+ }
383
+ // ---------------------------------------------------------------- repositories
384
+ export function getRepoByID(id) {
385
+ const row = db().prepare('SELECT * FROM repository WHERE id = ?').get(id);
386
+ if (!row)
387
+ return null;
388
+ const repo = new Repository(row);
389
+ repo.owner = getUserByID(repo.owner_id) ?? undefined;
390
+ return repo;
391
+ }
392
+ export function getRepoByName(ownerName, repoName) {
393
+ const owner = getUserByUsername(ownerName);
394
+ if (!owner)
395
+ return null;
396
+ const row = db()
397
+ .prepare('SELECT * FROM repository WHERE owner_id = ? AND lower_name = ?')
398
+ .get(owner.id, repoName.toLowerCase());
399
+ if (!row)
400
+ return null;
401
+ const repo = new Repository(row);
402
+ repo.owner = owner;
403
+ return repo;
404
+ }
405
+ export function getRepoByOwnerAndName(owner, repoName) {
406
+ const row = db()
407
+ .prepare('SELECT * FROM repository WHERE owner_id = ? AND lower_name = ?')
408
+ .get(owner.id, repoName.toLowerCase());
409
+ if (!row)
410
+ return null;
411
+ const repo = new Repository(row);
412
+ repo.owner = owner;
413
+ return repo;
414
+ }
415
+ /** dsh 桥用:取第一个管理员(映射 UM 凭据的目标账号)。 */
416
+ export function getFirstAdmin() {
417
+ const r = db().prepare('SELECT * FROM user WHERE type = 0 AND is_admin = 1 ORDER BY id LIMIT 1').get();
418
+ return r ? new User(r) : null;
419
+ }
420
+ export function updateRepoColumns(id, cols) {
421
+ const keys = Object.keys(cols);
422
+ if (!keys.length)
423
+ return;
424
+ const setSql = keys.map((k) => `${k} = ?`).join(', ');
425
+ db()
426
+ .prepare(`UPDATE repository SET ${setSql}, updated_unix = ? WHERE id = ?`)
427
+ .run(...keys.map((k) => cols[k]), nowUnix(), id);
428
+ }
429
+ export function countUserRepos(ownerID) {
430
+ const r = db().prepare('SELECT COUNT(*) AS c FROM repository WHERE owner_id = ?').get(ownerID);
431
+ return r.c;
432
+ }
433
+ export function listReposByOwner(ownerID) {
434
+ return db().prepare('SELECT * FROM repository WHERE owner_id = ? ORDER BY updated_unix DESC').all(ownerID).map((r) => {
435
+ const repo = new Repository(r);
436
+ repo.owner = getUserByID(repo.owner_id) ?? undefined;
437
+ return repo;
438
+ });
439
+ }
440
+ /** Repositories visible to a viewer across all owners (explore). */
441
+ export function listVisibleRepos(viewerID, keyword, page, pageSize, ownerID = 0) {
442
+ const kw = `%${keyword.toLowerCase()}%`;
443
+ let visSQL;
444
+ const visArgs = [];
445
+ if (viewerID > 0) {
446
+ visSQL = `(r.is_private = 0 OR r.owner_id = ? OR r.id IN (
447
+ SELECT repo_id FROM collaboration WHERE user_id = ?
448
+ ) OR r.owner_id IN (
449
+ SELECT org_id FROM org_user WHERE uid = ?
450
+ ) OR r.id IN (
451
+ SELECT tr.repo_id FROM team_repo tr
452
+ JOIN team_user tu ON tu.team_id = tr.team_id WHERE tu.uid = ?
453
+ ))`;
454
+ visArgs.push(viewerID, viewerID, viewerID, viewerID);
455
+ }
456
+ else {
457
+ visSQL = 'r.is_private = 0';
458
+ }
459
+ const ownerSQL = ownerID > 0 ? ' AND r.owner_id = ?' : '';
460
+ const ownerArgs = ownerID > 0 ? [ownerID] : [];
461
+ const where = `WHERE ${visSQL} AND (r.is_bare = 0 OR 1) AND lower(r.lower_name) LIKE ? AND r.is_mirror = 0${ownerSQL}`;
462
+ const total = db()
463
+ .prepare(`SELECT COUNT(*) AS c FROM repository r ${where.replace(/r\./g, 'r.')}`)
464
+ .get(...visArgs, kw, ...ownerArgs).c;
465
+ const rows = db()
466
+ .prepare(`SELECT r.* FROM repository r ${where} ORDER BY r.updated_unix DESC LIMIT ? OFFSET ?`)
467
+ .all(...visArgs, kw, ...ownerArgs, pageSize, (page - 1) * pageSize);
468
+ const repos = rows.map((r) => {
469
+ const repo = new Repository(r);
470
+ repo.owner = getUserByID(repo.owner_id) ?? undefined;
471
+ return repo;
472
+ });
473
+ return { total, repos };
474
+ }
475
+ // ---------------------------------------------------------------- access / permissions
476
+ export const AccessMode = { NONE: 0, READ: 1, WRITE: 2, ADMIN: 3, OWNER: 4 };
477
+ export function accessMode(userID, repo) {
478
+ if (userID <= 0)
479
+ return repo.is_private ? AccessMode.NONE : AccessMode.READ;
480
+ // 站点管理员对任何仓库都有管理权(gogs 语义)
481
+ const siteAdmin = db().prepare('SELECT 1 FROM user WHERE id = ? AND is_admin = 1').get(userID);
482
+ if (siteAdmin)
483
+ return AccessMode.ADMIN;
484
+ if (repo.owner_id === userID)
485
+ return AccessMode.OWNER;
486
+ if (repo.owner && repo.owner.type === 1) {
487
+ // organization ownership
488
+ const ou = db().prepare('SELECT 1 FROM org_user WHERE uid = ? AND org_id = ? AND is_owner = 1').get(userID, repo.owner_id);
489
+ if (ou)
490
+ return AccessMode.OWNER;
491
+ // team access
492
+ const teamMode = db()
493
+ .prepare(`SELECT MAX(t.authorize) AS m FROM team t
494
+ JOIN team_repo tr ON tr.team_id = t.id
495
+ JOIN team_user tu ON tu.team_id = t.id
496
+ WHERE tu.uid = ? AND tr.repo_id = ?`)
497
+ .get(userID, repo.id);
498
+ if (teamMode?.m > 0)
499
+ return Math.max(teamMode.m, AccessMode.READ);
500
+ const member = db().prepare('SELECT 1 FROM org_user WHERE uid = ? AND org_id = ?').get(userID, repo.owner_id);
501
+ if (member && !repo.is_private)
502
+ return AccessMode.READ;
503
+ }
504
+ const col = db().prepare('SELECT mode FROM collaboration WHERE user_id = ? AND repo_id = ?').get(userID, repo.id);
505
+ if (col)
506
+ return col.mode;
507
+ if (!repo.is_private)
508
+ return AccessMode.READ;
509
+ return AccessMode.NONE;
510
+ }
511
+ export function hasAccess(userID, repo) {
512
+ return accessMode(userID, repo) >= AccessMode.READ;
513
+ }
514
+ // ---------------------------------------------------------------- orgs & teams
515
+ export function listUserOrgs(userID, all) {
516
+ // upstream GetOrgsByUserID(all=false) exposes only public memberships
517
+ let sql = `SELECT u.* FROM user u JOIN org_user ou ON ou.org_id = u.id WHERE u.type = 1 AND ou.uid = ?`;
518
+ const args = [userID];
519
+ if (!all)
520
+ sql += ` AND ou.is_public = 1`;
521
+ const rows = db().prepare(sql).all(...args);
522
+ return rows.map((r) => new User(r));
523
+ }
524
+ export function listOrgMembers(orgID, all) {
525
+ let sql = `SELECT u.* FROM user u JOIN org_user ou ON ou.uid = u.id WHERE ou.org_id = ?`;
526
+ if (!all)
527
+ sql += ' AND ou.is_public = 1';
528
+ return db().prepare(sql).all(orgID).map((r) => new User(r));
529
+ }
530
+ export function isOrgMember(userID, orgID) {
531
+ return !!db().prepare('SELECT 1 FROM org_user WHERE uid = ? AND org_id = ?').get(userID, orgID);
532
+ }
533
+ export function isOrgOwner(userID, orgID) {
534
+ return !!db().prepare('SELECT 1 FROM org_user WHERE uid = ? AND org_id = ? AND is_owner = 1').get(userID, orgID);
535
+ }
536
+ export function getTeamByID(id) {
537
+ return db().prepare('SELECT * FROM team WHERE id = ?').get(id) ?? null;
538
+ }
539
+ export function getTeamByName(orgID, name) {
540
+ return db().prepare('SELECT * FROM team WHERE org_id = ? AND lower_name = ?').get(orgID, name.toLowerCase()) ?? null;
541
+ }
542
+ export function listTeamsByOrg(orgID) {
543
+ return db().prepare('SELECT * FROM team WHERE org_id = ? ORDER BY name').all(orgID);
544
+ }
545
+ export function listTeamMembers(teamID) {
546
+ return db().prepare('SELECT u.* FROM user u JOIN team_user tu ON tu.uid = u.id WHERE tu.team_id = ?').all(teamID).map((r) => new User(r));
547
+ }
548
+ export function listTeamRepos(teamID) {
549
+ return db().prepare('SELECT r.* FROM repository r JOIN team_repo tr ON tr.repo_id = r.id WHERE tr.team_id = ?').all(teamID).map((r) => {
550
+ const repo = new Repository(r);
551
+ repo.owner = getUserByID(repo.owner_id) ?? undefined;
552
+ return repo;
553
+ });
554
+ }
555
+ // ---------------------------------------------------------------- follows / stars / watches
556
+ export function isFollowing(userID, targetID) {
557
+ return !!db().prepare('SELECT 1 FROM follow WHERE user_id = ? AND follow_id = ?').get(userID, targetID);
558
+ }
559
+ export function followUser(userID, targetID) {
560
+ if (userID === targetID || isFollowing(userID, targetID))
561
+ return;
562
+ db().prepare('INSERT INTO follow (user_id, follow_id) VALUES (?,?)').run(userID, targetID);
563
+ db().prepare('UPDATE user SET num_following = num_following + 1 WHERE id = ?').run(userID);
564
+ db().prepare('UPDATE user SET num_followers = num_followers + 1 WHERE id = ?').run(targetID);
565
+ }
566
+ export function unfollowUser(userID, targetID) {
567
+ if (!isFollowing(userID, targetID))
568
+ return;
569
+ db().prepare('DELETE FROM follow WHERE user_id = ? AND follow_id = ?').run(userID, targetID);
570
+ db().prepare('UPDATE user SET num_following = num_following - 1 WHERE id = ?').run(userID);
571
+ db().prepare('UPDATE user SET num_followers = num_followers - 1 WHERE id = ?').run(targetID);
572
+ }
573
+ export function listFollowers(userID, page, size) {
574
+ return db()
575
+ .prepare('SELECT u.* FROM user u JOIN follow f ON f.user_id = u.id WHERE f.follow_id = ? LIMIT ? OFFSET ?')
576
+ .all(userID, size, (page - 1) * size).map((r) => new User(r));
577
+ }
578
+ export function listFollowing(userID, page, size) {
579
+ return db()
580
+ .prepare('SELECT u.* FROM user u JOIN follow f ON f.follow_id = u.id WHERE f.user_id = ? LIMIT ? OFFSET ?')
581
+ .all(userID, size, (page - 1) * size).map((r) => new User(r));
582
+ }
583
+ export function isWatching(userID, repoID) {
584
+ return !!db().prepare('SELECT 1 FROM watch WHERE user_id = ? AND repo_id = ?').get(userID, repoID);
585
+ }
586
+ export function isStaring(userID, repoID) {
587
+ return !!db().prepare('SELECT 1 FROM star WHERE uid = ? AND repo_id = ?').get(userID, repoID);
588
+ }
589
+ export function watchRepo(userID, repoID, watch) {
590
+ if (watch && !isWatching(userID, repoID)) {
591
+ db().prepare('INSERT INTO watch (user_id, repo_id) VALUES (?,?)').run(userID, repoID);
592
+ db().prepare('UPDATE repository SET num_watches = num_watches + 1 WHERE id = ?').run(repoID);
593
+ }
594
+ else if (!watch && isWatching(userID, repoID)) {
595
+ db().prepare('DELETE FROM watch WHERE user_id = ? AND repo_id = ?').run(userID, repoID);
596
+ db().prepare('UPDATE repository SET num_watches = num_watches - 1 WHERE id = ?').run(repoID);
597
+ }
598
+ }
599
+ export function starRepo(userID, repoID, star) {
600
+ if (star && !isStaring(userID, repoID)) {
601
+ db().prepare('INSERT INTO star (uid, repo_id) VALUES (?,?)').run(userID, repoID);
602
+ db().prepare('UPDATE repository SET num_stars = num_stars + 1 WHERE id = ?').run(repoID);
603
+ db().prepare('UPDATE user SET num_stars = num_stars + 1 WHERE id = ?').run(userID);
604
+ }
605
+ else if (!star && isStaring(userID, repoID)) {
606
+ db().prepare('DELETE FROM star WHERE uid = ? AND repo_id = ?').run(userID, repoID);
607
+ db().prepare('UPDATE repository SET num_stars = num_stars - 1 WHERE id = ?').run(repoID);
608
+ db().prepare('UPDATE user SET num_stars = num_stars - 1 WHERE id = ?').run(userID);
609
+ }
610
+ }
611
+ export function listStarredRepos(userID) {
612
+ return db().prepare('SELECT r.* FROM repository r JOIN star s ON s.repo_id = r.id WHERE s.uid = ?').all(userID).map((r) => {
613
+ const repo = new Repository(r);
614
+ repo.owner = getUserByID(repo.owner_id) ?? undefined;
615
+ return repo;
616
+ });
617
+ }
618
+ // ---------------------------------------------------------------- access tokens
619
+ export function getAccessTokenBySHA1(sha1) {
620
+ return db().prepare('SELECT * FROM access_token WHERE sha1 = ?').get(sha1) ?? null;
621
+ }
622
+ export function touchAccessToken(id) {
623
+ db().prepare('UPDATE access_token SET updated_unix = ? WHERE id = ?').run(nowUnix(), id);
624
+ }
625
+ export function listAccessTokens(uid) {
626
+ return db().prepare('SELECT * FROM access_token WHERE uid = ? ORDER BY id ASC').all(uid);
627
+ }
628
+ // ---------------------------------------------------------------- notifications for dashboard
629
+ export function countUnreadIssues(userID) {
630
+ const r = db()
631
+ .prepare('SELECT COUNT(*) AS c FROM issue_user WHERE uid = ? AND is_closed = 0 AND is_read = 0')
632
+ .get(userID);
633
+ return r.c;
634
+ }
635
+ // ---------------------------------------------------------------- webhooks
636
+ export function updateWebhookLastStatus(id, status) {
637
+ db().prepare('UPDATE webhook SET last_status = ? WHERE id = ?').run(status, id);
638
+ }
639
+ export function listWebhooks(repoID) {
640
+ return db().prepare('SELECT * FROM webhook WHERE repo_id = ? ORDER BY id DESC').all(repoID);
641
+ }
642
+ export function getWebhookByID(repoID, id) {
643
+ return db().prepare('SELECT * FROM webhook WHERE id = ? AND (repo_id = ? OR org_id = ?)').get(id, repoID, repoID) ?? null;
644
+ }
645
+ // ---------------------------------------------------------------- collaborations
646
+ export function listCollaborations(repoID) {
647
+ return db().prepare('SELECT * FROM collaboration WHERE repo_id = ?').all(repoID);
648
+ }
649
+ export function getCollaboration(repoID, userID) {
650
+ return db().prepare('SELECT * FROM collaboration WHERE repo_id = ? AND user_id = ?').get(repoID, userID) ?? null;
651
+ }
652
+ // ---------------------------------------------------------------- SSH keys
653
+ export function listPublicKeys(ownerID) {
654
+ return db().prepare('SELECT * FROM public_key WHERE owner_id = ? AND type = 1 ORDER BY id DESC').all(ownerID);
655
+ }
656
+ export function getPublicKeyByID(id) {
657
+ return db().prepare('SELECT * FROM public_key WHERE id = ?').get(id) ?? null;
658
+ }
659
+ // ---------------------------------------------------------------- issues
660
+ export function getIssueByIndex(repoID, index) {
661
+ return db().prepare('SELECT * FROM issue WHERE repo_id = ? AND "index" = ?').get(repoID, index) ?? null;
662
+ }
663
+ export function getIssueByID(id) {
664
+ return db().prepare('SELECT * FROM issue WHERE id = ?').get(id) ?? null;
665
+ }
666
+ export function maxIssueIndex(repoID) {
667
+ const r = db().prepare('SELECT MAX("index") AS m FROM issue WHERE repo_id = ?').get(repoID);
668
+ return r.m ?? 0;
669
+ }
670
+ export function listIssues(repoID, isClosed, page, size) {
671
+ let where = 'repo_id = ?';
672
+ const args = [repoID];
673
+ if (isClosed !== null) {
674
+ where += ' AND is_closed = ?';
675
+ args.push(isClosed);
676
+ }
677
+ const total = db().prepare(`SELECT COUNT(*) AS c FROM issue WHERE ${where}`).get(...args).c;
678
+ const rows = db()
679
+ .prepare(`SELECT * FROM issue WHERE ${where} ORDER BY created_unix DESC LIMIT ? OFFSET ?`)
680
+ .all(...args, size, (page - 1) * size);
681
+ return { total, issues: rows };
682
+ }
683
+ export function updateIssueColumns(id, cols) {
684
+ const keys = Object.keys(cols);
685
+ if (!keys.length)
686
+ return;
687
+ const setSql = keys.map((k) => `${k} = ?`).join(', ');
688
+ db().prepare(`UPDATE issue SET ${setSql}, updated_unix = ? WHERE id = ?`).run(...keys.map((k) => cols[k]), nowUnix(), id);
689
+ }
690
+ export function refreshIssueCounts(repoID) {
691
+ db().prepare(`UPDATE repository SET
692
+ num_issues = (SELECT COUNT(*) FROM issue WHERE repo_id = repository.id AND is_pull = 0),
693
+ num_closed_issues = (SELECT COUNT(*) FROM issue WHERE repo_id = repository.id AND is_pull = 0 AND is_closed = 1),
694
+ num_pulls = (SELECT COUNT(*) FROM issue WHERE repo_id = repository.id AND is_pull = 1),
695
+ num_closed_pulls = (SELECT COUNT(*) FROM issue WHERE repo_id = repository.id AND is_pull = 1 AND is_closed = 1)
696
+ WHERE id = ?`).run(repoID);
697
+ }
698
+ // ---------------------------------------------------------------- comments
699
+ export function listComments(issueID) {
700
+ return db().prepare('SELECT * FROM comment WHERE issue_id = ? ORDER BY id ASC').all(issueID);
701
+ }
702
+ export function getCommentByID(id) {
703
+ return db().prepare('SELECT * FROM comment WHERE id = ?').get(id) ?? null;
704
+ }
705
+ // ---------------------------------------------------------------- labels
706
+ export function listLabels(repoID) {
707
+ return db().prepare('SELECT * FROM label WHERE repo_id = ? ORDER BY id ASC').all(repoID);
708
+ }
709
+ export function getLabelByID(repoID, id) {
710
+ return db().prepare('SELECT * FROM label WHERE id = ? AND repo_id = ?').get(id, repoID) ?? null;
711
+ }
712
+ export function getLabelByName(repoID, name) {
713
+ return db().prepare('SELECT * FROM label WHERE name = ? AND repo_id = ?').get(name, repoID) ?? null;
714
+ }
715
+ export function listIssueLabels(issueID) {
716
+ return db().prepare('SELECT l.* FROM label l JOIN issue_label il ON il.label_id = l.id WHERE il.issue_id = ? ORDER BY l.id').all(issueID);
717
+ }
718
+ // ---------------------------------------------------------------- milestones
719
+ export function listMilestones(repoID, isClosed) {
720
+ if (isClosed === null) {
721
+ return db().prepare('SELECT * FROM milestone WHERE repo_id = ? ORDER BY deadline_unix ASC, id ASC').all(repoID);
722
+ }
723
+ return db().prepare('SELECT * FROM milestone WHERE repo_id = ? AND is_closed = ? ORDER BY deadline_unix ASC').all(repoID, isClosed);
724
+ }
725
+ export function getMilestoneByID(repoID, id) {
726
+ return db().prepare('SELECT * FROM milestone WHERE id = ? AND repo_id = ?').get(id, repoID) ?? null;
727
+ }
728
+ export function refreshMilestoneCounts(repoID) {
729
+ db().prepare(`UPDATE repository SET
730
+ num_milestones = (SELECT COUNT(*) FROM milestone WHERE repo_id = repository.id AND is_closed = 0),
731
+ num_closed_milestones = (SELECT COUNT(*) FROM milestone WHERE repo_id = repository.id AND is_closed = 1)
732
+ WHERE id = ?`).run(repoID);
733
+ }
734
+ // ---------------------------------------------------------------- releases
735
+ export function listReleases(repoID) {
736
+ return db().prepare('SELECT * FROM release WHERE repo_id = ? ORDER BY created_unix DESC').all(repoID);
737
+ }
738
+ export function getReleaseByID(repoID, id) {
739
+ return db().prepare('SELECT * FROM release WHERE id = ? AND repo_id = ?').get(id, repoID) ?? null;
740
+ }
741
+ // ---------------------------------------------------------------- email addresses
742
+ export function listEmailAddresses(uid) {
743
+ return db().prepare('SELECT * FROM email_address WHERE uid = ? ORDER BY id').all(uid);
744
+ }
745
+ export function getEmailAddress(email) {
746
+ return db().prepare('SELECT * FROM email_address WHERE lower(email) = ?').get(email.toLowerCase()) ?? null;
747
+ }
748
+ // ---------------------------------------------------------------- notices (admin)
749
+ export function createNotice(description) {
750
+ db().prepare('INSERT INTO notice (type, description, created_unix) VALUES (1, ?, ?)').run(description, nowUnix());
751
+ }
752
+ export function listNotices(page, size) {
753
+ const total = db().prepare('SELECT COUNT(*) AS c FROM notice').get().c;
754
+ const rows = db().prepare('SELECT * FROM notice ORDER BY id DESC LIMIT ? OFFSET ?').all(size, (page - 1) * size);
755
+ return { total, notices: rows };
756
+ }
757
+ // ---------------------------------------------------------------- Go field aliasing
758
+ // Gogs templates read exported Go fields (`.Repository.Name`, `.Owner.AvatarURLPath`).
759
+ // goAlias adds uppercase aliases over snake_case row columns plus method shims.
760
+ /** Go initialism exports (ID not Id, URL not Url, ...). */
761
+ const GO_INITIALISMS = {
762
+ id: 'ID', ids: 'IDs', uid: 'UID', uids: 'UIDs', url: 'URL', urls: 'URLs',
763
+ uri: 'URI', api: 'API', ssh: 'SSH', http: 'HTTP', https: 'HTTPS', html: 'HTML',
764
+ ssl: 'SSL', tls: 'TLS', ip: 'IP', uuid: 'UUID', json: 'JSON', sha: 'SHA', ok: 'OK',
765
+ };
766
+ function capSegment(seg) {
767
+ if (GO_INITIALISMS[seg.toLowerCase()])
768
+ return GO_INITIALISMS[seg.toLowerCase()];
769
+ return seg[0].toUpperCase() + seg.slice(1);
770
+ }
771
+ function goExport(key) {
772
+ if (key.includes('_')) {
773
+ return key.split('_').map(capSegment).join('');
774
+ }
775
+ return capSegment(key);
776
+ }
777
+ /** Unix-second columns that templates read as time.Time. */
778
+ const GO_TIME_COLUMNS = {
779
+ created_unix: 'Created',
780
+ updated_unix: 'Updated',
781
+ merged_unix: 'Merged',
782
+ closed_date_unix: 'ClosedDate',
783
+ deadline_unix: 'Deadline',
784
+ next_update_unix: 'NextUpdate',
785
+ updated_unix_created: 'Updated',
786
+ };
787
+ export function goAlias(row) {
788
+ for (const key of Object.keys(row)) {
789
+ // time columns: expose as Date under their Go time.Time field name
790
+ if (GO_TIME_COLUMNS[key] && !(GO_TIME_COLUMNS[key] in row)) {
791
+ const exported = GO_TIME_COLUMNS[key];
792
+ Object.defineProperty(row, exported, {
793
+ get() {
794
+ const v = this[key];
795
+ return v ? new Date(v * 1000) : new Date(0);
796
+ },
797
+ set(unix) {
798
+ this[key] = unix;
799
+ },
800
+ enumerable: false,
801
+ configurable: true,
802
+ });
803
+ continue;
804
+ }
805
+ if (key.includes('_')) {
806
+ const exported = goExport(key);
807
+ if (!(exported in row)) {
808
+ Object.defineProperty(row, exported, {
809
+ get() {
810
+ return this[key];
811
+ },
812
+ set(v) {
813
+ this[key] = v;
814
+ },
815
+ enumerable: false,
816
+ configurable: true,
817
+ });
818
+ }
819
+ }
820
+ else {
821
+ const exported = goExport(key);
822
+ if (!(exported in row)) {
823
+ Object.defineProperty(row, exported, {
824
+ get() {
825
+ return this[key];
826
+ },
827
+ set(v) {
828
+ this[key] = v;
829
+ },
830
+ enumerable: false,
831
+ configurable: true,
832
+ });
833
+ }
834
+ }
835
+ }
836
+ return row;
837
+ }
838
+ //# sourceMappingURL=db.js.map