7b9d2a75
- commit
- 7b9d2a756dc5a6dfb54129bb79ceaa9b03812a4b
- parent
- author
- Christopher K. Schmitt <me@shmish.dev>
- date
- 2026-07-08
- committer
- Christopher K. Schmitt <me@shmish.dev>
- date
- 2026-07-08
- changes
- 9
- insertions
- 417
- deletions
- 418
Msrc/routes.rs
~~use crate::error::{Error, Result};~+/// Returns the commit's author timestamp as Unix seconds.+pub(super) fn commit_timestamp(commit: &gix::Commit) -> Option<i64> {+ commit.time().ok().map(|t| t.seconds)+}+~/// Formats a commit's author date as a `YYYY-MM-DD` string in the commit's~/// local timezone, returning `None` if the time cannot be parsed.~pub(super) fn commit_date(commit: &gix::Commit) -> Option<String> {
Mstatic/base.css
-*, *::before, *::after { box-sizing: border-box; }--footer {- padding: 1rem;- margin-top: 2rem;- color: var(--secondary);- font-size: .875rem;- text-align: center;+@layer reset, layout, components, accents;++@layer reset {+ *, *::before, *::after { box-sizing: border-box; }+ html { overflow-x: clip; }+}++@layer layout {+ body {+ font-family: var(--font-body);+ max-width: 860px;+ margin: 0 auto;+ padding: 0;+ background: var(--background);+ color: var(--primary);+ }++ header {+ padding: .75rem 1rem;+ margin-bottom: .75rem;+ }++ header ul {+ list-style: none;+ margin: 0;+ padding: 0;+ display: flex;+ align-items: center;+ gap: .25rem;+ font-size: 1.2rem;+ min-width: 0;+ }++ header li {+ white-space: nowrap;+ overflow: hidden;+ text-overflow: ellipsis;+ min-width: 0;+ }++ header li:first-child { flex-shrink: 0; }+ header li:last-child { flex-shrink: 1; }++ header li + li::before {+ content: '/';+ margin-right: .25rem;+ color: var(--secondary);+ }++ header a {+ color: var(--primary);+ text-decoration: none;+ }++ header a:hover {+ color: var(--emphasized);+ text-decoration: underline;+ text-underline-offset: 3px;+ }++ main > h1, main > p { padding-inline: 1rem; }+ main > h1 { margin-bottom: .75rem; }+ main > p { margin-top: 0; }+ main > article { padding: 0 1rem; }++ footer {+ padding: 1rem;+ margin-top: 2rem;+ color: var(--secondary);+ font-size: .875rem;+ text-align: center;+ }+}++@layer components {+ /* Repo list */+ .repo-list { display: flex; flex-direction: column; }++ .repo-card {+ display: flex;+ flex-direction: column;+ gap: .5rem;+ padding: 1rem 1.25rem;+ text-decoration: none;+ color: inherit;+ }++ .repo-card:hover { background: var(--highlight); }+ .repo-name { font-size: 1.1rem; font-weight: 600; color: var(--emphasized); }+ .repo-desc { color: var(--primary); }+ .repo-meta { display: flex; justify-content: space-between; color: var(--secondary); }++ /* Repo nav items */+ .repo-nav-item {+ display: flex;+ align-items: center;+ gap: 1rem;+ padding: 0 1rem;+ min-height: 50px;+ text-decoration: none;+ color: var(--primary);+ font-family: var(--font-mono);+ border-top: 1px solid var(--secondary);+ transition: background-color .15s ease-out;+ }++ .repo-nav-item + .repo-nav-item { border-top: 1px solid var(--highlight); }+ .repo-nav-item:last-child { border-bottom: 1px solid var(--secondary); }+ .repo-nav-item:active { background-color: var(--highlight); transition: none; }+ .repo-nav-item:hover > span:first-of-type { text-decoration: underline; text-underline-offset: 3px; }+ .repo-nav-item-meta { margin-left: auto; color: var(--secondary); }+ .repo-nav-item-arrow { color: var(--secondary); }++ /* Tag info */+ .tag-info {+ display: grid;+ grid-template-columns: max-content 1fr;+ gap: .2rem 1.5rem;+ font-family: var(--font-mono);+ margin: 1.5rem 1rem 1.5rem;+ }++ .tag-info dt { color: var(--secondary); }+ .tag-info dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0; }+ .tag-info a { color: var(--primary); text-decoration: none; }+ .tag-info a:hover { text-decoration: underline; text-underline-offset: 3px; }++ .tag-message {+ font-family: var(--font-mono);+ padding: 1rem;+ margin: 0 1rem 1rem;+ background: var(--highlight);+ border-radius: 6px;+ white-space: pre-wrap;+ color: var(--primary);+ }++ /* Branch / tag lists */+ .branch-list, .tag-list { display: flex; flex-direction: column; }++ .branch-row, .tag-row {+ display: flex;+ justify-content: space-between;+ align-items: baseline;+ padding: .75rem 1rem;+ text-decoration: none;+ color: inherit;+ font-family: var(--font-mono);+ }++ .branch-row:hover, .tag-row:hover { background: var(--highlight); }++ .branch-name, .tag-name {+ overflow: hidden;+ text-overflow: ellipsis;+ white-space: nowrap;+ min-width: 0;+ }++ .branch-default { color: var(--secondary); margin-right: 1rem; }++ .branch-date, .tag-date {+ display: flex;+ align-items: baseline;+ flex-shrink: 0;+ color: var(--secondary);+ padding-left: 1rem;+ }++ /* Commit log */+ .commit-log { display: flex; flex-direction: column; }++ .commit-row {+ display: grid;+ grid-template-columns: 5rem 1fr;+ grid-template-rows: auto auto;+ gap: .15rem 1rem;+ padding: .75rem 1rem;+ text-decoration: none;+ color: inherit;+ font-family: var(--font-mono);+ }++ .commit-row:hover { background: var(--highlight); }+ .commit-sha { color: var(--secondary); grid-row: 1; }++ .commit-message {+ color: var(--primary);+ grid-row: 1;+ overflow: hidden;+ text-overflow: ellipsis;+ white-space: nowrap;+ }++ .commit-meta {+ display: flex;+ justify-content: space-between;+ color: var(--secondary);+ grid-column: 2;+ grid-row: 2;+ min-width: 0;+ }++ .commit-meta span:first-child {+ overflow: hidden;+ text-overflow: ellipsis;+ white-space: nowrap;+ min-width: 0;+ }++ .commit-meta span:last-child { flex-shrink: 0; padding-left: 1rem; }++ /* Pagination */+ .pagination { display: flex; justify-content: space-between; padding: .75rem 1rem; }+ .pagination a { color: var(--secondary); text-decoration: none; }+ .pagination a:hover { color: var(--primary); }++ /* Details / summary */+ details { border-top: 1px solid var(--highlight); }+ details:first-of-type { border-top: 1px solid var(--secondary); }+ details:last-of-type { border-bottom: 1px solid var(--highlight); }++ summary {+ cursor: pointer;+ display: flex;+ align-items: center;+ gap: 1rem;+ padding: 0 1rem;+ min-height: 50px;+ font-family: var(--font-mono);+ background: var(--highlight);+ }++ .summary-icon { width: 1rem; height: 1rem; flex-shrink: 0; }++ details[open] summary { color: var(--emphasized); }+ summary::after { content: '[+]'; color: var(--secondary); }+ summary:not(:has(span))::after { margin-left: auto; }+ details[open] summary::after { content: '[−]'; }+ summary span { color: var(--secondary); margin-left: auto; }++ details > *:not(summary) { padding: 0 1rem .75rem; margin-bottom: 0; }+ details > ul { list-style: none; padding: 0; margin: 0; }+ details ul li + li { border-top: 1px solid var(--highlight); }++ details ul li {+ display: flex;+ align-items: center;+ gap: 1rem;+ min-height: 50px;+ padding: 0 1rem;+ color: var(--secondary);+ font-family: var(--font-mono);+ }++ details ul li:has(a),+ details ul li:has(button) { padding: 0; }++ details ul li a,+ details ul li button {+ display: flex;+ align-items: center;+ gap: 1rem;+ padding: 0 1rem;+ min-height: 50px;+ width: 100%;+ text-align: left;+ text-decoration: none;+ color: inherit;+ background: transparent;+ border: none;+ cursor: pointer;+ font-family: var(--font-mono);+ font-size: 1rem;+ transition: background-color .15s ease-out;+ }++ details ul li a:active,+ details ul li button:active { background-color: var(--highlight); transition: none; }++ details ul li a:hover span:first-child,+ details ul li button:hover span:first-child { text-decoration: underline; text-underline-offset: 3px; }++ /* First span grows; truncates on overflow */+ details ul li a span:first-child,+ details ul li button span:first-child {+ flex: 1;+ min-width: 0;+ overflow: hidden;+ text-overflow: ellipsis;+ white-space: nowrap;+ color: var(--primary);+ }++ /* Clone rows: URL animates on copy */+ details ul li button span:first-child { transition: opacity .1s; }+ details ul li button span:nth-child(2) {+ flex-shrink: 0;+ min-width: 4rem;+ text-align: right;+ color: var(--secondary);+ }++ /* Branch/tag rows: remaining spans fixed right */+ details ul li a span:not(:first-child) { flex-shrink: 0; color: var(--secondary); }+}++@layer accents {+ .branch-name { color: var(--blue); }+ .tag-name { color: var(--violet); }+ .archive-link { color: var(--orange); }+ .commit-sha { color: var(--cyan); }~}--html { overflow-x: clip; }--body {- font-family: var(--font-body);- max-width: 860px;- margin: 0 auto;- padding: 0;- background: var(--background);- color: var(--primary);-}--header {- padding: .75rem 1rem;- margin-bottom: .75rem;-}--header ul {- list-style: none;- margin: 0;- padding: 0;- display: flex;- align-items: center;- gap: .25rem;- font-size: 1.2rem;- min-width: 0;-}--header li {- white-space: nowrap;- overflow: hidden;- text-overflow: ellipsis;- min-width: 0;-}--header li:first-child { flex-shrink: 0; }-header li:last-child { flex-shrink: 1; }--header li + li::before {- content: '/';- margin-right: .25rem;- color: var(--secondary);-}--header a {- color: var(--primary);- text-decoration: none;-}--header a:hover {- color: var(--emphasized);- text-decoration: underline;- text-underline-offset: 4px;-}--main > h1, main > p {- padding-left: 1rem;- padding-right: 1rem;-}--main > h1 { margin-bottom: .75rem; }-main > p { margin-top: 0; }--.repo-nav-item {- display: flex;- align-items: center;- gap: 1rem;- padding: 0 1rem;- min-height: 50px;- text-decoration: none;- color: var(--primary);- font-family: var(--font-mono);- border-top: 1px solid var(--secondary);- transition: background-color .3s ease-out;-}--.repo-nav-item + .repo-nav-item { border-top: 1px solid var(--highlight); }-.repo-nav-item:last-child { border-bottom: 1px solid var(--secondary); }-.repo-nav-item:active { background-color: var(--highlight); transition: none; }-.repo-nav-item:hover > span:first-of-type { text-decoration: underline; text-underline-offset: 3px; }--.repo-nav-item-meta {- margin-left: auto;- color: var(--secondary);-}--.repo-nav-item-arrow {- color: var(--secondary);-}-main > article { padding: 0 1rem; }--.repo-list {- display: flex;- flex-direction: column;-}--.repo-card {- display: flex;- flex-direction: column;- gap: .5rem;- padding: 1rem 1.25rem;- text-decoration: none;- color: inherit;-}--.repo-card:hover { background: var(--highlight); }--.repo-name {- font-size: 1.1rem;- font-weight: 600;- color: var(--emphasized);-}--.repo-desc {- font-size: 1rem;- color: var(--primary);-}--.repo-meta {- display: flex;- justify-content: space-between;- color: var(--secondary);-}--.tag-info {- display: grid;- grid-template-columns: max-content 1fr;- gap: .2rem 1.5rem;- font-family: var(--font-mono);- margin: 0 1rem 1rem;-}--.tag-info dt { color: var(--secondary); }-.tag-info dd { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0; }-.tag-info a { color: var(--primary); }--.tag-downloads {- font-family: var(--font-mono);- padding: 0 1rem;-}--.tag-message {- font-family: var(--font-mono);- font-size: 1rem;- padding: 1rem;- margin: 0 1rem 1rem;- background: var(--highlight);- border-radius: 6px;- white-space: pre-wrap;- color: var(--primary);-}--.branch-list, .tag-list { display: flex; flex-direction: column; }--.branch-row, .tag-row {- display: flex;- justify-content: space-between;- align-items: baseline;- padding: .75rem 1rem;- text-decoration: none;- color: inherit;- font-family: var(--font-mono);-}--.branch-row:hover, .tag-row:hover { background: var(--highlight); }--.branch-name,-.tag-name {- overflow: hidden;- text-overflow: ellipsis;- white-space: nowrap;- min-width: 0;-}---.branch-default {- color: var(--secondary);- margin-right: 1rem;-}--.branch-date, .tag-date {- display: flex;- align-items: baseline;- flex-shrink: 0;- color: var(--secondary);- padding-left: 1rem;-}--.commit-log {- display: flex;- flex-direction: column;-}--.commit-row {- display: grid;- grid-template-columns: 5rem 1fr;- grid-template-rows: auto auto;- gap: .15rem 1rem;- padding: .75rem 1rem;- text-decoration: none;- color: inherit;- font-family: var(--font-mono);-}--.commit-row:hover { background: var(--highlight); }--.commit-sha {- font-family: var(--font-mono);- color: var(--secondary);- grid-row: 1;-}--.commit-message {- color: var(--primary);- grid-row: 1;- overflow: hidden;- text-overflow: ellipsis;- white-space: nowrap;-}--.commit-meta {- display: flex;- justify-content: space-between;- color: var(--secondary);- grid-column: 2;- grid-row: 2;- min-width: 0;-}--.commit-meta span:first-child {- overflow: hidden;- text-overflow: ellipsis;- white-space: nowrap;- min-width: 0;-}--.commit-meta span:last-child {- flex-shrink: 0;- padding-left: 1rem;-}--.pagination {- display: flex;- justify-content: space-between;- padding: .75rem 1rem;-}--.pagination a { color: var(--secondary); text-decoration: none; }-.pagination a:hover { color: var(--primary); }--details {- border-top: 1px solid var(--highlight);-}--details:first-of-type {- border-top: 1px solid var(--secondary);-}--details:last-of-type {- border-bottom: 1px solid var(--highlight);-}--summary {- cursor: pointer;- display: flex;- align-items: center;- gap: 1rem;- padding: 0 1rem;- min-height: 50px;- font-family: var(--font-mono);- background: var(--highlight);-}--.summary-icon {- width: 1rem;- height: 1rem;- flex-shrink: 0;-}---details[open] summary {- color: var(--emphasized);-}--summary::after {- content: '[+]';- color: var(--secondary);-}--summary:not(:has(span))::after {- margin-left: auto;-}--details[open] summary::after {- content: '[−]';-}---summary span {- color: var(--secondary);- margin-left: auto;-}--details > *:not(summary) {- padding: 0 1rem .75rem;- margin-bottom: 0;-}--details > ul {- list-style: none;- padding: 0;- margin: 0;-}--details ul li + li {- border-top: 1px solid var(--highlight);-}--details ul li {- display: flex;- align-items: center;- gap: 1rem;- min-height: 50px;- padding: 0 1rem;- color: var(--secondary);- font-family: var(--font-mono);- font-size: 1rem;-}--details ul li:has(a), details ul li:has(button) {- padding: 0;-}--details ul li a,-details ul li button {- display: flex;- align-items: center;- gap: 1rem;- padding: 0 1rem;- min-height: 50px;- width: 100%;- text-align: left;- text-decoration: none;- color: inherit;- background: transparent;- border: none;- cursor: pointer;- font-family: var(--font-mono);- font-size: 1rem;-}--details ul li a,-details ul li button { transition: background-color .3s ease-out; }--details ul li a:active,-details ul li button:active { background-color: var(--highlight); transition: none; }--details ul li a:hover span:first-child,-details ul li button:hover span:first-child { text-decoration: underline; text-underline-offset: 3px; }--/* First span grows to fill available space in both link and button rows */-details ul li a span:first-child,-details ul li button span:first-child {- flex: 1;- min-width: 0;- overflow: hidden;- text-overflow: ellipsis;- white-space: nowrap;- color: var(--primary);-}--/* Clone rows: animate URL text on copy, label fixed width on right */-details ul li button span:first-child { transition: opacity .1s; }-details ul li button span:nth-child(2) {- flex-shrink: 0;- min-width: 4rem;- text-align: right;- color: var(--secondary);-}--/* Branch/tag rows: remaining spans fixed on the right */-details ul li a span:not(:first-child) {- flex-shrink: 0;- color: var(--secondary);-}--/* Accent colors — specificity matches details ul li a span:first-child */-details ul li a span.branch-name,-.branch-name { color: var(--blue); }--details ul li a span.tag-name,-.tag-name { color: var(--violet); }
Mtemplates/branch_detail.html
~{% block content %}~<h1>{{ branch }}</h1>~+<details>+ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>+ <ul>+ <li><a href="/{{ repo }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.gz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="/{{ repo }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.tar.xz"><span class="archive-link">{{ repo }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="/{{ repo }}/archive/zip/refs/heads/{{ branch }}" download="{{ repo }}-{{ branch }}.zip"><span class="archive-link">{{ repo }}-{{ branch }}.zip</span><span>zip</span></a></li>+ </ul>+</details>++<div class="repo-nav">+ <a href="/{{ repo }}/commits/{{ tip_sha }}/tree" class="repo-nav-item">+ <svg class="summary-icon"><use href="#icon-tree"/></svg>+ <span>Tree</span>+ <span class="repo-nav-item-meta">[{{ tip_sha[..8] }}]</span>+ <span class="repo-nav-item-arrow">[>]</span>+ </a>+</div>+~{% if commits.is_empty() %}~<p>No commits.</p>~{% else %}
Mtemplates/repo.html
~ </ul>~</details>~+{% if let Some(branch) = default_branch %}~<details>+ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>+ <ul>+ <li><a href="/{{ name }}/archive/tar.gz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.gz"><span class="archive-link">{{ name }}-{{ branch }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="/{{ name }}/archive/tar.xz/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.tar.xz"><span class="archive-link">{{ name }}-{{ branch }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="/{{ name }}/archive/zip/refs/heads/{{ branch }}" download="{{ name }}-{{ branch }}.zip"><span class="archive-link">{{ name }}-{{ branch }}.zip</span><span>zip</span></a></li>+ </ul>+</details>+{% endif %}++<details>~ <summary><svg class="summary-icon"><use href="#icon-branch"/></svg>Branches <span>[{{ branches.len() }}]</span></summary>~ <ul>~ {% for branch in branches.iter().take(5) %}~ <li><a href="/{{ name }}/branches/{{ branch.name }}"><span class="branch-name">{{ branch.name }}</span>{% if branch.is_default %}<span>default</span>{% endif %}<span>{{ branch.date }}</span></a></li>~ {% endfor %}~ {% if branches.len() > 5 %}- <li><a href="/{{ name }}/branches">see all {{ branches.len() }} branches</a></li>+ <li><a href="/{{ name }}/branches" class="see-all"><span>see all {{ branches.len() }} branches</span><span>[>]</span></a></li>~ {% endif %}~ {% if branches.is_empty() %}~ <li>no branches</li>
~ <li><a href="/{{ name }}/tags/{{ tag.name }}"><span class="tag-name">{{ tag.name }}</span><span>{{ tag.date }}</span></a></li>~ {% endfor %}~ {% if tags.len() > 5 %}- <li><a href="/{{ name }}/tags">see all {{ tags.len() }} tags</a></li>+ <li><a href="/{{ name }}/tags" class="see-all"><span>see all {{ tags.len() }} tags</span><span>[>]</span></a></li>~ {% endif %}~ {% if tags.is_empty() %}~ <li>no tags</li>
Mtemplates/sprites.svg
~ <path d="M3 5a2 2 0 0 0 2 2h3"/>~ <path d="M3 3v13a2 2 0 0 0 2 2h3"/>~ </symbol>+ <symbol id="icon-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">+ <rect width="20" height="5" x="2" y="3" rx="1"/>+ <path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/>+ <path d="M10 12h4"/>+ </symbol>~ <symbol id="icon-commits" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">~ <circle cx="12" cy="12" r="3"/>~ <line x1="3" x2="9" y1="12" y2="12"/>
Mtemplates/tag_detail.html
~{% block content %}~<h1>{{ tag }}</h1>~+<details>+ <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>+ <ul>+ <li><a href="/{{ repo }}/archive/tar.gz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.gz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.gz</span><span>tar.gz</span></a></li>+ <li><a href="/{{ repo }}/archive/tar.xz/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.tar.xz"><span class="archive-link">{{ repo }}-{{ tag }}.tar.xz</span><span>tar.xz</span></a></li>+ <li><a href="/{{ repo }}/archive/zip/refs/tags/{{ tag }}" download="{{ repo }}-{{ tag }}.zip"><span class="archive-link">{{ repo }}-{{ tag }}.zip</span><span>zip</span></a></li>+ </ul>+</details>++<div class="repo-nav">+ <a href="/{{ repo }}/commits/{{ commit_sha }}/tree" class="repo-nav-item">+ <svg class="summary-icon"><use href="#icon-tree"/></svg>+ <span>Tree</span>+ <span class="repo-nav-item-meta">[{{ commit_sha[..8] }}]</span>+ <span class="repo-nav-item-arrow">[>]</span>+ </a>+</div>+~{% if tagger.is_some() || tag_date.is_some() %}~<dl class="tag-info">~ {% if let Some(tagger) = tagger %}
~{% endif %}~~<dl class="tag-info">- <dt>commit</dt><dd><a href="/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>+ <dt>commit</dt><dd><a class="commit-sha" href="/{{ repo }}/commits/{{ commit_sha }}">{{ commit_sha }}</a></dd>~ <dt>author</dt><dd>{{ commit_author }}</dd>~ <dt>date</dt><dd>{{ commit_date }}</dd>~</dl>--<p class="tag-downloads">- [<a href="/{{ repo }}/archive/tar.gz/{{ commit_sha }}">tar.gz</a>]- [<a href="/{{ repo }}/archive/tar.xz/{{ commit_sha }}">tar.xz</a>]- [<a href="/{{ repo }}/archive/zip/{{ commit_sha }}">zip</a>]-</p>~~{% if let Some(message) = message %}~<pre class="tag-message">{{ message }}</pre>
Msrc/routes/branch.rs
~use serde::Deserialize;~~use crate::error::{Error, Result};-use crate::routes::{AppState, RepoName, commit_date};+use crate::routes::{AppState, RepoName, commit_date, commit_timestamp};~~const PAGE_SIZE: usize = 100;~
~struct BranchSummary {~ name: String,~ date: String,+ timestamp: i64,~ is_default: bool,~}~
~struct BranchDetail {~ repo: String,~ branch: String,+ tip_sha: String,~ commits: Vec<CommitSummary>,~ page: usize,~ has_prev: bool,
~ // `.local_branches()` returns a `Result<impl Iterator<Item = Result<Reference>>>`.~ // The first `.flatten()` unwraps the outer `Result`, the second unwraps~ // each `Result<Reference>`, silently discarding any errors.- let branches = git_repo+ let mut branches: Vec<BranchSummary> = git_repo~ .references()~ .map_err(|e| Error::GitCorrupt(e.to_string()))?~ .local_branches()
~ .filter_map(|mut branch| {~ let commit = branch.peel_to_commit().ok()?;~ let date = commit_date(&commit)?;+ let timestamp = commit_timestamp(&commit)?;~ let name = branch.name().shorten().to_string();~ let is_default = head_name~ .as_ref()
~ Some(BranchSummary {~ name,~ date,+ timestamp,~ is_default,~ })~ })~ .collect();~+ branches.sort_by(|a, b| {+ b.is_default+ .cmp(&a.is_default)+ .then(b.timestamp.cmp(&a.timestamp))+ });+~ Ok(BranchList { repo, branches })~}~
~ .map_err(|_| Error::NotFound(format!("branch {branch}")))?~ .peel_to_commit()~ .map_err(|e| Error::GitCorrupt(e.to_string()))?;++ let tip_sha = tip.id().to_string();~~ // Fetch one extra commit to determine if a next page exists.~ let mut commits: Vec<CommitSummary> = tip
~ Ok(BranchDetail {~ repo,~ branch,+ tip_sha,~ commits,~ page,~ has_prev: page > 0,
Msrc/routes/repo.rs
~use axum::response::IntoResponse;~~use crate::error::{Error, Result};-use crate::routes::{AppState, RepoName, commit_date};+use crate::routes::{AppState, RepoName, commit_date, commit_timestamp};~~/// Reads the repository description from `.git/description`, returning `None`~/// if the file is missing, empty, or contains the default git placeholder text.
~struct BranchSummary {~ name: String,~ date: String,+ timestamp: i64,~ /// Whether this branch is the repository's HEAD.~ is_default: bool,~}
~struct TagSummary {~ name: String,~ date: String,+ timestamp: i64,~}~~/// Template context for the repository detail page.
~ tags: Vec<TagSummary>,~ commit_count: usize,~ head_sha: Option<String>,+ default_branch: Option<String>,~ /// README rendered to HTML, or `None` if no README was found.~ readme: Option<String>,~}
~ // `.tags()` returns a `Result<impl Iterator<Item = Result<Reference>>>`.~ // The first `.flatten()` unwraps the outer `Result`, the second unwraps~ // each `Result<Reference>`, silently discarding any errors.- let tags: Vec<TagSummary> = platform+ let mut tags: Vec<TagSummary> = platform~ .tags()~ .into_iter()~ .flatten()
~ .filter_map(|mut tag| {~ let commit = tag.peel_to_commit().ok()?;~ let date = commit_date(&commit)?;+ let timestamp = commit_timestamp(&commit)?;~ let name = tag.name().shorten().to_string();~- Some(TagSummary { name, date })+ Some(TagSummary {+ name,+ date,+ timestamp,+ })~ })~ .collect();~+ tags.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));+~ // Same double-flatten pattern as tags above.- let branches: Vec<BranchSummary> = platform+ let mut branches: Vec<BranchSummary> = platform~ .local_branches()~ .into_iter()~ .flatten()
~ .filter_map(|mut branch| {~ let commit = branch.peel_to_commit().ok()?;~ let date = commit_date(&commit)?;+ let timestamp = commit_timestamp(&commit)?;~ let name = branch.name().shorten().to_string();~ let is_default = head_name~ .as_ref()
~ Some(BranchSummary {~ name,~ date,+ timestamp,~ is_default,~ })~ })~ .collect();~+ branches.sort_by(|a, b| {+ b.is_default+ .cmp(&a.is_default)+ .then(b.timestamp.cmp(&a.timestamp))+ });+~ let description = repo_description(&git_repo);~ let head_id = git_repo.head_id().ok();~ let commit_count = head_id
~~ Some(rendered)~ });++ let default_branch = head_name.map(|h| h.shorten().to_string());~~ Ok(Repo {~ name,
~ tags,~ commit_count,~ head_sha,+ default_branch,~ readme,~ })~}
Msrc/routes/tag.rs
~use serde::Deserialize;~~use crate::error::{Error, Result};-use crate::routes::{AppState, RepoName, commit_date};+use crate::routes::{AppState, RepoName, commit_date, commit_timestamp};~~#[derive(Deserialize)]~pub(super) struct TagPath {
~struct TagSummary {~ name: String,~ date: String,+ timestamp: i64,~}~~/// Template context for the tag list page.
~ .filter_map(|mut tag| {~ let commit = tag.peel_to_commit().ok()?;~ let date = commit_date(&commit)?;+ let timestamp = commit_timestamp(&commit)?;~ let name = tag.name().shorten().to_string();~- Some(TagSummary { name, date })+ Some(TagSummary { name, date, timestamp })~ })~ .collect();~- tags.sort_by(|a, b| b.date.cmp(&a.date));+ tags.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));~~ Ok(TagList { repo, tags })~}