@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,2029 @@
1
+ import { conf } from '../conf.js';
2
+ import * as db from '../db/db.js';
3
+ import { authenticateUserByToken } from '../context.js';
4
+ import { User, Repository } from '../db/db.js';
5
+ import * as git from '../gitx/git.js';
6
+ import { sanitizeHTML, markdown } from '../markup.js';
7
+ const DOCS_URL = 'https://github.com/gogs/docs-api';
8
+ const ITEMS_PER_PAGE = 40; // database.ItemsPerPage
9
+ // ---------------------------------------------------------------- API context
10
+ class APIContext {
11
+ c;
12
+ user;
13
+ isTokenAuth = false;
14
+ isBasicAuth = false;
15
+ repo = { Repository: null, Owner: null, AccessMode: 0 };
16
+ org = { Organization: null, Team: null };
17
+ constructor(c) {
18
+ this.c = c;
19
+ this.user = c.User;
20
+ this.isTokenAuth = c.isTokenAuth;
21
+ this.isBasicAuth = c.isBasicAuth;
22
+ }
23
+ UserID() {
24
+ return this.user?.id ?? 0;
25
+ }
26
+ get IsLogged() {
27
+ return this.user !== null;
28
+ }
29
+ error(message) {
30
+ this.c.JSON(500, { message, url: DOCS_URL });
31
+ }
32
+ errorStatus(status, message) {
33
+ this.c.JSON(status, { message, url: DOCS_URL });
34
+ }
35
+ notFound() {
36
+ this.c.Status(404);
37
+ this.c.res.end();
38
+ this.c.rendered = true;
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------- JSON types (contract shapes)
42
+ function toUser(u, exposeEmail) {
43
+ const out = {
44
+ id: u.id,
45
+ username: u.name,
46
+ login: u.name,
47
+ full_name: sanitizeHTML(u.full_name ?? ''),
48
+ email: exposeEmail ? u.email : '',
49
+ avatar_url: u.AvatarURL(),
50
+ };
51
+ return out;
52
+ }
53
+ function toRepository(repo, perms) {
54
+ const owner = repo.owner ?? new User({ id: repo.owner_id, name: repo.lower_name, lower_name: repo.lower_name, email: '' });
55
+ const base = {
56
+ id: repo.id,
57
+ owner: toUser(owner, true),
58
+ name: repo.name,
59
+ full_name: repo.FullName(),
60
+ description: repo.description ?? '',
61
+ private: !!repo.is_private,
62
+ fork: !!repo.is_fork,
63
+ mirror: !!repo.is_mirror,
64
+ size: repo.size,
65
+ html_url: repo.HTMLURL(),
66
+ ssh_url: repoSSHURL(repo),
67
+ clone_url: repo.CloneURL(),
68
+ website: repo.website ?? '',
69
+ stars_count: repo.num_stars,
70
+ forks_count: repo.num_forks,
71
+ watchers_count: repo.num_watches,
72
+ open_issues_count: repo.num_issues - repo.num_closed_issues,
73
+ default_branch: repo.default_branch,
74
+ created_at: new Date((repo.created_unix ?? 0) * 1000).toISOString(),
75
+ updated_at: new Date((repo.updated_unix ?? 0) * 1000).toISOString(),
76
+ };
77
+ if (repo.is_bare)
78
+ base.empty = true;
79
+ if (perms)
80
+ base.permissions = perms;
81
+ return base;
82
+ }
83
+ function repoSSHURL(repo) {
84
+ const user = conf.runUser || 'git';
85
+ const host = conf.sshDomain === 'localhost' ? conf.domain : conf.sshDomain;
86
+ if (conf.sshPort === 22)
87
+ return `${user}@${host}:${repo.FullName()}.git`;
88
+ return `ssh://${user}@${host}:${conf.sshPort}/${repo.FullName()}.git`;
89
+ }
90
+ function toIssue(issue, repo) {
91
+ const poster = db.getUserByID(issue.poster_id);
92
+ const milestone = issue.milestone_id ? db.getMilestoneByID(repo.id, issue.milestone_id) : null;
93
+ const assignee = issue.assignee_id ? db.getUserByID(issue.assignee_id) : null;
94
+ const pr = issue.is_pull ? db.db().prepare('SELECT * FROM pull_request WHERE issue_id = ?').get(issue.id) : null;
95
+ const labels = db.listIssueLabels(issue.id).map(toIssueLabel);
96
+ const out = {
97
+ id: issue.id,
98
+ number: issue.index,
99
+ user: poster ? toUser(poster, true) : null,
100
+ title: issue.name,
101
+ body: issue.content ?? '',
102
+ labels,
103
+ milestone: milestone ? toIssueMilestone(milestone) : null,
104
+ assignee: assignee ? toUser(assignee, true) : null,
105
+ state: issue.is_closed ? 'closed' : 'open',
106
+ comments: issue.num_comments ?? 0,
107
+ created_at: new Date((issue.created_unix ?? 0) * 1000).toISOString(),
108
+ updated_at: new Date((issue.updated_unix ?? 0) * 1000).toISOString(),
109
+ };
110
+ if (pr) {
111
+ out.pull_request = {
112
+ merged: !!pr.has_merged,
113
+ merged_at: pr.has_merged && pr.merged_unix ? new Date(pr.merged_unix * 1000).toISOString() : null,
114
+ };
115
+ }
116
+ return out;
117
+ }
118
+ function toIssueLabel(l) {
119
+ return {
120
+ id: l.id,
121
+ name: l.name,
122
+ color: String(l.color ?? '').replace(/^#/, ''),
123
+ url: '',
124
+ };
125
+ }
126
+ function toIssueMilestone(m) {
127
+ const out = {
128
+ id: m.id,
129
+ title: m.name,
130
+ description: m.content ?? '',
131
+ state: m.is_closed ? 'closed' : 'open',
132
+ open_issues: m.num_issues ?? 0,
133
+ closed_issues: m.num_closed_issues ?? 0,
134
+ closed_at: m.closed_date_unix ? new Date(m.closed_date_unix * 1000).toISOString() : null,
135
+ due_on: null,
136
+ };
137
+ if (m.deadline_unix && new Date(m.deadline_unix * 1000).getUTCFullYear() < 9999) {
138
+ out.due_on = new Date(m.deadline_unix * 1000).toISOString();
139
+ }
140
+ return out;
141
+ }
142
+ function toIssueComment(cm, issue, repo) {
143
+ const poster = db.getUserByID(cm.poster_id);
144
+ return {
145
+ id: cm.id,
146
+ html_url: `${repo.HTMLURL()}/issues/${issue.index}#issuecomment-${cm.id}`,
147
+ user: poster ? toUser(poster, true) : null,
148
+ body: cm.content ?? '',
149
+ created_at: new Date((cm.created_unix ?? 0) * 1000).toISOString(),
150
+ updated_at: new Date((cm.updated_unix ?? 0) * 1000).toISOString(),
151
+ };
152
+ }
153
+ function toOrganization(org) {
154
+ return {
155
+ id: org.id,
156
+ username: org.name,
157
+ full_name: org.full_name ?? '',
158
+ avatar_url: org.AvatarURL(),
159
+ description: org.description ?? '',
160
+ website: org.website ?? '',
161
+ location: org.location ?? '',
162
+ };
163
+ }
164
+ function toOrganizationTeam(t) {
165
+ const modeName = ['none', 'read', 'write', 'admin', 'owner'][t.authorize] ?? 'none';
166
+ return {
167
+ id: t.id,
168
+ name: t.name,
169
+ description: t.description ?? '',
170
+ permission: modeName,
171
+ };
172
+ }
173
+ function toRelease(r, repo) {
174
+ const publisher = db.getUserByID(r.publisher_id);
175
+ return {
176
+ id: r.id,
177
+ tag_name: r.tag_name,
178
+ target_commitish: r.target ?? '',
179
+ name: r.title ?? '',
180
+ body: r.note ?? '',
181
+ draft: !!r.is_draft,
182
+ prerelease: !!r.is_prerelease,
183
+ author: publisher ? toUser(publisher, true) : null,
184
+ created_at: new Date((r.created_unix ?? 0) * 1000).toISOString(),
185
+ ...(repo ? {} : {}),
186
+ };
187
+ }
188
+ function commitToWebhookPayloadCommit(repoDir, sha) {
189
+ return {
190
+ id: sha,
191
+ message: '',
192
+ url: 'Not implemented',
193
+ author: { name: '', email: '', username: '' },
194
+ committer: { name: '', email: '', username: '' },
195
+ added: null,
196
+ removed: null,
197
+ modified: null,
198
+ timestamp: new Date(0).toISOString(),
199
+ ...(repoDir ? {} : {}),
200
+ };
201
+ }
202
+ function commitToAPICommit(c, repo, commit, apiPath) {
203
+ const authorUser = db.getUserByEmail(commit.author.email);
204
+ const committerUser = db.getUserByEmail(commit.committer.email);
205
+ return {
206
+ url: conf.externalURL + apiPath.slice(1),
207
+ sha: commit.id,
208
+ html_url: `${repo.HTMLURL()}/commits/${commit.id}`,
209
+ commit: {
210
+ url: conf.externalURL + apiPath.slice(1),
211
+ author: {
212
+ name: commit.author.name,
213
+ email: commit.author.email,
214
+ date: commit.author.when.toISOString(),
215
+ },
216
+ committer: {
217
+ name: commit.committer.name,
218
+ email: commit.committer.email,
219
+ date: commit.committer.when.toISOString(),
220
+ },
221
+ message: commit.Summary(),
222
+ tree: {
223
+ url: conf.externalURL + apiPath.slice(1),
224
+ sha: commit.id,
225
+ },
226
+ },
227
+ author: authorUser ? toUser(authorUser, true) : null,
228
+ committer: committerUser ? toUser(committerUser, true) : null,
229
+ parents: commit.parents.map((p) => ({ url: conf.externalURL + apiPath.slice(1), sha: p })),
230
+ };
231
+ }
232
+ // ---------------------------------------------------------------- middleware
233
+ async function apiContext(c) {
234
+ // token auth (API paths)
235
+ const authHead = String(c.req.headers.authorization ?? '');
236
+ const parts = authHead.split(' ');
237
+ if (parts.length === 2 && parts[0] === 'token') {
238
+ const u = authenticateUserByToken(parts[1]);
239
+ if (u) {
240
+ c.User = u;
241
+ c.isTokenAuth = true;
242
+ }
243
+ }
244
+ else if (parts.length === 2 && parts[0] === 'Basic') {
245
+ const { authenticateUserByBasic } = await import('../context.js');
246
+ const r = await authenticateUserByBasic(authHead);
247
+ if (r) {
248
+ c.User = r.user;
249
+ c.isBasicAuth = true;
250
+ }
251
+ }
252
+ c.Data['IsLogged'] = c.IsLogged;
253
+ return new APIContext(c);
254
+ }
255
+ function reqToken(ctx) {
256
+ if (!ctx.isTokenAuth) {
257
+ ctx.c.Status(401);
258
+ ctx.c.res.end();
259
+ ctx.c.rendered = true;
260
+ return false;
261
+ }
262
+ return true;
263
+ }
264
+ function reqBasicAuth(ctx) {
265
+ if (!ctx.isBasicAuth) {
266
+ ctx.c.Status(401);
267
+ ctx.c.res.end();
268
+ ctx.c.rendered = true;
269
+ return false;
270
+ }
271
+ return true;
272
+ }
273
+ function reqAdmin(ctx) {
274
+ if (!ctx.IsLogged || ctx.user.is_admin !== 1) {
275
+ ctx.c.Status(403);
276
+ ctx.c.res.end();
277
+ ctx.c.rendered = true;
278
+ return false;
279
+ }
280
+ return true;
281
+ }
282
+ async function repoAssignment(ctx, username, reponame) {
283
+ let owner;
284
+ if (ctx.IsLogged && ctx.user.lower_name === username.toLowerCase()) {
285
+ owner = ctx.user;
286
+ }
287
+ else {
288
+ const found = db.getUserByUsername(username);
289
+ if (!found) {
290
+ ctx.notFound();
291
+ return false;
292
+ }
293
+ owner = found;
294
+ }
295
+ const repo = db.getRepoByOwnerAndName(owner, reponame);
296
+ if (!repo) {
297
+ ctx.notFound();
298
+ return false;
299
+ }
300
+ ctx.repo.Owner = owner;
301
+ ctx.repo.Repository = repo;
302
+ if (ctx.isTokenAuth && ctx.user?.is_admin === 1) {
303
+ ctx.repo.AccessMode = db.AccessMode.OWNER;
304
+ }
305
+ else {
306
+ ctx.repo.AccessMode = db.accessMode(ctx.UserID(), repo);
307
+ }
308
+ if (ctx.repo.AccessMode < db.AccessMode.READ) {
309
+ ctx.notFound();
310
+ return false;
311
+ }
312
+ return true;
313
+ }
314
+ function setLinkHeader(c, total, pageSize) {
315
+ const page = Math.max(1, c.QueryInt('page') || 1);
316
+ const last = Math.max(1, Math.ceil(total / pageSize));
317
+ const base = conf.externalURL.replace(/\/$/, '') + c.Path();
318
+ const links = [];
319
+ if (page < last)
320
+ links.push(`<${base}?page=${page + 1}>; rel="next"`, `<${base}?page=${last}>; rel="last"`);
321
+ if (page > 1)
322
+ links.push(`<${base}?page=1>; rel="first"`, `<${base}?page=${page - 1}>; rel="prev"`);
323
+ if (links.length)
324
+ c.SetHeader('Link', links.join(', '));
325
+ }
326
+ // ---------------------------------------------------------------- registration
327
+ export function registerAPIRoutes(m) {
328
+ // wrap every handler through the API contexter
329
+ const wrap = (fn) => async (c) => {
330
+ const ctx = await apiContext(c);
331
+ await fn(ctx);
332
+ };
333
+ m.options('/api/v1/*', () => { });
334
+ // Miscellaneous
335
+ m.post('/api/v1/markdown', wrap(async (ctx) => {
336
+ const body = await ctx.c.form();
337
+ const text = String(body.Text ?? body.text ?? '');
338
+ if (!text) {
339
+ ctx.c.PlainText(200, '');
340
+ return;
341
+ }
342
+ const context = String(body.Context ?? body.context ?? '');
343
+ ctx.c.PlainText(200, markdown(text, context, {}));
344
+ }));
345
+ m.post('/api/v1/markdown/raw', wrap(async (ctx) => {
346
+ const raw = (await ctx.c.body()).toString('utf8');
347
+ ctx.c.PlainText(200, markdown(raw, conf.subpath + '/', {}));
348
+ }));
349
+ // Users
350
+ m.get('/api/v1/users/search', wrap(async (ctx) => {
351
+ const q = ctx.c.Query('q');
352
+ const limit = Math.max(1, ctx.c.QueryInt('limit') || 10);
353
+ const rows = db.db().prepare('SELECT * FROM user WHERE type = 0 AND (lower_name LIKE ? OR lower(full_name) LIKE ?) ORDER BY id LIMIT ?').all(`%${q.toLowerCase()}%`, `%${q.toLowerCase()}%`, limit);
354
+ ctx.c.JSONSuccess({ ok: true, data: rows.map((r) => toUser(new User(r), ctx.IsLogged)) });
355
+ }));
356
+ m.get('/api/v1/users/:username', wrap(async (ctx) => {
357
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
358
+ if (!u) {
359
+ ctx.notFound();
360
+ return;
361
+ }
362
+ ctx.c.JSONSuccess(toUser(u, ctx.IsLogged));
363
+ }));
364
+ m.get('/api/v1/users/:username/tokens', wrap(async (ctx) => {
365
+ if (!reqBasicAuth(ctx))
366
+ return;
367
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
368
+ if (!u || (ctx.user.id !== u.id && ctx.user.is_admin !== 1)) {
369
+ ctx.notFound();
370
+ return;
371
+ }
372
+ const tokens = db.listAccessTokens(u.id);
373
+ ctx.c.JSONSuccess(tokens.map((t) => ({ name: t.name, sha1: t.sha1 })));
374
+ }));
375
+ m.post('/api/v1/users/:username/tokens', wrap(async (ctx) => {
376
+ if (!reqBasicAuth(ctx))
377
+ return;
378
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
379
+ if (!u || (ctx.user.id !== u.id && ctx.user.is_admin !== 1)) {
380
+ ctx.notFound();
381
+ return;
382
+ }
383
+ const body = await ctx.c.form();
384
+ const name = String(body.name ?? '').trim();
385
+ if (!name) {
386
+ ctx.c.JSON(422, [{ fieldNames: ['name'], classification: 'RequiredError', message: 'Required' }]);
387
+ return;
388
+ }
389
+ const exists = db.db().prepare('SELECT 1 FROM access_token WHERE uid = ? AND name = ?').get(u.id, name);
390
+ if (exists) {
391
+ ctx.errorStatus(422, `access token name has been used: ${name}`);
392
+ return;
393
+ }
394
+ const { newTokenSHA1, sha256 } = await import('../authx/password.js');
395
+ const sha1 = newTokenSHA1();
396
+ const now = Math.floor(Date.now() / 1000);
397
+ db.db().prepare('INSERT INTO access_token (uid, name, sha1, sha256, created_unix, updated_unix) VALUES (?,?,?,?,?,?)').run(u.id, name, sha1, sha256(sha1), now, now);
398
+ ctx.c.JSON(201, { name, sha1 });
399
+ }));
400
+ const reqTokenWrap = (fn) => wrap(async (ctx) => {
401
+ if (!reqToken(ctx))
402
+ return;
403
+ await fn(ctx);
404
+ });
405
+ m.get('/api/v1/users/:username/followers', reqTokenWrap(async (ctx) => {
406
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
407
+ if (!u) {
408
+ ctx.notFound();
409
+ return;
410
+ }
411
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
412
+ ctx.c.JSONSuccess(db.listFollowers(u.id, page, ITEMS_PER_PAGE).map((x) => toUser(x, true)));
413
+ }));
414
+ m.get('/api/v1/users/:username/following', reqTokenWrap(async (ctx) => {
415
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
416
+ if (!u) {
417
+ ctx.notFound();
418
+ return;
419
+ }
420
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
421
+ ctx.c.JSONSuccess(db.listFollowing(u.id, page, ITEMS_PER_PAGE).map((x) => toUser(x, true)));
422
+ }));
423
+ m.get('/api/v1/users/:username/following/:target', reqTokenWrap(async (ctx) => {
424
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
425
+ const target = db.getUserByUsername(ctx.c.Params(':target'));
426
+ if (!u || !target) {
427
+ ctx.notFound();
428
+ return;
429
+ }
430
+ if (db.isFollowing(u.id, target.id)) {
431
+ ctx.c.NoContent();
432
+ }
433
+ else {
434
+ ctx.notFound();
435
+ }
436
+ }));
437
+ // ----- /user (authenticated) -----
438
+ m.get('/api/v1/user', reqTokenWrap(async (ctx) => {
439
+ ctx.c.JSONSuccess(toUser(ctx.user, true));
440
+ }));
441
+ m.get('/api/v1/user/emails', reqTokenWrap(async (ctx) => {
442
+ const emails = db.listEmailAddresses(ctx.UserID());
443
+ ctx.c.JSONSuccess(emails.map((e) => ({
444
+ email: e.email,
445
+ verified: !!e.is_activated,
446
+ primary: e.email === ctx.user.email,
447
+ })));
448
+ }));
449
+ m.post('/api/v1/user/emails', reqTokenWrap(async (ctx) => {
450
+ const body = (await ctx.c.form());
451
+ const emails = Array.isArray(body.emails) ? body.emails : [];
452
+ if (!emails.length) {
453
+ ctx.c.Status(422);
454
+ ctx.c.res.end();
455
+ ctx.c.rendered = true;
456
+ return;
457
+ }
458
+ const created = [];
459
+ for (const emailRaw of emails) {
460
+ const email = String(emailRaw).toLowerCase().trim();
461
+ if (db.getUserByEmail(email) || db.getEmailAddress(email)) {
462
+ ctx.errorStatus(422, `email address has been used: ${email}`);
463
+ return;
464
+ }
465
+ db.db().prepare('INSERT INTO email_address (uid, email, is_activated) VALUES (?,?,?)').run(ctx.UserID(), email, conf.requireEmailConfirmation ? 0 : 1);
466
+ created.push({ email, verified: !conf.requireEmailConfirmation, primary: false });
467
+ }
468
+ ctx.c.JSON(201, created);
469
+ }));
470
+ m.delete('/api/v1/user/emails', reqTokenWrap(async (ctx) => {
471
+ const body = (await ctx.c.form());
472
+ const emails = Array.isArray(body.emails) ? body.emails : [];
473
+ for (const emailRaw of emails) {
474
+ const email = String(emailRaw).toLowerCase().trim();
475
+ if (email === ctx.user.email) {
476
+ ctx.errorStatus(400, `cannot delete primary email "${email}"`);
477
+ return;
478
+ }
479
+ const row = db.getEmailAddress(email);
480
+ if (row && row.uid === ctx.UserID()) {
481
+ db.db().prepare('DELETE FROM email_address WHERE id = ?').run(row.id);
482
+ }
483
+ }
484
+ ctx.c.NoContent();
485
+ }));
486
+ m.get('/api/v1/user/followers', reqTokenWrap(async (ctx) => {
487
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
488
+ ctx.c.JSONSuccess(db.listFollowers(ctx.UserID(), page, ITEMS_PER_PAGE).map((x) => toUser(x, true)));
489
+ }));
490
+ m.get('/api/v1/user/following', reqTokenWrap(async (ctx) => {
491
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
492
+ ctx.c.JSONSuccess(db.listFollowing(ctx.UserID(), page, ITEMS_PER_PAGE).map((x) => toUser(x, true)));
493
+ }));
494
+ m.get('/api/v1/user/following/:username', reqTokenWrap(async (ctx) => {
495
+ const target = db.getUserByUsername(ctx.c.Params(':username'));
496
+ if (target && db.isFollowing(ctx.UserID(), target.id)) {
497
+ ctx.c.NoContent();
498
+ }
499
+ else {
500
+ ctx.notFound();
501
+ }
502
+ }));
503
+ m.put('/api/v1/user/following/:username', reqTokenWrap(async (ctx) => {
504
+ const target = db.getUserByUsername(ctx.c.Params(':username'));
505
+ if (!target) {
506
+ ctx.errorStatus(422, `user does not exist [name: ${ctx.c.Params(':username')}]`);
507
+ return;
508
+ }
509
+ db.followUser(ctx.UserID(), target.id);
510
+ ctx.c.NoContent();
511
+ }));
512
+ m.delete('/api/v1/user/following/:username', reqTokenWrap(async (ctx) => {
513
+ const target = db.getUserByUsername(ctx.c.Params(':username'));
514
+ if (target)
515
+ db.unfollowUser(ctx.UserID(), target.id);
516
+ ctx.c.NoContent();
517
+ }));
518
+ m.get('/api/v1/user/issues', reqTokenWrap(async (ctx) => listUserIssues(ctx)));
519
+ m.get('/api/v1/issues', reqTokenWrap(async (ctx) => listUserIssues(ctx)));
520
+ // ----- repos -----
521
+ const listReposOfUser = reqTokenWrap(async (ctx) => {
522
+ const username = ctx.c.Params(':username') || ctx.c.Params(':org') || '';
523
+ const target = username ? db.getUserByUsername(username) : ctx.user;
524
+ if (!target) {
525
+ ctx.notFound();
526
+ return;
527
+ }
528
+ if (target.type === 1) {
529
+ const viewerMode = db.isOrgMember(ctx.UserID(), target.id) ? db.AccessMode.READ : db.AccessMode.NONE;
530
+ const repos = db.listReposByOwner(target.id).filter((r) => !r.is_private || viewerMode >= db.AccessMode.READ);
531
+ ctx.c.JSONSuccess(repos.map((r) => toRepository(r, { admin: true, push: true, pull: true })));
532
+ return;
533
+ }
534
+ if (target.id === ctx.UserID()) {
535
+ const own = db.listReposByOwner(target.id);
536
+ const collabRows = db.db().prepare('SELECT r.* FROM repository r JOIN collaboration col ON col.repo_id = r.id WHERE col.user_id = ?').all(ctx.UserID());
537
+ const out = own.map((r) => toRepository(r, { admin: true, push: true, pull: true }));
538
+ for (const row of collabRows) {
539
+ const r = new Repository(row);
540
+ r.owner = db.getUserByID(r.owner_id) ?? undefined;
541
+ const mode = db.accessMode(ctx.UserID(), r);
542
+ out.push(toRepository(r, { admin: mode >= db.AccessMode.ADMIN, push: mode >= db.AccessMode.WRITE, pull: mode >= db.AccessMode.READ }));
543
+ }
544
+ ctx.c.JSONSuccess(out);
545
+ return;
546
+ }
547
+ const repos = db.listReposByOwner(target.id).filter((r) => !r.is_private);
548
+ ctx.c.JSONSuccess(repos.map((r) => toRepository(r, { admin: false, push: false, pull: true })));
549
+ });
550
+ m.get('/api/v1/users/:username/repos', listReposOfUser);
551
+ m.get('/api/v1/orgs/:org/repos', listReposOfUser);
552
+ m.get('/api/v1/user/repos', listReposOfUser);
553
+ m.post('/api/v1/user/repos', reqTokenWrap(async (ctx) => createRepoHandler(ctx, ctx.user)));
554
+ m.post('/api/v1/org/:org/repos', reqTokenWrap(async (ctx) => {
555
+ const org = db.getUserByUsername(ctx.c.Params(':org'));
556
+ if (!org || org.type !== 1) {
557
+ ctx.notFound();
558
+ return;
559
+ }
560
+ if (!db.isOrgOwner(ctx.UserID(), org.id)) {
561
+ ctx.errorStatus(403, 'Given user is not owner of organization.');
562
+ return;
563
+ }
564
+ await createRepoHandler(ctx, org);
565
+ }));
566
+ m.get('/api/v1/repos/search', wrap(async (ctx) => {
567
+ const qRaw = ctx.c.Query('q');
568
+ const q = qRaw.split('/').pop() ?? '';
569
+ const uid = ctx.c.QueryInt('uid');
570
+ const limitRaw = ctx.c.QueryInt('limit');
571
+ const limit = limitRaw <= 0 ? 10 : Math.min(limitRaw, conf.maxResponseItems);
572
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
573
+ const { total, repos } = db.listVisibleRepos(ctx.UserID(), q, page, limit, uid);
574
+ setLinkHeader(ctx.c, total, limit);
575
+ ctx.c.JSONSuccess({ ok: true, data: repos.map((r) => toRepository(r, null)) });
576
+ }));
577
+ const repoGroup = (fn) => reqTokenWrap(async (ctx) => {
578
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
579
+ if (!ok)
580
+ return;
581
+ await fn(ctx);
582
+ });
583
+ const repoGroupNoToken = (fn) => wrap(async (ctx) => {
584
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
585
+ if (!ok)
586
+ return;
587
+ await fn(ctx);
588
+ });
589
+ m.post('/api/v1/repos/migrate', reqTokenWrap(async (ctx) => {
590
+ ctx.errorStatus(422, 'Migration is not supported by this build.');
591
+ }));
592
+ m.delete('/api/v1/repos/:username/:reponame', repoGroup(async (ctx) => {
593
+ if (ctx.repo.AccessMode < db.AccessMode.OWNER) {
594
+ ctx.c.Status(403);
595
+ ctx.c.res.end();
596
+ ctx.c.rendered = true;
597
+ return;
598
+ }
599
+ const repo = ctx.repo.Repository;
600
+ const fs = await import('node:fs');
601
+ fs.rmSync(repo.RepoPath(), { recursive: true, force: true });
602
+ fs.rmSync(repo.WikiPath(), { recursive: true, force: true });
603
+ db.db().prepare('DELETE FROM repository WHERE id = ?').run(repo.id);
604
+ db.db().prepare('UPDATE user SET num_repos = MAX(num_repos - 1, 0) WHERE id = ?').run(repo.owner_id);
605
+ ctx.c.NoContent();
606
+ }));
607
+ m.get('/api/v1/repos/:username/:reponame', repoGroupNoToken(async (ctx) => {
608
+ ctx.c.JSONSuccess(toRepository(ctx.repo.Repository, {
609
+ admin: ctx.repo.AccessMode >= db.AccessMode.ADMIN,
610
+ push: ctx.repo.AccessMode >= db.AccessMode.WRITE,
611
+ pull: true,
612
+ }));
613
+ }));
614
+ m.get('/api/v1/repos/:username/:reponame/releases', repoGroupNoToken(async (ctx) => {
615
+ const releases = db.listReleases(ctx.repo.Repository.id);
616
+ ctx.c.JSONSuccess(releases.map((r) => toRelease(r, ctx.repo.Repository)));
617
+ }));
618
+ registerRepoSubRoutes(m, wrap, reqTokenWrap, repoGroup, repoGroupNoToken);
619
+ // ----- orgs -----
620
+ m.get('/api/v1/user/orgs', reqTokenWrap(async (ctx) => {
621
+ const orgs = db.listUserOrgs(ctx.UserID(), true);
622
+ ctx.c.JSONSuccess(orgs.map(toOrganization));
623
+ }));
624
+ m.post('/api/v1/user/orgs', reqTokenWrap(async (ctx) => {
625
+ const body = (await ctx.c.form());
626
+ const username = String(body.username ?? '').trim();
627
+ if (!/^[a-zA-Z0-9_.-]+$/.test(username) || username.length > 35) {
628
+ ctx.c.JSON(422, [{ fieldNames: ['username'], classification: 'AlphaDashDotError', message: 'Must be valid alpha or numeric or dash(-) and dot(-) characters.' }]);
629
+ return;
630
+ }
631
+ try {
632
+ const org = db.createOrganization(ctx.user, username, {
633
+ fullName: String(body.full_name ?? ''),
634
+ description: String(body.description ?? ''),
635
+ website: String(body.website ?? ''),
636
+ location: String(body.location ?? ''),
637
+ });
638
+ ctx.c.JSON(201, toOrganization(org));
639
+ }
640
+ catch (e) {
641
+ ctx.errorStatus(422, String(e.message ?? e));
642
+ }
643
+ }));
644
+ m.get('/api/v1/users/:username/orgs', wrap(async (ctx) => {
645
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
646
+ if (!u) {
647
+ ctx.notFound();
648
+ return;
649
+ }
650
+ const orgs = db.listUserOrgs(u.id, false);
651
+ ctx.c.JSONSuccess(orgs.map(toOrganization));
652
+ }));
653
+ m.get('/api/v1/orgs/:orgname', reqTokenWrap(async (ctx) => {
654
+ const org = db.getUserByUsername(ctx.c.Params(':orgname'));
655
+ if (!org || org.type !== 1) {
656
+ ctx.notFound();
657
+ return;
658
+ }
659
+ ctx.c.JSONSuccess(toOrganization(org));
660
+ }));
661
+ m.patch('/api/v1/orgs/:orgname', reqTokenWrap(async (ctx) => {
662
+ const org = db.getUserByUsername(ctx.c.Params(':orgname'));
663
+ if (!org || org.type !== 1) {
664
+ ctx.notFound();
665
+ return;
666
+ }
667
+ if (!db.isOrgOwner(ctx.UserID(), org.id)) {
668
+ ctx.c.Status(403);
669
+ ctx.c.res.end();
670
+ ctx.c.rendered = true;
671
+ return;
672
+ }
673
+ const body = (await ctx.c.form());
674
+ db.updateUserColumns(org.id, {
675
+ full_name: String(body.full_name ?? ''),
676
+ description: String(body.description ?? ''),
677
+ website: String(body.website ?? ''),
678
+ location: String(body.location ?? ''),
679
+ });
680
+ ctx.c.JSONSuccess(toOrganization(db.getUserByID(org.id)));
681
+ }));
682
+ m.get('/api/v1/orgs/:orgname/teams', reqTokenWrap(async (ctx) => {
683
+ const org = db.getUserByUsername(ctx.c.Params(':orgname'));
684
+ if (!org || org.type !== 1) {
685
+ ctx.notFound();
686
+ return;
687
+ }
688
+ ctx.c.JSONSuccess(db.listTeamsByOrg(org.id).map(toOrganizationTeam));
689
+ }));
690
+ // ----- admin -----
691
+ // upstream: m.Group("/admin", ..., reqAdmin()) — any auth (basic/token), admin flag required
692
+ const adminWrap = (fn) => wrap(async (ctx) => {
693
+ if (!reqAdmin(ctx))
694
+ return;
695
+ await fn(ctx);
696
+ });
697
+ adminWrapRegister(m, adminWrap, createRepoHandler);
698
+ m.any('/api/v1/*', (c) => {
699
+ c.Status(404);
700
+ c.res.end();
701
+ c.rendered = true;
702
+ });
703
+ }
704
+ async function listUserIssues(ctx) {
705
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
706
+ const state = ctx.c.Query('state') === 'closed' ? 1 : 0;
707
+ const size = conf.issuePagingNum;
708
+ const total = db.db().prepare('SELECT COUNT(*) AS c FROM issue WHERE assignee_id = ? AND is_closed = ?').get(ctx.UserID(), state).c;
709
+ const rows = db.db().prepare('SELECT * FROM issue WHERE assignee_id = ? AND is_closed = ? ORDER BY updated_unix DESC LIMIT ? OFFSET ?').all(ctx.UserID(), state, size, (page - 1) * size);
710
+ setLinkHeader(ctx.c, total, size);
711
+ const out = [];
712
+ for (const issue of rows) {
713
+ const repo = db.getRepoByID(issue.repo_id);
714
+ if (repo)
715
+ out.push(toIssue(issue, repo));
716
+ }
717
+ ctx.c.JSONSuccess(out);
718
+ }
719
+ async function createRepoHandler(ctx, owner) {
720
+ if (owner.type === 1 && !db.isOrgOwner(ctx.user.id, owner.id)) {
721
+ ctx.errorStatus(422, 'Not allowed to create repository for organization.');
722
+ return;
723
+ }
724
+ const body = (await ctx.c.form());
725
+ const name = String(body.name ?? '').trim();
726
+ if (!name || !/^[a-zA-Z0-9_.-]+$/.test(name) || name.length > 100) {
727
+ ctx.c.JSON(422, [{ fieldNames: ['name'], classification: 'RequiredError', message: 'Required' }]);
728
+ return;
729
+ }
730
+ if (db.getRepoByName(owner.name, name)) {
731
+ ctx.errorStatus(422, 'The repository with the same name already exists.');
732
+ return;
733
+ }
734
+ const { createRepositoryRecord } = await import('../repox.js');
735
+ const repo = await createRepositoryRecord(ctx.user, owner, {
736
+ name,
737
+ description: String(body.description ?? ''),
738
+ private: conf.forcePrivate || !!body.private,
739
+ autoInit: !!body.auto_init,
740
+ gitignores: String(body.gitignores ?? ''),
741
+ license: String(body.license ?? ''),
742
+ readme: String(body.readme ?? ''),
743
+ });
744
+ ctx.c.JSON(201, toRepository(repo, { admin: true, push: true, pull: true }));
745
+ }
746
+ function adminWrapRegister(m, adminWrap, createRepoHandler) {
747
+ m.post('/api/v1/admin/users', adminWrap(async (ctx) => {
748
+ const body = (await ctx.c.form());
749
+ const username = String(body.username ?? '').trim();
750
+ const email = String(body.email ?? '').trim();
751
+ if (!username || !email) {
752
+ ctx.c.JSON(422, [{ fieldNames: [!username ? 'username' : 'email'], classification: 'RequiredError', message: 'Required' }]);
753
+ return;
754
+ }
755
+ try {
756
+ const u = db.createUser(username, email, {
757
+ fullName: String(body.full_name ?? ''),
758
+ password: String(body.password ?? ''),
759
+ activated: true,
760
+ });
761
+ ctx.c.JSON(201, toUser(u, true));
762
+ }
763
+ catch (e) {
764
+ ctx.errorStatus(422, String(e.message ?? e));
765
+ }
766
+ }));
767
+ m.patch('/api/v1/admin/users/:username', adminWrap(async (ctx) => {
768
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
769
+ if (!u) {
770
+ ctx.notFound();
771
+ return;
772
+ }
773
+ const body = (await ctx.c.form());
774
+ const cols = {
775
+ full_name: String(body.full_name ?? u.full_name),
776
+ location: String(body.location ?? u.location),
777
+ website: String(body.website ?? u.website),
778
+ };
779
+ if (body.active !== undefined)
780
+ cols['is_active'] = body.active ? 1 : 0;
781
+ if (body.admin !== undefined)
782
+ cols['is_admin'] = body.admin ? 1 : 0;
783
+ if (body.password) {
784
+ const { encodePassword, randomSalt } = await import('../authx/password.js');
785
+ const salt = randomSalt();
786
+ cols['passwd'] = encodePassword(String(body.password), salt);
787
+ cols['salt'] = salt;
788
+ }
789
+ db.updateUserColumns(u.id, cols);
790
+ ctx.c.JSONSuccess(toUser(db.getUserByID(u.id), true));
791
+ }));
792
+ m.delete('/api/v1/admin/users/:username', adminWrap(async (ctx) => {
793
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
794
+ if (!u) {
795
+ ctx.notFound();
796
+ return;
797
+ }
798
+ if (db.countUserRepos(u.id) > 0) {
799
+ ctx.errorStatus(422, 'user still has repository ownership');
800
+ return;
801
+ }
802
+ const orgOwner = db.db().prepare('SELECT COUNT(*) AS c FROM org_user WHERE uid = ? AND is_owner = 1').get(u.id);
803
+ if (orgOwner.c > 0) {
804
+ ctx.errorStatus(422, 'user still is an organization owner');
805
+ return;
806
+ }
807
+ for (const sql of ['DELETE FROM user WHERE id = ?', 'DELETE FROM email_address WHERE uid = ?', 'DELETE FROM org_user WHERE uid = ?', 'DELETE FROM team_user WHERE uid = ?', 'DELETE FROM access_token WHERE uid = ?']) {
808
+ db.db().prepare(sql).run(u.id);
809
+ }
810
+ ctx.c.NoContent();
811
+ }));
812
+ m.post('/api/v1/admin/users/:username/orgs', adminWrap(async (ctx) => {
813
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
814
+ if (!u) {
815
+ ctx.notFound();
816
+ return;
817
+ }
818
+ const body = (await ctx.c.form());
819
+ const username = String(body.username ?? '').trim();
820
+ if (!/^[a-zA-Z0-9_.-]+$/.test(username)) {
821
+ ctx.c.JSON(422, [{ fieldNames: ['username'], classification: 'AlphaDashDotError', message: 'Must be valid alpha or numeric or dash(-) and dot(-) characters.' }]);
822
+ return;
823
+ }
824
+ try {
825
+ const org = db.createOrganization(u, username, {
826
+ fullName: String(body.full_name ?? ''),
827
+ description: String(body.description ?? ''),
828
+ website: String(body.website ?? ''),
829
+ location: String(body.location ?? ''),
830
+ });
831
+ ctx.c.JSON(201, toOrganization(org));
832
+ }
833
+ catch (e) {
834
+ ctx.errorStatus(422, String(e.message ?? e));
835
+ }
836
+ }));
837
+ m.post('/api/v1/admin/users/:username/repos', adminWrap(async (ctx) => {
838
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
839
+ if (!u) {
840
+ ctx.notFound();
841
+ return;
842
+ }
843
+ await createRepoHandler(ctx, u);
844
+ }));
845
+ m.post('/api/v1/admin/orgs/:orgname/teams', adminWrap(async (ctx) => {
846
+ const org = db.getUserByUsername(ctx.c.Params(':orgname'));
847
+ if (!org || org.type !== 1) {
848
+ ctx.notFound();
849
+ return;
850
+ }
851
+ const body = (await ctx.c.form());
852
+ const name = String(body.name ?? '').trim();
853
+ if (!name) {
854
+ ctx.c.JSON(422, [{ fieldNames: ['name'], classification: 'RequiredError', message: 'Required' }]);
855
+ return;
856
+ }
857
+ if (db.getTeamByName(org.id, name)) {
858
+ ctx.errorStatus(422, 'team already exists');
859
+ return;
860
+ }
861
+ const permission = String(body.permission ?? 'read');
862
+ const authorize = permission === 'admin' ? 3 : permission === 'write' ? 2 : 1;
863
+ const info = db.db().prepare('INSERT INTO team (org_id, lower_name, name, description, authorize, num_repos, num_members) VALUES (?,?,?,?,?,0,0)').run(org.id, name.toLowerCase(), name, String(body.description ?? ''), authorize);
864
+ ctx.c.JSON(201, toOrganizationTeam(db.getTeamByID(Number(info.lastInsertRowid))));
865
+ }));
866
+ m.get('/api/v1/admin/teams/:teamid/members', adminWrap(async (ctx) => {
867
+ const team = db.getTeamByID(ctx.c.ParamsInt64(':teamid'));
868
+ if (!team) {
869
+ ctx.notFound();
870
+ return;
871
+ }
872
+ ctx.c.JSONSuccess(db.listTeamMembers(team.id).map((u) => toUser(u, true)));
873
+ }));
874
+ m.put('/api/v1/admin/teams/:teamid/members/:username', adminWrap(async (ctx) => {
875
+ const team = db.getTeamByID(ctx.c.ParamsInt64(':teamid'));
876
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
877
+ if (!team || !u) {
878
+ ctx.notFound();
879
+ return;
880
+ }
881
+ if (!db.db().prepare('SELECT 1 FROM team_user WHERE team_id = ? AND uid = ?').get(team.id, u.id)) {
882
+ db.db().prepare('INSERT INTO team_user (org_id, team_id, uid) VALUES (?,?,?)').run(team.org_id, team.id, u.id);
883
+ db.db().prepare('UPDATE team SET num_members = num_members + 1 WHERE id = ?').run(team.id);
884
+ }
885
+ ctx.c.NoContent();
886
+ }));
887
+ m.delete('/api/v1/admin/teams/:teamid/members/:username', adminWrap(async (ctx) => {
888
+ const team = db.getTeamByID(ctx.c.ParamsInt64(':teamid'));
889
+ const u = db.getUserByUsername(ctx.c.Params(':username'));
890
+ if (!team || !u) {
891
+ ctx.notFound();
892
+ return;
893
+ }
894
+ db.db().prepare('DELETE FROM team_user WHERE team_id = ? AND uid = ?').run(team.id, u.id);
895
+ db.db().prepare('UPDATE team SET num_members = MAX(num_members - 1, 0) WHERE id = ?').run(team.id);
896
+ ctx.c.NoContent();
897
+ }));
898
+ m.put('/api/v1/admin/teams/:teamid/repos/:reponame', adminWrap(async (ctx) => {
899
+ const team = db.getTeamByID(ctx.c.ParamsInt64(':teamid'));
900
+ if (!team) {
901
+ ctx.notFound();
902
+ return;
903
+ }
904
+ const org = db.getUserByID(team.org_id);
905
+ const repo = org ? db.getRepoByOwnerAndName(org, ctx.c.Params(':reponame')) : null;
906
+ if (!repo) {
907
+ ctx.notFound();
908
+ return;
909
+ }
910
+ if (!db.db().prepare('SELECT 1 FROM team_repo WHERE team_id = ? AND repo_id = ?').get(team.id, repo.id)) {
911
+ db.db().prepare('INSERT INTO team_repo (org_id, team_id, repo_id) VALUES (?,?,?)').run(team.org_id, team.id, repo.id);
912
+ db.db().prepare('UPDATE team SET num_repos = num_repos + 1 WHERE id = ?').run(team.id);
913
+ }
914
+ ctx.c.NoContent();
915
+ }));
916
+ m.delete('/api/v1/admin/teams/:teamid/repos/:reponame', adminWrap(async (ctx) => {
917
+ const team = db.getTeamByID(ctx.c.ParamsInt64(':teamid'));
918
+ if (!team) {
919
+ ctx.notFound();
920
+ return;
921
+ }
922
+ const org = db.getUserByID(team.org_id);
923
+ const repo = org ? db.getRepoByOwnerAndName(org, ctx.c.Params(':reponame')) : null;
924
+ if (!repo) {
925
+ ctx.notFound();
926
+ return;
927
+ }
928
+ db.db().prepare('DELETE FROM team_repo WHERE team_id = ? AND repo_id = ?').run(team.id, repo.id);
929
+ db.db().prepare('UPDATE team SET num_repos = MAX(num_repos - 1, 0) WHERE id = ?').run(team.id);
930
+ ctx.c.NoContent();
931
+ }));
932
+ }
933
+ function registerRepoSubRoutes(m, _wrap, reqTokenWrap, repoGroup, repoGroupNoToken) {
934
+ // hooks
935
+ m.get('/api/v1/repos/:username/:reponame/hooks', repoAdminGroup(repoGroup, async (ctx) => {
936
+ const hooks = db.listWebhooks(ctx.repo.Repository.id);
937
+ ctx.c.JSONSuccess(hooks.map(toRepositoryHook));
938
+ }));
939
+ m.get('/api/v1/repos/:username/:reponame/hooks/:id', repoAdminGroup(repoGroup, async (ctx) => {
940
+ const hook = db.db().prepare('SELECT * FROM webhook WHERE id = ? AND repo_id = ?').get(ctx.c.ParamsInt64(':id'), ctx.repo.Repository.id);
941
+ if (!hook) {
942
+ ctx.notFound();
943
+ return;
944
+ }
945
+ ctx.c.JSONSuccess(toRepositoryHook(hook));
946
+ }));
947
+ m.post('/api/v1/repos/:username/:reponame/hooks', repoAdminGroup(repoGroup, async (ctx) => {
948
+ const body = (await ctx.c.form());
949
+ const type = String(body.type ?? 'gogs');
950
+ if (!['gogs', 'slack', 'discord', 'dingtalk'].includes(type)) {
951
+ ctx.errorStatus(422, 'Invalid hook type.');
952
+ return;
953
+ }
954
+ const cfg = body.config ?? {};
955
+ if (!cfg.url) {
956
+ ctx.errorStatus(422, 'Missing config option: url');
957
+ return;
958
+ }
959
+ if (!cfg.content_type) {
960
+ ctx.errorStatus(422, 'Missing config option: content_type');
961
+ return;
962
+ }
963
+ if (!['json', 'form'].includes(String(cfg.content_type))) {
964
+ ctx.errorStatus(422, 'Invalid content type.');
965
+ return;
966
+ }
967
+ const events = Array.isArray(body.events) && body.events.length ? body.events : ['push'];
968
+ const eventsObj = { push_only: false, send_everything: false, choose_events: true, events: {} };
969
+ for (const ev of ['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release']) {
970
+ eventsObj.events[ev] = events.includes(ev);
971
+ }
972
+ const now = Math.floor(Date.now() / 1000);
973
+ const hookTaskType = { gogs: 1, slack: 2, discord: 3, dingtalk: 4 }[type] ?? 1;
974
+ const meta = type === 'slack' ? JSON.stringify({ channel: cfg.channel ?? '', username: cfg.username ?? '', icon_url: cfg.icon_url ?? '', color: cfg.color ?? '' }) : '{}';
975
+ if (type === 'slack' && !cfg.channel) {
976
+ ctx.errorStatus(422, 'Missing config option: channel');
977
+ return;
978
+ }
979
+ const info = db.db()
980
+ .prepare('INSERT INTO webhook (repo_id, org_id, url, content_type, secret, events, is_ssl, is_active, hook_task_type, meta, last_status, created_unix, updated_unix) VALUES (?,0,?,?,?,?,0,?,?,?,?,?,?)')
981
+ .run(ctx.repo.Repository.id, String(cfg.url), cfg.content_type === 'form' ? 2 : 1, String(cfg.secret ?? ''), JSON.stringify(eventsObj), body.active === false ? 0 : 1, hookTaskType, meta, 0, now, now);
982
+ const hook = db.getWebhookByID(ctx.repo.Repository.id, Number(info.lastInsertRowid));
983
+ ctx.c.JSON(201, toRepositoryHook(hook));
984
+ }));
985
+ m.patch('/api/v1/repos/:username/:reponame/hooks/:id', repoAdminGroup(repoGroup, async (ctx) => {
986
+ const hook = db.getWebhookByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
987
+ if (!hook) {
988
+ ctx.notFound();
989
+ return;
990
+ }
991
+ const body = (await ctx.c.form());
992
+ const events = Array.isArray(body.events) && body.events.length ? body.events : ['push'];
993
+ const eventsObj = { push_only: false, send_everything: false, choose_events: true, events: {} };
994
+ for (const ev of ['create', 'delete', 'fork', 'push', 'issues', 'issue_comment', 'pull_request', 'release']) {
995
+ eventsObj.events[ev] = events.includes(ev);
996
+ }
997
+ const cfg = body.config ?? {};
998
+ db.db()
999
+ .prepare('UPDATE webhook SET url = ?, content_type = ?, secret = ?, events = ?, is_active = ?, updated_unix = ? WHERE id = ?')
1000
+ .run(cfg.url !== undefined ? String(cfg.url) : hook.url, cfg.content_type !== undefined ? (cfg.content_type === 'form' ? 2 : 1) : hook.content_type, cfg.secret !== undefined ? String(cfg.secret) : hook.secret, JSON.stringify(eventsObj), body.active !== undefined ? (body.active ? 1 : 0) : hook.is_active, Math.floor(Date.now() / 1000), hook.id);
1001
+ ctx.c.JSONSuccess(toRepositoryHook(db.getWebhookByID(ctx.repo.Repository.id, hook.id)));
1002
+ }));
1003
+ m.delete('/api/v1/repos/:username/:reponame/hooks/:id', repoAdminGroup(repoGroup, async (ctx) => {
1004
+ const hook = db.getWebhookByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1005
+ if (!hook) {
1006
+ ctx.notFound();
1007
+ return;
1008
+ }
1009
+ db.db().prepare('DELETE FROM webhook WHERE id = ?').run(hook.id);
1010
+ ctx.c.NoContent();
1011
+ }));
1012
+ // collaborators
1013
+ m.get('/api/v1/repos/:username/:reponame/collaborators', repoAdminGroup(repoGroup, async (ctx) => {
1014
+ const cols = db.listCollaborations(ctx.repo.Repository.id);
1015
+ ctx.c.JSONSuccess(cols.map((col) => {
1016
+ const u = db.getUserByID(col.user_id);
1017
+ const user = toUser(u, true);
1018
+ user.permissions = {
1019
+ admin: col.mode >= db.AccessMode.ADMIN,
1020
+ push: col.mode >= db.AccessMode.WRITE,
1021
+ pull: true,
1022
+ };
1023
+ return user;
1024
+ }));
1025
+ }));
1026
+ m.get('/api/v1/repos/:username/:reponame/collaborators/:collaborator', repoAdminGroup(repoGroup, async (ctx) => {
1027
+ const u = db.getUserByUsername(ctx.c.Params(':collaborator'));
1028
+ if (!u) {
1029
+ ctx.c.Status(422);
1030
+ ctx.c.res.end();
1031
+ ctx.c.rendered = true;
1032
+ return;
1033
+ }
1034
+ if (db.getCollaboration(ctx.repo.Repository.id, u.id)) {
1035
+ ctx.c.NoContent();
1036
+ }
1037
+ else {
1038
+ ctx.notFound();
1039
+ }
1040
+ }));
1041
+ m.put('/api/v1/repos/:username/:reponame/collaborators/:collaborator', repoAdminGroup(repoGroup, async (ctx) => {
1042
+ const u = db.getUserByUsername(ctx.c.Params(':collaborator'));
1043
+ if (!u) {
1044
+ ctx.c.Status(422);
1045
+ ctx.c.res.end();
1046
+ ctx.c.rendered = true;
1047
+ return;
1048
+ }
1049
+ const body = (await ctx.c.form());
1050
+ const perm = String(body.permission ?? 'read');
1051
+ const mode = perm === 'admin' ? db.AccessMode.ADMIN : perm === 'write' ? db.AccessMode.WRITE : db.AccessMode.READ;
1052
+ if (!db.getCollaboration(ctx.repo.Repository.id, u.id)) {
1053
+ db.db().prepare('INSERT INTO collaboration (user_id, repo_id, mode) VALUES (?,?,?)').run(u.id, ctx.repo.Repository.id, mode);
1054
+ }
1055
+ else {
1056
+ db.db().prepare('UPDATE collaboration SET mode = ? WHERE user_id = ? AND repo_id = ?').run(mode, u.id, ctx.repo.Repository.id);
1057
+ }
1058
+ ctx.c.NoContent();
1059
+ }));
1060
+ m.delete('/api/v1/repos/:username/:reponame/collaborators/:collaborator', repoAdminGroup(repoGroup, async (ctx) => {
1061
+ const u = db.getUserByUsername(ctx.c.Params(':collaborator'));
1062
+ if (!u) {
1063
+ ctx.c.Status(422);
1064
+ ctx.c.res.end();
1065
+ ctx.c.rendered = true;
1066
+ return;
1067
+ }
1068
+ db.db().prepare('DELETE FROM collaboration WHERE user_id = ? AND repo_id = ?').run(u.id, ctx.repo.Repository.id);
1069
+ ctx.c.NoContent();
1070
+ }));
1071
+ // raw file
1072
+ m.get('/api/v1/repos/:username/:reponame/raw/*', reqTokenWrap(async (ctx) => {
1073
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1074
+ if (!ok)
1075
+ return;
1076
+ const wildcard = ctx.c.Params(':*');
1077
+ // greedy ref resolution so slash-named branches work (upstream unescapes {ref})
1078
+ const parts = wildcard.split('/');
1079
+ const repoDir = ctx.repo.Repository.RepoPath();
1080
+ let ref = '';
1081
+ let filePath = '';
1082
+ for (let i = 1; i < parts.length; i++) {
1083
+ const cand = parts.slice(0, i).join('/');
1084
+ if (await git.resolveRef(repoDir, cand)) {
1085
+ ref = cand;
1086
+ filePath = parts.slice(i).join('/');
1087
+ break;
1088
+ }
1089
+ }
1090
+ if (!ref || !filePath) {
1091
+ ctx.notFound();
1092
+ return;
1093
+ }
1094
+ const resolved = await git.resolveRef(repoDir, ref);
1095
+ if (!resolved) {
1096
+ ctx.notFound();
1097
+ return;
1098
+ }
1099
+ const commit = await git.getCommit(repoDir, resolved);
1100
+ if (!commit) {
1101
+ ctx.notFound();
1102
+ return;
1103
+ }
1104
+ const tree = await git.lsTree(repoDir, commit.id, filePath);
1105
+ const entry = tree?.entries[0];
1106
+ if (!entry || entry.type !== 'blob') {
1107
+ ctx.notFound();
1108
+ return;
1109
+ }
1110
+ const content = await git.blobBytes(repoDir, entry.sha);
1111
+ ctx.c.SetHeader('Content-Type', 'application/octet-stream');
1112
+ ctx.c.res.end(content);
1113
+ ctx.c.rendered = true;
1114
+ }));
1115
+ // contents
1116
+ m.get('/api/v1/repos/:username/:reponame/contents', repoGroupNoToken(contentsHandler));
1117
+ m.get('/api/v1/repos/:username/:reponame/contents/*', repoGroupNoToken(contentsHandler));
1118
+ m.put('/api/v1/repos/:username/:reponame/contents/*', reqTokenWrap(async (ctx) => {
1119
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1120
+ if (!ok)
1121
+ return;
1122
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1123
+ ctx.c.Status(403);
1124
+ ctx.c.res.end();
1125
+ ctx.c.rendered = true;
1126
+ return;
1127
+ }
1128
+ const body = (await ctx.c.form());
1129
+ const message = String(body.message ?? '');
1130
+ const content = String(body.content ?? '');
1131
+ const branch = String(body.branch ?? '') || ctx.repo.Repository.default_branch || conf.defaultBranch;
1132
+ if (!message || !content) {
1133
+ ctx.c.JSON(422, [{ fieldNames: [!message ? 'message' : 'content'], classification: 'RequiredError', message: 'Required' }]);
1134
+ return;
1135
+ }
1136
+ let fileContent;
1137
+ try {
1138
+ fileContent = Buffer.from(content, 'base64');
1139
+ }
1140
+ catch {
1141
+ ctx.error(500 - 1 === 499 ? 'bad' : 'content is not base64');
1142
+ return;
1143
+ }
1144
+ const filePath = ctx.c.Params(':*');
1145
+ const repo = ctx.repo.Repository;
1146
+ const repoDir = repo.RepoPath();
1147
+ const tmpDir = `${conf.appDataPath}/tmp/api-put-${Date.now()}`;
1148
+ const fs = await import('node:fs');
1149
+ fs.mkdirSync(tmpDir, { recursive: true });
1150
+ try {
1151
+ await git.git(process.cwd(), 'clone', repoDir, tmpDir);
1152
+ await git.git(tmpDir, 'checkout', '--end-of-options', branch);
1153
+ const target = `${tmpDir}/${filePath}`;
1154
+ fs.mkdirSync(target.substring(0, target.lastIndexOf('/')), { recursive: true });
1155
+ fs.writeFileSync(target, fileContent);
1156
+ await git.git(tmpDir, 'add', '--all');
1157
+ const author = `${ctx.user.name} <${ctx.user.email}>`;
1158
+ await git.git(tmpDir, 'commit', `--author=${author}`, '-m', message);
1159
+ await git.git(tmpDir, 'push', 'origin', `HEAD:refs/heads/${branch}`);
1160
+ const commit = await git.getCommit(repoDir, branch);
1161
+ const sha = commit ? commit.id : '';
1162
+ const tree = await git.lsTree(repoDir, sha, filePath);
1163
+ const entry = tree?.entries[0];
1164
+ ctx.c.JSON(201, {
1165
+ content: contentsEntry(ctx.repo.Repository, filePath, entry, branch),
1166
+ commit: commit ? commitToAPICommit(ctx, repo, commit, `/api/v1/repos/${repo.FullName()}/git/commits/${sha}`) : null,
1167
+ });
1168
+ }
1169
+ catch (e) {
1170
+ ctx.error(String(e.message ?? e));
1171
+ }
1172
+ finally {
1173
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1174
+ }
1175
+ }));
1176
+ // archive
1177
+ m.get('/api/v1/repos/:username/:reponame/archive/\*', repoGroup(async (ctx) => {
1178
+ const wildcard = ctx.c.Params(':*');
1179
+ const isZip = wildcard.endsWith('.zip');
1180
+ const isTarGz = wildcard.endsWith('.tar.gz');
1181
+ if (!isZip && !isTarGz) {
1182
+ ctx.notFound();
1183
+ return;
1184
+ }
1185
+ const ref = isZip ? wildcard.slice(0, -4) : wildcard.slice(0, -7);
1186
+ const repoDir = ctx.repo.Repository.RepoPath();
1187
+ const resolved = await git.resolveRef(repoDir, ref);
1188
+ if (!resolved) {
1189
+ ctx.notFound();
1190
+ return;
1191
+ }
1192
+ const sha = (await git.gitOK(repoDir, 'rev-parse', '--verify', '--end-of-options', resolved)).toString().trim();
1193
+ const ext = isZip ? '.zip' : '.tar.gz';
1194
+ const dst = `/tmp/ts-gogs-archive-${Date.now()}${ext}`;
1195
+ await git.archive(repoDir, sha, isZip ? 'zip' : 'tar.gz', dst, `${ctx.repo.Repository.name}/`);
1196
+ const fs = await import('node:fs');
1197
+ ctx.c.SetHeader('Content-Type', isZip ? 'application/zip' : 'application/x-gzip');
1198
+ ctx.c.SetHeader('Content-Disposition', `attachment; filename=${ctx.repo.Repository.name}-${sha.slice(0, 10)}${ext}`);
1199
+ ctx.c.res.end(fs.readFileSync(dst));
1200
+ fs.unlinkSync(dst);
1201
+ ctx.c.rendered = true;
1202
+ }));
1203
+ // git trees & blobs
1204
+ m.get('/api/v1/repos/:username/:reponame/git/trees/:sha', repoGroup(async (ctx) => {
1205
+ const repo = ctx.repo.Repository;
1206
+ const repoDir = repo.RepoPath();
1207
+ const sha = ctx.c.Params(':sha');
1208
+ const resolved = await git.resolveRef(repoDir, sha);
1209
+ const target = resolved ?? sha;
1210
+ const tree = await git.lsTree(repoDir, target, '');
1211
+ if (!tree) {
1212
+ ctx.notFound();
1213
+ return;
1214
+ }
1215
+ const entries = [];
1216
+ for (const e of tree.entries) {
1217
+ const type = e.type === 'commit' ? '160000' : e.type === 'tree' ? '040000' : e.type === 'tag' ? '100644' : e.mode;
1218
+ entries.push({
1219
+ path: e.name,
1220
+ mode: e.mode === '160000' ? '160000' : type === '040000' ? '040000' : e.mode,
1221
+ type: e.type,
1222
+ size: e.type === 'blob' ? await git.entrySize(repoDir, e.sha) : 0,
1223
+ sha: e.sha,
1224
+ url: `${conf.externalURL}api/v1/repos/${repo.FullName()}/git/trees/${e.sha}`,
1225
+ });
1226
+ }
1227
+ ctx.c.JSONSuccess({
1228
+ sha,
1229
+ url: `${conf.externalURL}api/v1/repos/${repo.FullName()}/git/trees/${sha}`,
1230
+ tree: entries.length ? entries : null,
1231
+ });
1232
+ }));
1233
+ m.get('/api/v1/repos/:username/:reponame/git/blobs/:sha', repoGroup(async (ctx) => {
1234
+ const repo = ctx.repo.Repository;
1235
+ const repoDir = repo.RepoPath();
1236
+ const sha = ctx.c.Params(':sha');
1237
+ try {
1238
+ const content = await git.blobBytes(repoDir, sha);
1239
+ ctx.c.JSONSuccess({
1240
+ content: content.toString('base64'),
1241
+ encoding: 'base64',
1242
+ url: `${conf.externalURL}api/v1/repos/${repo.FullName()}/git/blobs/${sha}`,
1243
+ sha,
1244
+ size: content.length,
1245
+ });
1246
+ }
1247
+ catch {
1248
+ ctx.notFound();
1249
+ }
1250
+ }));
1251
+ m.get('/api/v1/repos/:username/:reponame/forks', repoGroup(async (ctx) => {
1252
+ const rows = db.db().prepare('SELECT * FROM repository WHERE fork_id = ?').all(ctx.repo.Repository.id);
1253
+ const out = rows.map((r) => {
1254
+ const fork = new Repository(r);
1255
+ fork.owner = db.getUserByID(fork.owner_id) ?? undefined;
1256
+ const mode = db.accessMode(ctx.UserID(), fork);
1257
+ return toRepository(fork, { admin: mode >= db.AccessMode.ADMIN, push: mode >= db.AccessMode.WRITE, pull: mode >= db.AccessMode.READ });
1258
+ });
1259
+ ctx.c.JSONSuccess(out);
1260
+ }));
1261
+ m.get('/api/v1/repos/:username/:reponame/tags', repoGroup(async (ctx) => {
1262
+ const repoDir = ctx.repo.Repository.RepoPath();
1263
+ const tags = await git.getTags(repoDir);
1264
+ ctx.c.JSONSuccess(tags.map((t) => ({
1265
+ name: t.name,
1266
+ commit: {
1267
+ id: t.commit.id,
1268
+ message: t.commit.message,
1269
+ url: 'Not implemented',
1270
+ author: { name: t.commit.author.name, email: t.commit.author.email, username: db.getUserByEmail(t.commit.author.email)?.name ?? '' },
1271
+ committer: { name: t.commit.committer.name, email: t.commit.committer.email, username: db.getUserByEmail(t.commit.committer.email)?.name ?? '' },
1272
+ added: null,
1273
+ removed: null,
1274
+ modified: null,
1275
+ timestamp: t.commit.committer.when.toISOString(),
1276
+ },
1277
+ })));
1278
+ }));
1279
+ m.get('/api/v1/repos/:username/:reponame/branches', repoGroup(async (ctx) => {
1280
+ const repoDir = ctx.repo.Repository.RepoPath();
1281
+ const branches = await git.getBranches(repoDir);
1282
+ ctx.c.JSONSuccess(branches.map((b) => ({
1283
+ name: b.name,
1284
+ commit: webhookCommitFromCommit(b.commit),
1285
+ })));
1286
+ }));
1287
+ m.get('/api/v1/repos/:username/:reponame/branches/\*', repoGroup(async (ctx) => {
1288
+ const repoDir = ctx.repo.Repository.RepoPath();
1289
+ const name = ctx.c.Params(':*');
1290
+ const branch = (await git.getBranches(repoDir)).find((b) => b.name === name);
1291
+ if (!branch) {
1292
+ ctx.notFound();
1293
+ return;
1294
+ }
1295
+ ctx.c.JSONSuccess({ name: branch.name, commit: webhookCommitFromCommit(branch.commit) });
1296
+ }));
1297
+ m.get('/api/v1/repos/:username/:reponame/commits', repoGroup(async (ctx) => {
1298
+ const repoDir = ctx.repo.Repository.RepoPath();
1299
+ const pageSize = Math.max(1, ctx.c.QueryInt('pageSize') || 30);
1300
+ const commits = await git.commitsByPage(repoDir, ctx.repo.Repository.default_branch || conf.defaultBranch, Math.max(1, ctx.c.QueryInt('page') || 1), pageSize);
1301
+ ctx.c.JSONSuccess(commits.map((cm) => commitToAPICommit(ctx, ctx.repo.Repository, cm, `/api/v1/repos/${ctx.repo.Repository.FullName()}/commits/${cm.id}`)));
1302
+ }));
1303
+ m.get('/api/v1/repos/:username/:reponame/commits/:sha', repoGroup(async (ctx) => {
1304
+ const sha = ctx.c.Params(':sha');
1305
+ if (ctx.c.req.headers.accept?.toString().includes('application/vnd.gogs.sha')) {
1306
+ return getReferenceSHAHandler(ctx, sha);
1307
+ }
1308
+ const repoDir = ctx.repo.Repository.RepoPath();
1309
+ const commit = await git.getCommit(repoDir, sha);
1310
+ if (!commit) {
1311
+ ctx.notFound();
1312
+ return;
1313
+ }
1314
+ ctx.c.JSONSuccess(commitToAPICommit(ctx, ctx.repo.Repository, commit, `/api/v1/repos/${ctx.repo.Repository.FullName()}/commits/${commit.id}`));
1315
+ }));
1316
+ m.get('/api/v1/repos/:username/:reponame/commits/\*', repoGroup(async (ctx) => {
1317
+ await getReferenceSHAHandler(ctx, ctx.c.Params(':*'));
1318
+ }));
1319
+ // deploy keys
1320
+ // issues
1321
+ const issuesGroup = (fn) => reqTokenWrap(async (ctx) => {
1322
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1323
+ if (!ok)
1324
+ return;
1325
+ const repo = ctx.repo.Repository;
1326
+ if (!repo.enable_issues || repo.enable_external_tracker) {
1327
+ ctx.notFound();
1328
+ return;
1329
+ }
1330
+ await fn(ctx);
1331
+ });
1332
+ m.get('/api/v1/repos/:username/:reponame/issues', issuesGroup(async (ctx) => {
1333
+ const page = Math.max(1, ctx.c.QueryInt('page') || 1);
1334
+ const state = ctx.c.Query('state') === 'closed' ? 1 : 0;
1335
+ const { total, issues } = db.listIssues(ctx.repo.Repository.id, state, page, conf.issuePagingNum);
1336
+ setLinkHeader(ctx.c, total, conf.issuePagingNum);
1337
+ ctx.c.JSONSuccess(issues.map((i) => toIssue(i, ctx.repo.Repository)));
1338
+ }));
1339
+ m.post('/api/v1/repos/:username/:reponame/issues', issuesGroup(async (ctx) => {
1340
+ const body = (await ctx.c.form());
1341
+ const title = String(body.title ?? '').trim();
1342
+ if (!title) {
1343
+ ctx.c.JSON(422, [{ fieldNames: ['title'], classification: 'RequiredError', message: 'Required' }]);
1344
+ return;
1345
+ }
1346
+ const repo = ctx.repo.Repository;
1347
+ const isWriter = ctx.repo.AccessMode >= db.AccessMode.WRITE;
1348
+ const index = db.maxIssueIndex(repo.id) + 1;
1349
+ const now = Math.floor(Date.now() / 1000);
1350
+ let assigneeID = null;
1351
+ if (isWriter && body.assignee) {
1352
+ const assignee = db.getUserByUsername(String(body.assignee));
1353
+ if (!assignee) {
1354
+ ctx.errorStatus(422, `assignee does not exist: [name: ${body.assignee}]`);
1355
+ return;
1356
+ }
1357
+ assigneeID = assignee.id;
1358
+ }
1359
+ const info = db.db()
1360
+ .prepare('INSERT INTO issue (repo_id, "index", poster_id, name, content, milestone_id, assignee_id, is_closed, is_pull, num_comments, created_unix, updated_unix) VALUES (?,?,?,?,?,?,?,0,0,0,?,?)')
1361
+ .run(repo.id, index, ctx.UserID(), title, String(body.body ?? ''), isWriter ? Number(body.milestone ?? 0) || null : null, assigneeID, now, now);
1362
+ const issueID = Number(info.lastInsertRowid);
1363
+ db.db().prepare('INSERT INTO issue_user (uid, issue_id, repo_id, is_poster, is_read) VALUES (?,?,?,1,1)').run(ctx.UserID(), issueID, repo.id);
1364
+ if (isWriter && Array.isArray(body.labels)) {
1365
+ for (const lid of body.labels) {
1366
+ db.db().prepare('INSERT OR IGNORE INTO issue_label (issue_id, label_id) VALUES (?,?)').run(issueID, Number(lid));
1367
+ }
1368
+ }
1369
+ db.refreshIssueCounts(repo.id);
1370
+ if (body.closed) {
1371
+ db.updateIssueColumns(issueID, { is_closed: 1 });
1372
+ db.refreshIssueCounts(repo.id);
1373
+ }
1374
+ const { issueAction, ActionType } = await import('../db/actions.js');
1375
+ const issue = db.getIssueByID(issueID);
1376
+ await issueAction(ActionType.CREATE_ISSUE, ctx.user, repo, issue);
1377
+ ctx.c.JSON(201, toIssue(db.getIssueByID(issueID), repo));
1378
+ }));
1379
+ m.get('/api/v1/repos/:username/:reponame/issues/comments', issuesGroup(async (ctx) => {
1380
+ const since = ctx.c.Query('since');
1381
+ let sinceUnix = 0;
1382
+ if (since) {
1383
+ const t = new Date(since);
1384
+ if (Number.isNaN(t.getTime())) {
1385
+ ctx.errorStatus(422, `invalid since: ${since}`);
1386
+ return;
1387
+ }
1388
+ sinceUnix = Math.floor(t.getTime() / 1000);
1389
+ }
1390
+ const rows = db.db().prepare('SELECT c.* FROM comment c JOIN issue i ON i.id = c.issue_id WHERE i.repo_id = ? AND c.type = 0 ORDER BY c.id').all(ctx.repo.Repository.id);
1391
+ const out = [];
1392
+ for (const cm of rows) {
1393
+ if (sinceUnix && (cm.created_unix ?? 0) < sinceUnix)
1394
+ continue;
1395
+ const issue = db.getIssueByID(cm.issue_id);
1396
+ out.push(toIssueComment(cm, issue, ctx.repo.Repository));
1397
+ }
1398
+ ctx.c.JSONSuccess(out);
1399
+ }));
1400
+ m.patch('/api/v1/repos/:username/:reponame/issues/comments/:id', issuesGroup(async (ctx) => {
1401
+ await editIssueComment(ctx);
1402
+ }));
1403
+ m.delete('/api/v1/repos/:username/:reponame/issues/comments/:id', issuesGroup(async (ctx) => {
1404
+ await deleteIssueComment(ctx);
1405
+ }));
1406
+ m.get('/api/v1/repos/:username/:reponame/issues/:index', issuesGroup(async (ctx) => {
1407
+ const issue = db.getIssueByIndex(ctx.repo.Repository.id, ctx.c.ParamsInt64(':index'));
1408
+ if (!issue) {
1409
+ ctx.notFound();
1410
+ return;
1411
+ }
1412
+ ctx.c.JSONSuccess(toIssue(issue, ctx.repo.Repository));
1413
+ }));
1414
+ m.patch('/api/v1/repos/:username/:reponame/issues/:index', issuesGroup(async (ctx) => {
1415
+ const repo = ctx.repo.Repository;
1416
+ const issue = db.getIssueByIndex(repo.id, ctx.c.ParamsInt64(':index'));
1417
+ if (!issue) {
1418
+ ctx.notFound();
1419
+ return;
1420
+ }
1421
+ if (issue.poster_id !== ctx.UserID() && ctx.repo.AccessMode < db.AccessMode.WRITE) {
1422
+ ctx.c.Status(403);
1423
+ ctx.c.res.end();
1424
+ ctx.c.rendered = true;
1425
+ return;
1426
+ }
1427
+ const body = (await ctx.c.form());
1428
+ const cols = {};
1429
+ if (body.title)
1430
+ cols['name'] = String(body.title);
1431
+ if (body.body !== undefined)
1432
+ cols['content'] = String(body.body);
1433
+ const isWriter = ctx.repo.AccessMode >= db.AccessMode.WRITE;
1434
+ if (isWriter && body.assignee !== undefined) {
1435
+ const assignee = body.assignee ? db.getUserByUsername(String(body.assignee)) : null;
1436
+ cols['assignee_id'] = assignee?.id ?? null;
1437
+ }
1438
+ if (isWriter && body.milestone !== undefined) {
1439
+ cols['milestone_id'] = Number(body.milestone) || null;
1440
+ }
1441
+ if (body.state !== undefined) {
1442
+ cols['is_closed'] = String(body.state) === 'closed' ? 1 : 0;
1443
+ }
1444
+ db.updateIssueColumns(issue.id, cols);
1445
+ db.refreshIssueCounts(repo.id);
1446
+ ctx.c.JSON(201, toIssue(db.getIssueByID(issue.id), repo));
1447
+ }));
1448
+ m.get('/api/v1/repos/:username/:reponame/issues/:index/comments', issuesGroup(async (ctx) => {
1449
+ const issue = db.getIssueByIndex(ctx.repo.Repository.id, ctx.c.ParamsInt64(':index'));
1450
+ if (!issue) {
1451
+ ctx.notFound();
1452
+ return;
1453
+ }
1454
+ const since = ctx.c.Query('since');
1455
+ let sinceUnix = 0;
1456
+ if (since) {
1457
+ const t = new Date(since);
1458
+ if (Number.isNaN(t.getTime())) {
1459
+ ctx.errorStatus(422, `invalid since: ${since}`);
1460
+ return;
1461
+ }
1462
+ sinceUnix = Math.floor(t.getTime() / 1000);
1463
+ }
1464
+ const comments = db.listComments(issue.id).filter((cm) => !sinceUnix || (cm.created_unix ?? 0) >= sinceUnix);
1465
+ ctx.c.JSONSuccess(comments.map((cm) => toIssueComment(cm, issue, ctx.repo.Repository)));
1466
+ }));
1467
+ m.post('/api/v1/repos/:username/:reponame/issues/:index/comments', issuesGroup(async (ctx) => {
1468
+ const repo = ctx.repo.Repository;
1469
+ const issue = db.getIssueByIndex(repo.id, ctx.c.ParamsInt64(':index'));
1470
+ if (!issue) {
1471
+ ctx.notFound();
1472
+ return;
1473
+ }
1474
+ const body = (await ctx.c.form());
1475
+ const content = String(body.body ?? '').trim();
1476
+ if (!content) {
1477
+ ctx.c.JSON(422, [{ fieldNames: ['body'], classification: 'RequiredError', message: 'Required' }]);
1478
+ return;
1479
+ }
1480
+ const now = Math.floor(Date.now() / 1000);
1481
+ const info = db.db().prepare('INSERT INTO comment (type, poster_id, issue_id, content, created_unix, updated_unix) VALUES (0,?,?,?,?,?)').run(ctx.UserID(), issue.id, content, now, now);
1482
+ db.db().prepare('UPDATE issue SET num_comments = num_comments + 1 WHERE id = ?').run(issue.id);
1483
+ const cm = db.getCommentByID(Number(info.lastInsertRowid));
1484
+ ctx.c.JSON(201, toIssueComment(cm, issue, repo));
1485
+ }));
1486
+ m.delete('/api/v1/repos/:username/:reponame/issues/:index/comments/:id', issuesGroup(async (ctx) => {
1487
+ await deleteIssueComment(ctx);
1488
+ }));
1489
+ m.get('/api/v1/repos/:username/:reponame/issues/:index/labels', issuesGroup(async (ctx) => {
1490
+ const issue = db.getIssueByIndex(ctx.repo.Repository.id, ctx.c.ParamsInt64(':index'));
1491
+ if (!issue) {
1492
+ ctx.notFound();
1493
+ return;
1494
+ }
1495
+ ctx.c.JSONSuccess(db.listIssueLabels(issue.id).map(toIssueLabel));
1496
+ }));
1497
+ const issueLabelsWriter = (fn) => reqTokenWrap(async (ctx) => {
1498
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1499
+ if (!ok)
1500
+ return;
1501
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1502
+ ctx.c.Status(403);
1503
+ ctx.c.res.end();
1504
+ ctx.c.rendered = true;
1505
+ return;
1506
+ }
1507
+ const issue = db.getIssueByIndex(ctx.repo.Repository.id, ctx.c.ParamsInt64(':index'));
1508
+ if (!issue) {
1509
+ ctx.notFound();
1510
+ return;
1511
+ }
1512
+ await fn(ctx, issue);
1513
+ });
1514
+ m.post('/api/v1/repos/:username/:reponame/issues/:index/labels', issueLabelsWriter(async (ctx, issue) => {
1515
+ const body = (await ctx.c.form());
1516
+ const labels = Array.isArray(body.labels) ? body.labels.map(Number) : [];
1517
+ for (const lid of labels) {
1518
+ db.db().prepare('INSERT OR IGNORE INTO issue_label (issue_id, label_id) VALUES (?,?)').run(issue.id, lid);
1519
+ }
1520
+ ctx.c.JSONSuccess(db.listIssueLabels(issue.id).map(toIssueLabel));
1521
+ }));
1522
+ m.put('/api/v1/repos/:username/:reponame/issues/:index/labels', issueLabelsWriter(async (ctx, issue) => {
1523
+ const body = (await ctx.c.form());
1524
+ const labels = Array.isArray(body.labels) ? body.labels.map(Number) : [];
1525
+ db.db().prepare('DELETE FROM issue_label WHERE issue_id = ?').run(issue.id);
1526
+ for (const lid of labels) {
1527
+ db.db().prepare('INSERT OR IGNORE INTO issue_label (issue_id, label_id) VALUES (?,?)').run(issue.id, lid);
1528
+ }
1529
+ ctx.c.JSONSuccess(db.listIssueLabels(issue.id).map(toIssueLabel));
1530
+ }));
1531
+ m.delete('/api/v1/repos/:username/:reponame/issues/:index/labels', issueLabelsWriter(async (ctx, issue) => {
1532
+ db.db().prepare('DELETE FROM issue_label WHERE issue_id = ?').run(issue.id);
1533
+ ctx.c.NoContent();
1534
+ }));
1535
+ m.delete('/api/v1/repos/:username/:reponame/issues/:index/labels/:id', issueLabelsWriter(async (ctx, issue) => {
1536
+ const label = db.getLabelByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1537
+ if (!label) {
1538
+ ctx.errorStatus(422, `label does not exist [id: ${ctx.c.Params(':id')}]`);
1539
+ return;
1540
+ }
1541
+ db.db().prepare('DELETE FROM issue_label WHERE issue_id = ? AND label_id = ?').run(issue.id, label.id);
1542
+ ctx.c.NoContent();
1543
+ }));
1544
+ // labels
1545
+ m.get('/api/v1/repos/:username/:reponame/labels', repoGroup(async (ctx) => {
1546
+ ctx.c.JSONSuccess(db.listLabels(ctx.repo.Repository.id).map(toIssueLabel));
1547
+ }));
1548
+ m.get('/api/v1/repos/:username/:reponame/labels/:id', repoGroup(async (ctx) => {
1549
+ const idParam = ctx.c.Params(':id');
1550
+ let label;
1551
+ if (/^\d+$/.test(idParam) && Number(idParam) > 0) {
1552
+ label = db.getLabelByID(ctx.repo.Repository.id, Number(idParam));
1553
+ }
1554
+ else {
1555
+ label = db.getLabelByName(ctx.repo.Repository.id, idParam);
1556
+ }
1557
+ if (!label) {
1558
+ ctx.notFound();
1559
+ return;
1560
+ }
1561
+ ctx.c.JSONSuccess(toIssueLabel(label));
1562
+ }));
1563
+ m.post('/api/v1/repos/:username/:reponame/labels', reqTokenWrap(async (ctx) => {
1564
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1565
+ if (!ok)
1566
+ return;
1567
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1568
+ ctx.c.Status(403);
1569
+ ctx.c.res.end();
1570
+ ctx.c.rendered = true;
1571
+ return;
1572
+ }
1573
+ const body = (await ctx.c.form());
1574
+ const name = String(body.name ?? '').trim();
1575
+ let color = String(body.color ?? '').trim();
1576
+ if (!name || !color) {
1577
+ ctx.c.JSON(422, [{ fieldNames: [!name ? 'name' : 'color'], classification: 'RequiredError', message: 'Required' }]);
1578
+ return;
1579
+ }
1580
+ if (!color.startsWith('#'))
1581
+ color = '#' + color;
1582
+ if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
1583
+ ctx.c.JSON(422, [{ fieldNames: ['color'], classification: 'SizeError', message: 'Size must be between 6 and 7.' }]);
1584
+ return;
1585
+ }
1586
+ const info = db.db().prepare('INSERT INTO label (repo_id, name, color, num_issues, num_closed_issues) VALUES (?,?,?,0,0)').run(ctx.repo.Repository.id, name, color);
1587
+ const label = db.getLabelByID(ctx.repo.Repository.id, Number(info.lastInsertRowid));
1588
+ ctx.c.JSON(201, toIssueLabel(label));
1589
+ }));
1590
+ m.patch('/api/v1/repos/:username/:reponame/labels/:id', reqTokenWrap(async (ctx) => {
1591
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1592
+ if (!ok)
1593
+ return;
1594
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1595
+ ctx.c.Status(403);
1596
+ ctx.c.res.end();
1597
+ ctx.c.rendered = true;
1598
+ return;
1599
+ }
1600
+ const label = db.getLabelByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1601
+ if (!label) {
1602
+ ctx.notFound();
1603
+ return;
1604
+ }
1605
+ const body = (await ctx.c.form());
1606
+ let color = label.color;
1607
+ if (body.color !== undefined) {
1608
+ color = String(body.color).startsWith('#') ? String(body.color) : '#' + String(body.color);
1609
+ }
1610
+ db.db().prepare('UPDATE label SET name = ?, color = ? WHERE id = ?').run(String(body.name ?? label.name), color, label.id);
1611
+ ctx.c.JSONSuccess(toIssueLabel(db.getLabelByID(ctx.repo.Repository.id, label.id)));
1612
+ }));
1613
+ m.delete('/api/v1/repos/:username/:reponame/labels/:id', reqTokenWrap(async (ctx) => {
1614
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1615
+ if (!ok)
1616
+ return;
1617
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1618
+ ctx.c.Status(403);
1619
+ ctx.c.res.end();
1620
+ ctx.c.rendered = true;
1621
+ return;
1622
+ }
1623
+ const label = db.getLabelByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1624
+ if (!label) {
1625
+ ctx.notFound();
1626
+ return;
1627
+ }
1628
+ db.db().prepare('DELETE FROM label WHERE id = ?').run(label.id);
1629
+ db.db().prepare('DELETE FROM issue_label WHERE label_id = ?').run(label.id);
1630
+ ctx.c.NoContent();
1631
+ }));
1632
+ // milestones
1633
+ m.get('/api/v1/repos/:username/:reponame/milestones', repoGroup(async (ctx) => {
1634
+ ctx.c.JSONSuccess(db.listMilestones(ctx.repo.Repository.id, null).map(toIssueMilestone));
1635
+ }));
1636
+ m.get('/api/v1/repos/:username/:reponame/milestones/:id', repoGroup(async (ctx) => {
1637
+ const m = db.getMilestoneByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1638
+ if (!m) {
1639
+ ctx.notFound();
1640
+ return;
1641
+ }
1642
+ ctx.c.JSONSuccess(toIssueMilestone(m));
1643
+ }));
1644
+ m.post('/api/v1/repos/:username/:reponame/milestones', reqTokenWrap(async (ctx) => {
1645
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1646
+ if (!ok)
1647
+ return;
1648
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1649
+ ctx.c.Status(403);
1650
+ ctx.c.res.end();
1651
+ ctx.c.rendered = true;
1652
+ return;
1653
+ }
1654
+ const body = (await ctx.c.form());
1655
+ let deadline = 4102444800; // 9999-12-31 gogs default sentinel
1656
+ if (body.due_on) {
1657
+ const t = new Date(String(body.due_on));
1658
+ if (Number.isNaN(t.getTime())) {
1659
+ ctx.c.JSON(422, [{ fieldNames: ['due_on'], classification: 'DeserializationError', message: 'Invalid due_on' }]);
1660
+ return;
1661
+ }
1662
+ deadline = Math.floor(t.getTime() / 1000);
1663
+ }
1664
+ const info = db.db().prepare('INSERT INTO milestone (repo_id, name, content, deadline_unix, num_issues, num_closed_issues, completeness) VALUES (?,?,?,?,0,0,0)').run(ctx.repo.Repository.id, String(body.title ?? ''), String(body.description ?? ''), deadline);
1665
+ db.refreshMilestoneCounts(ctx.repo.Repository.id);
1666
+ ctx.c.JSON(201, toIssueMilestone(db.getMilestoneByID(ctx.repo.Repository.id, Number(info.lastInsertRowid))));
1667
+ }));
1668
+ m.patch('/api/v1/repos/:username/:reponame/milestones/:id', reqTokenWrap(async (ctx) => {
1669
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1670
+ if (!ok)
1671
+ return;
1672
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1673
+ ctx.c.Status(403);
1674
+ ctx.c.res.end();
1675
+ ctx.c.rendered = true;
1676
+ return;
1677
+ }
1678
+ const m = db.getMilestoneByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1679
+ if (!m) {
1680
+ ctx.notFound();
1681
+ return;
1682
+ }
1683
+ const body = (await ctx.c.form());
1684
+ if (body.title !== undefined || body.description !== undefined || body.due_on !== undefined) {
1685
+ let deadline = m.deadline_unix ?? 0;
1686
+ if (body.due_on !== undefined) {
1687
+ if (body.due_on === null)
1688
+ deadline = 0;
1689
+ else {
1690
+ const t = new Date(String(body.due_on));
1691
+ deadline = Number.isNaN(t.getTime()) ? deadline : Math.floor(t.getTime() / 1000);
1692
+ }
1693
+ }
1694
+ db.db().prepare('UPDATE milestone SET name = ?, content = ?, deadline_unix = ? WHERE id = ?').run(String(body.title ?? m.name), body.description !== undefined ? String(body.description) : m.content, deadline, m.id);
1695
+ }
1696
+ if (body.state !== undefined) {
1697
+ const isClosed = String(body.state) === 'closed' ? 1 : 0;
1698
+ db.db().prepare('UPDATE milestone SET is_closed = ?, closed_date_unix = ? WHERE id = ?').run(isClosed, isClosed ? Math.floor(Date.now() / 1000) : 0, m.id);
1699
+ }
1700
+ db.refreshMilestoneCounts(ctx.repo.Repository.id);
1701
+ ctx.c.JSONSuccess(toIssueMilestone(db.getMilestoneByID(ctx.repo.Repository.id, m.id)));
1702
+ }));
1703
+ m.delete('/api/v1/repos/:username/:reponame/milestones/:id', reqTokenWrap(async (ctx) => {
1704
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1705
+ if (!ok)
1706
+ return;
1707
+ if (ctx.repo.AccessMode < db.AccessMode.WRITE) {
1708
+ ctx.c.Status(403);
1709
+ ctx.c.res.end();
1710
+ ctx.c.rendered = true;
1711
+ return;
1712
+ }
1713
+ const m = db.getMilestoneByID(ctx.repo.Repository.id, ctx.c.ParamsInt64(':id'));
1714
+ if (!m) {
1715
+ ctx.notFound();
1716
+ return;
1717
+ }
1718
+ db.db().prepare('UPDATE issue SET milestone_id = 0 WHERE milestone_id = ?').run(m.id);
1719
+ db.db().prepare('DELETE FROM milestone WHERE id = ?').run(m.id);
1720
+ db.refreshMilestoneCounts(ctx.repo.Repository.id);
1721
+ ctx.c.NoContent();
1722
+ }));
1723
+ // repo settings
1724
+ m.patch('/api/v1/repos/:username/:reponame/issue-tracker', reqTokenWrap(async (ctx) => {
1725
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1726
+ if (!ok)
1727
+ return;
1728
+ if (ctx.repo.AccessMode < db.AccessMode.ADMIN) {
1729
+ ctx.c.Status(403);
1730
+ ctx.c.res.end();
1731
+ ctx.c.rendered = true;
1732
+ return;
1733
+ }
1734
+ const body = (await ctx.c.form());
1735
+ const repo = ctx.repo.Repository;
1736
+ const cols = {};
1737
+ if (body.enable_issues !== undefined)
1738
+ cols['enable_issues'] = body.enable_issues ? 1 : 0;
1739
+ if (body.enable_external_tracker !== undefined)
1740
+ cols['enable_external_tracker'] = body.enable_external_tracker ? 1 : 0;
1741
+ if (body.external_tracker_url !== undefined)
1742
+ cols['external_tracker_url'] = String(body.external_tracker_url);
1743
+ if (body.tracker_url_format !== undefined)
1744
+ cols['external_tracker_format'] = String(body.tracker_url_format);
1745
+ if (body.tracker_issue_style !== undefined)
1746
+ cols['external_tracker_style'] = String(body.tracker_issue_style);
1747
+ db.updateRepoColumns(repo.id, cols);
1748
+ ctx.c.NoContent();
1749
+ }));
1750
+ m.patch('/api/v1/repos/:username/:reponame/wiki', reqTokenWrap(async (ctx) => {
1751
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1752
+ if (!ok)
1753
+ return;
1754
+ if (ctx.repo.AccessMode < db.AccessMode.ADMIN) {
1755
+ ctx.c.Status(403);
1756
+ ctx.c.res.end();
1757
+ ctx.c.rendered = true;
1758
+ return;
1759
+ }
1760
+ const body = (await ctx.c.form());
1761
+ const repo = ctx.repo.Repository;
1762
+ const cols = {};
1763
+ if (body.enable_wiki !== undefined)
1764
+ cols['enable_wiki'] = body.enable_wiki ? 1 : 0;
1765
+ if (body.allow_public_wiki !== undefined)
1766
+ cols['allow_public_wiki'] = body.allow_public_wiki ? 1 : 0;
1767
+ if (body.enable_external_wiki !== undefined)
1768
+ cols['enable_external_wiki'] = body.enable_external_wiki ? 1 : 0;
1769
+ if (body.external_wiki_url !== undefined)
1770
+ cols['external_wiki_url'] = String(body.external_wiki_url);
1771
+ db.updateRepoColumns(repo.id, cols);
1772
+ ctx.c.NoContent();
1773
+ }));
1774
+ m.post('/api/v1/repos/:username/:reponame/mirror-sync', reqTokenWrap(async (ctx) => {
1775
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1776
+ if (!ok)
1777
+ return;
1778
+ if (ctx.repo.AccessMode < db.AccessMode.ADMIN) {
1779
+ ctx.c.Status(403);
1780
+ ctx.c.res.end();
1781
+ ctx.c.rendered = true;
1782
+ return;
1783
+ }
1784
+ if (!ctx.repo.Repository.is_mirror) {
1785
+ ctx.notFound();
1786
+ return;
1787
+ }
1788
+ ctx.c.Status(202);
1789
+ ctx.c.res.end();
1790
+ ctx.c.rendered = true;
1791
+ // immediate sync in background (gogs MirrorQueue)
1792
+ setImmediate(async () => {
1793
+ try {
1794
+ const { syncMirror } = await import('../mirror.js');
1795
+ await syncMirror(ctx.repo.Repository.id, ctx.UserID());
1796
+ }
1797
+ catch (e) {
1798
+ console.error('[mirror-sync]', e);
1799
+ }
1800
+ });
1801
+ }));
1802
+ m.get('/api/v1/repos/:username/:reponame/editorconfig/:filename', reqTokenWrap(async (ctx) => {
1803
+ const ok = await repoAssignment(ctx, ctx.c.Params(':username'), ctx.c.Params(':reponame'));
1804
+ if (!ok)
1805
+ return;
1806
+ const repo = ctx.repo.Repository;
1807
+ const repoDir = repo.RepoPath();
1808
+ const ref = repo.default_branch || conf.defaultBranch;
1809
+ const { repoEditorconfig, getDefinitionForFilename } = await import('../editorconfig.js');
1810
+ const ec = await repoEditorconfig(repoDir, ref);
1811
+ if (!ec) {
1812
+ ctx.notFound();
1813
+ return;
1814
+ }
1815
+ const def = getDefinitionForFilename(ec, ctx.c.Params(':filename'));
1816
+ if (!def) {
1817
+ ctx.notFound();
1818
+ return;
1819
+ }
1820
+ ctx.c.JSONSuccess(def);
1821
+ }));
1822
+ }
1823
+ function repoAdminGroup(repoGroup, fn) {
1824
+ return reqTokenAdminWrap(repoGroup, fn);
1825
+ }
1826
+ function reqTokenAdminWrap(repoGroup, fn) {
1827
+ return async (c) => {
1828
+ // repoGroup already applies reqToken + repoAssignment; layer admin check
1829
+ let adminChecked = false;
1830
+ const wrapped = async (ctx) => {
1831
+ if (!adminChecked) {
1832
+ adminChecked = true;
1833
+ if (ctx.repo.AccessMode < db.AccessMode.ADMIN) {
1834
+ ctx.c.Status(403);
1835
+ ctx.c.res.end();
1836
+ ctx.c.rendered = true;
1837
+ return;
1838
+ }
1839
+ }
1840
+ await fn(ctx);
1841
+ };
1842
+ await repoGroup(wrapped)(c);
1843
+ };
1844
+ }
1845
+ function toRepositoryHook(hook) {
1846
+ const eventsObj = JSON.parse(hook.events ?? '{}');
1847
+ const events = [];
1848
+ for (const [ev, on] of Object.entries(eventsObj.events ?? {})) {
1849
+ if (on)
1850
+ events.push(ev);
1851
+ }
1852
+ if (eventsObj.push_only)
1853
+ events.push('push');
1854
+ if (eventsObj.send_everything)
1855
+ events.push('*');
1856
+ const config = {
1857
+ url: hook.url,
1858
+ content_type: hook.content_type === 2 ? 'form' : 'json',
1859
+ };
1860
+ if (hook.hook_task_type === 2) {
1861
+ const meta = JSON.parse(hook.meta ?? '{}');
1862
+ config.channel = meta.channel ?? '';
1863
+ config.username = meta.username ?? '';
1864
+ config.icon_url = meta.icon_url ?? '';
1865
+ config.color = meta.color ?? '';
1866
+ }
1867
+ return {
1868
+ id: hook.id,
1869
+ type: { 1: 'gogs', 2: 'slack', 3: 'discord', 4: 'dingtalk' }[String(hook.hook_task_type)] ?? 'gogs',
1870
+ config,
1871
+ events,
1872
+ active: !!hook.is_active,
1873
+ updated_at: new Date((hook.updated_unix ?? 0) * 1000).toISOString(),
1874
+ created_at: new Date((hook.created_unix ?? 0) * 1000).toISOString(),
1875
+ };
1876
+ }
1877
+ function webhookCommitFromCommit(cm) {
1878
+ return {
1879
+ id: cm.id,
1880
+ message: cm.message,
1881
+ url: 'Not implemented',
1882
+ author: { name: cm.author.name, email: cm.author.email, username: db.getUserByEmail(cm.author.email)?.name ?? '' },
1883
+ committer: { name: cm.committer.name, email: cm.committer.email, username: db.getUserByEmail(cm.committer.email)?.name ?? '' },
1884
+ added: null,
1885
+ removed: null,
1886
+ modified: null,
1887
+ timestamp: cm.committer.when.toISOString(),
1888
+ };
1889
+ }
1890
+ async function contentsHandler(ctx) {
1891
+ const repo = ctx.repo.Repository;
1892
+ const repoDir = repo.RepoPath();
1893
+ const ref = ctx.c.Query('ref') || repo.default_branch || conf.defaultBranch;
1894
+ const filePath = ctx.c.Params(':*') ?? '';
1895
+ const resolved = await git.resolveRef(repoDir, ref);
1896
+ if (!resolved) {
1897
+ ctx.notFound();
1898
+ return;
1899
+ }
1900
+ const commit = await git.getCommit(repoDir, resolved);
1901
+ if (!commit) {
1902
+ ctx.notFound();
1903
+ return;
1904
+ }
1905
+ const tree = await git.lsTree(repoDir, commit.id, filePath);
1906
+ if (!tree) {
1907
+ ctx.notFound();
1908
+ return;
1909
+ }
1910
+ if (filePath) {
1911
+ // file / symlink / submodule case
1912
+ const target = tree.entries[0];
1913
+ if (target && target.type === 'blob') {
1914
+ const content = await git.blobBytes(repoDir, target.sha);
1915
+ ctx.c.JSONSuccess(contentsEntry(repo, filePath, target, ref, content));
1916
+ return;
1917
+ }
1918
+ if (target && target.type === 'commit') {
1919
+ ctx.c.JSONSuccess(contentsEntry(repo, filePath, target, ref));
1920
+ return;
1921
+ }
1922
+ }
1923
+ // directory listing
1924
+ const out = [];
1925
+ for (const e of tree.entries) {
1926
+ out.push(contentsEntry(repo, filePath ? `${filePath}/${e.name}` : e.name, e, ref));
1927
+ }
1928
+ ctx.c.JSONSuccess(out);
1929
+ }
1930
+ function contentsEntry(repo, filePath, entry, ref, content) {
1931
+ const base = conf.externalURL + repo.FullName();
1932
+ const isFile = entry.type === 'blob';
1933
+ const isSubmodule = entry.type === 'commit';
1934
+ const name = filePath.split('/').pop() ?? '';
1935
+ const out = {
1936
+ type: isSubmodule ? 'submodule' : isFile ? 'file' : 'dir',
1937
+ size: isFile ? (entry.size ?? 0) : 0,
1938
+ name,
1939
+ path: filePath,
1940
+ sha: entry.sha,
1941
+ url: `${base}/api/v1/contents/${filePath}?ref=${ref}`,
1942
+ git_url: `${base}/api/v1/git/${isFile ? 'blobs' : 'trees'}/${entry.sha}`,
1943
+ html_url: `${base}/src/${ref}/${filePath}`,
1944
+ download_url: isFile ? `${base}/raw/${ref}/${filePath}` : null,
1945
+ _links: {
1946
+ git: `${base}/api/v1/git/${isFile ? 'blobs' : 'trees'}/${entry.sha}`,
1947
+ self: `${base}/api/v1/contents/${filePath}?ref=${ref}`,
1948
+ html: `${base}/src/${ref}/${filePath}`,
1949
+ },
1950
+ };
1951
+ if (content !== undefined) {
1952
+ out.encoding = 'base64';
1953
+ out.content = content.toString('base64');
1954
+ }
1955
+ // strip null download_url like omitempty would
1956
+ if (out.download_url === null)
1957
+ delete out.download_url;
1958
+ return out;
1959
+ }
1960
+ async function editIssueComment(ctx) {
1961
+ const cm = db.getCommentByID(ctx.c.ParamsInt64(':id'));
1962
+ if (!cm) {
1963
+ ctx.notFound();
1964
+ return;
1965
+ }
1966
+ const issue = db.getIssueByID(cm.issue_id);
1967
+ const repo = db.getRepoByID(issue.repo_id);
1968
+ if (!repo || repo.id !== ctx.repo.Repository.id) {
1969
+ ctx.notFound();
1970
+ return;
1971
+ }
1972
+ if (cm.poster_id !== ctx.UserID() && ctx.repo.AccessMode < db.AccessMode.ADMIN) {
1973
+ ctx.c.Status(403);
1974
+ ctx.c.res.end();
1975
+ ctx.c.rendered = true;
1976
+ return;
1977
+ }
1978
+ if (cm.type !== 0) {
1979
+ ctx.c.NoContent();
1980
+ return;
1981
+ }
1982
+ const body = (await ctx.c.form());
1983
+ const content = String(body.body ?? '').trim();
1984
+ if (!content) {
1985
+ ctx.c.JSON(422, [{ fieldNames: ['body'], classification: 'RequiredError', message: 'Required' }]);
1986
+ return;
1987
+ }
1988
+ db.db().prepare('UPDATE comment SET content = ?, updated_unix = ? WHERE id = ?').run(content, Math.floor(Date.now() / 1000), cm.id);
1989
+ ctx.c.JSONSuccess(toIssueComment(db.getCommentByID(cm.id), issue, ctx.repo.Repository));
1990
+ }
1991
+ async function deleteIssueComment(ctx) {
1992
+ const cm = db.getCommentByID(ctx.c.ParamsInt64(':id'));
1993
+ if (!cm) {
1994
+ ctx.notFound();
1995
+ return;
1996
+ }
1997
+ const issue = db.getIssueByID(cm.issue_id);
1998
+ const repo = db.getRepoByID(issue.repo_id);
1999
+ if (!repo || repo.id !== ctx.repo.Repository.id) {
2000
+ ctx.notFound();
2001
+ return;
2002
+ }
2003
+ if (cm.poster_id !== ctx.UserID() && ctx.repo.AccessMode < db.AccessMode.ADMIN) {
2004
+ ctx.c.Status(403);
2005
+ ctx.c.res.end();
2006
+ ctx.c.rendered = true;
2007
+ return;
2008
+ }
2009
+ db.db().prepare('DELETE FROM comment WHERE id = ?').run(cm.id);
2010
+ db.db().prepare('UPDATE issue SET num_comments = MAX(num_comments - 1, 0) WHERE id = ?').run(issue.id);
2011
+ ctx.c.NoContent();
2012
+ }
2013
+ async function getReferenceSHAHandler(ctx, ref) {
2014
+ const repoDir = ctx.repo.Repository.RepoPath();
2015
+ let refName = ref;
2016
+ if (!refName.startsWith('refs/')) {
2017
+ if (await git.refExists(repoDir, 'refs/heads/' + refName))
2018
+ refName = 'refs/heads/' + refName;
2019
+ else if (await git.refExists(repoDir, 'refs/tags/' + refName))
2020
+ refName = 'refs/tags/' + refName;
2021
+ }
2022
+ const sha = (await git.gitOK(repoDir, 'rev-parse', '--verify', '--quiet', '--end-of-options', refName))?.toString().trim();
2023
+ if (!sha) {
2024
+ ctx.notFound();
2025
+ return;
2026
+ }
2027
+ ctx.c.PlainText(200, sha);
2028
+ }
2029
+ //# sourceMappingURL=v1.js.map