7b9d2a75

Archive
Tree [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
Add archive links, sort branches, and polish navigation
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">[&gt;]</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>[&gt;]</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>[&gt;]</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">[&gt;]</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 })~}