From f958d2459c13eb6259dbfd1c5c8f052ab723abdd Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 28 Jun 2026 16:22:46 +0300 Subject: [PATCH] pr diff file tree; pay button only on gitly.org --- pr_routes.v | 59 +++++++++++++++ repo/repo_routes.v | 12 ++- static/assets/version | 2 +- static/css/gitly.scss | 154 +++++++++++++++++++++++++++++++++----- static/js/gitly.js | 126 +++++++++++++++++++++++++++++++ templates/pull_files.html | 68 +++++++++++------ 6 files changed, 381 insertions(+), 40 deletions(-) diff --git a/pr_routes.v b/pr_routes.v index e4ddfa7..d284e60 100644 --- a/pr_routes.v +++ b/pr_routes.v @@ -38,6 +38,64 @@ struct PrReviewCommentWithUser { user User } +struct PrFileTreeRow { + path string + name string + depth int + indent_px int + is_dir bool + is_new bool + is_deleted bool + is_renamed bool + additions int + deletions int +} + +fn build_pr_file_tree_rows(file_diffs []FileDiff) []PrFileTreeRow { + mut sorted := file_diffs.clone() + sorted.sort(a.path < b.path) + mut rows := []PrFileTreeRow{} + mut seen_dirs := map[string]bool{} + for fd in sorted { + parts := fd.path.split('/') + if parts.len == 0 { + continue + } + mut current_path := '' + if parts.len > 1 { + for idx := 0; idx < parts.len - 1; idx++ { + part := parts[idx] + if part == '' { + continue + } + current_path = if current_path == '' { part } else { '${current_path}/${part}' } + if current_path !in seen_dirs { + seen_dirs[current_path] = true + rows << PrFileTreeRow{ + path: current_path + name: part + depth: idx + indent_px: 10 + idx * 16 + is_dir: true + } + } + } + } + rows << PrFileTreeRow{ + path: fd.path + name: parts[parts.len - 1] + depth: parts.len - 1 + indent_px: 10 + (parts.len - 1) * 16 + is_new: fd.is_new + is_deleted: fd.is_deleted + is_renamed: fd.is_renamed + additions: fd.additions + deletions: fd.deletions + } + } + return rows +} + // GET /:username/:repo_name/pulls @['/:username/:repo_name/pulls'] pub fn (mut app App) handle_get_repo_pulls(mut ctx Context, username string, repo_name string) veb.Result { @@ -231,6 +289,7 @@ pub fn (mut app App) pull_request_files(mut ctx Context, username string, repo_n all_adds += fd.additions all_dels += fd.deletions } + file_tree := build_pr_file_tree_rows(file_diffs) rcomments := app.get_pr_review_comments(pr.id) mut comments_by_key := map[string][]PrReviewCommentWithUser{} for rc in rcomments { diff --git a/repo/repo_routes.v b/repo/repo_routes.v index b66169d..5bf3082 100644 --- a/repo/repo_routes.v +++ b/repo/repo_routes.v @@ -319,8 +319,10 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str if !is_clone_url_empty { app.debug('cloning') clone_job_repo := *new_repo + enforce_clone_size_limit := should_enforce_clone_size_limit(ctx.is_admin(), + clone_size_limit_flag_enabled()) spawn clone_repo(clone_job_repo, app.config, import_issues, import_prs, ctx.user.id, - !ctx.is_admin()) + enforce_clone_size_limit) } new_repo2 := app.find_repo_by_name_and_username(new_repo.name, owner_name) or { app.info('Repo was not inserted') @@ -377,6 +379,14 @@ fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Confi app.db.close() or {} } +fn clone_size_limit_flag_enabled() bool { + return os.exists('gitly_not_self_hosted') +} + +fn should_enforce_clone_size_limit(is_admin bool, not_self_hosted bool) bool { + return not_self_hosted && !is_admin +} + fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, import_prs bool, owner_user_id int, enforce_clone_size_limit bool) { mut cloned_repo := new_repo cloned_repo.clone(enforce_clone_size_limit) diff --git a/static/assets/version b/static/assets/version index 0752d15..d7a4bac 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -80b7125 \ No newline at end of file +fca4242 \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index f04e01e..346c1fb 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -2295,19 +2295,85 @@ form { .pr-files-sidebar__list { display: block; - max-height: calc(100vh - 116px); + max-height: calc(100vh - 170px); overflow: auto; } -.pr-file-link { +.pr-files-filter { display: grid; - grid-template-columns: 22px minmax(0, 1fr) auto; + grid-template-columns: 18px minmax(0, 1fr) 28px; align-items: center; gap: 8px; - padding: 7px 10px; + padding: 10px; + border-bottom: 1px solid #d8dee4; + background-color: $white; +} + +.pr-files-filter__icon { + color: #57606a; + + svg { + display: block; + width: 16px; + height: 16px; + } +} + +.pr-files-filter__input { + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: 5px 8px; + font-size: 13px; + background-color: $white; +} + +.pr-files-filter__clear { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px !important; + height: 28px; + padding: 0; + color: #57606a; + background-color: $white; + border-color: #d8dee4; + + svg { + width: 14px; + height: 14px; + } + + &:hover { + color: $black; + background-color: #f6f8fa; + border-color: #d8dee4; + } + + &[hidden] { + display: none; + } +} + +.pr-file-tree-row { + display: grid; + grid-template-columns: 16px 18px minmax(0, 1fr) auto auto; + align-items: center; + gap: 7px; + width: 100% !important; + box-sizing: border-box; + padding: 6px 10px; border-top: 1px solid #eef1f4; + border-right: 0; + border-bottom: 0; + border-left: 0; + border-radius: 0; + background-color: $white; color: $black !important; + cursor: pointer; font-size: 12px; + text-align: left; + font-family: inherit; &:first-child { border-top: none; @@ -2315,30 +2381,77 @@ form { &:hover { background-color: #f6f8fa; + border-color: #eef1f4; text-decoration: none; + color: $black !important; } } -.pr-file-link__status { +.pr-file-tree-row--dir { + grid-template-columns: 16px 18px minmax(0, 1fr); + font-weight: 600; +} + +.pr-file-tree-row__chevron, +.pr-file-tree-row__spacer, +.pr-file-tree-row__folder, +.pr-file-tree-row__file { display: inline-flex; align-items: center; justify-content: center; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: #eef3f8; + width: 16px; + height: 16px; color: #57606a; - font-size: 10px; - font-weight: 700; - line-height: 1; } -.pr-file-link__path { +.pr-file-tree-row__chevron svg { + width: 14px; + height: 14px; + transition: transform 0.08s; +} + +.pr-file-tree-row--dir[aria-expanded="true"] .pr-file-tree-row__chevron svg { + transform: rotate(90deg); +} + +.pr-file-tree-row__folder { + color: #54aeff; + + svg { + width: 16px; + height: 16px; + } +} + +.pr-file-tree-row__file { + color: #57606a; + + svg { + width: 15px; + height: 15px; + } +} + +.pr-file-tree-row__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; + line-height: 1.35; +} + +.pr-file-link__status { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: #eef3f8; + color: #57606a; + font-size: 10px; + font-weight: 700; + line-height: 1; } .pr-file-link__counts { @@ -2349,6 +2462,13 @@ form { font-weight: 600; } +.pr-files-filter-empty { + padding: 12px; + border-top: 1px solid #eef1f4; + color: #57606a; + font-size: 13px; +} + .pr-files-diffs { min-width: 0; } @@ -2617,12 +2737,12 @@ form { flex-direction: column; } - .pr-file-link { - grid-template-columns: 22px minmax(0, 1fr); + .pr-file-tree-row--file { + grid-template-columns: 16px 18px minmax(0, 1fr) 18px; } .pr-file-link__counts { - grid-column: 2; + grid-column: 3 / -1; } } diff --git a/static/js/gitly.js b/static/js/gitly.js index 640258c..faacecb 100644 --- a/static/js/gitly.js +++ b/static/js/gitly.js @@ -43,3 +43,129 @@ function change_lang(x) { window.location.reload(); } ) } + +function initPrFilesTree() { + const input = document.querySelector("[data-pr-files-filter]"); + const tree = document.querySelector("[data-pr-files-tree]"); + if (!input || !tree) { + return; + } + + const clearButton = document.querySelector("[data-pr-files-filter-clear]"); + const countEl = document.querySelector("[data-pr-files-count]"); + const emptyEl = document.querySelector("[data-pr-files-filter-empty]"); + const rows = Array.from(tree.querySelectorAll("[data-pr-tree-row]")); + const fileRows = rows.filter((row) => row.hasAttribute("data-file-path")); + const diffEls = Array.from(document.querySelectorAll("[data-diff-path]")); + const totalFiles = fileRows.length; + const collapsedDirs = new Set(); + + function pathForRow(row) { + return row.getAttribute("data-file-path") || row.getAttribute("data-dir-path") || ""; + } + + function addParentDirs(path, visibleDirs) { + const parts = path.split("/"); + let current = ""; + for (let i = 0; i < parts.length - 1; i++) { + current = current === "" ? parts[i] : current + "/" + parts[i]; + visibleDirs.add(current); + } + } + + function isUnderCollapsed(path) { + for (const dir of collapsedDirs) { + if (path !== dir && path.indexOf(dir + "/") === 0) { + return true; + } + } + return false; + } + + function applyPrFilesFilter() { + const query = input.value.trim().toLowerCase(); + const visibleDirs = new Set(); + let visibleFileCount = 0; + + for (const row of fileRows) { + const path = row.getAttribute("data-file-path") || ""; + if (query === "" || path.toLowerCase().indexOf(query) !== -1) { + visibleFileCount++; + addParentDirs(path, visibleDirs); + } + } + + for (const row of rows) { + const filePath = row.getAttribute("data-file-path"); + const dirPath = row.getAttribute("data-dir-path"); + let visible = true; + + if (filePath) { + visible = query === "" || filePath.toLowerCase().indexOf(query) !== -1; + } else if (dirPath) { + visible = query === "" || visibleDirs.has(dirPath); + } + + if (visible && query === "" && isUnderCollapsed(pathForRow(row))) { + visible = false; + } + row.hidden = !visible; + } + + for (const diffEl of diffEls) { + const path = diffEl.getAttribute("data-diff-path") || ""; + diffEl.hidden = query !== "" && path.toLowerCase().indexOf(query) === -1; + } + + if (countEl) { + countEl.textContent = query === "" ? String(totalFiles) : visibleFileCount + " / " + totalFiles; + } + if (emptyEl) { + emptyEl.hidden = query === "" || visibleFileCount > 0; + } + if (clearButton) { + clearButton.hidden = input.value === ""; + } + } + + for (const row of rows) { + if (!row.hasAttribute("data-dir-path")) { + continue; + } + row.addEventListener("click", () => { + const path = row.getAttribute("data-dir-path"); + if (!path) { + return; + } + if (collapsedDirs.has(path)) { + collapsedDirs.delete(path); + row.setAttribute("aria-expanded", "true"); + } else { + collapsedDirs.add(path); + row.setAttribute("aria-expanded", "false"); + } + applyPrFilesFilter(); + }); + } + + input.addEventListener("input", applyPrFilesFilter); + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }); + + if (clearButton) { + clearButton.addEventListener("click", () => { + input.value = ""; + input.focus(); + applyPrFilesFilter(); + }); + } + + applyPrFilesFilter(); +} + +document.addEventListener("DOMContentLoaded", () => { + initPrFilesTree(); +}); diff --git a/templates/pull_files.html b/templates/pull_files.html index 7447d30..cf97541 100644 --- a/templates/pull_files.html +++ b/templates/pull_files.html @@ -54,35 +54,61 @@
@for fd in file_diffs -
+
@if fd.is_renamed && fd.old_path != fd.path -- 2.39.5