ggdgsdbsdbbb / repo / repo_routes.v
789 lines · 680 sloc · 23.77 KB · 1fbadcea1b606763dc692b8ffd2f84b3648759cf
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 return $veb.html()
219}
220
221@['/new'; post]
222pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url string, description string, no_redirect string) veb.Result {
223 println('NEW POST')
224 mut valid_clone_url := clone_url
225 is_clone_url_empty := validation.is_string_empty(clone_url)
226 is_public := ctx.form['repo_visibility'] == 'public'
227 if !ctx.logged_in {
228 return ctx.redirect_to_login()
229 }
230 if !ctx.is_admin() && app.get_count_user_repos(ctx.user.id) >= max_user_repos {
231 ctx.error('You have reached the limit for the number of repositories')
232 return app.new(mut ctx)
233 }
234 if name.len > max_repo_name_len {
235 ctx.error('The repository name is too long (should be fewer than ${max_repo_name_len} characters)')
236 return app.new(mut ctx)
237 }
238 eprintln(1)
239 if _ := app.find_repo_by_name_and_username(name, ctx.user.username) {
240 ctx.error('A repository with the name "${name}" already exists')
241 return app.new(mut ctx)
242 }
243 eprintln(2)
244 if name.contains(' ') {
245 ctx.error('Repository name cannot contain spaces')
246 return app.new(mut ctx)
247 }
248 eprintln(3)
249 is_repo_name_valid := validation.is_repository_name_valid(name)
250 if !is_repo_name_valid {
251 ctx.error('The repository name is not valid')
252 return app.new(mut ctx)
253 }
254 eprintln(4)
255 has_clone_url_https_prefix := clone_url.starts_with('https://')
256 if !is_clone_url_empty {
257 if !has_clone_url_https_prefix {
258 valid_clone_url = 'https://' + clone_url
259 }
260 println('checking')
261 is_git_repo := git.check_git_repo_url(valid_clone_url)
262 println('done')
263 if !is_git_repo {
264 ctx.error('The repository URL does not contain any git repository or the server does not respond')
265 return app.new(mut ctx)
266 }
267 }
268 println('OK')
269 repo_path := os.join_path(app.config.repo_storage_path, ctx.user.username, name)
270 id := app.get_max_repo_id() + 1
271 mut new_repo := &Repo{
272 name: name
273 id: id
274 description: description
275 git_dir: repo_path
276 user_id: ctx.user.id
277 primary_branch: 'master'
278 user_name: ctx.user.username
279 clone_url: valid_clone_url
280 is_public: is_public
281 }
282 import_issues := ctx.form['import_issues'] == '1'
283 if is_clone_url_empty {
284 os.mkdir(new_repo.git_dir) or { panic(err) }
285 new_repo.git('init --bare')
286 } else {
287 new_repo.status = .cloning
288 }
289 // Insert the repo row BEFORE spawning the clone thread, so that the
290 // background `set_repo_status(.done)` UPDATE has a row to match.
291 app.add_repo(new_repo) or {
292 ctx.error('There was an error while adding the repo ${err}')
293 return app.new(mut ctx)
294 }
295 if !is_clone_url_empty {
296 app.debug('cloning')
297 clone_job_repo := *new_repo
298 spawn clone_repo(clone_job_repo, app.config, import_issues, ctx.user.id)
299 }
300 new_repo2 := app.find_repo_by_name_and_user_id(new_repo.name, ctx.user.id) or {
301 app.info('Repo was not inserted')
302 return ctx.redirect('/new')
303 }
304 repo_id := new_repo2.id
305 // $dbg;
306 // primary_branch := git.get_repository_primary_branch(repo_path)
307 primary_branch := new_repo2.primary_branch
308 // app.debug("new_repo2: ${new_repo2}")
309
310 app.update_repo_primary_branch(repo_id, primary_branch) or {
311 ctx.error('There was an error while adding the repo')
312 return app.new(mut ctx)
313 }
314 app.find_repo_by_id(repo_id) or { return app.new(mut ctx) }
315 // Update only cloned repositories
316 /*
317 if !is_clone_url_empty {
318 app.update_repo_from_fs(mut new_repo, true) or {
319 ctx.error('There was an error while cloning the repo')
320 return app.new(mut ctx)
321 }
322 }
323 */
324 if no_redirect == '1' {
325 return ctx.text('ok')
326 }
327 has_first_repo_activity := app.has_activity(ctx.user.id, 'first_repo')
328 if !has_first_repo_activity {
329 app.add_activity(ctx.user.id, 'first_repo') or { app.info(err.str()) }
330 }
331 return ctx.redirect('/${ctx.user.username}/${new_repo.name}')
332}
333
334fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Config) {
335 mut repo := repo_
336 mut app := &App{
337 db: connect_db(conf) or {
338 eprintln('cannot open ${db_backend_name()} db connection for bg_fetch thread: ${err}')
339 return
340 }
341 config: conf
342 }
343 app.load_settings()
344 app.slow_fetch_files_info(mut repo, branch, path) or {
345 eprintln('bg_fetch_files_info error: ${err}')
346 }
347 if app.settings.tree_folder_size_enabled() {
348 app.slow_fetch_folder_sizes(mut repo, branch, path) or {
349 eprintln('bg_fetch_folder_sizes error: ${err}')
350 }
351 }
352 app.db.close() or {}
353}
354
355fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, owner_user_id int) {
356 mut cloned_repo := new_repo
357 cloned_repo.clone()
358 // Use a dedicated DB connection for the clone thread to avoid
359 // sharing a connection across threads.
360 mut app := &App{
361 db: connect_db(conf) or {
362 eprintln('cannot open ${db_backend_name()} db connection for clone thread: ${err}')
363 return
364 }
365 config: conf
366 }
367 // Mark repo as done immediately so the user can browse it.
368 // The tree page will fetch files from git on demand.
369 app.set_repo_status(cloned_repo.id, .done) or { eprintln('cannot set repo status ${err}') }
370 eprintln('clone done, repo available — indexing in background')
371 // For GitHub clones, also pull the repo description and contributors list
372 // (the issue import is gated on a separate user opt-in).
373 if cloned_repo.clone_url.contains('github.com') {
374 spawn bg_import_github_repo_info(cloned_repo.id, cloned_repo.clone_url,
375 cloned_repo.description, conf)
376 if import_issues {
377 spawn bg_import_github_issues(cloned_repo.id, cloned_repo.clone_url, owner_user_id,
378 conf)
379 }
380 }
381 // Index branches, commits, and language stats in the background.
382 app.update_repo_from_fs(mut cloned_repo, true) or {
383 eprintln('cannot update repo from fs ${err}')
384 }
385 eprintln('background indexing complete')
386 app.db.close() or {}
387}
388
389fn bg_import_github_repo_info(repo_id int, clone_url string, existing_description string, conf config.Config) {
390 eprintln('[github-info] spawned thread for repo_id=${repo_id}')
391 mut app := &App{
392 db: connect_db(conf) or {
393 eprintln('[github-info] cannot open db connection: ${err}')
394 return
395 }
396 config: conf
397 }
398 defer {
399 app.db.close() or {}
400 }
401 if existing_description.trim_space() == '' {
402 description := fetch_github_repo_description(clone_url)
403 if description != '' {
404 app.set_repo_description(repo_id, description) or {
405 eprintln('[github-info] cannot save description: ${err}')
406 }
407 }
408 }
409 app.import_github_contributors(repo_id, clone_url) or {
410 eprintln('[github-contrib] FAILED: ${err}')
411 }
412}
413
414fn bg_import_github_issues(repo_id int, clone_url string, owner_user_id int, conf config.Config) {
415 eprintln('[github-import] spawned thread for repo_id=${repo_id}')
416 mut app := &App{
417 db: connect_db(conf) or {
418 eprintln('[github-import] cannot open db connection for import thread: ${err}')
419 return
420 }
421 config: conf
422 }
423 app.import_github_issues(repo_id, clone_url, owner_user_id) or {
424 eprintln('[github-import] FAILED: ${err}')
425 }
426 app.db.close() or {}
427}
428
429pub fn (mut app App) kekw(mut ctx Context) veb.Result {
430 clone_url := ''
431 clone_progress := ''
432 return $veb.html('templates/cloning_in_process.html')
433}
434
435// read_clone_progress parses a git `--progress` log file and returns
436// the latest output as a single newline-separated string, ready to be
437// shown inside a <pre> block. Git emits live progress with `\r` and
438// stage transitions with `\n`; we collapse repeated progress lines for
439// the same phase ("Counting objects", "Receiving objects", …) so only
440// the most recent value for each phase remains.
441fn read_clone_progress(progress_path string) string {
442 raw := os.read_file(progress_path) or { return '' }
443 if raw.len == 0 {
444 return ''
445 }
446 lines := raw.replace('\r', '\n').split('\n')
447 mut stages := []string{}
448 mut phase_index := map[string]int{}
449 for raw_line in lines {
450 line := raw_line.trim_space()
451 if line == '' {
452 continue
453 }
454 mut body := line
455 if body.starts_with('remote: ') {
456 body = body[8..]
457 }
458 colon := body.index(':') or { -1 }
459 key := if colon == -1 { body } else { body[..colon].trim_space() }
460 if key in phase_index {
461 stages[phase_index[key]] = line
462 } else {
463 phase_index[key] = stages.len
464 stages << line
465 }
466 }
467 return stages.join('\n')
468}
469
470@['/:username/:repo_name/tree/:branch_name/:path...']
471pub fn (mut app App) tree(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
472 mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
473 eprintln('tree() repo ${repo_name} not found')
474 return ctx.not_found()
475 }
476 mut clone_url := ''
477 mut clone_progress := ''
478 if repo.status == .cloning {
479 clone_url = repo.clone_url
480 clone_progress = read_clone_progress(repo.clone_progress_path())
481 return $veb.html('templates/cloning_in_process.html')
482 }
483
484 _, user := app.check_username(username)
485 if !repo.is_public {
486 if user.id != ctx.user.id {
487 return ctx.not_found()
488 }
489 }
490
491 repo_id := repo.id
492
493 // XTODO
494 // app.fetch_tags(repo) or { app.info(err.str()) }
495
496 ctx.current_path = path
497 if path.contains('favicon.svg') {
498 return ctx.not_found()
499 }
500
501 ctx.path_split = [repo_name]
502 if path != '' {
503 ctx.path_split << path.split('/')
504 }
505
506 ctx.is_tree = true
507 ctx.branch = branch_name
508
509 app.increment_repo_views(repo.id) or { app.info(err.str()) }
510
511 mut up := '/'
512 can_up := path != ''
513 if can_up {
514 if !path.contains('/') {
515 up = '../..'
516 } else {
517 up = ctx.req.url.all_before_last('/')
518 }
519 }
520
521 tree_mode := if 'mode' in ctx.query { ctx.query['mode'] } else { 'tree' }
522 is_top_files_mode := tree_mode == 'top-files'
523 top_files := if is_top_files_mode {
524 repo.top_files(branch_name, top_files_limit)
525 } else {
526 []File{}
527 }
528 tree_url := if path == '' {
529 '/${username}/${repo_name}/tree/${branch_name}'
530 } else {
531 '/${username}/${repo_name}/tree/${branch_name}/${path}'
532 }
533 top_files_url := '/${username}/${repo_name}/tree/${branch_name}?mode=top-files'
534
535 mut items := app.find_repository_items(repo_id, branch_name, ctx.current_path)
536 branch := app.find_repo_branch_by_name(repo.id, branch_name)
537
538 show_folder_size := app.settings.tree_folder_size_enabled()
539
540 if !is_top_files_mode {
541 if items.len == 0 {
542 // No files in the db, fetch them from git and cache in db
543 items = app.cache_repository_items(mut repo, branch_name, ctx.current_path) or {
544 app.info(err.str())
545 []File{}
546 }
547 // Fetch commit info in background — don't block the page
548 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
549 } else if items.any(it.last_msg == '') {
550 // Some files still need commit info — fetch in background
551 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
552 } else if show_folder_size && items.any(it.is_dir && !it.is_size_calculated) {
553 // Some folders still need size info, fetch in background
554 spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
555 }
556 }
557
558 // Fetch last commit message for this directory, printed at the top of the tree
559 mut last_commit := Commit{}
560 mut dir := File{}
561 if can_up {
562 mut p := path
563 if p.ends_with('/') {
564 p = p[0..path.len - 1]
565 }
566 if !p.contains('/') {
567 p = '/${p}'
568 }
569 dir = app.find_repo_file_by_path(repo.id, branch_name, p) or { File{} }
570 if dir.id != 0 {
571 last_commit = app.find_repo_commit_by_hash(repo.id, dir.last_hash)
572 }
573 } else {
574 last_commit = app.find_repo_last_commit(repo.id, branch.id)
575 }
576
577 mut next_dir_idx := 0
578 for scan_idx in 0 .. items.len {
579 if items[scan_idx].is_dir {
580 if scan_idx != next_dir_idx {
581 moving_dir := items[scan_idx]
582 mut move_idx := scan_idx
583 for move_idx > next_dir_idx {
584 items[move_idx] = items[move_idx - 1]
585 move_idx--
586 }
587 items[next_dir_idx] = moving_dir
588 }
589 next_dir_idx++
590 }
591 }
592
593 commits_count := app.get_repo_commit_count(repo.id, branch.id)
594 has_commits := commits_count > 0
595
596 // Get readme after updating repository
597 readme_file := find_readme_file(items) or { File{} }
598 readme := render_readme(repo, branch_name, path, readme_file)
599
600 license_file := find_license_file(items) or { File{} }
601 mut license_file_path := ''
602
603 if license_file.id != 0 {
604 license_file_path = '/${username}/${repo_name}/blob/${branch_name}/${license_file.name}'
605 }
606
607 watcher_count := app.get_count_repo_watchers(repo_id)
608 is_repo_starred := app.check_repo_starred(repo_id, ctx.user.id)
609 is_repo_watcher := app.check_repo_watcher_status(repo_id, ctx.user.id)
610 is_top_directory := ctx.current_path == ''
611
612 // CI status for last commit
613 ci_status := app.find_ci_status_for_commit(repo_id, last_commit.hash) or {
614 app.find_ci_status_for_branch(repo_id, branch_name) or { CiStatus{} }
615 }
616 has_ci := ci_status.id != 0
617
618 mut sidebar_contributors := []User{}
619 mut sidebar_releases := []Release{}
620 if is_top_directory {
621 all_contributors := app.find_repo_registered_contributor(repo_id)
622 sidebar_contributors = if all_contributors.len > 12 {
623 all_contributors[..12]
624 } else {
625 all_contributors
626 }
627
628 rels := app.find_repo_releases_as_page(repo_id, 0)
629 tags := app.get_all_repo_tags(repo_id)
630 for rel in rels {
631 mut r := rel
632 for tag in tags {
633 if tag.id == rel.tag_id {
634 r.tag_name = tag.name
635 r.tag_hash = tag.hash
636 r.date = time.unix(tag.created_at)
637 break
638 }
639 }
640 sidebar_releases << r
641 if sidebar_releases.len >= 3 {
642 break
643 }
644 }
645 }
646
647 return $veb.html()
648}
649
650fn render_readme(repo Repo, branch_name string, path string, readme_file File) veb.RawHtml {
651 if readme_file.id == 0 {
652 return veb.RawHtml('')
653 }
654
655 readme_path := '${path}/${readme_file.name}'
656 readme_content := repo.read_file(branch_name, readme_path)
657 highlighted_readme, _, _ := highlight.highlight_text(readme_content, readme_path, false)
658
659 return veb.RawHtml(highlighted_readme)
660}
661
662@['/api/v1/repos/:repo_id/star'; 'post']
663pub fn (mut app App) handle_api_repo_star(mut ctx Context, repo_id_str string) veb.Result {
664 repo_id := repo_id_str.int()
665
666 has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
667
668 if !has_access {
669 return ctx.json_error('Not found')
670 }
671
672 user_id := ctx.user.id
673 app.toggle_repo_star(repo_id, user_id) or {
674 return ctx.json_error('There was an error while starring the repo')
675 }
676 is_repo_starred := app.check_repo_starred(repo_id, user_id)
677
678 return ctx.json(api.ApiSuccessResponse[bool]{
679 success: true
680 result: is_repo_starred
681 })
682}
683
684@['/api/v1/repos/:repo_id/watch'; 'post']
685pub fn (mut app App) handle_api_repo_watch(mut ctx Context, repo_id_str string) veb.Result {
686 repo_id := repo_id_str.int()
687
688 has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
689
690 if !has_access {
691 return ctx.json_error('Not found')
692 }
693
694 user_id := ctx.user.id
695 app.toggle_repo_watcher_status(repo_id, user_id) or {
696 return ctx.json_error('There was an error while toggling to watch')
697 }
698 is_watching := app.check_repo_watcher_status(repo_id, user_id)
699
700 return ctx.json(api.ApiSuccessResponse[bool]{
701 success: true
702 result: is_watching
703 })
704}
705
706// API: get file listing with commit info for a directory (used by JS polling)
707// Path uses /tree/files to avoid colliding with /api/v1/repos/:username/:repo_name.
708@['/api/v1/repos/:repo_id_str/tree/files']
709pub fn (mut app App) handle_api_repo_files(mut ctx Context, repo_id_str string) veb.Result {
710 repo_id := repo_id_str.int()
711 repo := app.find_repo_by_id(repo_id) or { return ctx.json_error('Not found') }
712
713 if !repo.is_public && repo.user_id != ctx.user.id {
714 return ctx.json_error('Not found')
715 }
716
717 branch := if 'branch' in ctx.query { ctx.query['branch'] } else { '' }
718 path := if 'path' in ctx.query { ctx.query['path'] } else { '' }
719
720 if branch == '' {
721 return ctx.json_error('branch is required')
722 }
723
724 items := app.find_repository_items(repo_id, branch, path)
725 mut result := []FileInfo{}
726 for item in items {
727 result << FileInfo{
728 name: item.name
729 last_msg: item.last_msg
730 last_hash: item.last_hash
731 last_time: item.pretty_last_time()
732 size: item.pretty_tree_size()
733 }
734 }
735
736 return ctx.json(api.ApiSuccessResponse[[]FileInfo]{
737 success: true
738 result: result
739 })
740}
741
742@['/:username/:repo_name/contributors']
743pub fn (mut app App) contributors(mut ctx Context, username string, repo_name string) veb.Result {
744 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
745
746 contributors := app.find_repo_registered_contributor(repo.id)
747
748 return $veb.html()
749}
750
751@['/:username/:repo_name/blob/:branch_name/:path...']
752pub fn (mut app App) blob(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
753 repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
754
755 mut path_parts := path.split('/')
756 path_parts.pop()
757
758 ctx.current_path = path
759 ctx.path_split = [repo_name]
760 ctx.path_split << path_parts
761
762 if !app.contains_repo_branch(repo.id, branch_name) && branch_name != repo.primary_branch {
763 app.info('Branch ${branch_name} not found')
764 return ctx.not_found()
765 }
766
767 raw_url := '/${username}/${repo_name}/raw/${branch_name}/${path}'
768 file := app.find_repo_file_by_path(repo.id, branch_name, path) or {
769 repo.lookup_file_via_git(branch_name, path) or { return ctx.not_found() }
770 }
771 is_markdown := file.name.to_lower().ends_with('.md')
772 plain_text := repo.read_file(branch_name, path)
773 highlighted_source, _, _ := highlight.highlight_text(plain_text, file.name, false)
774 source := veb.RawHtml(highlighted_source)
775 loc, sloc := calculate_lines_of_code(plain_text)
776
777 return $veb.html()
778}
779
780@['/:user/:repository/raw/:branch_name/:path...']
781pub fn (mut app App) handle_raw(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
782 user := app.get_user_by_username(username) or { return ctx.not_found() }
783 repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
784
785 // TODO: throw error when git returns non-zero status
786 file_source := repo.git('--no-pager show ${branch_name}:${path}')
787
788 return ctx.ok(file_source)
789}
790