plz / repo / repo_routes.v
841 lines · 732 sloc · 25.78 KB · 39bb677e657ad02c120a500c85e41b6e16aa7750
Raw
1module main
2
3import veb
4import api
5import crypto.sha1
6import os
7import time
8import highlight
9import validation
10import git
11import config
12
13const top_files_limit = 50
14
15@['/:username/repos']
16pub fn (mut app App) user_repos(username string) veb.Result {
17 exists, user := app.check_username(username)
18
19 if !exists {
20 return ctx.not_found()
21 }
22
23 mut repos := app.find_user_public_repos(user.id)
24
25 if user.id == ctx.user.id {
26 repos = app.find_user_repos(user.id)
27 }
28
29 for mut repo in repos {
30 repo.lang_stats = app.find_repo_lang_stats(repo.id)
31 repo.latest_commit_at = app.find_repo_last_commit_time(repo.id)
32 repo.activity_buckets = app.get_repo_activity_buckets(repo.id)
33 }
34
35 return $veb.html('templates/user/repos.html')
36}
37
38@['/:username/stars']
39pub fn (mut app App) user_stars(username string) veb.Result {
40 exists, user := app.check_username(username)
41
42 if !exists {
43 return ctx.not_found()
44 }
45
46 repos := app.find_user_starred_repos(ctx.user.id)
47
48 return $veb.html('templates/user/stars.html')
49}
50
51@['/:username/:repo_name/settings']
52pub fn (mut app App) repo_settings(username string, repo_name string) veb.Result {
53 repo := app.find_repo_by_name_and_username(repo_name, username) or {
54 return ctx.redirect_to_repository(username, repo_name)
55 }
56 is_owner := app.check_repo_owner(ctx.user.username, repo_name)
57
58 if !is_owner {
59 return ctx.redirect_to_repository(username, repo_name)
60 }
61
62 return $veb.html('templates/repo/settings.html')
63}
64
65@['/:username/:repo_name/settings'; post]
66pub fn (mut app App) handle_update_repo_settings(username string, repo_name string, webhook_secret string) veb.Result {
67 repo := app.find_repo_by_name_and_username(repo_name, username) or {
68 return ctx.redirect_to_repository(username, repo_name)
69 }
70 is_owner := app.check_repo_owner(ctx.user.username, repo_name)
71
72 if !is_owner {
73 return ctx.redirect_to_repository(username, repo_name)
74 }
75
76 if webhook_secret != '' && webhook_secret != repo.webhook_secret {
77 webhook := sha1.hexhash(webhook_secret)
78 app.set_repo_webhook_secret(repo.id, webhook) or { app.info(err.str()) }
79 }
80
81 return ctx.redirect_to_repository(username, repo_name)
82}
83
84@['/:username/:repo_name/settings/features'; post]
85pub fn (mut app App) handle_update_repo_features(username string, repo_name string) veb.Result {
86 repo := app.find_repo_by_name_and_username(repo_name, username) or {
87 return ctx.redirect_to_repository(username, repo_name)
88 }
89 is_owner := app.check_repo_owner(ctx.user.username, repo_name)
90
91 if !is_owner {
92 return ctx.redirect_to_repository(username, repo_name)
93 }
94
95 disable_discussions := 'discussions_enabled' !in ctx.form
96 disable_projects := 'projects_enabled' !in ctx.form
97 disable_milestones := 'milestones_enabled' !in ctx.form
98 disable_wiki := 'wiki_enabled' !in ctx.form
99
100 app.update_repo_features(repo.id, disable_discussions, disable_projects, disable_milestones,
101 disable_wiki) or { app.info(err.str()) }
102
103 return ctx.redirect('/${username}/${repo_name}/settings')
104}
105
106@['/:user/:repo_name/delete'; post]
107pub fn (mut app App) handle_repo_delete(username string, repo_name string) veb.Result {
108 repo := app.find_repo_by_name_and_username(repo_name, username) or {
109 return ctx.redirect_to_repository(username, repo_name)
110 }
111 is_owner := app.check_repo_owner(ctx.user.username, repo_name)
112
113 if !is_owner {
114 return ctx.redirect_to_repository(username, repo_name)
115 }
116
117 if ctx.form['verify'] == '${username}/${repo_name}' {
118 spawn app.delete_repository(repo.id, repo.git_dir, repo.name)
119 } else {
120 ctx.error('Verification failed')
121 return app.repo_settings(mut ctx, username, repo_name)
122 }
123
124 return ctx.redirect_to_index()
125}
126
127@['/:username/:repo_name/move'; post]
128pub fn (mut app App) handle_repo_move(username string, repo_name string, dest string, verify string) veb.Result {
129 repo := app.find_repo_by_name_and_username(repo_name, username) or {
130 return ctx.redirect_to_index()
131 }
132 is_owner := app.check_repo_owner(ctx.user.username, repo_name)
133
134 if !is_owner {
135 return ctx.redirect_to_repository(username, repo_name)
136 }
137
138 if dest != '' && verify == '${username}/${repo_name}' {
139 dest_user := app.get_user_by_username(dest) or {
140 ctx.error('Unknown user ${dest}')
141 return app.repo_settings(mut ctx, username, repo_name)
142 }
143
144 if app.user_has_repo(dest_user.id, repo.name) {
145 ctx.error('User already owns repo ${repo.name}')
146 return app.repo_settings(mut ctx, username, repo_name)
147 }
148
149 if app.get_count_user_repos(dest_user.id) >= max_user_repos {
150 ctx.error('User already reached the repo limit')
151 return app.repo_settings(mut ctx, username, repo_name)
152 }
153
154 app.move_repo_to_user(repo.id, dest_user.id, dest_user.username) or {
155 ctx.error('There was an error while moving the repo')
156 return app.repo_settings(mut ctx, username, repo_name)
157 }
158
159 return ctx.redirect('/${dest_user.username}/${repo.name}')
160 } else {
161 ctx.error('Verification failed')
162
163 return app.repo_settings(mut ctx, username, repo_name)
164 }
165
166 return ctx.redirect_to_index()
167}
168
169@['/:username/:repo_name']
170pub fn (mut app App) handle_tree(mut ctx Context, username string, repo_name string) veb.Result {
171 match repo_name {
172 'repos' {
173 return app.user_repos(mut ctx, username)
174 }
175 'issues' {
176 return app.handle_get_user_issues(mut ctx, username)
177 }
178 'pulls' {
179 return app.handle_get_user_pulls(mut ctx, username)
180 }
181 'settings' {
182 return app.user_settings(mut ctx, username)
183 }
184 else {}
185 }
186
187 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
188
189 return app.tree(mut ctx, username, repo_name, repo.primary_branch, '')
190}
191
192@['/:username/:repo_name/tree/:branch_name']
193pub fn (mut app App) handle_branch_tree(mut ctx Context, username string, repo_name string, branch_name string) veb.Result {
194 app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
195
196 return app.tree(mut ctx, username, repo_name, branch_name, '')
197}
198
199@['/:username/:repo_name/update']
200pub fn (mut app App) handle_repo_update(mut ctx Context, username string, repo_name string) veb.Result {
201 mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
202 return ctx.not_found()
203 }
204
205 if ctx.user.is_admin {
206 app.update_repo_from_remote(mut repo) or { app.info(err.str()) }
207 app.slow_fetch_files_info(mut repo, 'master', '.') or { app.info(err.str()) }
208 }
209
210 return ctx.redirect_to_repository(username, repo_name)
211}
212
213@['/new']
214pub fn (mut app App) new() veb.Result {
215 if !ctx.logged_in {
216 return ctx.redirect_to_login()
217 }
218 orgs := app.find_orgs_for_user(ctx.user.id)
219 selected_owner := ctx.query['owner'] or { ctx.user.username }
220 return $veb.html()
221}
222
223@['/new'; post]
224pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url string, description string, no_redirect string) veb.Result {
225 println('NEW POST')
226 mut valid_clone_url := clone_url
227 is_clone_url_empty := validation.is_string_empty(clone_url)
228 is_public := ctx.form['repo_visibility'] == 'public'
229 if !ctx.logged_in {
230 return ctx.redirect_to_login()
231 }
232 owner := ctx.form['owner'] or { ctx.user.username }
233 mut owner_name := ctx.user.username
234 mut owner_org_id := 0
235 if owner != ctx.user.username {
236 org := app.get_org_by_name(owner) or {
237 ctx.error('Unknown owner "${owner}"')
238 return app.new(mut ctx)
239 }
240 if !app.is_org_member(org.id, ctx.user.id) {
241 ctx.error('You are not a member of "${owner}"')
242 return app.new(mut ctx)
243 }
244 owner_name = org.name
245 owner_org_id = org.id
246 }
247 if owner_org_id == 0 && !ctx.is_admin() && app.get_count_user_repos(ctx.user.id) >= max_user_repos {
248 ctx.error('You have reached the limit for the number of repositories')
249 return app.new(mut ctx)
250 }
251 if name.len > max_repo_name_len {
252 ctx.error('The repository name is too long (should be fewer than ${max_repo_name_len} characters)')
253 return app.new(mut ctx)
254 }
255 eprintln(1)
256 if _ := app.find_repo_by_name_and_username(name, owner_name) {
257 ctx.error('A repository with the name "${name}" already exists')
258 return app.new(mut ctx)
259 }
260 eprintln(2)
261 if name.contains(' ') {
262 ctx.error('Repository name cannot contain spaces')
263 return app.new(mut ctx)
264 }
265 eprintln(3)
266 is_repo_name_valid := validation.is_repository_name_valid(name)
267 if !is_repo_name_valid {
268 ctx.error('The repository name is not valid')
269 return app.new(mut ctx)
270 }
271 eprintln(4)
272 has_clone_url_https_prefix := clone_url.starts_with('https://')
273 if !is_clone_url_empty {
274 if !has_clone_url_https_prefix {
275 valid_clone_url = 'https://' + clone_url
276 }
277 println('checking')
278 is_git_repo := git.check_git_repo_url(valid_clone_url)
279 println('done')
280 if !is_git_repo {
281 ctx.error('The repository URL does not contain any git repository or the server does not respond')
282 return app.new(mut ctx)
283 }
284 }
285 println('OK')
286 owner_dir := os.join_path(app.config.repo_storage_path, owner_name)
287 if !os.exists(owner_dir) {
288 os.mkdir(owner_dir) or { app.info('failed to create owner dir ${owner_dir}: ${err}') }
289 }
290 repo_path := os.join_path(owner_dir, name)
291 id := app.get_max_repo_id() + 1
292 mut new_repo := &Repo{
293 name: name
294 id: id
295 description: description
296 git_dir: repo_path
297 user_id: ctx.user.id
298 primary_branch: 'master'
299 user_name: owner_name
300 clone_url: valid_clone_url
301 is_public: is_public
302 }
303 import_issues := ctx.form['import_issues'] == '1'
304 if is_clone_url_empty {
305 os.mkdir(new_repo.git_dir) or { panic(err) }
306 new_repo.git('init --bare')
307 } else {
308 new_repo.status = .cloning
309 }
310 // Insert the repo row BEFORE spawning the clone thread, so that the
311 // background `set_repo_status(.done)` UPDATE has a row to match.
312 app.add_repo(new_repo) or {
313 ctx.error('There was an error while adding the repo ${err}')
314 return app.new(mut ctx)
315 }
316 if !is_clone_url_empty {
317 app.debug('cloning')
318 clone_job_repo := *new_repo
319 spawn clone_repo(clone_job_repo, app.config, import_issues, ctx.user.id)
320 }
321 new_repo2 := app.find_repo_by_name_and_username(new_repo.name, owner_name) or {
322 app.info('Repo was not inserted')
323 return ctx.redirect('/new')
324 }
325 repo_id := new_repo2.id
326 // $dbg;
327 // primary_branch := git.get_repository_primary_branch(repo_path)
328 primary_branch := new_repo2.primary_branch
329 // app.debug("new_repo2: ${new_repo2}")
330
331 app.update_repo_primary_branch(repo_id, primary_branch) or {
332 ctx.error('There was an error while adding the repo')
333 return app.new(mut ctx)
334 }
335 app.find_repo_by_id(repo_id) or { return app.new(mut ctx) }
336 // Update only cloned repositories
337 /*
338 if !is_clone_url_empty {
339 app.update_repo_from_fs(mut new_repo, true) or {
340 ctx.error('There was an error while cloning the repo')
341 return app.new(mut ctx)
342 }
343 }
344 */
345 if no_redirect == '1' {
346 return ctx.text('ok')
347 }
348 has_first_repo_activity := app.has_activity(ctx.user.id, 'first_repo')
349 if !has_first_repo_activity {
350 app.add_activity(ctx.user.id, 'first_repo') or { app.info(err.str()) }
351 }
352 return ctx.redirect('/${owner_name}/${new_repo.name}')
353}
354
355fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Config) {
356 mut repo := repo_
357 mut app := &App{
358 db: connect_db(conf) or {
359 eprintln('cannot open ${db_backend_name()} db connection for bg_fetch thread: ${err}')
360 return
361 }
362 config: conf
363 }
364 app.load_settings()
365 app.slow_fetch_files_info(mut repo, branch, path) or {
366 eprintln('bg_fetch_files_info error: ${err}')
367 }
368 if app.settings.tree_folder_size_enabled() {
369 app.slow_fetch_folder_sizes(mut repo, branch, path) or {
370 eprintln('bg_fetch_folder_sizes error: ${err}')
371 }
372 }
373 app.db.close() or {}
374}
375
376fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, owner_user_id int) {
377 mut cloned_repo := new_repo
378 cloned_repo.clone()
379 // Use a dedicated DB connection for the clone thread to avoid
380 // sharing a connection across threads.
381 mut app := &App{
382 db: connect_db(conf) or {
383 eprintln('cannot open ${db_backend_name()} db connection for clone thread: ${err}')
384 return
385 }
386 config: conf
387 }
388 // Mark repo as done immediately so the user can browse it.
389 // The tree page will fetch files from git on demand.
390 app.set_repo_status(cloned_repo.id, .done) or { eprintln('cannot set repo status ${err}') }
391 eprintln('clone done, repo available — indexing in background')
392 // For GitHub clones, also pull the repo description and contributors list
393 // (the issue import is gated on a separate user opt-in).
394 if cloned_repo.clone_url.contains('github.com') {
395 spawn bg_import_github_repo_info(cloned_repo.id, cloned_repo.clone_url,
396 cloned_repo.description, conf)
397 if import_issues {
398 spawn bg_import_github_issues(cloned_repo.id, cloned_repo.clone_url, owner_user_id,
399 conf)
400 }
401 }
402 // Index branches, commits, and language stats in the background.
403 app.update_repo_from_fs(mut cloned_repo, true) or {
404 eprintln('cannot update repo from fs ${err}')
405 }
406 eprintln('background indexing complete')
407 app.db.close() or {}
408}
409
410fn bg_import_github_repo_info(repo_id int, clone_url string, existing_description string, conf config.Config) {
411 eprintln('[github-info] spawned thread for repo_id=${repo_id}')
412 mut app := &App{
413 db: connect_db(conf) or {
414 eprintln('[github-info] cannot open db connection: ${err}')
415 return
416 }
417 config: conf
418 }
419 defer {
420 app.db.close() or {}
421 }
422 if existing_description.trim_space() == '' {
423 description := fetch_github_repo_description(clone_url)
424 if description != '' {
425 app.set_repo_description(repo_id, description) or {
426 eprintln('[github-info] cannot save description: ${err}')
427 }
428 }
429 }
430 app.import_github_contributors(repo_id, clone_url) or {
431 eprintln('[github-contrib] FAILED: ${err}')
432 }
433}
434
435fn bg_import_github_issues(repo_id int, clone_url string, owner_user_id int, conf config.Config) {
436 eprintln('[github-import] spawned thread for repo_id=${repo_id}')
437 mut app := &App{
438 db: connect_db(conf) or {
439 eprintln('[github-import] cannot open db connection for import thread: ${err}')
440 return
441 }
442 config: conf
443 }
444 app.import_github_issues(repo_id, clone_url, owner_user_id) or {
445 eprintln('[github-import] FAILED: ${err}')
446 }
447 app.db.close() or {}
448}
449
450pub fn (mut app App) kekw(mut ctx Context) veb.Result {
451 clone_url := ''
452 clone_progress := ''
453 return $veb.html('templates/cloning_in_process.html')
454}
455
456// read_clone_progress parses a git `--progress` log file and returns
457// the latest output as a single newline-separated string, ready to be
458// shown inside a <pre> block. Git emits live progress with `\r` and
459// stage transitions with `\n`; we collapse repeated progress lines for
460// the same phase ("Counting objects", "Receiving objects", …) so only
461// the most recent value for each phase remains.
462fn read_clone_progress(progress_path string) string {
463 raw := os.read_file(progress_path) or { return '' }
464 if raw.len == 0 {
465 return ''
466 }
467 lines := raw.replace('\r', '\n').split('\n')
468 mut stages := []string{}
469 mut phase_index := map[string]int{}
470 for raw_line in lines {
471 line := raw_line.trim_space()
472 if line == '' {
473 continue
474 }
475 mut body := line
476 if body.starts_with('remote: ') {
477 body = body[8..]
478 }
479 colon := body.index(':') or { -1 }
480 key := if colon == -1 { body } else { body[..colon].trim_space() }
481 if key in phase_index {
482 stages[phase_index[key]] = line
483 } else {
484 phase_index[key] = stages.len
485 stages << line
486 }
487 }
488 return stages.join('\n')
489}
490
491@['/:username/:repo_name/tree/:branch_name/:path...']
492pub fn (mut app App) tree(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
493 tree_t0 := time.ticks()
494 mut tree_t := tree_t0
495 mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
496 eprintln('tree() repo ${repo_name} not found')
497 return ctx.not_found()
498 }
499 eprintln('[tree] find_repo: ${time.ticks() - tree_t}ms')
500 tree_t = time.ticks()
501 mut clone_url := ''
502 mut clone_progress := ''
503 if repo.status == .cloning {
504 clone_url = repo.clone_url
505 clone_progress = read_clone_progress(repo.clone_progress_path())
506 return $veb.html('templates/cloning_in_process.html')
507 }
508
509 _, user := app.check_username(username)
510 eprintln('[tree] check_username: ${time.ticks() - tree_t}ms')
511 tree_t = time.ticks()
512 if !repo.is_public {
513 if user.id != ctx.user.id {
514 return ctx.not_found()
515 }
516 }
517
518 repo_id := repo.id
519
520 // XTODO
521 // app.fetch_tags(repo) or { app.info(err.str()) }
522
523 ctx.current_path = path
524 if path.contains('favicon.svg') {
525 return ctx.not_found()
526 }
527
528 ctx.path_split = [repo_name]
529 if path != '' {
530 ctx.path_split << path.split('/')
531 }
532
533 ctx.is_tree = true
534 ctx.branch = branch_name
535
536 app.increment_repo_views(repo.id) or { app.info(err.str()) }
537 eprintln('[tree] increment_repo_views: ${time.ticks() - tree_t}ms')
538 tree_t = time.ticks()
539
540 mut up := '/'
541 can_up := path != ''
542 if can_up {
543 if !path.contains('/') {
544 up = '../..'
545 } else {
546 up = ctx.req.url.all_before_last('/')
547 }
548 }
549
550 tree_mode := if 'mode' in ctx.query { ctx.query['mode'] } else { 'tree' }
551 is_top_files_mode := tree_mode == 'top-files'
552 top_files := if is_top_files_mode {
553 repo.top_files(branch_name, top_files_limit)
554 } else {
555 []File{}
556 }
557 if is_top_files_mode {
558 eprintln('[tree] top_files: ${time.ticks() - tree_t}ms')
559 tree_t = time.ticks()
560 }
561 tree_url := if path == '' {
562 '/${username}/${repo_name}/tree/${branch_name}'
563 } else {
564 '/${username}/${repo_name}/tree/${branch_name}/${path}'
565 }
566 top_files_url := '/${username}/${repo_name}/tree/${branch_name}?mode=top-files'
567
568 mut items := app.find_repository_items(repo_id, branch_name, ctx.current_path)
569 eprintln('[tree] find_repository_items (${items.len} items): ${time.ticks() - tree_t}ms')
570 tree_t = time.ticks()
571 branch := app.find_repo_branch_by_name(repo.id, branch_name)
572 eprintln('[tree] find_repo_branch_by_name: ${time.ticks() - tree_t}ms')
573 tree_t = time.ticks()
574
575 show_folder_size := app.settings.tree_folder_size_enabled()
576
577 if !is_top_files_mode {
578 if items.len == 0 {
579 // No files in the db, fetch them from git and cache in db
580 items = app.cache_repository_items(mut repo, branch_name, ctx.current_path) or {
581 app.info(err.str())
582 []File{}
583 }
584 eprintln('[tree] cache_repository_items: ${time.ticks() - tree_t}ms')
585 tree_t = time.ticks()
586 // Fetch commit info in background — don't block the page
587 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
588 } else if items.any(it.last_msg == '') {
589 // Some files still need commit info — fetch in background
590 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
591 } else if show_folder_size && items.any(it.is_dir && !it.is_size_calculated) {
592 // Some folders still need size info, fetch in background
593 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
594 }
595 }
596
597 // Fetch last commit message for this directory, printed at the top of the tree
598 mut last_commit := Commit{}
599 mut dir := File{}
600 if can_up {
601 mut p := path
602 if p.ends_with('/') {
603 p = p[0..path.len - 1]
604 }
605 if !p.contains('/') {
606 p = '/${p}'
607 }
608 dir = app.find_repo_file_by_path(repo.id, branch_name, p) or { File{} }
609 if dir.id != 0 {
610 last_commit = app.find_repo_commit_by_hash(repo.id, dir.last_hash)
611 }
612 } else {
613 last_commit = app.find_repo_last_commit(repo.id, branch.id)
614 }
615 eprintln('[tree] last_commit lookup: ${time.ticks() - tree_t}ms')
616 tree_t = time.ticks()
617
618 mut next_dir_idx := 0
619 for scan_idx in 0 .. items.len {
620 if items[scan_idx].is_dir {
621 if scan_idx != next_dir_idx {
622 moving_dir := items[scan_idx]
623 mut move_idx := scan_idx
624 for move_idx > next_dir_idx {
625 items[move_idx] = items[move_idx - 1]
626 move_idx--
627 }
628 items[next_dir_idx] = moving_dir
629 }
630 next_dir_idx++
631 }
632 }
633
634 commits_count := app.get_repo_commit_count(repo.id, branch.id)
635 has_commits := commits_count > 0
636 eprintln('[tree] get_repo_commit_count: ${time.ticks() - tree_t}ms')
637 tree_t = time.ticks()
638
639 // Get readme after updating repository
640 readme_file := find_readme_file(items) or { File{} }
641 readme := render_readme(repo, branch_name, path, readme_file)
642 eprintln('[tree] render_readme: ${time.ticks() - tree_t}ms')
643 tree_t = time.ticks()
644
645 license_file := find_license_file(items) or { File{} }
646 mut license_file_path := ''
647
648 if license_file.id != 0 {
649 license_file_path = '/${username}/${repo_name}/blob/${branch_name}/${license_file.name}'
650 }
651
652 watcher_count := app.get_count_repo_watchers(repo_id)
653 is_repo_starred := app.check_repo_starred(repo_id, ctx.user.id)
654 is_repo_watcher := app.check_repo_watcher_status(repo_id, ctx.user.id)
655 is_top_directory := ctx.current_path == ''
656 eprintln('[tree] watcher/star/watcher_status: ${time.ticks() - tree_t}ms')
657 tree_t = time.ticks()
658
659 // CI status for last commit
660 ci_status := app.find_ci_status_for_commit(repo_id, last_commit.hash) or {
661 app.find_ci_status_for_branch(repo_id, branch_name) or { CiStatus{} }
662 }
663 has_ci := ci_status.id != 0
664 eprintln('[tree] ci_status: ${time.ticks() - tree_t}ms')
665 tree_t = time.ticks()
666
667 mut sidebar_contributors := []User{}
668 mut sidebar_releases := []Release{}
669 if is_top_directory {
670 all_contributors := app.find_repo_registered_contributor(repo_id)
671 sidebar_contributors = if all_contributors.len > 12 {
672 all_contributors[..12]
673 } else {
674 all_contributors
675 }
676
677 rels := app.find_repo_releases_as_page(repo_id, 0)
678 tags := app.get_all_repo_tags(repo_id)
679 for rel in rels {
680 mut r := rel
681 for tag in tags {
682 if tag.id == rel.tag_id {
683 r.tag_name = tag.name
684 r.tag_hash = tag.hash
685 r.date = time.unix(tag.created_at)
686 break
687 }
688 }
689 sidebar_releases << r
690 if sidebar_releases.len >= 3 {
691 break
692 }
693 }
694 eprintln('[tree] sidebar contributors/releases: ${time.ticks() - tree_t}ms')
695 tree_t = time.ticks()
696 }
697
698 eprintln('[tree] pre-render TOTAL ${username}/${repo_name}: ${time.ticks() - tree_t0}ms')
699 return $veb.html()
700}
701
702fn render_readme(repo Repo, branch_name string, path string, readme_file File) veb.RawHtml {
703 if readme_file.id == 0 {
704 return veb.RawHtml('')
705 }
706
707 readme_path := '${path}/${readme_file.name}'
708 readme_content := repo.read_file(branch_name, readme_path)
709 highlighted_readme, _, _ := highlight.highlight_text(readme_content, readme_path, false)
710
711 return veb.RawHtml(highlighted_readme)
712}
713
714@['/api/v1/repos/:repo_id/star'; 'post']
715pub fn (mut app App) handle_api_repo_star(mut ctx Context, repo_id_str string) veb.Result {
716 repo_id := repo_id_str.int()
717
718 has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
719
720 if !has_access {
721 return ctx.json_error('Not found')
722 }
723
724 user_id := ctx.user.id
725 app.toggle_repo_star(repo_id, user_id) or {
726 return ctx.json_error('There was an error while starring the repo')
727 }
728 is_repo_starred := app.check_repo_starred(repo_id, user_id)
729
730 return ctx.json(api.ApiSuccessResponse[bool]{
731 success: true
732 result: is_repo_starred
733 })
734}
735
736@['/api/v1/repos/:repo_id/watch'; 'post']
737pub fn (mut app App) handle_api_repo_watch(mut ctx Context, repo_id_str string) veb.Result {
738 repo_id := repo_id_str.int()
739
740 has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
741
742 if !has_access {
743 return ctx.json_error('Not found')
744 }
745
746 user_id := ctx.user.id
747 app.toggle_repo_watcher_status(repo_id, user_id) or {
748 return ctx.json_error('There was an error while toggling to watch')
749 }
750 is_watching := app.check_repo_watcher_status(repo_id, user_id)
751
752 return ctx.json(api.ApiSuccessResponse[bool]{
753 success: true
754 result: is_watching
755 })
756}
757
758// API: get file listing with commit info for a directory (used by JS polling)
759// Path uses /tree/files to avoid colliding with /api/v1/repos/:username/:repo_name.
760@['/api/v1/repos/:repo_id_str/tree/files']
761pub fn (mut app App) handle_api_repo_files(mut ctx Context, repo_id_str string) veb.Result {
762 repo_id := repo_id_str.int()
763 repo := app.find_repo_by_id(repo_id) or { return ctx.json_error('Not found') }
764
765 if !repo.is_public && repo.user_id != ctx.user.id {
766 return ctx.json_error('Not found')
767 }
768
769 branch := if 'branch' in ctx.query { ctx.query['branch'] } else { '' }
770 path := if 'path' in ctx.query { ctx.query['path'] } else { '' }
771
772 if branch == '' {
773 return ctx.json_error('branch is required')
774 }
775
776 items := app.find_repository_items(repo_id, branch, path)
777 mut result := []FileInfo{}
778 for item in items {
779 result << FileInfo{
780 name: item.name
781 last_msg: item.last_msg
782 last_hash: item.last_hash
783 last_time: item.pretty_last_time()
784 size: item.pretty_tree_size()
785 }
786 }
787
788 return ctx.json(api.ApiSuccessResponse[[]FileInfo]{
789 success: true
790 result: result
791 })
792}
793
794@['/:username/:repo_name/contributors']
795pub fn (mut app App) contributors(mut ctx Context, username string, repo_name string) veb.Result {
796 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
797
798 contributors := app.find_repo_registered_contributor(repo.id)
799
800 return $veb.html()
801}
802
803@['/:username/:repo_name/blob/:branch_name/:path...']
804pub fn (mut app App) blob(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
805 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
806
807 mut path_parts := path.split('/')
808 path_parts.pop()
809
810 ctx.current_path = path
811 ctx.path_split = [repo_name]
812 ctx.path_split << path_parts
813
814 if !app.contains_repo_branch(repo.id, branch_name) && branch_name != repo.primary_branch {
815 app.info('Branch ${branch_name} not found')
816 return ctx.not_found()
817 }
818
819 raw_url := '/${username}/${repo_name}/raw/${branch_name}/${path}'
820 file := app.find_repo_file_by_path(repo.id, branch_name, path) or {
821 repo.lookup_file_via_git(branch_name, path) or { return ctx.not_found() }
822 }
823 is_markdown := file.name.to_lower().ends_with('.md')
824 plain_text := repo.read_file(branch_name, path)
825 highlighted_source, _, _ := highlight.highlight_text(plain_text, file.name, false)
826 source := veb.RawHtml(highlighted_source)
827 loc, sloc := calculate_lines_of_code(plain_text)
828
829 return $veb.html()
830}
831
832@['/:user/:repository/raw/:branch_name/:path...']
833pub fn (mut app App) handle_raw(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
834 user := app.get_user_by_username(username) or { return ctx.not_found() }
835 repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
836
837 // TODO: throw error when git returns non-zero status
838 file_source := repo.git('--no-pager show ${branch_name}:${path}')
839
840 return ctx.ok(file_source)
841}
842