module main import veb import api import crypto.sha1 import os import time import highlight import validation import git import config const top_files_limit = 50 @['/:username/repos'] pub fn (mut app App) user_repos(username string) veb.Result { exists, user := app.check_username(username) if !exists { return ctx.not_found() } mut repos := app.find_user_public_repos(user.id) if user.id == ctx.user.id { repos = app.find_user_repos(user.id) } for mut repo in repos { repo.lang_stats = app.find_repo_lang_stats(repo.id) repo.latest_commit_at = app.find_repo_last_commit_time(repo.id) repo.activity_buckets = app.get_repo_activity_buckets(repo.id) } return $veb.html('templates/user/repos.html') } @['/:username/stars'] pub fn (mut app App) user_stars(username string) veb.Result { exists, user := app.check_username(username) if !exists { return ctx.not_found() } repos := app.find_user_starred_repos(ctx.user.id) return $veb.html('templates/user/stars.html') } @['/:username/:repo_name/settings'] pub fn (mut app App) repo_settings(username string, repo_name string) veb.Result { repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.redirect_to_repository(username, repo_name) } is_owner := app.check_repo_owner(ctx.user.username, repo_name) if !is_owner { return ctx.redirect_to_repository(username, repo_name) } return $veb.html('templates/repo/settings.html') } @['/:username/:repo_name/settings'; post] pub fn (mut app App) handle_update_repo_settings(username string, repo_name string, webhook_secret string) veb.Result { repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.redirect_to_repository(username, repo_name) } is_owner := app.check_repo_owner(ctx.user.username, repo_name) if !is_owner { return ctx.redirect_to_repository(username, repo_name) } if webhook_secret != '' && webhook_secret != repo.webhook_secret { webhook := sha1.hexhash(webhook_secret) app.set_repo_webhook_secret(repo.id, webhook) or { app.info(err.str()) } } return ctx.redirect_to_repository(username, repo_name) } @['/:username/:repo_name/settings/features'; post] pub fn (mut app App) handle_update_repo_features(username string, repo_name string) veb.Result { repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.redirect_to_repository(username, repo_name) } is_owner := app.check_repo_owner(ctx.user.username, repo_name) if !is_owner { return ctx.redirect_to_repository(username, repo_name) } disable_discussions := 'discussions_enabled' !in ctx.form disable_projects := 'projects_enabled' !in ctx.form disable_milestones := 'milestones_enabled' !in ctx.form disable_wiki := 'wiki_enabled' !in ctx.form app.update_repo_features(repo.id, disable_discussions, disable_projects, disable_milestones, disable_wiki) or { app.info(err.str()) } return ctx.redirect('/${username}/${repo_name}/settings') } @['/:user/:repo_name/delete'; post] pub fn (mut app App) handle_repo_delete(username string, repo_name string) veb.Result { repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.redirect_to_repository(username, repo_name) } is_owner := app.check_repo_owner(ctx.user.username, repo_name) if !is_owner { return ctx.redirect_to_repository(username, repo_name) } if ctx.form['verify'] == '${username}/${repo_name}' { spawn app.delete_repository(repo.id, repo.git_dir, repo.name) } else { ctx.error('Verification failed') return app.repo_settings(mut ctx, username, repo_name) } return ctx.redirect_to_index() } @['/:username/:repo_name/move'; post] pub fn (mut app App) handle_repo_move(username string, repo_name string, dest string, verify string) veb.Result { repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.redirect_to_index() } is_owner := app.check_repo_owner(ctx.user.username, repo_name) if !is_owner { return ctx.redirect_to_repository(username, repo_name) } if dest != '' && verify == '${username}/${repo_name}' { dest_user := app.get_user_by_username(dest) or { ctx.error('Unknown user ${dest}') return app.repo_settings(mut ctx, username, repo_name) } if app.user_has_repo(dest_user.id, repo.name) { ctx.error('User already owns repo ${repo.name}') return app.repo_settings(mut ctx, username, repo_name) } if app.get_count_user_repos(dest_user.id) >= max_user_repos { ctx.error('User already reached the repo limit') return app.repo_settings(mut ctx, username, repo_name) } app.move_repo_to_user(repo.id, dest_user.id, dest_user.username) or { ctx.error('There was an error while moving the repo') return app.repo_settings(mut ctx, username, repo_name) } return ctx.redirect('/${dest_user.username}/${repo.name}') } else { ctx.error('Verification failed') return app.repo_settings(mut ctx, username, repo_name) } return ctx.redirect_to_index() } @['/:username/:repo_name'] pub fn (mut app App) handle_tree(mut ctx Context, username string, repo_name string) veb.Result { match repo_name { 'repos' { return app.user_repos(mut ctx, username) } 'issues' { return app.handle_get_user_issues(mut ctx, username) } 'pulls' { return app.handle_get_user_pulls(mut ctx, username) } 'settings' { return app.user_settings(mut ctx, username) } else {} } repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } return app.tree(mut ctx, username, repo_name, repo.primary_branch, '') } @['/:username/:repo_name/tree/:branch_name'] pub fn (mut app App) handle_branch_tree(mut ctx Context, username string, repo_name string, branch_name string) veb.Result { app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } return app.tree(mut ctx, username, repo_name, branch_name, '') } @['/:username/:repo_name/update'] pub fn (mut app App) handle_repo_update(mut ctx Context, username string, repo_name string) veb.Result { mut repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() } if ctx.user.is_admin { app.update_repo_from_remote(mut repo) or { app.info(err.str()) } app.slow_fetch_files_info(mut repo, 'master', '.') or { app.info(err.str()) } } return ctx.redirect_to_repository(username, repo_name) } @['/new'] pub fn (mut app App) new() veb.Result { if !ctx.logged_in { return ctx.redirect_to_login() } return $veb.html() } @['/new'; post] pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url string, description string, no_redirect string) veb.Result { println('NEW POST') mut valid_clone_url := clone_url is_clone_url_empty := validation.is_string_empty(clone_url) is_public := ctx.form['repo_visibility'] == 'public' if !ctx.logged_in { return ctx.redirect_to_login() } if !ctx.is_admin() && app.get_count_user_repos(ctx.user.id) >= max_user_repos { ctx.error('You have reached the limit for the number of repositories') return app.new(mut ctx) } if name.len > max_repo_name_len { ctx.error('The repository name is too long (should be fewer than ${max_repo_name_len} characters)') return app.new(mut ctx) } eprintln(1) if _ := app.find_repo_by_name_and_username(name, ctx.user.username) { ctx.error('A repository with the name "${name}" already exists') return app.new(mut ctx) } eprintln(2) if name.contains(' ') { ctx.error('Repository name cannot contain spaces') return app.new(mut ctx) } eprintln(3) is_repo_name_valid := validation.is_repository_name_valid(name) if !is_repo_name_valid { ctx.error('The repository name is not valid') return app.new(mut ctx) } eprintln(4) has_clone_url_https_prefix := clone_url.starts_with('https://') if !is_clone_url_empty { if !has_clone_url_https_prefix { valid_clone_url = 'https://' + clone_url } println('checking') is_git_repo := git.check_git_repo_url(valid_clone_url) println('done') if !is_git_repo { ctx.error('The repository URL does not contain any git repository or the server does not respond') return app.new(mut ctx) } } println('OK') repo_path := os.join_path(app.config.repo_storage_path, ctx.user.username, name) id := app.get_max_repo_id() + 1 mut new_repo := &Repo{ name: name id: id description: description git_dir: repo_path user_id: ctx.user.id primary_branch: 'master' user_name: ctx.user.username clone_url: valid_clone_url is_public: is_public } import_issues := ctx.form['import_issues'] == '1' if is_clone_url_empty { os.mkdir(new_repo.git_dir) or { panic(err) } new_repo.git('init --bare') } else { new_repo.status = .cloning } // Insert the repo row BEFORE spawning the clone thread, so that the // background `set_repo_status(.done)` UPDATE has a row to match. app.add_repo(new_repo) or { ctx.error('There was an error while adding the repo ${err}') return app.new(mut ctx) } if !is_clone_url_empty { app.debug('cloning') clone_job_repo := *new_repo spawn clone_repo(clone_job_repo, app.config, import_issues, ctx.user.id) } new_repo2 := app.find_repo_by_name_and_user_id(new_repo.name, ctx.user.id) or { app.info('Repo was not inserted') return ctx.redirect('/new') } repo_id := new_repo2.id // $dbg; // primary_branch := git.get_repository_primary_branch(repo_path) primary_branch := new_repo2.primary_branch // app.debug("new_repo2: ${new_repo2}") app.update_repo_primary_branch(repo_id, primary_branch) or { ctx.error('There was an error while adding the repo') return app.new(mut ctx) } app.find_repo_by_id(repo_id) or { return app.new(mut ctx) } // Update only cloned repositories /* if !is_clone_url_empty { app.update_repo_from_fs(mut new_repo, true) or { ctx.error('There was an error while cloning the repo') return app.new(mut ctx) } } */ if no_redirect == '1' { return ctx.text('ok') } has_first_repo_activity := app.has_activity(ctx.user.id, 'first_repo') if !has_first_repo_activity { app.add_activity(ctx.user.id, 'first_repo') or { app.info(err.str()) } } return ctx.redirect('/${ctx.user.username}/${new_repo.name}') } fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Config) { mut repo := repo_ mut app := &App{ db: connect_db(conf) or { eprintln('cannot open ${db_backend_name()} db connection for bg_fetch thread: ${err}') return } config: conf } app.load_settings() app.slow_fetch_files_info(mut repo, branch, path) or { eprintln('bg_fetch_files_info error: ${err}') } if app.settings.tree_folder_size_enabled() { app.slow_fetch_folder_sizes(mut repo, branch, path) or { eprintln('bg_fetch_folder_sizes error: ${err}') } } app.db.close() or {} } fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, owner_user_id int) { mut cloned_repo := new_repo cloned_repo.clone() // Use a dedicated DB connection for the clone thread to avoid // sharing a connection across threads. mut app := &App{ db: connect_db(conf) or { eprintln('cannot open ${db_backend_name()} db connection for clone thread: ${err}') return } config: conf } // Mark repo as done immediately so the user can browse it. // The tree page will fetch files from git on demand. app.set_repo_status(cloned_repo.id, .done) or { eprintln('cannot set repo status ${err}') } eprintln('clone done, repo available — indexing in background') // For GitHub clones, also pull the repo description and contributors list // (the issue import is gated on a separate user opt-in). if cloned_repo.clone_url.contains('github.com') { spawn bg_import_github_repo_info(cloned_repo.id, cloned_repo.clone_url, cloned_repo.description, conf) if import_issues { spawn bg_import_github_issues(cloned_repo.id, cloned_repo.clone_url, owner_user_id, conf) } } // Index branches, commits, and language stats in the background. app.update_repo_from_fs(mut cloned_repo, true) or { eprintln('cannot update repo from fs ${err}') } eprintln('background indexing complete') app.db.close() or {} } fn bg_import_github_repo_info(repo_id int, clone_url string, existing_description string, conf config.Config) { eprintln('[github-info] spawned thread for repo_id=${repo_id}') mut app := &App{ db: connect_db(conf) or { eprintln('[github-info] cannot open db connection: ${err}') return } config: conf } defer { app.db.close() or {} } if existing_description.trim_space() == '' { description := fetch_github_repo_description(clone_url) if description != '' { app.set_repo_description(repo_id, description) or { eprintln('[github-info] cannot save description: ${err}') } } } app.import_github_contributors(repo_id, clone_url) or { eprintln('[github-contrib] FAILED: ${err}') } } fn bg_import_github_issues(repo_id int, clone_url string, owner_user_id int, conf config.Config) { eprintln('[github-import] spawned thread for repo_id=${repo_id}') mut app := &App{ db: connect_db(conf) or { eprintln('[github-import] cannot open db connection for import thread: ${err}') return } config: conf } app.import_github_issues(repo_id, clone_url, owner_user_id) or { eprintln('[github-import] FAILED: ${err}') } app.db.close() or {} } pub fn (mut app App) kekw(mut ctx Context) veb.Result { clone_url := '' clone_progress := '' return $veb.html('templates/cloning_in_process.html') } // read_clone_progress parses a git `--progress` log file and returns // the latest output as a single newline-separated string, ready to be // shown inside a
block. Git emits live progress with `\r` and
// stage transitions with `\n`; we collapse repeated progress lines for
// the same phase ("Counting objects", "Receiving objects", …) so only
// the most recent value for each phase remains.
fn read_clone_progress(progress_path string) string {
raw := os.read_file(progress_path) or { return '' }
if raw.len == 0 {
return ''
}
lines := raw.replace('\r', '\n').split('\n')
mut stages := []string{}
mut phase_index := map[string]int{}
for raw_line in lines {
line := raw_line.trim_space()
if line == '' {
continue
}
mut body := line
if body.starts_with('remote: ') {
body = body[8..]
}
colon := body.index(':') or { -1 }
key := if colon == -1 { body } else { body[..colon].trim_space() }
if key in phase_index {
stages[phase_index[key]] = line
} else {
phase_index[key] = stages.len
stages << line
}
}
return stages.join('\n')
}
@['/:username/:repo_name/tree/:branch_name/:path...']
pub fn (mut app App) tree(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
mut repo := app.find_repo_by_name_and_username(repo_name, username) or {
eprintln('tree() repo ${repo_name} not found')
return ctx.not_found()
}
mut clone_url := ''
mut clone_progress := ''
if repo.status == .cloning {
clone_url = repo.clone_url
clone_progress = read_clone_progress(repo.clone_progress_path())
return $veb.html('templates/cloning_in_process.html')
}
_, user := app.check_username(username)
if !repo.is_public {
if user.id != ctx.user.id {
return ctx.not_found()
}
}
repo_id := repo.id
// XTODO
// app.fetch_tags(repo) or { app.info(err.str()) }
ctx.current_path = path
if path.contains('favicon.svg') {
return ctx.not_found()
}
ctx.path_split = [repo_name]
if path != '' {
ctx.path_split << path.split('/')
}
ctx.is_tree = true
ctx.branch = branch_name
app.increment_repo_views(repo.id) or { app.info(err.str()) }
mut up := '/'
can_up := path != ''
if can_up {
if !path.contains('/') {
up = '../..'
} else {
up = ctx.req.url.all_before_last('/')
}
}
tree_mode := if 'mode' in ctx.query { ctx.query['mode'] } else { 'tree' }
is_top_files_mode := tree_mode == 'top-files'
top_files := if is_top_files_mode {
repo.top_files(branch_name, top_files_limit)
} else {
[]File{}
}
tree_url := if path == '' {
'/${username}/${repo_name}/tree/${branch_name}'
} else {
'/${username}/${repo_name}/tree/${branch_name}/${path}'
}
top_files_url := '/${username}/${repo_name}/tree/${branch_name}?mode=top-files'
mut items := app.find_repository_items(repo_id, branch_name, ctx.current_path)
branch := app.find_repo_branch_by_name(repo.id, branch_name)
show_folder_size := app.settings.tree_folder_size_enabled()
if !is_top_files_mode {
if items.len == 0 {
// No files in the db, fetch them from git and cache in db
items = app.cache_repository_items(mut repo, branch_name, ctx.current_path) or {
app.info(err.str())
[]File{}
}
// Fetch commit info in background — don't block the page
spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
} else if items.any(it.last_msg == '') {
// Some files still need commit info — fetch in background
spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
} else if show_folder_size && items.any(it.is_dir && !it.is_size_calculated) {
// Some folders still need size info, fetch in background
spawn bg_fetch_files_info(repo, branch_name, ctx.current_path, app.config)
}
}
// Fetch last commit message for this directory, printed at the top of the tree
mut last_commit := Commit{}
mut dir := File{}
if can_up {
mut p := path
if p.ends_with('/') {
p = p[0..path.len - 1]
}
if !p.contains('/') {
p = '/${p}'
}
dir = app.find_repo_file_by_path(repo.id, branch_name, p) or { File{} }
if dir.id != 0 {
last_commit = app.find_repo_commit_by_hash(repo.id, dir.last_hash)
}
} else {
last_commit = app.find_repo_last_commit(repo.id, branch.id)
}
mut next_dir_idx := 0
for scan_idx in 0 .. items.len {
if items[scan_idx].is_dir {
if scan_idx != next_dir_idx {
moving_dir := items[scan_idx]
mut move_idx := scan_idx
for move_idx > next_dir_idx {
items[move_idx] = items[move_idx - 1]
move_idx--
}
items[next_dir_idx] = moving_dir
}
next_dir_idx++
}
}
commits_count := app.get_repo_commit_count(repo.id, branch.id)
has_commits := commits_count > 0
// Get readme after updating repository
readme_file := find_readme_file(items) or { File{} }
readme := render_readme(repo, branch_name, path, readme_file)
license_file := find_license_file(items) or { File{} }
mut license_file_path := ''
if license_file.id != 0 {
license_file_path = '/${username}/${repo_name}/blob/${branch_name}/${license_file.name}'
}
watcher_count := app.get_count_repo_watchers(repo_id)
is_repo_starred := app.check_repo_starred(repo_id, ctx.user.id)
is_repo_watcher := app.check_repo_watcher_status(repo_id, ctx.user.id)
is_top_directory := ctx.current_path == ''
// CI status for last commit
ci_status := app.find_ci_status_for_commit(repo_id, last_commit.hash) or {
app.find_ci_status_for_branch(repo_id, branch_name) or { CiStatus{} }
}
has_ci := ci_status.id != 0
mut sidebar_contributors := []User{}
mut sidebar_releases := []Release{}
if is_top_directory {
all_contributors := app.find_repo_registered_contributor(repo_id)
sidebar_contributors = if all_contributors.len > 12 {
all_contributors[..12]
} else {
all_contributors
}
rels := app.find_repo_releases_as_page(repo_id, 0)
tags := app.get_all_repo_tags(repo_id)
for rel in rels {
mut r := rel
for tag in tags {
if tag.id == rel.tag_id {
r.tag_name = tag.name
r.tag_hash = tag.hash
r.date = time.unix(tag.created_at)
break
}
}
sidebar_releases << r
if sidebar_releases.len >= 3 {
break
}
}
}
return $veb.html()
}
fn render_readme(repo Repo, branch_name string, path string, readme_file File) veb.RawHtml {
if readme_file.id == 0 {
return veb.RawHtml('')
}
readme_path := '${path}/${readme_file.name}'
readme_content := repo.read_file(branch_name, readme_path)
highlighted_readme, _, _ := highlight.highlight_text(readme_content, readme_path, false)
return veb.RawHtml(highlighted_readme)
}
@['/api/v1/repos/:repo_id/star'; 'post']
pub fn (mut app App) handle_api_repo_star(mut ctx Context, repo_id_str string) veb.Result {
repo_id := repo_id_str.int()
has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
if !has_access {
return ctx.json_error('Not found')
}
user_id := ctx.user.id
app.toggle_repo_star(repo_id, user_id) or {
return ctx.json_error('There was an error while starring the repo')
}
is_repo_starred := app.check_repo_starred(repo_id, user_id)
return ctx.json(api.ApiSuccessResponse[bool]{
success: true
result: is_repo_starred
})
}
@['/api/v1/repos/:repo_id/watch'; 'post']
pub fn (mut app App) handle_api_repo_watch(mut ctx Context, repo_id_str string) veb.Result {
repo_id := repo_id_str.int()
has_access := app.has_user_repo_read_access(ctx, ctx.user.id, repo_id)
if !has_access {
return ctx.json_error('Not found')
}
user_id := ctx.user.id
app.toggle_repo_watcher_status(repo_id, user_id) or {
return ctx.json_error('There was an error while toggling to watch')
}
is_watching := app.check_repo_watcher_status(repo_id, user_id)
return ctx.json(api.ApiSuccessResponse[bool]{
success: true
result: is_watching
})
}
// API: get file listing with commit info for a directory (used by JS polling)
// Path uses /tree/files to avoid colliding with /api/v1/repos/:username/:repo_name.
@['/api/v1/repos/:repo_id_str/tree/files']
pub fn (mut app App) handle_api_repo_files(mut ctx Context, repo_id_str string) veb.Result {
repo_id := repo_id_str.int()
repo := app.find_repo_by_id(repo_id) or { return ctx.json_error('Not found') }
if !repo.is_public && repo.user_id != ctx.user.id {
return ctx.json_error('Not found')
}
branch := if 'branch' in ctx.query { ctx.query['branch'] } else { '' }
path := if 'path' in ctx.query { ctx.query['path'] } else { '' }
if branch == '' {
return ctx.json_error('branch is required')
}
items := app.find_repository_items(repo_id, branch, path)
mut result := []FileInfo{}
for item in items {
result << FileInfo{
name: item.name
last_msg: item.last_msg
last_hash: item.last_hash
last_time: item.pretty_last_time()
size: item.pretty_tree_size()
}
}
return ctx.json(api.ApiSuccessResponse[[]FileInfo]{
success: true
result: result
})
}
@['/:username/:repo_name/contributors']
pub fn (mut app App) contributors(mut ctx Context, username string, repo_name string) veb.Result {
repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
contributors := app.find_repo_registered_contributor(repo.id)
return $veb.html()
}
@['/:username/:repo_name/blob/:branch_name/:path...']
pub fn (mut app App) blob(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
repo := app.find_repo_by_name_and_username(repo_name, username) or { return ctx.not_found() }
mut path_parts := path.split('/')
path_parts.pop()
ctx.current_path = path
ctx.path_split = [repo_name]
ctx.path_split << path_parts
if !app.contains_repo_branch(repo.id, branch_name) && branch_name != repo.primary_branch {
app.info('Branch ${branch_name} not found')
return ctx.not_found()
}
raw_url := '/${username}/${repo_name}/raw/${branch_name}/${path}'
file := app.find_repo_file_by_path(repo.id, branch_name, path) or {
repo.lookup_file_via_git(branch_name, path) or { return ctx.not_found() }
}
is_markdown := file.name.to_lower().ends_with('.md')
plain_text := repo.read_file(branch_name, path)
highlighted_source, _, _ := highlight.highlight_text(plain_text, file.name, false)
source := veb.RawHtml(highlighted_source)
loc, sloc := calculate_lines_of_code(plain_text)
return $veb.html()
}
@['/:user/:repository/raw/:branch_name/:path...']
pub fn (mut app App) handle_raw(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result {
user := app.get_user_by_username(username) or { return ctx.not_found() }
repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
// TODO: throw error when git returns non-zero status
file_source := repo.git('--no-pager show ${branch_name}:${path}')
return ctx.ok(file_source)
}