Commit cb22311

Nick Faro committed on
Polish file and commit navigation
commit cb22311e686e058ffd8b08ded4e86bfdea173a66 parent e6f79a5
5 changed files +114−49
Modifiedinternal/site/diff.go +4−0
@@ -43,6 +43,7 @@ type DiffFile struct {
43 OldPath string 43 OldPath string
44 NewPath string 44 NewPath string
45 DisplayPath string 45 DisplayPath string
46 TreePath string
46 Status string 47 Status string
47 Rows []DiffRow 48 Rows []DiffRow
48 Additions int 49 Additions int
@@ -371,10 +372,13 @@ func parsePatch(patch []byte) []DiffFile {
371 switch { 372 switch {
372 case current.Status == "Renamed": 373 case current.Status == "Renamed":
373 current.DisplayPath = current.OldPath + " → " + current.NewPath 374 current.DisplayPath = current.OldPath + " → " + current.NewPath
375 current.TreePath = current.NewPath
374 case current.Status == "Deleted": 376 case current.Status == "Deleted":
375 current.DisplayPath = current.OldPath 377 current.DisplayPath = current.OldPath
378 current.TreePath = current.OldPath
376 default: 379 default:
377 current.DisplayPath = current.NewPath 380 current.DisplayPath = current.NewPath
381 current.TreePath = current.NewPath
378 } 382 }
379 if current.Status == "" { 383 if current.Status == "" {
380 current.Status = "Modified" 384 current.Status = "Modified"
Modifiedinternal/site/site.go +16−12
@@ -80,15 +80,17 @@ type Commit struct {
80 } 80 }
81 81
82 type TreeEntry struct { 82 type TreeEntry struct {
83 Mode string 83 Mode string
84 Type string 84 Type string
85 OID string 85 OID string
86 Size int64 86 Size int64
87 Name string 87 Name string
88 URL string 88 URL string
89 CommitMessage string 89 CommitMessage string
90 CommitURL string 90 CommitURL string
91 SubmoduleURL string 91 CommitDate string
92 CommitDateTime string
93 SubmoduleURL string
92 } 94 }
93 95
94 type Crumb struct { 96 type Crumb struct {
@@ -499,6 +501,8 @@ func (g *Generator) populateEntryCommits(ctx context.Context, oid, dir string, e
499 } 501 }
500 entries[index].CommitMessage = commit.Message 502 entries[index].CommitMessage = commit.Message
501 entries[index].CommitURL = routeURL(g.BaseURL, path.Join("commit", commit.OID)) 503 entries[index].CommitURL = routeURL(g.BaseURL, path.Join("commit", commit.OID))
504 entries[index].CommitDate = commit.Date
505 entries[index].CommitDateTime = commit.DateTime
502 } 506 }
503 }() 507 }()
504 } 508 }
@@ -516,15 +520,15 @@ sendJobs:
516 } 520 }
517 521
518 func (g *Generator) entryCommit(ctx context.Context, oid, filePath string) (Commit, error) { 522 func (g *Generator) entryCommit(ctx context.Context, oid, filePath string) (Commit, error) {
519 out, err := g.Git.Run(ctx, "--literal-pathspecs", "log", "-1", "--format=%H%x00%s", oid, "--", filePath) 523 out, err := g.Git.Run(ctx, "--literal-pathspecs", "log", "-1", "--format=%H%x00%s%x00%aI", oid, "--", filePath)
520 if err != nil { 524 if err != nil {
521 return Commit{}, err 525 return Commit{}, err
522 } 526 }
523 fields := strings.Split(strings.TrimSpace(string(out)), "\x00") 527 fields := strings.Split(strings.TrimSpace(string(out)), "\x00")
524 if len(fields) != 2 || fields[0] == "" { 528 if len(fields) != 3 || fields[0] == "" {
525 return Commit{}, fmt.Errorf("could not find the latest commit for %s", filePath) 529 return Commit{}, fmt.Errorf("could not find the latest commit for %s", filePath)
526 } 530 }
527 return Commit{OID: fields[0], Message: fields[1]}, nil 531 return Commit{OID: fields[0], Message: fields[1], Date: displayDate(fields[2]), DateTime: fields[2]}, nil
528 } 532 }
529 533
530 func (g *Generator) blobPages(ctx context.Context, branch, oid string, entry TreeEntry, summary branchSummary) ([]Page, error) { 534 func (g *Generator) blobPages(ctx context.Context, branch, oid string, entry TreeEntry, summary branchSummary) ([]Page, error) {
Modifiedinternal/site/site_test.go +7−0
@@ -46,6 +46,8 @@ func TestFullAndIncrementalBuild(t *testing.T) {
46 assertPage(t, full, "", "Go to file") 46 assertPage(t, full, "", "Go to file")
47 assertPage(t, full, "", "data-branch-menu") 47 assertPage(t, full, "", "data-branch-menu")
48 assertPage(t, full, "", "data-copied-icon") 48 assertPage(t, full, "", "data-copied-icon")
49 assertPage(t, full, "", `data-smart-time`)
50 assertPage(t, full, "", `absolute >= 604800`)
49 assertPage(t, full, "", ".dumbforge/assets/gabriel-mark.svg") 51 assertPage(t, full, "", ".dumbforge/assets/gabriel-mark.svg")
50 assertPage(t, full, "", `.dumbforge/assets/site.css?v=`+generator.AssetVersion()) 52 assertPage(t, full, "", `.dumbforge/assets/site.css?v=`+generator.AssetVersion())
51 assertPage(t, full, ".dumbforge/assets/site.css", "@media (prefers-color-scheme: light)") 53 assertPage(t, full, ".dumbforge/assets/site.css", "@media (prefers-color-scheme: light)")
@@ -76,10 +78,15 @@ func TestFullAndIncrementalBuild(t *testing.T) {
76 assertPage(t, full, "tree/main/src", `<a class="file-icon parent-icon" href="https://example.invalid/repo.git" aria-label="Parent directory">`) 78 assertPage(t, full, "tree/main/src", `<a class="file-icon parent-icon" href="https://example.invalid/repo.git" aria-label="Parent directory">`)
77 assertPage(t, full, "tree/main/src", `<a class="file-name" href="https://example.invalid/repo.git">..</a>`) 79 assertPage(t, full, "tree/main/src", `<a class="file-name" href="https://example.invalid/repo.git">..</a>`)
78 assertPage(t, full, "tree/main/src", `commit/`+first+`" title="initial">initial</a>`) 80 assertPage(t, full, "tree/main/src", `commit/`+first+`" title="initial">initial</a>`)
81 assertPage(t, full, "tree/main/src", `class="file-date" datetime="`)
82 assertPage(t, full, "tree/main/src", `data-relative-time`)
79 assertPage(t, full, "commits/main", "Commit history") 83 assertPage(t, full, "commits/main", "Commit history")
80 assertPage(t, full, "commits/main", "gravatar.com/avatar") 84 assertPage(t, full, "commits/main", "gravatar.com/avatar")
81 assertPage(t, full, "commits/main", `class="commit-list-message" href="https://example.invalid/repo.git/commit/`+first+`">initial</a>`) 85 assertPage(t, full, "commits/main", `class="commit-list-message" href="https://example.invalid/repo.git/commit/`+first+`">initial</a>`)
86 assertPage(t, full, "commits/main", `class="secondary-button commit-list-copy" type="button" data-copy="`+first+`"`)
82 assertPage(t, full, "commit/"+first, "changed files") 87 assertPage(t, full, "commit/"+first, "changed files")
88 assertPage(t, full, "commit/"+first, `data-diff-file-tree`)
89 assertPage(t, full, "commit/"+first, `data-diff-file data-path="README.md"`)
83 assertPage(t, full, "commit/"+first, `aria-label="Side-by-side diff for README.md"`) 90 assertPage(t, full, "commit/"+first, `aria-label="Side-by-side diff for README.md"`)
84 assertPage(t, full, "commit/"+first, `class="diff-code new addition`) 91 assertPage(t, full, "commit/"+first, `class="diff-code new addition`)
85 assertPage(t, full, "commit/"+first, "A longer explanation of the initial change.") 92 assertPage(t, full, "commit/"+first, "A longer explanation of the initial change.")
Modifiedinternal/site/style.go +7−9
@@ -173,7 +173,7 @@ button, input { font: inherit; }
173 .file-name:hover { text-decoration: underline; } 173 .file-name:hover { text-decoration: underline; }
174 .row-message { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 174 .row-message { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
175 .row-message:hover { color: var(--blue); text-decoration: underline; } 175 .row-message:hover { color: var(--blue); text-decoration: underline; }
176 .file-size { color: var(--muted); text-align: right; font-size: 12px; white-space: nowrap; } 176 .file-date { color: var(--muted); text-align: right; font-size: 12px; white-space: nowrap; }
177 .parent-row { grid-template-columns: 24px 1fr 1fr 75px; } 177 .parent-row { grid-template-columns: 24px 1fr 1fr 75px; }
178 .readme-card { margin-top: 18px; } 178 .readme-card { margin-top: 18px; }
179 .card-heading { height: 46px; display: flex; align-items: center; gap: 9px; padding: 0 16px; border-bottom: 1px solid var(--border); font-weight: 650; } 179 .card-heading { height: 46px; display: flex; align-items: center; gap: 9px; padding: 0 16px; border-bottom: 1px solid var(--border); font-weight: 650; }
@@ -284,8 +284,11 @@ button, input { font: inherit; }
284 .commit-list-message:hover, .commit-list-message:focus { color: var(--text); text-decoration: underline; } 284 .commit-list-message:hover, .commit-list-message:focus { color: var(--text); text-decoration: underline; }
285 .commit-list-body > span { color: var(--muted); font-size: 12px; } 285 .commit-list-body > span { color: var(--muted); font-size: 12px; }
286 .commit-list-body > span strong { color: var(--text); } 286 .commit-list-body > span strong { color: var(--text); }
287 .commit-list-actions { display: inline-flex; align-items: center; gap: 6px; }
287 .commit-sha { padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; color: var(--text); background: var(--canvas); font: 12px ui-monospace, SFMono-Regular, Consolas, monospace; } 288 .commit-sha { padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; color: var(--text); background: var(--canvas); font: 12px ui-monospace, SFMono-Regular, Consolas, monospace; }
288 .commit-sha:hover { color: var(--blue); text-decoration: none; } 289 .commit-sha:hover { color: var(--blue); text-decoration: none; }
290 .commit-list-copy { width: 30px; min-height: 30px; padding: 4px; }
291 .commit-list-copy.is-copied { color: var(--green); background: var(--success-bg); }
289 292
290 .commit-view { max-width: 100%; } 293 .commit-view { max-width: 100%; }
291 .commit-page-heading { min-height: 42px; margin-bottom: 12px; display: flex; align-items: center; } 294 .commit-page-heading { min-height: 42px; margin-bottom: 12px; display: flex; align-items: center; }
@@ -309,12 +312,7 @@ button, input { font: inherit; }
309 .addition { color: var(--success-text); } 312 .addition { color: var(--success-text); }
310 .deletion { color: var(--danger-text); } 313 .deletion { color: var(--danger-text); }
311 .diff-layout { display: grid; grid-template-columns: 260px minmax(0,1fr); align-items: start; gap: 20px; } 314 .diff-layout { display: grid; grid-template-columns: 260px minmax(0,1fr); align-items: start; gap: 20px; }
312 .diff-sidebar { position: sticky; top: 12px; min-width: 0; } 315 .diff-sidebar { position: sticky; top: 12px; min-width: 0; height: calc(100vh - 24px); min-height: 320px; }
313 .diff-sidebar h2 { margin: 0 0 8px; font-size: 14px; }
314 .diff-file-nav { padding: 7px 0; overflow: hidden; border: 1px solid var(--border); border-radius: 7px; }
315 .diff-file-nav a { min-height: 31px; padding: 5px 12px; display: block; overflow: hidden; color: var(--text); font: 12px ui-monospace, SFMono-Regular, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }
316 .diff-file-nav a:hover { background: var(--canvas); text-decoration: none; }
317 .diff-file-nav a span { width: 68px; display: inline-block; color: var(--muted); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
318 .diff-files { display: flex; flex-direction: column; gap: 24px; } 316 .diff-files { display: flex; flex-direction: column; gap: 24px; }
319 .diff-file { scroll-margin-top: 12px; overflow: hidden; border: 1px solid var(--border); border-radius: 7px; background: var(--bg); } 317 .diff-file { scroll-margin-top: 12px; overflow: hidden; border: 1px solid var(--border); border-radius: 7px; background: var(--bg); }
320 .diff-file:target { border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue); } 318 .diff-file:target { border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue); }
@@ -365,7 +363,7 @@ button, input { font: inherit; }
365 .blob-layout { grid-template-columns: 1fr; } 363 .blob-layout { grid-template-columns: 1fr; }
366 .file-browser { display: none; } 364 .file-browser { display: none; }
367 .diff-layout { grid-template-columns: 1fr; } 365 .diff-layout { grid-template-columns: 1fr; }
368 .diff-sidebar { position: static; } 366 .diff-sidebar.file-browser { position: static; height: auto; min-height: 0; max-height: 320px; display: flex; }
369 .sidebar { padding-top: 0; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 0 24px; } 367 .sidebar { padding-top: 0; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 0 24px; }
370 .about-section { grid-column: 1 / -1; } 368 .about-section { grid-column: 1 / -1; }
371 .commit-row { grid-template-columns: 32px minmax(0,1fr) auto; } 369 .commit-row { grid-template-columns: 32px minmax(0,1fr) auto; }
@@ -391,7 +389,7 @@ button, input { font: inherit; }
391 .markdown-body { padding: 22px 18px; } 389 .markdown-body { padding: 22px 18px; }
392 .branch-popover, .clone-popover { position: fixed; top: 74px; left: 12px; right: 12px; width: auto; } 390 .branch-popover, .clone-popover { position: fixed; top: 74px; left: 12px; right: 12px; width: auto; }
393 .commit-list-row { grid-template-columns: 34px minmax(0,1fr); } 391 .commit-list-row { grid-template-columns: 34px minmax(0,1fr); }
394 .commit-sha { display: none; } 392 .commit-list-actions { display: none; }
395 .commit-summary-title { align-items: center; } 393 .commit-summary-title { align-items: center; }
396 .commit-page-heading h1 { font-size: 20px; } 394 .commit-page-heading h1 { font-size: 20px; }
397 .commit-page-heading code { font-size: 17px; } 395 .commit-page-heading code { font-size: 17px; }
Modifiedinternal/site/template.go +80−28
@@ -107,9 +107,12 @@ const pageTemplate = `<!doctype html>
107 <img class="avatar commit-avatar" src="{{.AvatarURL}}" alt="" loading="lazy" referrerpolicy="no-referrer"> 107 <img class="avatar commit-avatar" src="{{.AvatarURL}}" alt="" loading="lazy" referrerpolicy="no-referrer">
108 <div class="commit-list-body"> 108 <div class="commit-list-body">
109 <a class="commit-list-message" href="{{.URL}}">{{.Message}}</a> 109 <a class="commit-list-message" href="{{.URL}}">{{.Message}}</a>
110 <span><strong>{{.Author}}</strong> committed on <time datetime="{{.DateTime}}">{{.Date}}</time></span> 110 <span><strong>{{.Author}}</strong> committed on <time datetime="{{.DateTime}}" data-smart-time>{{.Date}}</time></span>
111 </div>
112 <div class="commit-list-actions">
113 <a class="commit-sha" href="{{.URL}}">{{.ShortOID}}</a>
114 <button class="secondary-button commit-list-copy" type="button" data-copy="{{.OID}}" aria-label="Copy commit ID" title="Copy full commit ID"><span data-copy-icon>{{icon "copy"}}</span><span data-copied-icon hidden>{{icon "check"}}</span><span class="sr-only" data-copy-label>Copy commit ID</span></button>
111 </div> 115 </div>
112 <a class="commit-sha" href="{{.URL}}">{{.ShortOID}}</a>
113 </article>{{end}} 116 </article>{{end}}
114 </div> 117 </div>
115 </section>{{end}} 118 </section>{{end}}
@@ -119,7 +122,7 @@ const pageTemplate = `<!doctype html>
119 <div class="commit-page-heading"><h1>Commit <code>{{.ViewedCommit.Commit.ShortOID}}</code></h1></div> 122 <div class="commit-page-heading"><h1>Commit <code>{{.ViewedCommit.Commit.ShortOID}}</code></h1></div>
120 <div class="commit-author-row"> 123 <div class="commit-author-row">
121 <img class="avatar commit-avatar" src="{{.ViewedCommit.Commit.AvatarURL}}" alt="" referrerpolicy="no-referrer"> 124 <img class="avatar commit-avatar" src="{{.ViewedCommit.Commit.AvatarURL}}" alt="" referrerpolicy="no-referrer">
122 <span><strong>{{.ViewedCommit.Commit.Author}}</strong> committed on <time datetime="{{.ViewedCommit.Commit.DateTime}}">{{.ViewedCommit.Commit.Date}}</time></span> 125 <span><strong>{{.ViewedCommit.Commit.Author}}</strong> committed on <time datetime="{{.ViewedCommit.Commit.DateTime}}" data-smart-time>{{.ViewedCommit.Commit.Date}}</time></span>
123 </div> 126 </div>
124 <article class="commit-summary-card"> 127 <article class="commit-summary-card">
125 <div class="commit-summary-title"> 128 <div class="commit-summary-title">
@@ -142,10 +145,11 @@ const pageTemplate = `<!doctype html>
142 <div class="diff-limit-notice"><strong>Diff too large to render</strong><p>This commit's patch exceeds the 8 MB static-page limit. Clone the repository to inspect the complete change.</p></div> 145 <div class="diff-limit-notice"><strong>Diff too large to render</strong><p>This commit's patch exceeds the 8 MB static-page limit. Clone the repository to inspect the complete change.</p></div>
143 {{else if .ViewedCommit.Files}} 146 {{else if .ViewedCommit.Files}}
144 <div class="diff-layout"> 147 <div class="diff-layout">
145 <aside class="diff-sidebar"> 148 <aside class="file-browser diff-sidebar" aria-label="Changed files">
146 <h2>Files changed</h2> 149 <div class="file-browser-heading">{{icon "folder"}}<strong>Files changed</strong></div>
147 <nav class="diff-file-nav" aria-label="Changed files"> 150 <label class="file-tree-search">{{icon "search"}}<input type="search" placeholder="Search changed files" autocomplete="off" data-diff-file-tree-query></label>
148 {{range .ViewedCommit.Files}}<a href="#{{.Anchor}}"><span>{{.Status}}</span>{{.DisplayPath}}</a>{{end}} 151 <nav class="file-tree" data-diff-file-tree>
152 {{range .ViewedCommit.Files}}<a class="file-tree-file" href="#{{.Anchor}}" data-diff-file data-path="{{.TreePath}}">{{icon "file"}}<span>{{.DisplayPath}}</span></a>{{end}}
149 </nav> 153 </nav>
150 </aside> 154 </aside>
151 <div class="diff-files"> 155 <div class="diff-files">
@@ -260,6 +264,34 @@ const pageTemplate = `<!doctype html>
260 button.addEventListener('click', function () { copyText(button.getAttribute('data-copy'), button); }); 264 button.addEventListener('click', function () { copyText(button.getAttribute('data-copy'), button); });
261 }); 265 });
262 266
267 var relativeFormatter = window.Intl && Intl.RelativeTimeFormat ? new Intl.RelativeTimeFormat(undefined, {numeric: 'always'}) : null;
268 function updateRelativeTimes() {
269 if (!relativeFormatter) return;
270 var now = Date.now();
271 all('[data-relative-time], [data-smart-time]').forEach(function (node) {
272 var date = new Date(node.getAttribute('datetime'));
273 if (isNaN(date.getTime())) return;
274 if (!node.dataset.absoluteLabel) node.dataset.absoluteLabel = node.textContent;
275 var seconds = (date.getTime() - now) / 1000;
276 var absolute = Math.abs(seconds);
277 if (node.hasAttribute('data-smart-time') && absolute >= 604800) {
278 node.textContent = node.dataset.absoluteLabel;
279 return;
280 }
281 var divisor = 1;
282 var unit = 'second';
283 if (absolute >= 31536000) { divisor = 31536000; unit = 'year'; }
284 else if (absolute >= 2592000) { divisor = 2592000; unit = 'month'; }
285 else if (absolute >= 86400) { divisor = 86400; unit = 'day'; }
286 else if (absolute >= 3600) { divisor = 3600; unit = 'hour'; }
287 else if (absolute >= 60) { divisor = 60; unit = 'minute'; }
288 node.textContent = relativeFormatter.format(Math.round(seconds / divisor), unit);
289 if (!node.title) node.title = date.toLocaleString();
290 });
291 }
292 updateRelativeTimes();
293 if (relativeFormatter) window.setInterval(updateRelativeTimes, 60000);
294
263 function closeMenu(menu) { 295 function closeMenu(menu) {
264 if (!menu) return; 296 if (!menu) return;
265 var panel = one('[data-popover]', menu); 297 var panel = one('[data-popover]', menu);
@@ -399,8 +431,6 @@ const pageTemplate = `<!doctype html>
399 if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; } 431 if (event.key === 'Enter') { var first = one('.file-result', fileResults); if (first) window.location.href = first.href; }
400 }); 432 });
401 433
402 var fileTree = one('[data-file-tree]');
403 var fileTreeQuery = one('[data-file-tree-query]');
404 function addTreeIcon(target, selector) { 434 function addTreeIcon(target, selector) {
405 var source = one(selector); 435 var source = one(selector);
406 if (source) target.appendChild(source.content.cloneNode(true)); 436 if (source) target.appendChild(source.content.cloneNode(true));
@@ -414,13 +444,13 @@ const pageTemplate = `<!doctype html>
414 text.textContent = label; 444 text.textContent = label;
415 if (compact) text.title = file.path; 445 if (compact) text.title = file.path;
416 link.appendChild(text); 446 link.appendChild(text);
417 if (file.path === body.dataset.routePath) { 447 if (file.path === body.dataset.routePath || (file.url.charAt(0) === '#' && file.url === window.location.hash)) {
418 link.classList.add('selected'); 448 link.classList.add('selected');
419 link.setAttribute('aria-current', 'page'); 449 link.setAttribute('aria-current', 'page');
420 } 450 }
421 return link; 451 return link;
422 } 452 }
423 function renderTreeNode(node, container, prefix) { 453 function renderTreeNode(node, container, prefix, expandAll) {
424 Object.keys(node.folders).sort().forEach(function (name) { 454 Object.keys(node.folders).sort().forEach(function (name) {
425 var child = node.folders[name]; 455 var child = node.folders[name];
426 var childPath = prefix ? prefix + '/' + name : name; 456 var childPath = prefix ? prefix + '/' + name : name;
@@ -439,7 +469,7 @@ const pageTemplate = `<!doctype html>
439 toggle.appendChild(label); 469 toggle.appendChild(label);
440 var children = document.createElement('div'); 470 var children = document.createElement('div');
441 children.className = 'file-tree-children'; 471 children.className = 'file-tree-children';
442 var open = body.dataset.routePath === childPath || body.dataset.routePath.indexOf(childPath + '/') === 0; 472 var open = expandAll || body.dataset.routePath === childPath || body.dataset.routePath.indexOf(childPath + '/') === 0;
443 children.hidden = !open; 473 children.hidden = !open;
444 caret.classList.toggle('open', open); 474 caret.classList.toggle('open', open);
445 toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 475 toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
@@ -452,24 +482,24 @@ const pageTemplate = `<!doctype html>
452 wrapper.appendChild(toggle); 482 wrapper.appendChild(toggle);
453 wrapper.appendChild(children); 483 wrapper.appendChild(children);
454 container.appendChild(wrapper); 484 container.appendChild(wrapper);
455 renderTreeNode(child, children, childPath); 485 renderTreeNode(child, children, childPath, expandAll);
456 }); 486 });
457 node.files.sort(function (a, b) { return a.name.localeCompare(b.name); }).forEach(function (entry) { 487 node.files.sort(function (a, b) { return a.name.localeCompare(b.name); }).forEach(function (entry) {
458 container.appendChild(fileTreeLink(entry.file, entry.name, false)); 488 container.appendChild(fileTreeLink(entry.file, entry.name, false));
459 }); 489 });
460 } 490 }
461 function renderFileTree() { 491 function renderFileTree(target, queryInput, treeFiles, expandAll) {
462 if (!fileTree || !files) return; 492 if (!target || !treeFiles) return;
463 var query = fileTreeQuery.value.trim().toLowerCase(); 493 var query = queryInput ? queryInput.value.trim().toLowerCase() : '';
464 fileTree.textContent = ''; 494 target.textContent = '';
465 if (query) { 495 if (query) {
466 var matches = files.filter(function (file) { return file.path.toLowerCase().indexOf(query) !== -1; }); 496 var matches = treeFiles.filter(function (file) { return file.path.toLowerCase().indexOf(query) !== -1; });
467 matches.slice(0, 200).forEach(function (file) { fileTree.appendChild(fileTreeLink(file, file.path, true)); }); 497 matches.slice(0, 200).forEach(function (file) { target.appendChild(fileTreeLink(file, file.path, true)); });
468 if (!matches.length) fileTree.innerHTML = '<p class="menu-status">No matching files.</p>'; 498 if (!matches.length) target.innerHTML = '<p class="menu-status">No matching files.</p>';
469 return; 499 return;
470 } 500 }
471 var root = {folders: {}, files: []}; 501 var root = {folders: {}, files: []};
472 files.forEach(function (file) { 502 treeFiles.forEach(function (file) {
473 var parts = file.path.split('/'); 503 var parts = file.path.split('/');
474 var node = root; 504 var node = root;
475 parts.slice(0, -1).forEach(function (part) { 505 parts.slice(0, -1).forEach(function (part) {
@@ -478,13 +508,35 @@ const pageTemplate = `<!doctype html>
478 }); 508 });
479 node.files.push({name: parts[parts.length - 1], file: file}); 509 node.files.push({name: parts[parts.length - 1], file: file});
480 }); 510 });
481 renderTreeNode(root, fileTree, ''); 511 renderTreeNode(root, target, '', expandAll);
482 var selected = one('.file-tree-file.selected', fileTree); 512 var selected = one('.file-tree-file.selected', target);
483 if (selected) fileTree.scrollTop = Math.max(0, selected.offsetTop - (fileTree.clientHeight - selected.offsetHeight) / 2); 513 if (selected) target.scrollTop = Math.max(0, selected.offsetTop - (target.clientHeight - selected.offsetHeight) / 2);
484 } 514 }
515
516 var fileTree = one('[data-file-tree]');
517 var fileTreeQuery = one('[data-file-tree-query]');
485 if (fileTree) { 518 if (fileTree) {
486 loadFiles().then(renderFileTree).catch(function () { fileTree.innerHTML = '<p class="menu-status">Could not load files.</p>'; }); 519 loadFiles().then(function (loadedFiles) { renderFileTree(fileTree, fileTreeQuery, loadedFiles, false); })
487 fileTreeQuery.addEventListener('input', renderFileTree); 520 .catch(function () { fileTree.innerHTML = '<p class="menu-status">Could not load files.</p>'; });
521 fileTreeQuery.addEventListener('input', function () { renderFileTree(fileTree, fileTreeQuery, files, false); });
522 }
523
524 var diffFileTree = one('[data-diff-file-tree]');
525 var diffFileTreeQuery = one('[data-diff-file-tree-query]');
526 if (diffFileTree) {
527 var diffFiles = all('[data-diff-file]', diffFileTree).map(function (link) {
528 return {path: link.getAttribute('data-path'), url: link.getAttribute('href')};
529 });
530 renderFileTree(diffFileTree, diffFileTreeQuery, diffFiles, true);
531 diffFileTreeQuery.addEventListener('input', function () { renderFileTree(diffFileTree, diffFileTreeQuery, diffFiles, true); });
532 window.addEventListener('hashchange', function () {
533 all('.file-tree-file', diffFileTree).forEach(function (link) {
534 var selected = link.getAttribute('href') === window.location.hash;
535 link.classList.toggle('selected', selected);
536 if (selected) link.setAttribute('aria-current', 'page');
537 else link.removeAttribute('aria-current');
538 });
539 });
488 } 540 }
489 541
490 document.addEventListener('keydown', function (event) { 542 document.addEventListener('keydown', function (event) {
@@ -530,7 +582,7 @@ const pageTemplate = `<!doctype html>
530 <img class="avatar commit-avatar" src="{{.Commit.AvatarURL}}" alt="" referrerpolicy="no-referrer"> 582 <img class="avatar commit-avatar" src="{{.Commit.AvatarURL}}" alt="" referrerpolicy="no-referrer">
531 <div class="latest-commit"><strong>{{.Commit.Author}}</strong><a class="commit-message" href="{{.Commit.URL}}">{{.Commit.Message}}</a></div> 583 <div class="latest-commit"><strong>{{.Commit.Author}}</strong><a class="commit-message" href="{{.Commit.URL}}">{{.Commit.Message}}</a></div>
532 <a class="commit-hash" href="{{.Commit.URL}}">{{.Commit.ShortOID}}</a> 584 <a class="commit-hash" href="{{.Commit.URL}}">{{.Commit.ShortOID}}</a>
533 <time datetime="{{.Commit.DateTime}}">{{.Commit.Date}}</time> 585 <time datetime="{{.Commit.DateTime}}" data-smart-time>{{.Commit.Date}}</time>
534 <a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a> 586 <a class="commit-count-link" href="{{.CommitsURL}}">{{icon "history"}}<strong>{{.CommitCount}}</strong> commits</a>
535 </div> 587 </div>
536 <div class="tree-rows"> 588 <div class="tree-rows">
@@ -539,7 +591,7 @@ const pageTemplate = `<!doctype html>
539 {{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else if eq .Type "commit"}}<span class="file-icon">{{icon "repo"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}} 591 {{if eq .Type "tree"}}<span class="file-icon folder">{{icon "folder"}}</span>{{else if eq .Type "commit"}}<span class="file-icon">{{icon "repo"}}</span>{{else}}<span class="file-icon">{{icon "file"}}</span>{{end}}
540 <a class="file-name" href="{{.URL}}">{{.Name}}</a> 592 <a class="file-name" href="{{.URL}}">{{.Name}}</a>
541 <a class="row-message" href="{{.CommitURL}}" title="{{.CommitMessage}}">{{.CommitMessage}}</a> 593 <a class="row-message" href="{{.CommitURL}}" title="{{.CommitMessage}}">{{.CommitMessage}}</a>
542 <span class="file-size">{{humanSize .Size}}</span> 594 <time class="file-date" datetime="{{.CommitDateTime}}" data-relative-time>{{.CommitDate}}</time>
543 </div>{{end}} 595 </div>{{end}}
544 </div> 596 </div>
545 </div> 597 </div>