From 99cc809d26a7224c6a2736ff4722d984f9e42e16 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 15 May 2026 13:14:01 +0300 Subject: [PATCH] user repos: add primary language indicator and activity sparkline; soft-delete repo flag --- commit/commit.v | 29 +++++++++++ gitly.v | 1 + repo/file.v | 5 +- repo/repo.v | 104 ++++++++++++++++++++++---------------- repo/repo_routes.v | 6 +++ static/assets/version | 2 +- static/css/gitly.scss | 48 ++++++++++++++++-- templates/user/repos.html | 27 +++++++--- 8 files changed, 166 insertions(+), 56 deletions(-) diff --git a/commit/commit.v b/commit/commit.v index 29ea655..6ad86f6 100644 --- a/commit/commit.v +++ b/commit/commit.v @@ -137,3 +137,32 @@ fn (mut app App) find_repo_last_commit(repo_id int, branch_id int) Commit { return commits.first() } + +fn (app App) find_repo_last_commit_time(repo_id int) int { + commits := sql app.db { + select from Commit where repo_id == repo_id order by created_at desc limit 1 + } or { return 0 } + if commits.len == 0 { + return 0 + } + return commits[0].created_at +} + +const activity_weeks = 12 + +fn (app App) get_repo_activity_buckets(repo_id int) []int { + week_seconds := 7 * 24 * 3600 + now := int(time.now().unix()) + cutoff := now - activity_weeks * week_seconds + commits := sql app.db { + select from Commit where repo_id == repo_id && created_at >= cutoff + } or { []Commit{} } + mut buckets := []int{len: activity_weeks} + for c in commits { + idx := (c.created_at - cutoff) / week_seconds + if idx >= 0 && idx < activity_weeks { + buckets[idx]++ + } + } + return buckets +} diff --git a/gitly.v b/gitly.v index 4c314d4..6b8da11 100644 --- a/gitly.v +++ b/gitly.v @@ -273,6 +273,7 @@ fn (mut app App) create_tables() ! { fn (mut app App) migrate_tables() ! { app.add_missing_column('File', 'is_size_calculated', db_bool_column_type())! app.add_missing_column('Settings', 'disable_tree_folder_size', db_bool_column_type())! + app.add_missing_column('Repo', 'is_deleted', db_bool_column_type())! } fn (mut app App) add_missing_column(table_name string, column_name string, column_type string) ! { diff --git a/repo/file.v b/repo/file.v index 9992395..ca830b9 100644 --- a/repo/file.v +++ b/repo/file.v @@ -149,5 +149,8 @@ fn (mut app App) delete_repository_files_in_branch(repository_id int, branch_nam } fn (mut app App) delete_repo_folder(path string) { - os.rmdir_all(os.real_path(path)) or { panic(err) } + if path == '' { + return + } + os.rmdir_all(os.real_path(path)) or { app.warn('failed to remove repo folder ${path}: ${err}') } } diff --git a/repo/repo.v b/repo/repo.v index fa7864a..54a6f79 100644 --- a/repo/repo.v +++ b/repo/repo.v @@ -18,6 +18,7 @@ struct Repo { primary_branch string description string is_public bool + is_deleted bool users_contributed []string @[skip] users_authorized []string @[skip] nr_topics int @[skip] @@ -25,20 +26,22 @@ struct Repo { latest_update_hash string @[skip] latest_activity time.Time @[skip] mut: - webhook_secret string - tags_count int - nr_open_issues int @[orm: 'open_issues_count'] - nr_open_prs int @[orm: 'open_prs_count'] - nr_releases int @[orm: 'releases_count'] - nr_branches int @[orm: 'branches_count'] - nr_tags int - nr_stars int @[orm: 'stars_count'] - lang_stats []LangStat @[skip] - created_at int - nr_contributors int - labels []Label @[skip] - status RepoStatus - msg_cache map[string]string @[skip] + webhook_secret string + tags_count int + nr_open_issues int @[orm: 'open_issues_count'] + nr_open_prs int @[orm: 'open_prs_count'] + nr_releases int @[orm: 'releases_count'] + nr_branches int @[orm: 'branches_count'] + nr_tags int + nr_stars int @[orm: 'stars_count'] + lang_stats []LangStat @[skip] + created_at int + nr_contributors int + labels []Label @[skip] + status RepoStatus + msg_cache map[string]string @[skip] + latest_commit_at int @[skip] + activity_buckets []int @[skip] } // log_field_separator is declared as constant in case we need to change it later @@ -92,7 +95,7 @@ fn (mut app App) save_repo(repo Repo) ! { fn (app App) find_repo_by_name_and_user_id(repo_name string, user_id int) ?Repo { repos := sql app.db { - select from Repo where name == repo_name && user_id == user_id limit 1 + select from Repo where name == repo_name && user_id == user_id && is_deleted == false limit 1 } or { return none } if repos.len == 0 { @@ -114,25 +117,25 @@ fn (app App) find_repo_by_name_and_username(repo_name string, username string) ? fn (mut app App) get_count_user_repos(user_id int) int { return sql app.db { - select count from Repo where user_id == user_id + select count from Repo where user_id == user_id && is_deleted == false } or { 0 } } fn (mut app App) find_user_repos(user_id int) []Repo { return sql app.db { - select from Repo where user_id == user_id + select from Repo where user_id == user_id && is_deleted == false } or { []Repo{} } } fn (mut app App) find_user_public_repos(user_id int) []Repo { return sql app.db { - select from Repo where user_id == user_id && is_public == true + select from Repo where user_id == user_id && is_public == true && is_deleted == false } or { []Repo{} } } fn (app &App) search_public_repos(query string) []Repo { repo_rows := db_exec_values(app.db, - 'select id, name, user_id, description, stars_count from ${sql_table('Repo')} where is_public is true and name like ${sql_like_pattern(query)}') or { + 'select id, name, user_id, description, stars_count from ${sql_table('Repo')} where is_public is true and is_deleted is false and name like ${sql_like_pattern(query)}') or { return [] } @@ -156,7 +159,7 @@ fn (app &App) search_public_repos(query string) []Repo { fn (app &App) find_repo_by_id(repo_id int) ?Repo { repos := sql app.db { - select from Repo where id == repo_id + select from Repo where id == repo_id && is_deleted == false } or { []Repo{} } if repos.len == 0 { @@ -213,7 +216,7 @@ fn (mut app App) increment_repo_issues(repo_id int) ! { fn (mut app App) get_count_repo() int { return sql app.db { - select count from Repo + select count from Repo where is_deleted == false } or { 0 } } @@ -223,34 +226,47 @@ fn (mut app App) add_repo(repo Repo) ! { }! } -fn (mut app App) delete_repository(id int, path string, name string) ! { - sql app.db { - delete from Repo where id == id - }! - app.info('Removed repo entry (${id}, ${name})') +fn (r &Repo) activity_svg_points() string { + if r.activity_buckets.len == 0 { + return '' + } + mut max := 1 + for v in r.activity_buckets { + if v > max { + max = v + } + } + width := 120.0 + height := 28.0 + step := if r.activity_buckets.len > 1 { + width / f64(r.activity_buckets.len - 1) + } else { + width + } + mut points := []string{cap: r.activity_buckets.len} + for i, v in r.activity_buckets { + x := f64(i) * step + y := height - (f64(v) / f64(max)) * (height - 2.0) - 1.0 + points << '${x:.1f},${y:.1f}' + } + return points.join(' ') +} +fn (r &Repo) last_updated_str() string { + if r.latest_commit_at <= 0 { + return '' + } + return time.unix(r.latest_commit_at).relative() +} + +fn (mut app App) delete_repository(id int, path string, name string) ! { sql app.db { - delete from Commit where repo_id == id + update Repo set is_deleted = true where id == id }! - - app.info('Removed repo commits (${id}, ${name})') - app.delete_repo_issues(id)! - app.info('Removed repo issues (${id}, ${name})') - - app.delete_repo_branches(id)! - app.info('Removed repo branches (${id}, ${name})') - - app.delete_repo_releases(id)! - app.info('Removed repo releases (${id}, ${name})') - - app.delete_repository_files(id)! - app.info('Removed repo files (${id}, ${name})') + app.info('Marked repo as deleted (${id}, ${name})') app.delete_repo_folder(path) app.info('Removed repo folder (${id}, ${name})') - - app.delete_repo_ci_statuses(id) or {} - app.info('Removed repo CI statuses (${id}, ${name})') } fn (mut app App) move_repo_to_user(repo_id int, user_id int, user_name string) ! { @@ -261,7 +277,7 @@ fn (mut app App) move_repo_to_user(repo_id int, user_id int, user_name string) ! fn (mut app App) user_has_repo(user_id int, repo_name string) bool { count := sql app.db { - select count from Repo where user_id == user_id && name == repo_name + select count from Repo where user_id == user_id && name == repo_name && is_deleted == false } or { 0 } return count >= 0 } diff --git a/repo/repo_routes.v b/repo/repo_routes.v index 68d8917..1bc7295 100644 --- a/repo/repo_routes.v +++ b/repo/repo_routes.v @@ -25,6 +25,12 @@ pub fn (mut app App) user_repos(username string) veb.Result { 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') } diff --git a/static/assets/version b/static/assets/version index a0f51e7..c0f7ce6 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -2760a45 \ No newline at end of file +8304d7f \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index 122ccc8..43d0117 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -459,7 +459,6 @@ form { } .repo-card { - display: block; border: 1px solid $gray; border-radius: $medium-radius; padding: 14px 18px; @@ -484,10 +483,53 @@ form { font-size: 17px; font-weight: 600; color: $link-color; + + &:hover { + text-decoration: underline; + } } -.repo-card:hover .repo-card__title { - text-decoration: underline; +.repo-card__footer { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} + +.repo-card__footer-left { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; + font-size: 12px; + color: $gray-dark; + flex: 1; + min-width: 0; +} + +.repo-card__lang { + display: inline-flex; + align-items: center; + gap: 5px; +} + +.repo-card__lang-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.repo-card__updated { + color: $gray-dark; +} + +.repo-card__activity { + flex-shrink: 0; + width: 120px; + height: 28px; + overflow: visible; } .repo-card__badge { diff --git a/templates/user/repos.html b/templates/user/repos.html index 16fd536..a4cb6ad 100644 --- a/templates/user/repos.html +++ b/templates/user/repos.html @@ -17,9 +17,9 @@ @if repos.len > 0
@for repo in repos - +
- @repo.name + @repo.name @if repo.is_public Public @else @@ -29,12 +29,25 @@ @if repo.description.len > 0

@repo.description

@end -
- ★ @repo.nr_stars - ● @repo.nr_open_issues - ⚒ @repo.nr_branches + - +
@end
@else -- 2.39.5