Use typed paths for commit URLs and add commit view styling
- Add commit_url field to ChangeRow and CommitParent, generated in
handlers from the repo name instead of hardcoded in templates
- Rename /commit/ route to /commits/ (plural)
- Add CSS for commit detail page (subject, body, meta, diff)
- Add CSS for repo list page
- Fix file view code surface padding and background
- Update all template links to use commit_url
Assisted-by: Owl Alpha via pi
diff --git a/quire-server/src/quire/web/handlers/commit.rs b/quire-server/src/quire/web/handlers/commit.rs
index a9431c7..13cb9ee 100644
--- a/quire-server/src/quire/web/handlers/commit.rs
+++ b/quire-server/src/quire/web/handlers/commit.rs
@@ -61,10 +61,10 @@ pub async fn commit_view(
let timestamp_ms: i64 = timestamp_str.parse().ok().map(|secs: i64| secs * 1000)?;
- let parents: Vec<CommitParent> = parents_str
+ let parent_shars: Vec<String> = parents_str
.split_whitespace()
.filter(|s| !s.is_empty())
- .map(|p| CommitParent { sha: p.to_string() })
+ .map(|p| p.to_string())
.collect();
let diff = reader
@@ -78,18 +78,26 @@ pub async fn commit_view(
timestamp_ms,
subject,
body,
- parents,
+ parent_shars,
diff,
))
})
.await
.unwrap_or(None);
- let (sha, author, email, timestamp_ms, subject, body, parents, diff) = match result {
+ let (sha, author, email, timestamp_ms, subject, body, parent_shas, diff) = match result {
Some(data) => data,
None => return 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 {
diff --git a/quire-server/src/quire/web/handlers/git.rs b/quire-server/src/quire/web/handlers/git.rs
index 8853c61..bece1e1 100644
--- a/quire-server/src/quire/web/handlers/git.rs
+++ b/quire-server/src/quire/web/handlers/git.rs
@@ -33,13 +33,13 @@ impl<'a> RepoView<'a> {
}
/// Read all summary data from the repo for the home page.
- pub(super) fn read_all(&self) -> GitData {
+ pub(super) fn read_all(&self, repo: &str) -> GitData {
(
self.head_info(),
self.readme(),
self.bookmarks(),
self.tags(),
- self.recent_changes(),
+ self.recent_changes(repo),
)
}
@@ -114,11 +114,11 @@ impl<'a> RepoView<'a> {
.collect()
}
- pub(super) fn recent_changes(&self) -> Vec<ChangeRow> {
- self.recent_changes_for(None)
+ pub(super) fn recent_changes(&self, repo: &str) -> Vec<ChangeRow> {
+ self.recent_changes_for(None, repo)
}
- pub(super) fn recent_changes_for(&self, path: Option<&str>) -> Vec<ChangeRow> {
+ pub(super) fn recent_changes_for(&self, path: Option<&str>, repo: &str) -> Vec<ChangeRow> {
let mut args = vec!["log", "-12", "--format=%H|%s|%cr"];
if let Some(p) = path
&& !p.is_empty()
@@ -131,8 +131,10 @@ impl<'a> RepoView<'a> {
out.lines()
.filter_map(|line| {
let mut parts = line.splitn(3, '|');
+ let sha = parts.next()?.to_string();
Some(ChangeRow {
- sha: parts.next()?.to_string(),
+ commit_url: format!("/{repo}/commits/{sha}"),
+ sha,
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 2e946e5..9f21e8e 100644
--- a/quire-server/src/quire/web/handlers/log_view.rs
+++ b/quire-server/src/quire/web/handlers/log_view.rs
@@ -23,9 +23,10 @@ pub async fn log_view(
_ => return StatusCode::NOT_FOUND.into_response(),
};
+ let repo_d = repo_display.clone();
let (changes, bookmark, sha_short) = tokio::task::spawn_blocking(move || {
let reader = RepoView::new(&git_repo);
- let changes = reader.recent_changes();
+ let changes = reader.recent_changes(&repo_d);
let bookmark = reader
.run(&["symbolic-ref", "--short", "HEAD"])
.unwrap_or_else(|| "main".to_string());
diff --git a/quire-server/src/quire/web/handlers/repo.rs b/quire-server/src/quire/web/handlers/repo.rs
index 4223024..256df98 100644
--- a/quire-server/src/quire/web/handlers/repo.rs
+++ b/quire-server/src/quire/web/handlers/repo.rs
@@ -52,8 +52,9 @@ pub async fn repo_home(
Err(_) => vec![],
};
+ let rd = repo_display.clone();
let (head, readme_html, bookmarks, tags, recent_changes) =
- tokio::task::spawn_blocking(move || RepoView::new(&git_repo).read_all())
+ tokio::task::spawn_blocking(move || RepoView::new(&git_repo).read_all(&rd))
.await
.unwrap_or_default();
diff --git a/quire-server/src/quire/web/handlers/tree.rs b/quire-server/src/quire/web/handlers/tree.rs
index 9fe374f..c7344ba 100644
--- a/quire-server/src/quire/web/handlers/tree.rs
+++ b/quire-server/src/quire/web/handlers/tree.rs
@@ -39,6 +39,7 @@ async fn tree_or_file_at_path(quire: Quire, repo: String, path: String, authed:
};
let path_clone = path.clone();
+ let repo_d = repo_display.clone();
let result = tokio::task::spawn_blocking(move || {
let reader = RepoView::new(&git_repo);
@@ -46,7 +47,7 @@ async fn tree_or_file_at_path(quire: Quire, repo: String, path: String, authed:
if let Some(tree_data) = read_tree_data(&reader, &path_clone) {
let bookmarks = reader.bookmarks();
let tags = reader.tags();
- let recent_changes = reader.recent_changes_for(Some(&path_clone));
+ let recent_changes = reader.recent_changes_for(Some(&path_clone), &repo_d);
Some(Ok((tree_data, bookmarks, tags, recent_changes)))
} else {
// ls-tree failed — try reading as a file blob.
diff --git a/quire-server/src/quire/web/mod.rs b/quire-server/src/quire/web/mod.rs
index 5f822e8..28919eb 100644
--- a/quire-server/src/quire/web/mod.rs
+++ b/quire-server/src/quire/web/mod.rs
@@ -84,7 +84,7 @@ pub mod paths {
}
#[derive(TypedPath, Deserialize)]
- #[typed_path("/{repo}/commit/{sha}")]
+ #[typed_path("/{repo}/commits/{sha}")]
pub struct CommitPath {
pub repo: String,
pub sha: String,
diff --git a/quire-server/src/quire/web/templates.rs b/quire-server/src/quire/web/templates.rs
index 43ce791..b386b8a 100644
--- a/quire-server/src/quire/web/templates.rs
+++ b/quire-server/src/quire/web/templates.rs
@@ -382,6 +382,7 @@ pub struct ChangeRow {
pub sha: String,
pub description: String,
pub age: String,
+ pub commit_url: String,
}
impl ChangeRow {
@@ -553,6 +554,7 @@ impl CommitTemplate {
pub struct CommitParent {
pub sha: String,
+ pub commit_url: String,
}
impl CommitParent {
diff --git a/quire-server/static/style.css b/quire-server/static/style.css
index a015891..4d1bf8f 100644
--- a/quire-server/static/style.css
+++ b/quire-server/static/style.css
@@ -614,6 +614,15 @@ html.dark {
color: var(--mutedFaint);
}
+.change-mini-desc-link {
+ color: inherit;
+ text-decoration: none;
+}
+
+.change-mini-desc-link:hover {
+ color: var(--accent);
+}
+
/* ── CI run list (full page) ───────────────────────────────────── */
.ci-run-list {
@@ -787,16 +796,16 @@ html.dark {
line-height: 1.7;
display: grid;
grid-template-columns: auto 1fr;
+ background: var(--code);
}
.code-gutter {
- background: var(--code);
- padding: 18px 14px 18px 56px;
+ padding: 16px 10px 16px 12px;
color: var(--mutedFaint);
text-align: right;
font-variant-numeric: tabular-nums;
user-select: none;
- border-right: 1px solid var(--rule2);
+ border-right: 1px solid var(--rule);
}
.code-line-num {
@@ -804,20 +813,20 @@ html.dark {
}
.code-body {
- padding: 18px 56px 18px 20px;
+ padding: 16px 16px 16px 12px;
overflow: auto;
margin: 0;
- white-space: pre-wrap;
- word-break: break-all;
+ white-space: pre;
font-family: inherit;
font-size: inherit;
line-height: inherit;
- background: var(--code) !important;
+ background: transparent !important;
border-radius: 0 !important;
}
-.code-body > code[data-lang] {
- display: block;
+.code-body > code[data-lang],
+.code-body > code[data-lang] * {
+ background: transparent !important;
}
/* ── Arborium syntax highlighting integration ──────────────────── */
@@ -1200,6 +1209,134 @@ pre code[data-lang] {
color: var(--muted);
}
+/* ── Commit detail ─────────────────────────────────────────────── */
+
+.commit-detail {
+ padding: var(--space-l) var(--space-xl);
+ min-width: 0;
+}
+
+.commit-message {
+ margin-bottom: var(--space-m);
+}
+
+.commit-subject {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--ink);
+ margin-bottom: var(--space-s);
+}
+
+.commit-body {
+ font-family: var(--font-humanist);
+ font-size: 15px;
+ line-height: 1.7;
+ color: var(--muted);
+ white-space: pre-wrap;
+ word-break: break-word;
+ margin: 0;
+}
+
+.commit-meta-list {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ line-height: 2;
+ margin-bottom: var(--space-l);
+}
+
+.commit-meta-row {
+ display: flex;
+ align-items: baseline;
+ gap: var(--space-s);
+}
+
+.commit-meta-key {
+ color: var(--mutedFaint);
+ min-width: 70px;
+ font-size: 11px;
+ letter-spacing: 0.8px;
+ text-transform: uppercase;
+}
+
+.commit-meta-val {
+ color: var(--ink);
+}
+
+.commit-meta-val .change-id {
+ margin-right: var(--space-2xs);
+}
+
+.commit-diff {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ line-height: 1.7;
+ background: var(--code);
+ padding: var(--space-s) var(--space-m);
+ margin: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+ overflow: auto;
+ border-left: 2px solid var(--accent);
+}
+
+/* ── Repo list ──────────────────────────────────────────────────── */
+
+.repo-list-page {
+ padding: var(--space-l) var(--space-xl);
+ max-width: 820px;
+}
+
+.repo-list-heading {
+ font-size: 22px;
+ font-weight: 600;
+ margin: 0 0 var(--space-m);
+ color: var(--ink);
+}
+
+.repo-list {
+ border-top: 1px solid var(--rule);
+}
+
+.repo-list-item {
+ display: flex;
+ align-items: baseline;
+ gap: var(--space-s);
+ padding: var(--space-xs) 0;
+ border-bottom: 1px solid var(--rule);
+ text-decoration: none;
+ color: var(--ink);
+}
+
+.repo-list-item:hover {
+ color: var(--accent);
+}
+
+.repo-list-name {
+ font-family: var(--font-mono);
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--ink);
+}
+
+.repo-list-item:hover .repo-list-name {
+ color: var(--accent);
+}
+
+.repo-list-desc {
+ font-size: 14px;
+ color: var(--muted);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.repo-list-empty {
+ color: var(--muted);
+ font-style: italic;
+ padding: var(--space-l) 0;
+}
+
/* ── Mobile (≤768px) ───────────────────────────────────────────── */
@media (max-width: 768px) {
diff --git a/quire-server/templates/ci/run_detail.html b/quire-server/templates/ci/run_detail.html
index ca8b964..1be27c6 100644
--- a/quire-server/templates/ci/run_detail.html
+++ b/quire-server/templates/ci/run_detail.html
@@ -11,7 +11,7 @@
<nav class="repo-section-nav">
{% include "_repo_section_nav.html" %}
<span class="repo-position">
- <a class="ci-commit-link" href="/{{ repo }}/commit/{{ run.sha }}">{{ run.sha_short() }}</a>
+ <a class="ci-commit-link" href="/{{ repo }}/commits/{{ run.sha }}">{{ run.sha_short() }}</a>
<span class="repo-meta-dot">·</span>
<span>{{ run.branch_short() }}</span>
<span class="repo-meta-dot">·</span>
diff --git a/quire-server/templates/commit.html b/quire-server/templates/commit.html
index cb330c2..b0ff93d 100644
--- a/quire-server/templates/commit.html
+++ b/quire-server/templates/commit.html
@@ -49,7 +49,7 @@
<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="/{{ repo }}/commit/{{ p.sha_full() }}" title="commit {{ p.sha_full() }}">
+ <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>
{% endfor %}
diff --git a/quire-server/templates/file.html b/quire-server/templates/file.html
index 976f349..adaf4c3 100644
--- a/quire-server/templates/file.html
+++ b/quire-server/templates/file.html
@@ -66,7 +66,7 @@
<div class="side-block">
<div class="side-block-title">Last change</div>
<div class="file-last-change-side">
- <a class="change-id" href="/{{ repo }}/log" title="commit {{ last_change_sha }}">
+ <a class="change-id" href="/{{ repo }}/commits/{{ last_change_sha }}" title="commit {{ last_change_sha }}">
<span class="change-head">{{ last_change_head }}</span><span class="change-tail">{{ last_change_tail }}</span>
</a>
<div class="file-last-change-msg">{{ last_change_msg }}</div>
diff --git a/quire-server/templates/log.html b/quire-server/templates/log.html
index 2cee7fa..62f9867 100644
--- a/quire-server/templates/log.html
+++ b/quire-server/templates/log.html
@@ -23,10 +23,10 @@
<div class="log-body">
{% for change in changes %}
<div class="log-row">
- <a class="log-sha" href="/{{ repo }}/commit/{{ change.sha_full() }}" title="commit {{ change.sha_full() }}">
+ <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>
- <a class="log-subject" href="/{{ repo }}/commit/{{ change.sha_full() }}">
+ <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 %}
</a>
<span class="log-age"><time>{{ change.age }}</time></span>
diff --git a/quire-server/templates/repo_home.html b/quire-server/templates/repo_home.html
index 8bc2023..9d948d6 100644
--- a/quire-server/templates/repo_home.html
+++ b/quire-server/templates/repo_home.html
@@ -16,7 +16,7 @@
<span class="bookmark-glyph">※</span>
<span class="bookmark-name">{{ h.bookmark }}</span>
<span class="repo-meta-sep">→</span>
- <a class="change-id" href="/{{ repo }}/commit/{{ h.sha }}" title="commit {{ h.sha_short() }}">
+ <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>
<span class="commit-id-secondary">{{ h.sha_short() }}</span>
@@ -115,13 +115,13 @@
{% for ch in recent_changes %}
<div class="change-mini-row">
<div class="change-mini-header">
- <a class="change-id" href="/{{ repo }}/commit/{{ ch.sha_full() }}" title="commit {{ ch.sha_full() }}">
+ <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>
<span class="change-mini-age">{{ ch.age }}</span>
</div>
<div class="change-mini-desc">
- <a class="change-mini-desc-link" href="/{{ repo }}/commit/{{ ch.sha_full() }}">
+ <a class="change-mini-desc-link" href="{{ ch.commit_url }}">
{% if ch.description.is_empty() %}<span class="no-desc">(no description set)</span>{% else %}{{ ch.description }}{% endif %}
</a>
</div>
diff --git a/quire-server/templates/tree.html b/quire-server/templates/tree.html
index eeb9db8..e0cce17 100644
--- a/quire-server/templates/tree.html
+++ b/quire-server/templates/tree.html
@@ -69,7 +69,7 @@
{# RIGHT — recent commit log for this ref #}
<aside class="tree-sidebar">
{% for change in recent_changes %}
- <a class="tree-log-item" href="/{{ repo }}/commit/{{ change.sha_full() }}">
+ <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>