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