From 38963f077e3284a770d2093deec9a2dd865b9f42 Mon Sep 17 00:00:00 2001 From: walking dev <104449470+walkingdevel@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:27:35 +0000 Subject: [PATCH] releases page (#191) --- .gitignore | 1 + branch_service.v | 20 +++++------ gitly.v | 18 ++++------ release_entities.v | 2 +- release_routes.v | 11 +++--- release_service.v | 9 +++-- repo_routes.v | 2 ++ repo_service.v | 53 ++++++++++++++++++---------- repo_template.v | 62 ++++++++------------------------- settings_entities.v | 1 + static/css/gitly.scss | 2 +- static/css/releases.scss | 6 ++++ tag_entities.v | 9 ++--- tag_routes.v | 26 ++++++++++++++ tag_service.v | 53 ++++++++++++++++++++++++++-- templates/releases.html | 8 +++-- utils.v | 7 ++++ validation/validation_service.v | 2 +- 18 files changed, 187 insertions(+), 105 deletions(-) create mode 100644 static/css/releases.scss create mode 100644 tag_routes.v diff --git a/.gitignore b/.gitignore index 2e76c70..735aa85 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ gitly static/assets/version logs/ repos/ +archives/ *.code-workspace .vscode diff --git a/branch_service.v b/branch_service.v index 7697137..3257496 100644 --- a/branch_service.v +++ b/branch_service.v @@ -3,6 +3,16 @@ module main import time import git +fn (mut app App) fetch_branches(repo Repo) { + branches_output := repo.git('branch -a') + + for branch_output in branches_output.split_into_lines() { + branch_name := git.parse_git_branch_output(branch_output) + + app.fetch_branch(repo, branch_name) + } +} + fn (mut app App) fetch_branch(repo Repo, branch_name string) { last_commit_hash := repo.get_last_branch_commit_hash(branch_name) @@ -26,16 +36,6 @@ fn (mut app App) fetch_branch(repo Repo, branch_name string) { int(committed_at.unix)) } -fn (mut app App) fetch_branches(repo Repo) { - branches_output := repo.git('branch -a') - - for branch_output in branches_output.split_into_lines() { - branch_name := git.parse_git_branch_output(branch_output) - - app.fetch_branch(repo, branch_name) - } -} - fn (mut app App) create_branch_or_update(repository_id int, branch_name string, author string, hash string, date int) { branch := sql app.db { select from Branch where repo_id == repository_id && name == branch_name limit 1 diff --git a/gitly.v b/gitly.v index 747f9d0..6cc719c 100644 --- a/gitly.v +++ b/gitly.v @@ -64,7 +64,7 @@ fn new_app() &App { app.create_tables() - create_log_directory_if_not_exists('logs') + create_directory_if_not_exists('logs') app.setup_logger() @@ -85,7 +85,8 @@ fn new_app() &App { app.load_settings() - create_repo_storage_directory_if_not_exists(app.settings.repo_storage_path) + create_directory_if_not_exists(app.settings.repo_storage_path) + create_directory_if_not_exists(app.settings.archive_path) // Create the first admin user if the db is empty app.find_user_by_id(1) or {} @@ -227,14 +228,9 @@ fn (mut app App) create_tables() { } } -fn create_log_directory_if_not_exists(path string) { - if !os.is_dir(path) { - os.mkdir(path) or { panic('cannot create logs directory') } - } -} +// maybe it should be implemented with another static server, in dev +fn (mut app App) send_file(filname string, content string) vweb.Result { + app.add_header('Content-Disposition', 'attachment; filename="$filname"') -fn create_repo_storage_directory_if_not_exists(path string) { - if !os.exists(path) { - os.mkdir(path) or { panic('cannot create storage directory') } - } + return app.ok(content) } diff --git a/release_entities.v b/release_entities.v index 6c81aac..ce9de83 100644 --- a/release_entities.v +++ b/release_entities.v @@ -11,5 +11,5 @@ mut: tag_name string [skip] tag_hash string [skip] user string [skip] - date time.Time [skip] + date time.Time } diff --git a/release_routes.v b/release_routes.v index e209d42..d1e28a5 100644 --- a/release_routes.v +++ b/release_routes.v @@ -3,9 +3,10 @@ module main import vweb import time -['/:user/:repo/releases'] -pub fn (mut app App) releases(user_str string, repo string) vweb.Result { - if !app.exists_user_repo(user_str, repo) { +// TODO: add pagination +['/:username/:repo_name/releases'] +pub fn (mut app App) releases(username string, repo_name string) vweb.Result { + if !app.exists_user_repo(username, repo_name) { return app.not_found() } @@ -18,6 +19,8 @@ pub fn (mut app App) releases(user_str string, repo string) vweb.Result { rels := app.find_repo_releases(app.repo.id) users := app.find_repo_registered_contributor(app.repo.id) + download_archive_prefix := '/$username/$repo_name/tag' + for rel in rels { release.notes = rel.notes mut user_id := 0 @@ -26,7 +29,7 @@ pub fn (mut app App) releases(user_str string, repo string) vweb.Result { if tag.id == rel.tag_id { release.tag_name = tag.name release.tag_hash = tag.hash - release.date = time.unix(tag.date) + release.date = time.unix(tag.created_at) user_id = tag.user_id break } diff --git a/release_service.v b/release_service.v index 9b30818..292ad8c 100644 --- a/release_service.v +++ b/release_service.v @@ -1,10 +1,13 @@ module main -pub fn (mut app App) add_release(tag_id int, repo_id int) { +import time + +pub fn (mut app App) add_release(tag_id int, repo_id int, date time.Time, notes string) { release := Release{ tag_id: tag_id repo_id: repo_id - notes: 'Some notes about this release...' + notes: notes + date: date } sql app.db { @@ -14,7 +17,7 @@ pub fn (mut app App) add_release(tag_id int, repo_id int) { pub fn (mut app App) find_repo_releases(repo_id int) []Release { return sql app.db { - select from Release where repo_id == repo_id + select from Release where repo_id == repo_id order by date desc } } diff --git a/repo_routes.v b/repo_routes.v index fe6e3c8..70cdd20 100644 --- a/repo_routes.v +++ b/repo_routes.v @@ -264,6 +264,8 @@ pub fn (mut app App) tree(username string, repository_name string, branch string repo_id := app.repo.id log_prefix := '$username/$repository_name' + app.fetch_tags(app.repo) + app.current_path = '/$path' if app.current_path.contains('/favicon.svg') { return vweb.not_found() diff --git a/repo_service.v b/repo_service.v index 26df2e9..b6e8fd3 100644 --- a/repo_service.v +++ b/repo_service.v @@ -23,6 +23,18 @@ enum RepoStatus { clone_done } +enum ArchiveFormat { + zip + tar +} + +fn (f ArchiveFormat) str() string { + return match f { + .zip { 'zip' } + .tar { 'tar' } + } +} + fn (mut app App) save_repository(repository Repo) { id := repository.id desc := repository.description @@ -187,12 +199,12 @@ fn (mut app App) user_has_repo(user_id int, repo_name string) bool { return count >= 0 } -fn (mut app App) update_repository(mut repository Repo) { - repository_id := repository.id +fn (mut app App) update_repository(mut repo Repo) { + repo_id := repo.id - repository.analyse_lang(app) + repo.analyse_lang(app) - data := repository.git('--no-pager log --abbrev-commit --abbrev=7 --pretty="%h$log_field_separator%aE$log_field_separator%cD$log_field_separator%s$log_field_separator%aN"') + data := repo.git('--no-pager log --abbrev-commit --abbrev=7 --pretty="%h$log_field_separator%aE$log_field_separator%cD$log_field_separator%s$log_field_separator%aN"') app.db.exec('BEGIN TRANSACTION') for line in data.split_into_lines() { @@ -212,34 +224,34 @@ fn (mut app App) update_repository(mut repository Repo) { user := app.find_user_by_email(commit_author_email) or { User{} } if user.id > 0 { - app.add_contributor(user.id, repository_id) + app.add_contributor(user.id, repo_id) commit_author_id = user.id } - app.add_commit_if_not_exist(repository_id, commit_hash, commit_author, commit_author_id, + app.add_commit_if_not_exist(repo_id, commit_hash, commit_author, commit_author_id, commit_message, int(commit_date.unix)) } } - app.info(repository.contributors_count.str()) - app.fetch_branches(repository) + app.info(repo.contributors_count.str()) + app.fetch_branches(repo) + app.fetch_tags(repo) - repository.commits_count = app.get_count_repo_commits(repository_id) - repository.contributors_count = app.get_count_repo_contributors(repository_id) - repository.branches_count = app.get_count_repo_branches(repository_id) + repo.commits_count = app.get_count_repo_commits(repo_id) + repo.contributors_count = app.get_count_repo_contributors(repo_id) + repo.branches_count = app.get_count_repo_branches(repo_id) - app.update_repo_commits_count(repository_id, repository.commits_count) - app.update_repo_contributors_count(repository_id, repository.contributors_count) + app.update_repo_commits_count(repo_id, repo.commits_count) + app.update_repo_contributors_count(repo_id, repo.contributors_count) - // TODO: TEMPORARY - UNTIL WE GET PERSISTENT RELEASE INFO - for tag in app.get_all_repo_tags(repository_id) { - app.add_release(tag.id, repository_id) + for tag in app.get_all_repo_tags(repo_id) { + app.add_release(tag.id, repo_id, time.unix(tag.created_at), tag.message) - repository.releases_count++ + repo.releases_count++ } - app.save_repository(repository) + app.save_repository(repo) app.db.exec('END TRANSACTION') app.info('Repository updated') } @@ -603,6 +615,11 @@ fn (r Repo) git_advertise(service string) string { return git_output } +fn (r Repo) archive_tag(tag string, path string, format ArchiveFormat) { + // TODO: check tag name before running command + r.git('archive $tag --format=$format --output="$path"') +} + fn (r Repo) get_commit_patch(commit_hash string) ?string { patch := r.git('format-patch --stdout -1 $commit_hash') diff --git a/repo_template.v b/repo_template.v index a091c3d..2e61db0 100644 --- a/repo_template.v +++ b/repo_template.v @@ -2,72 +2,38 @@ module main import vweb -fn (r &Repo) format_commits_count() vweb.RawHtml { - nr := r.commits_count - - if nr == 1 { - return '1 commit' +fn get_declension_form(count int, first_form string, second_form string) string { + if count == 1 { + return '1 $first_form' } - return '$nr commits' + return '$count $second_form' } -fn (r &Repo) format_branches_count() vweb.RawHtml { - nr := r.branches_count - - if nr == 1 { - return '1 branch' - } +fn (r &Repo) format_commits_count() vweb.RawHtml { + return get_declension_form(r.commits_count, 'commit', 'commits') +} - return '$nr branches' +fn (r &Repo) format_branches_count() vweb.RawHtml { + return get_declension_form(r.branches_count, 'branch', 'branches') } fn (r &Repo) format_open_prs_count() vweb.RawHtml { - nr := r.open_prs_count - - if nr == 1 { - return '1 pull request' - } - - return '$nr pull requests' + return get_declension_form(r.open_prs_count, 'pull request', 'pull requests') } fn (r &Repo) format_open_issues_count() vweb.RawHtml { - nr := r.open_issues_count - - if nr == 1 { - return '1 issue' - } - - return '$nr issues' + return get_declension_form(r.open_issues_count, 'issue', 'issues') } fn (r &Repo) format_contributors_count() vweb.RawHtml { - nr := r.contributors_count - - if nr == 1 { - return '1 contributor' - } - - return '$nr contributors' + return get_declension_form(r.contributors_count, 'contributor', 'contributors') } fn (r &Repo) format_topics_count() vweb.RawHtml { - nr := r.topics_count - - if nr == 1 { - return '1 discussion' - } - - return '$nr discussions' + return get_declension_form(r.topics_count, 'discussion', 'discussions') } fn (r &Repo) format_releases_count() vweb.RawHtml { - nr := r.releases_count - - if nr == 1 { - return '1 release' - } - - return '$nr releases' + return get_declension_form(r.releases_count, 'release', 'releases') } diff --git a/settings_entities.v b/settings_entities.v index 36dd77c..8cdf109 100644 --- a/settings_entities.v +++ b/settings_entities.v @@ -8,5 +8,6 @@ mut: oauth_client_id string oauth_client_secret string repo_storage_path string = './repos' + archive_path string = './archives' hostname string = 'gitly.org' } diff --git a/static/css/gitly.scss b/static/css/gitly.scss index c1d3996..290fea3 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -271,4 +271,4 @@ form { } } -@import 'files', 'langs', 'tree', 'commits', 'hl_table', 'branches', 'admin', 'blob', 'new', 'contributors', 'issues', 'user'; +@import 'files', 'langs', 'tree', 'commits', 'hl_table', 'branches', 'admin', 'blob', 'new', 'contributors', 'issues', 'user', 'releases'; diff --git a/static/css/releases.scss b/static/css/releases.scss new file mode 100644 index 0000000..e04326c --- /dev/null +++ b/static/css/releases.scss @@ -0,0 +1,6 @@ +.release { + border: 1px solid #dfdfdf; + border-radius: 3px; + margin-top: 10px; + padding: 10px; +} diff --git a/tag_entities.v b/tag_entities.v index a262304..d20cc46 100644 --- a/tag_entities.v +++ b/tag_entities.v @@ -4,8 +4,9 @@ struct Tag { id int [primary; sql: serial] repo_id int [unique: 'tag'] mut: - name string [unique: 'tag'] // tag name - hash string // hash of latest commit on tag - user_id int // id of user that created the tag - date int // time of latest commit on tag + name string [unique: 'tag'] + hash string + message string + user_id int + created_at int } diff --git a/tag_routes.v b/tag_routes.v new file mode 100644 index 0000000..8592585 --- /dev/null +++ b/tag_routes.v @@ -0,0 +1,26 @@ +module main + +import vweb +import os + +['/:username/:repo_name/tag/:tag/:format'] +pub fn (mut app App) handle_download_tag_archive(username string, repo_name string, tag string, format string) vweb.Result { + // access checking will be implemented in another module + user := app.find_user_by_username(username) or { return app.not_found() } + repo := app.find_repo_by_name(user.id, repo_name) or { return app.not_found() } + + archive_abs_path := os.abs_path(app.settings.archive_path) + snapshot_format := if format == 'zip' { 'zip' } else { 'tar.gz' } + snapshot_name := '${username}_${repo_name}_${tag}.$snapshot_format' + archive_path := '$archive_abs_path/$snapshot_name' + + if format == 'zip' { + repo.archive_tag(tag, archive_path, .zip) + } else { + repo.archive_tag(tag, archive_path, .tar) + } + + archive_content := os.read_file(archive_path) or { return app.not_found() } + + return app.send_file(snapshot_name, archive_content) +} diff --git a/tag_service.v b/tag_service.v index 12bda15..43e8410 100644 --- a/tag_service.v +++ b/tag_service.v @@ -2,8 +2,57 @@ // Use of this source code is governed by a GPL license that can be found in the LICENSE file. module main -pub fn (mut app App) get_all_repo_tags(repo_id int) []Tag { +import time + +fn (mut app App) fetch_tags(repo Repo) { + tags_output := repo.git('tag --format="%(refname:lstrip=2)$log_field_separator%(objectname)$log_field_separator%(subject)$log_field_separator%(authoremail)$log_field_separator%(creatordate:rfc)"') + + for tag_output in tags_output.split_into_lines() { + tag_parts := tag_output.split(log_field_separator) + tag_name := tag_parts[0] + commit_hash := tag_parts[1] + commit_message := tag_parts[2] + author_email := tag_parts[3] + commit_date := time.parse_rfc2822(tag_parts[4]) or { + app.info('Error: $err') + return + } + + user := app.find_user_by_email(author_email) or { + User{ + username: author_email + } + } + + app.add_tag(repo.id, tag_name, commit_hash, commit_message, user.id, int(commit_date.unix)) + } +} + +fn (mut app App) add_tag(repo_id int, tag_name string, commit_hash string, commit_message string, user_id int, date int) { + tag := sql app.db { + select from Tag where repo_id == repo_id && name == tag_name limit 1 + } + + if tag.id != 0 { + return + } + + new_tag := Tag{ + repo_id: repo_id + name: tag_name + hash: commit_hash + message: commit_message + user_id: user_id + created_at: date + } + + sql app.db { + insert new_tag into Tag + } +} + +fn (mut app App) get_all_repo_tags(repo_id int) []Tag { return sql app.db { - select from Tag where repo_id == repo_id order by date desc + select from Tag where repo_id == repo_id order by created_at desc } } diff --git a/templates/releases.html b/templates/releases.html index b42097e..2a28166 100644 --- a/templates/releases.html +++ b/templates/releases.html @@ -8,10 +8,14 @@ .release { Release: @r.tag_name
Hash: @r.tag_hash
- Created by: @r.user
+ @if r.user != '' + Created by: @r.user
+ @end + Created at: @r.date
Notes: @r.notes
+ Download zip, + download tar } -
@end } @else diff --git a/utils.v b/utils.v index cb08aae..7b3cf9a 100644 --- a/utils.v +++ b/utils.v @@ -2,6 +2,7 @@ module main import time import math +import os pub fn (mut app App) running_since() string { duration := time.now().unix - app.started_at @@ -28,3 +29,9 @@ pub fn (mut app App) make_path(i int) string { return s } + +fn create_directory_if_not_exists(path string) { + if !os.exists(path) { + os.mkdir(path) or { panic('cannot create $path directory') } + } +} diff --git a/validation/validation_service.v b/validation/validation_service.v index fde995e..4f4b835 100644 --- a/validation/validation_service.v +++ b/validation/validation_service.v @@ -11,7 +11,7 @@ pub fn is_username_valid(value string) bool { } pub fn is_repository_name_valid(value string) bool { - query := r'^[A-Za-z][A-Za-z0-9_\.\-]{1,100}$' + query := r'^[A-Za-z][A-Za-z0-9_\.\-]{0,100}$' mut re := regex.regex_opt(query) or { panic(err) } -- 2.39.5