Unify commit ID rendering behind a CommitId type
Show jj change IDs in every commit badge — log, tree, file, repo home,
and parents — not just the commit detail page, by replacing the
scattered sha/change-id fields and helpers with one shared type.
Assisted-by: Claude Opus 4.8 via Claude Code
diff --git a/quire-server/src/quire/web/handlers/commit.rs b/quire-server/src/quire/web/handlers/commit.rs
index 3193af6..d1c8d45 100644
--- a/quire-server/src/quire/web/handlers/commit.rs
+++ b/quire-server/src/quire/web/handlers/commit.rs
@@ -5,7 +5,7 @@ use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use super::super::error::WebError;
-use super::super::templates::{CommitParent, CommitTemplate, nav_sections};
+use super::super::templates::{CommitId, CommitParent, CommitTemplate, nav_sections};
use super::git::RepoView;
use super::render;
use crate::Quire;
@@ -21,6 +21,7 @@ pub async fn commit_view(
let git_repo = quire.repo(&repo_name)?;
let sha_clone = sha.clone();
+ let repo_d = repo_display.clone();
let result = tokio::task::spawn_blocking(move || {
let reader = RepoView::new(&git_repo);
@@ -58,18 +59,21 @@ pub async fn commit_view(
let timestamp_ms: i64 = timestamp_str.parse().ok().map(|secs: i64| secs * 1000)?;
- let parent_shars: Vec<String> = parents_str
- .split_whitespace()
- .filter(|s| !s.is_empty())
- .map(|p| p.to_string())
- .collect();
-
let diff = reader
.run(&["log", "-1", "--patch", "--format=", &full_sha])
.unwrap_or_default();
let change_id = reader.change_id(&full_sha).unwrap_or_default();
+ let parents: Vec<CommitParent> = parents_str
+ .split_whitespace()
+ .filter(|s| !s.is_empty())
+ .map(|p| CommitParent {
+ commit_url: format!("/{repo_d}/commits/{p}"),
+ id: CommitId::new(p.to_string(), reader.change_id(p)),
+ })
+ .collect();
+
Some((
sha,
author,
@@ -77,45 +81,37 @@ pub async fn commit_view(
timestamp_ms,
subject,
body,
- parent_shars,
+ parents,
diff,
change_id,
))
})
.await?;
- let Some((sha, author, email, timestamp_ms, subject, body, parent_shas, diff, change_id)) =
- result
+ let Some((sha, author, email, timestamp_ms, subject, body, parents, diff, change_id)) = result
else {
return Ok(StatusCode::NOT_FOUND.into_response());
};
- let parents: Vec<CommitParent> = parent_shas
- .into_iter()
- .map(|p| CommitParent {
- commit_url: format!("/{repo_display}/commits/{p}"),
- sha: p,
- })
- .collect();
-
let sha_short = if sha.len() >= 8 {
sha[..8].to_string()
} else {
sha.clone()
};
+ let nav_id = if change_id.is_empty() {
+ sha.as_str()
+ } else {
+ change_id.as_str()
+ };
let tmpl = CommitTemplate {
sections: nav_sections(&repo_display, "log", auth.is_authenticated()),
repo: repo_display,
crumbs: None,
sha: sha.clone(),
sha_short,
- sha_head: sha[..sha.len().min(4)].to_string(),
- sha_tail: if sha.len() > 4 {
- sha[4..sha.len().min(8)].to_string()
- } else {
- String::new()
- },
+ sha_head: nav_id[..nav_id.len().min(4)].to_string(),
+ sha_tail: nav_id[nav_id.len().min(4)..nav_id.len().min(8)].to_string(),
author,
email,
date_relative: super::super::format::format_timestamp_relative(timestamp_ms),
diff --git a/quire-server/src/quire/web/handlers/git.rs b/quire-server/src/quire/web/handlers/git.rs
index 6f00382..169cadd 100644
--- a/quire-server/src/quire/web/handlers/git.rs
+++ b/quire-server/src/quire/web/handlers/git.rs
@@ -1,6 +1,6 @@
//! Shared git-reading helpers used by multiple handlers.
-use super::super::templates::{ChangeRow, HeadInfo};
+use super::super::templates::{ChangeRow, CommitId, HeadInfo};
use crate::quire::Repo;
pub(super) type GitData = (Option<HeadInfo>, Option<String>, Vec<ChangeRow>);
@@ -41,8 +41,9 @@ impl<'a> RepoView<'a> {
let sha = lines.next()?.to_string();
let description = lines.next().unwrap_or("").to_string();
let age = lines.next().unwrap_or("").to_string();
+ let change_id = self.change_id(&sha);
Some(HeadInfo {
- sha,
+ id: CommitId::new(sha, change_id),
description,
age,
bookmark,
@@ -77,9 +78,10 @@ impl<'a> RepoView<'a> {
.filter_map(|line| {
let mut parts = line.splitn(3, '|');
let sha = parts.next()?.to_string();
+ let change_id = self.change_id(&sha);
Some(ChangeRow {
commit_url: format!("/{repo}/commits/{sha}"),
- sha,
+ id: CommitId::new(sha, change_id),
description: parts.next().unwrap_or("").to_string(),
age: parts.next().unwrap_or("").to_string(),
})
diff --git a/quire-server/src/quire/web/handlers/log_view.rs b/quire-server/src/quire/web/handlers/log_view.rs
index 09f2d7d..e8e11c2 100644
--- a/quire-server/src/quire/web/handlers/log_view.rs
+++ b/quire-server/src/quire/web/handlers/log_view.rs
@@ -4,7 +4,7 @@ use axum::extract::State;
use axum::response::Response;
use super::super::error::WebError;
-use super::super::templates::{LogTemplate, nav_sections};
+use super::super::templates::{CommitId, LogTemplate, nav_sections};
use super::git::RepoView;
use super::render;
use crate::Quire;
@@ -20,16 +20,15 @@ pub async fn log_view(
let git_repo = quire.repo(&repo_name)?;
let repo_d = repo_display.clone();
- let (changes, bookmark, sha_short) = tokio::task::spawn_blocking(move || {
+ let (changes, bookmark, head) = tokio::task::spawn_blocking(move || {
let reader = RepoView::new(&git_repo);
let changes = reader.recent_changes(&repo_d);
let bookmark = reader
.run(&["symbolic-ref", "--short", "HEAD"])
.unwrap_or_else(|| "main".to_string());
- let sha_short = reader
- .run(&["rev-parse", "--short", "HEAD"])
- .unwrap_or_else(|| "unknown".to_string());
- (changes, bookmark, sha_short)
+ let head_sha = reader.run(&["rev-parse", "HEAD"]).unwrap_or_default();
+ let head_change_id = reader.change_id(&head_sha);
+ (changes, bookmark, CommitId::new(head_sha, head_change_id))
})
.await?;
@@ -39,7 +38,7 @@ pub async fn log_view(
crumbs: None,
changes,
bookmark,
- sha_short,
+ head,
};
Ok(render(&tmpl))
}
diff --git a/quire-server/src/quire/web/handlers/tree.rs b/quire-server/src/quire/web/handlers/tree.rs
index 055d587..a7127f7 100644
--- a/quire-server/src/quire/web/handlers/tree.rs
+++ b/quire-server/src/quire/web/handlers/tree.rs
@@ -8,7 +8,7 @@ use super::super::auth::Auth;
use super::super::db;
use super::super::error::WebError;
use super::super::templates::{
- Crumb, FileViewTemplate, TreeEntry, TreeEntryKind, TreeTemplate, nav_sections,
+ CommitId, Crumb, FileViewTemplate, TreeEntry, TreeEntryKind, TreeTemplate, nav_sections,
};
use super::git::RepoView;
use super::render;
@@ -70,7 +70,7 @@ async fn tree_or_file_at_path(
crumbs: Some(crumbs),
path,
bookmark: tree_data.bookmark,
- sha_short: tree_data.sha_short,
+ head: tree_data.head,
entries: tree_data.entries,
recent_changes,
};
@@ -85,12 +85,12 @@ async fn tree_or_file_at_path(
crumbs: Some(crumbs),
path,
bookmark: file_data.bookmark,
- sha_short: file_data.sha_short.clone(),
- sha_head: file_data.sha_short[..file_data.sha_short.len().min(4)].to_string(),
- sha_tail: file_data.sha_short[file_data.sha_short.len().min(4)..].to_string(),
- last_change_sha: file_data.last_change_sha,
- last_change_head: file_data.last_change_head,
- last_change_tail: file_data.last_change_tail,
+ sha_short: file_data.head.sha_short().to_string(),
+ sha_head: file_data.head.head().to_string(),
+ sha_tail: file_data.head.tail().to_string(),
+ last_change_sha: file_data.last_change.sha.clone(),
+ last_change_head: file_data.last_change.head().to_string(),
+ last_change_tail: file_data.last_change.tail().to_string(),
last_change_msg: file_data.last_change_msg,
last_change_author: file_data.last_change_author,
last_change_age: file_data.last_change_age,
@@ -112,7 +112,7 @@ async fn tree_or_file_at_path(
struct TreeData {
bookmark: String,
- sha_short: String,
+ head: CommitId,
entries: Vec<TreeEntry>,
}
@@ -134,9 +134,8 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
.run(&["symbolic-ref", "--short", "HEAD"])
.unwrap_or_else(|| "main".to_string());
- let sha_short = reader
- .run(&["rev-parse", "--short", "HEAD"])
- .unwrap_or_else(|| "unknown".to_string());
+ let head_sha = reader.run(&["rev-parse", "HEAD"]).unwrap_or_default();
+ let head = CommitId::new(head_sha.clone(), reader.change_id(&head_sha));
let ls_target = if path.is_empty() {
"HEAD".to_string()
@@ -204,7 +203,7 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
Some(TreeData {
bookmark,
- sha_short,
+ head,
entries,
})
}
@@ -213,10 +212,8 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
struct FileData {
bookmark: String,
- sha_short: String,
- last_change_sha: String,
- last_change_head: String,
- last_change_tail: String,
+ head: CommitId,
+ last_change: CommitId,
last_change_msg: String,
last_change_author: String,
last_change_age: String,
@@ -247,9 +244,8 @@ fn read_file_data(reader: &RepoView<'_>, path: &str) -> Option<FileData> {
.run(&["symbolic-ref", "--short", "HEAD"])
.unwrap_or_else(|| "main".to_string());
- let sha_short = reader
- .run(&["rev-parse", "--short", "HEAD"])
- .unwrap_or_else(|| "unknown".to_string());
+ let head_sha = reader.run(&["rev-parse", "HEAD"]).unwrap_or_default();
+ let head = CommitId::new(head_sha.clone(), reader.change_id(&head_sha));
let blob = reader.run(&["show", &format!("HEAD:{path}")])?;
@@ -259,10 +255,8 @@ fn read_file_data(reader: &RepoView<'_>, path: &str) -> Option<FileData> {
let log = reader.run(&["log", "-1", "--format=%H|%s|%an|%cr", "HEAD", "--", path])?;
let mut log_parts = log.splitn(4, '|');
- let last_change_sha = log_parts.next()?.to_string();
- let last_change_head = last_change_sha[..last_change_sha.len().min(4)].to_string();
- let last_change_tail =
- last_change_sha[last_change_sha.len().min(4)..last_change_sha.len().min(8)].to_string();
+ let last_sha = log_parts.next()?.to_string();
+ let last_change = CommitId::new(last_sha.clone(), reader.change_id(&last_sha));
let last_change_msg = log_parts.next().unwrap_or("").to_string();
let last_change_author = log_parts.next().unwrap_or("").to_string();
let last_change_age = log_parts.next().unwrap_or("").to_string();
@@ -283,10 +277,8 @@ fn read_file_data(reader: &RepoView<'_>, path: &str) -> Option<FileData> {
Some(FileData {
bookmark,
- sha_short,
- last_change_sha,
- last_change_head,
- last_change_tail,
+ head,
+ last_change,
last_change_msg,
last_change_author,
last_change_age,
diff --git a/quire-server/src/quire/web/templates.rs b/quire-server/src/quire/web/templates.rs
index 5ca2906..d193e9f 100644
--- a/quire-server/src/quire/web/templates.rs
+++ b/quire-server/src/quire/web/templates.rs
@@ -9,6 +9,49 @@ fn pkg_version() -> &'static str {
env!("QUIRE_VERSION")
}
+// ── Commit ID ─────────────────────────────────────────────────────────
+
+/// A git commit SHA paired with an optional jj change ID.
+///
+/// Display methods prefer the change ID when present, falling back to the
+/// git SHA. Used wherever a commit badge or reference is shown in templates.
+pub struct CommitId {
+ pub sha: String,
+ pub change_id: Option<String>,
+}
+
+impl CommitId {
+ pub fn new(sha: String, change_id: Option<String>) -> Self {
+ Self { sha, change_id }
+ }
+
+ fn display(&self) -> &str {
+ self.change_id.as_deref().unwrap_or(self.sha.as_str())
+ }
+
+ /// First 4 chars of the display ID (bold prefix in commit badges).
+ pub fn head(&self) -> &str {
+ let s = self.display();
+ &s[..s.len().min(4)]
+ }
+
+ /// Chars 4–8 of the display ID (dimmed suffix in commit badges).
+ pub fn tail(&self) -> &str {
+ let s = self.display();
+ let start = s.len().min(4);
+ &s[start..s.len().min(8)]
+ }
+
+ /// First 8 chars of the git SHA, used for tooltips and secondary display.
+ pub fn sha_short(&self) -> &str {
+ &self.sha[..self.sha.len().min(8)]
+ }
+
+ pub fn sha_full(&self) -> &str {
+ &self.sha
+ }
+}
+
/// A section nav link in the repo tab bar.
pub struct SectionLink {
pub label: &'static str,
@@ -322,53 +365,19 @@ impl RepoHomeTemplate {
}
pub struct HeadInfo {
- pub sha: String,
+ pub id: CommitId,
pub description: String,
pub age: String,
pub bookmark: String,
}
-impl HeadInfo {
- pub fn change_head(&self) -> &str {
- let end = self.sha.len().min(4);
- &self.sha[..end]
- }
-
- pub fn change_tail(&self) -> &str {
- let start = self.sha.len().min(4);
- let end = self.sha.len().min(8);
- &self.sha[start..end]
- }
-
- pub fn sha_short(&self) -> &str {
- &self.sha[..self.sha.len().min(8)]
- }
-}
-
pub struct ChangeRow {
- pub sha: String,
+ pub id: CommitId,
pub description: String,
pub age: String,
pub commit_url: String,
}
-impl ChangeRow {
- pub fn change_head(&self) -> &str {
- let end = self.sha.len().min(4);
- &self.sha[..end]
- }
-
- pub fn change_tail(&self) -> &str {
- let start = self.sha.len().min(4);
- let end = self.sha.len().min(8);
- &self.sha[start..end]
- }
-
- pub fn sha_full(&self) -> &str {
- &self.sha
- }
-}
-
// ── Config ─────────────────────────────────────────────────────────
#[derive(Template)]
@@ -402,8 +411,7 @@ pub struct TreeTemplate {
pub path: String,
/// Active bookmark name (e.g. "main").
pub bookmark: String,
- /// Short commit hash for HEAD.
- pub sha_short: String,
+ pub head: CommitId,
pub entries: Vec<TreeEntry>,
pub recent_changes: Vec<ChangeRow>,
}
@@ -442,15 +450,6 @@ impl TreeTemplate {
pub fn file_count(&self) -> usize {
self.entries.iter().filter(|e| e.is_file()).count()
}
-
- pub fn sha_head(&self) -> &str {
- &self.sha_short[..self.sha_short.len().min(4)]
- }
-
- pub fn sha_tail(&self) -> &str {
- let start = self.sha_short.len().min(4);
- &self.sha_short[start..]
- }
}
pub struct TreeEntry {
@@ -527,25 +526,10 @@ impl CommitTemplate {
}
pub struct CommitParent {
- pub sha: String,
+ pub id: CommitId,
pub commit_url: String,
}
-impl CommitParent {
- pub fn sha_full(&self) -> &str {
- &self.sha
- }
-
- pub fn sha_head(&self) -> &str {
- &self.sha[..self.sha.len().min(4)]
- }
-
- pub fn sha_tail(&self) -> &str {
- let start = self.sha.len().min(4);
- &self.sha[start..self.sha.len().min(8)]
- }
-}
-
// ── Commit log ────────────────────────────────────────────────────
#[derive(Template)]
@@ -556,22 +540,13 @@ pub struct LogTemplate {
pub sections: Vec<SectionLink>,
pub changes: Vec<ChangeRow>,
pub bookmark: String,
- pub sha_short: String,
+ pub head: CommitId,
}
impl LogTemplate {
pub fn version(&self) -> &'static str {
pkg_version()
}
-
- pub fn sha_head(&self) -> &str {
- &self.sha_short[..self.sha_short.len().min(4)]
- }
-
- pub fn sha_tail(&self) -> &str {
- let start = self.sha_short.len().min(4);
- &self.sha_short[start..]
- }
}
// ── Error ──────────────────────────────────────────────────────────
diff --git a/quire-server/templates/commit.html b/quire-server/templates/commit.html
index ba03c68..21e613c 100644
--- a/quire-server/templates/commit.html
+++ b/quire-server/templates/commit.html
@@ -55,8 +55,8 @@
<span class="commit-meta-key">parent{% if parents.len() > 1 %}s{% endif %}</span>
<span class="commit-meta-val">
{% for p in parents %}
- <a class="change-id" href="{{ p.commit_url }}" title="commit {{ p.sha_full() }}">
- <span class="change-head">{{ p.sha_head() }}</span><span class="change-tail">{{ p.sha_tail() }}</span>
+ <a class="change-id" href="{{ p.commit_url }}" title="commit {{ p.id.sha_full() }}">
+ <span class="change-head">{{ p.id.head() }}</span><span class="change-tail">{{ p.id.tail() }}</span>
</a>
{% endfor %}
</span>
diff --git a/quire-server/templates/log.html b/quire-server/templates/log.html
index 62f9867..cb3c864 100644
--- a/quire-server/templates/log.html
+++ b/quire-server/templates/log.html
@@ -14,8 +14,8 @@
<span class="bookmark-glyph">※</span>
<span class="bookmark-name">{{ bookmark }}</span>
<span class="repo-meta-sep">→</span>
- <span class="change-id" title="{{ sha_short }}">
- <span class="change-head">{{ self.sha_head() }}</span><span class="change-tail">{{ self.sha_tail() }}</span>
+ <span class="change-id" title="{{ head.sha_short() }}">
+ <span class="change-head">{{ head.head() }}</span><span class="change-tail">{{ head.tail() }}</span>
</span>
</span>
</nav>
@@ -23,8 +23,8 @@
<div class="log-body">
{% for change in changes %}
<div class="log-row">
- <a class="log-sha" href="{{ change.commit_url }}" title="commit {{ change.sha_full() }}">
- <span class="change-head">{{ change.change_head() }}</span><span class="change-tail">{{ change.change_tail() }}</span>
+ <a class="log-sha" href="{{ change.commit_url }}" title="commit {{ change.id.sha_full() }}">
+ <span class="change-head">{{ change.id.head() }}</span><span class="change-tail">{{ change.id.tail() }}</span>
</a>
<a class="log-subject" href="{{ change.commit_url }}">
{% if change.description.is_empty() %}<span class="no-desc">(no description set)</span>{% else %}{{ change.description }}{% endif %}
diff --git a/quire-server/templates/repo_home.html b/quire-server/templates/repo_home.html
index a0bda89..c82cfcc 100644
--- a/quire-server/templates/repo_home.html
+++ b/quire-server/templates/repo_home.html
@@ -16,10 +16,10 @@
<span class="bookmark-glyph">※</span>
<span class="bookmark-name">{{ h.bookmark }}</span>
<span class="repo-meta-sep">→</span>
- <a class="change-id" href="/{{ repo }}/commits/{{ h.sha }}" title="commit {{ h.sha_short() }}">
- <span class="change-head">{{ h.change_head() }}</span><span class="change-tail">{{ h.change_tail() }}</span>
+ <a class="change-id" href="/{{ repo }}/commits/{{ h.id.sha }}" title="commit {{ h.id.sha_short() }}">
+ <span class="change-head">{{ h.id.head() }}</span><span class="change-tail">{{ h.id.tail() }}</span>
</a>
- <span class="commit-id-secondary">{{ h.sha_short() }}</span>
+ <span class="commit-id-secondary">{{ h.id.sha_short() }}</span>
<span class="repo-meta-dot">·</span>
<span class="repo-position-age">{{ h.age }}</span>
{% if !recent_runs.is_empty() %}
@@ -78,8 +78,8 @@
{% for ch in recent_changes %}
<div class="change-mini-row">
<div class="change-mini-header">
- <a class="change-id" href="{{ ch.commit_url }}" title="commit {{ ch.sha_full() }}">
- <span class="change-head">{{ ch.change_head() }}</span><span class="change-tail">{{ ch.change_tail() }}</span>
+ <a class="change-id" href="{{ ch.commit_url }}" title="commit {{ ch.id.sha_full() }}">
+ <span class="change-head">{{ ch.id.head() }}</span><span class="change-tail">{{ ch.id.tail() }}</span>
</a>
<span class="change-mini-age">{{ ch.age }}</span>
</div>
diff --git a/quire-server/templates/tree.html b/quire-server/templates/tree.html
index e0cce17..599aaa8 100644
--- a/quire-server/templates/tree.html
+++ b/quire-server/templates/tree.html
@@ -15,8 +15,8 @@
<span class="bookmark-glyph">※</span>
<span class="bookmark-name">{{ bookmark }}</span>
<span class="repo-meta-sep">→</span>
- <span class="change-id" title="{{ sha_short }}">
- <span class="change-head">{{ self.sha_head() }}</span><span class="change-tail">{{ self.sha_tail() }}</span>
+ <span class="change-id" title="{{ head.sha_short() }}">
+ <span class="change-head">{{ head.head() }}</span><span class="change-tail">{{ head.tail() }}</span>
</span>
</span>
@@ -72,7 +72,7 @@
<a class="tree-log-item" href="{{ change.commit_url }}">
<div class="tree-log-subject">{{ change.description }}</div>
<div class="tree-log-meta">
- <span class="tree-log-sha">{{ change.change_head() }}{{ change.change_tail() }}</span>
+ <span class="tree-log-sha">{{ change.id.head() }}{{ change.id.tail() }}</span>
<span class="tree-log-age">{{ change.age }}</span>
</div>
</a>