e9a538e7

Archive
Tree [e9a538e7] [>]
refs
commit
e9a538e7599fcfe0f2ae9db7aa073210a51e327f
parent
author
Christopher K. Schmitt <me@shmish.dev>
date
2026-08-11
committer
Christopher K. Schmitt <me@shmish.dev>
date
2026-08-11
changes
14
insertions
74
deletions
32
Fix short SHA panic in tree view
Msrc/filters.rs
~    })~}~+/// Returns the first 8 characters of `s`, or the whole string if shorter.+///+/// Unlike a raw `s[..8]` slice, this never panics on short input, so it is+/// safe to apply to SHAs and refs taken directly from request paths.+fn short_chars(s: &str) -> String {+    s.chars().take(8).collect()+}++/// Askama filter `{{ value|short }}`: the first 8 characters of the input.+#[askama::filter_fn]+pub fn short<T: std::fmt::Display>(s: T, _: &dyn askama::Values) -> askama::Result<String> {+    Ok(short_chars(&s.to_string()))+}+~#[cfg(test)]~mod tests {~    use super::*;
~    #[test]~    fn pluralize_multiple() {~        assert_eq!(pluralize(&3, "item", "items").unwrap(), "items");+    }++    #[test]+    fn short_full() {+        assert_eq!(short_chars("0123456789abcdef"), "01234567");+    }++    #[test]+    fn short_abbreviated() {+        assert_eq!(short_chars("863a47b"), "863a47b");+    }++    #[test]+    fn short_branch_name() {+        assert_eq!(short_chars("main"), "main");+    }++    #[test]+    fn short_empty() {+        assert_eq!(short_chars(""), "");~    }~}
Mtemplates/blame.html
~<ul>~  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha|short }}</a></li>~  {% for crumb in breadcrumbs %}~  <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~  {% endfor %}
Mtemplates/branch_detail.html
~  <a href="{{ site.base_url }}/{{ 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-meta">[{{ tip_sha|short }}]</span>~    <span class="repo-nav-item-arrow">[&gt;]</span>~  </a>~</div>
~<div class="commit-log">~  {% for commit in commits %}~  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-row">-    <span class="commit-sha">{{ commit.sha[..8] }}</span>+    <span class="commit-sha">{{ commit.sha|short }}</span>~    <span class="commit-message">{{ commit.message }}</span>~    <span class="commit-meta"><span>{{ commit.author }}</span><span>{{ commit.date }}</span></span>~  </a>
Mtemplates/commit.html
~{% extends "base.html" %}~-{% block title %}{{ sha[..8] }} - {{ repo }}{% endblock %}-{% block og_title %}{{ sha[..8] }} - {{ repo }}{% endblock %}+{% block title %}{{ sha|short }} - {{ repo }}{% endblock %}+{% block og_title %}{{ sha|short }} - {{ repo }}{% endblock %}~{% block og_description %}{{ message }}{% endblock %}~~{% block breadcrumbs %}
~  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>~  <li><a href="{{ site.base_url }}/{{ repo }}/commits">commits</a></li>-  <li>{{ sha[..8] }}</li>+  <li>{{ sha|short }}</li>~</ul>~{% endblock %}~~{% block content %}-<h1>{{ sha[..8] }}</h1>+<h1>{{ sha|short }}</h1>~~<details>~  <summary><svg class="summary-icon"><use href="#icon-archive"/></svg>Archive</summary>~  <ul>-    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.gz</span><span>tar.gz</span></a></li>-    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.tar.xz</span><span>tar.xz</span></a></li>-    <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha[..8] }}.zip"><span class="archive-link">{{ repo }}-{{ sha[..8] }}.zip</span><span>zip</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.gz/{{ sha }}" download="{{ repo }}-{{ sha|short }}.tar.gz"><span class="archive-link">{{ repo }}-{{ sha|short }}.tar.gz</span><span>tar.gz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/tar.xz/{{ sha }}" download="{{ repo }}-{{ sha|short }}.tar.xz"><span class="archive-link">{{ repo }}-{{ sha|short }}.tar.xz</span><span>tar.xz</span></a></li>+    <li><a href="{{ site.base_url }}/{{ repo }}/archive/zip/{{ sha }}" download="{{ repo }}-{{ sha|short }}.zip"><span class="archive-link">{{ repo }}-{{ sha|short }}.zip</span><span>zip</span></a></li>~  </ul>~</details>~
~  <a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>-    <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>+    <span class="repo-nav-item-meta">[{{ sha|short }}]</span>~    <span class="repo-nav-item-arrow">[&gt;]</span>~  </a>~</div>
Mtemplates/commit_list.html
~    {% for commit in commits %}~    <a href="{{ site.base_url }}/{{ repo }}/commits/{{ commit.sha }}" class="commit-graph-row" style="border-left: 3px solid {{ commit.color }}">~      <span class="commit-message">{% for r in commit.refs %}<span class="commit-ref commit-ref-{{ r.kind }}">{{ r.name }}</span> {% endfor %}{{ commit.message }}</span>-      <span class="commit-sha">{{ commit.sha[..8] }}</span>+      <span class="commit-sha">{{ commit.sha|short }}</span>~      <span class="commit-date">{{ commit.date }}</span>~    </a>~    {% endfor %}
Mtemplates/repo.html
~  <a href="{{ site.base_url }}/{{ name }}/commits/{{ sha }}/tree" class="repo-nav-item">~    <svg class="summary-icon"><use href="#icon-tree"/></svg>~    <span>Tree</span>-    <span class="repo-nav-item-meta">[{{ sha[..8] }}]</span>+    <span class="repo-nav-item-meta">[{{ sha|short }}]</span>~    <span class="repo-nav-item-arrow">[&gt;]</span>~  </a>~  {% endif %}
Mtemplates/tag_detail.html
~  <a href="{{ site.base_url }}/{{ 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-meta">[{{ commit_sha|short }}]</span>~    <span class="repo-nav-item-arrow">[&gt;]</span>~  </a>~</div>
Mtemplates/tree.html
~<ul>~  <li><a href="{{ site.base_url }}/">{{ site.site_name }}</a></li>~  <li><a href="{{ site.base_url }}/{{ repo }}">{{ repo }}</a></li>-  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha[..8] }}</a></li>+  <li><a href="{{ site.base_url }}/{{ repo }}/commits/{{ sha }}">{{ sha|short }}</a></li>~  {% for crumb in breadcrumbs %}~  <li>{% if loop.last %}{{ crumb.label }}{% else %}<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% endif %}</li>~  {% endfor %}
Msrc/routes/blame.rs
~~use crate::config::SiteConfig;~use crate::error::Result;+use crate::filters;~use crate::git;~use crate::routes::{AppState, Breadcrumb, RepoName};~
Msrc/routes/branch.rs
~~use crate::config::SiteConfig;~use crate::error::Result;+use crate::filters;~use crate::git;~use crate::routes::{AppState, RepoName};~
Msrc/routes/commit.rs
~~use crate::config::SiteConfig;~use crate::error::{Error, Result};+use crate::filters;~use crate::git::{self, GitResultExt as _, commit_date, committer_date};~use crate::routes::{AppState, RepoName};~
Msrc/routes/repo.rs
~~use crate::config::SiteConfig;~use crate::error::Result;+use crate::filters;~use crate::git::{self, commit_date};~use crate::routes::{AppState, RepoName};~
Msrc/routes/tag.rs
~~use crate::config::SiteConfig;~use crate::error::Result;+use crate::filters;~use crate::git;~use crate::routes::{AppState, RepoName};~
Msrc/routes/tree.rs
~~use crate::config::SiteConfig;~use crate::error::{Error, Result};+use crate::filters;~use crate::git::{self, GitResultExt as _};~use crate::routes::{AppState, Breadcrumb, RepoName};~
~~    let git_repo = git::open_repo(&state.root, &name)?;~-    let (object, resolved_path) = if path.is_empty() {+    let (object, resolved_path, sha) = if path.is_empty() {~        // When path is empty, resolve the sha to a commit and get its tree.~        let oid = gix::ObjectId::from_hex(params.sha.as_bytes())~            .map_err(|_| Error::BadRequest(format!("invalid sha: {}", params.sha)))?;+        let sha = oid.to_string();~        let commit = git_repo~            .find_object(oid)~            .map_err(|_| Error::NotFound(params.sha.clone()))?
~            .map_err(|_| Error::BadRequest(format!("{} is not a commit", params.sha)))?;~        let tree_id = commit.tree_id().corrupt()?;~        let tree = git_repo.find_object(tree_id).corrupt()?;-        (tree, String::new())+        (tree, String::new(), sha)~    } else {-        let spec = format!("{}:{}", params.sha, path);+        let oid = git_repo+            .rev_parse_single(params.sha.as_str())+            .map_err(|_| Error::NotFound(params.sha.clone()))?;+        let sha = oid.to_string();+        let spec = format!("{sha}:{path}");~        let obj = git_repo~            .rev_parse_single(spec.as_str())~            .map_err(|_| Error::NotFound(spec.clone()))?~            .object()~            .corrupt()?;-        (obj, path)+        (obj, path, sha)~    };~~    let b = &state.site.base_url;-    let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, &params.sha, &resolved_path);+    let breadcrumbs = crate::routes::build_breadcrumbs(b, &name, &sha, &resolved_path);~    let raw_url = if resolved_path.is_empty() {~        String::new()~    } else {-        format!("{b}/{name}/commits/{}/raw/{}", params.sha, resolved_path)+        format!("{b}/{name}/commits/{sha}/raw/{resolved_path}")~    };~    let parent_url = if resolved_path.is_empty() {~        String::new()~    } else {~        let parent_path = resolved_path.rsplit_once('/').map_or("", |(p, _)| p);~        if parent_path.is_empty() {-            format!("{b}/{name}/commits/{}/tree", params.sha)+            format!("{b}/{name}/commits/{sha}/tree")~        } else {-            format!("{b}/{name}/commits/{}/tree/{parent_path}", params.sha)+            format!("{b}/{name}/commits/{sha}/tree/{parent_path}")~        }~    };~
~                            .unwrap_or_default()~                    };~                    let url = if resolved_path.is_empty() {-                        format!("{b}/{name}/commits/{}/tree/{entry_name}", params.sha)+                        format!("{b}/{name}/commits/{sha}/tree/{entry_name}")~                    } else {-                        format!(-                            "{b}/{name}/commits/{}/tree/{resolved_path}/{entry_name}",-                            params.sha-                        )+                        format!("{b}/{name}/commits/{sha}/tree/{resolved_path}/{entry_name}")~                    };~                    Some(TreeEntry {~                        name: entry_name,
~~            Ok(TreeTemplate {~                repo: name,-                sha: params.sha,+                sha,~                path: resolved_path,~                breadcrumbs,~                is_file: false,
~~            Ok(TreeTemplate {~                repo: name,-                sha: params.sha,+                sha,~                path: resolved_path,~                breadcrumbs,~                is_file: true,
~        }~        _ => {~            let spec = if resolved_path.is_empty() {-                format!("{}:", params.sha)+                format!("{sha}:")~            } else {-                format!("{}:{}", params.sha, resolved_path)+                format!("{sha}:{resolved_path}")~            };~            Err(Error::NotFound(spec))~        }