Long hashes show their two ends, the whole in a tip addec2bd6423
Opening the overview at a state address printed all of it (21 heads joined by +) in the heads warning and the bookmark picker, off the page. A value over 24 characters now shows its first and last eight, with the whole of it in a wrapped, square-ish tip on hover or focus. Closes #198
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-14 15:55 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
addec2bd64233dc417bc7fe3def21815b5960fcb4381bd5d35a8fc2db07594f3- Depends on
- 6ae3fa0af4f6 d23464c0ac60 ffb67a81188c
- Depended on by
- 62cdf34daec1 810896d41be7
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 7 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified crates/hub/src/web/filters.rs +29 −0
| … | 53 unchanged lines |
||
| 54 | 54 | Ok(hash.chars().take(12).collect()) |
|
| 55 | 55 | } |
|
| 56 | 56 | ||
| 57 | + | /// A long hash or state address cut to its two ends, `0826ac0c…f8d97241` |
|
| 58 | + | /// (oxygenhub#198): people check a hash by its start and its end. Short |
|
| 59 | + | /// values stay whole. |
|
| 60 | + | pub fn ends(value: &str, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 61 | + | Ok(cut_ends(value)) |
|
| 62 | + | } |
|
| 63 | + | ||
| 64 | + | pub fn cut_ends(value: &str) -> String { |
|
| 65 | + | let n = value.chars().count(); |
|
| 66 | + | if n <= 24 { |
|
| 67 | + | return value.to_string(); |
|
| 68 | + | } |
|
| 69 | + | let start: String = value.chars().take(8).collect(); |
|
| 70 | + | let end: String = value.chars().skip(n - 8).collect(); |
|
| 71 | + | format!("{start}…{end}") |
|
| 72 | + | } |
|
| 73 | + | ||
| 57 | 74 | /// Markdown to HTML (already sanitised), marked safe for the template. |
|
| 58 | 75 | pub fn markdown(text: &str, _: &dyn askama::Values, base_file: &str, base_raw: &str) -> askama::Result<askama::filters::Safe<String>> { |
|
| 59 | 76 | Ok(askama::filters::Safe(render::markdown::render(text, base_file, base_raw))) |
|
| … | 14 unchanged lines |
||
| 74 | 91 | assert_eq!(crate::web::state_label("02a6c9d4746a"), "02a6c9d4746a", "one head as it is"); |
|
| 75 | 92 | assert_eq!(crate::web::state_label("web, with no approval"), "web, with no approval"); |
|
| 76 | 93 | assert_eq!(crate::web::state_label("C++ is fine"), "C++ is fine", "not heads"); |
|
| 94 | + | } |
|
| 95 | + | } |
|
| 96 | + | ||
| 97 | + | #[cfg(test)] |
|
| 98 | + | mod ends_tests { |
|
| 99 | + | #[test] |
|
| 100 | + | fn long_values_keep_both_ends() { |
|
| 101 | + | assert_eq!(super::cut_ends("main"), "main"); |
|
| 102 | + | let h = "0826ac0cca8ca616b79f8fe6f81ff21a41daca9d0c1f05501c146d9299445564"; |
|
| 103 | + | assert_eq!(super::cut_ends(h), "0826ac0c…99445564"); |
|
| 104 | + | let two = format!("{h}+1e470ca931015aa55dbb9514cf34e9e5c40bede11241eedf23c6576d23d2609e"); |
|
| 105 | + | assert_eq!(super::cut_ends(&two), "0826ac0c…23d2609e"); |
|
| 77 | 106 | } |
|
| 78 | 107 | } |
modified crates/hub/tests/browse.rs +4 −0
| … | 154 unchanged lines |
||
| 155 | 155 | assert!(at_tag.contains("README.md") && at_tag.contains("class=\"ref-pick\"") && at_tag.contains("side cards"), "{at_tag}"); |
|
| 156 | 156 | assert!(at_tag.contains("href=\"/andy/demo?at=v0.1\" class=\"on\""), "the tag is the one chosen"); |
|
| 157 | 157 | assert!(page(&h.app, &format!("/andy/demo?at={}", &c1[..10])).await.contains("README.md")); |
|
| 158 | + | // A whole hash shows its two ends, the rest in a tip (#198). |
|
| 159 | + | let at_hash = page(&h.app, &format!("/andy/demo?at={c1}")).await; |
|
| 160 | + | let ends = format!("{}…{}", &c1[..8], &c1[c1.len() - 8..]); |
|
| 161 | + | assert!(at_hash.contains(&format!("data-full=\"{c1}\"")) && at_hash.contains(&format!(">{ends}</span>")), "{at_hash}"); |
|
| 158 | 162 | assert_eq!(get(&h.app, "/andy/demo/tree/main/nope").await.0, StatusCode::NOT_FOUND); |
|
| 159 | 163 | let (hits_before, _, _) = oxygenhub::render::highlight::cache_stats(); |
|
| 160 | 164 | let f = page(&h.app, "/andy/demo/file/main/src/main.rs").await; |
|
| … | 139 unchanged lines |
modified templates/overview.html +2 −2
| … | 7 unchanged lines |
||
| 8 | 8 | <div data-backdrop-avoid> |
|
| 9 | 9 | <div class="side-layout rev"> |
|
| 10 | 10 | <div> |
|
| 11 | - | {% if o.heads > 1 %}<div class="strip warn">{{ at }} has {{ o.heads }} heads; the tree below is their union. <a href="{{ repo.url }}/bookmarks">Bookmarks</a></div>{% endif %} |
|
| 11 | + | {% if o.heads > 1 %}<div class="strip warn">{% if at.len() > 24 %}<span class="hash-tip" tabindex="0" data-full="{{ at }}" aria-label="{{ at }}">{{ at|ends }}</span>{% else %}{{ at }}{% endif %} has {{ o.heads }} heads; the tree below is their union. <a href="{{ repo.url }}/bookmarks">Bookmarks</a></div>{% endif %} |
|
| 12 | 12 | {% if o.conflicted %}<div class="strip bad">This state carries conflicts. <a href="{{ repo.url }}/conflicts">See them</a></div>{% endif %} |
|
| 13 | 13 | <div class="bar"> |
|
| 14 | 14 | <details class="ref-pick"> |
|
| 15 | - | <summary class="go small" aria-haspopup="true" title="Choose a bookmark or tag"><span aria-hidden="true">{% if o.tree.target.kind == crate::repos::view::RefKind::Tag %}🏷{% else %}◆{% endif %}</span> {{ at }} <span aria-hidden="true">▾</span></summary> |
|
| 15 | + | <summary class="go small" aria-haspopup="true" title="Choose a bookmark or tag"><span aria-hidden="true">{% if o.tree.target.kind == crate::repos::view::RefKind::Tag %}🏷{% else %}◆{% endif %}</span> {% if at.len() > 24 %}<span class="hash-tip" tabindex="0" data-full="{{ at }}" aria-label="{{ at }}">{{ at|ends }}</span>{% else %}{{ at }}{% endif %} <span aria-hidden="true">▾</span></summary> |
|
| 16 | 16 | <div class="ref-panel" role="menu"> |
|
| 17 | 17 | <p class="small mute">Bookmarks</p> |
|
| 18 | 18 | <ul class="plain">{% for b in o.bookmarks %}<li><a role="menuitem" href="{{ repo.url }}{% if b.name != repo.default_bookmark %}?at={{ b.name|urlencode }}{% endif %}"{% if b.name == at %} class="on"{% endif %}>◆ {{ b.name }}{% if b.name == repo.default_bookmark %} <span class="tag">default</span>{% endif %}</a></li>{% endfor %}</ul> |
|
| … | 95 unchanged lines |
modified templates/style.css +5 −0
| … | 1244 unchanged lines |
||
| 1245 | 1245 | .wiki-side-col .clone input { flex: 1; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; } |
|
| 1246 | 1246 | .wiki-doc h1[id], .wiki-doc h2[id], .wiki-doc h3[id] { scroll-margin-top: 16px; } |
|
| 1247 | 1247 | .EasyMDEContainer .editor-toolbar button.mde-btn.mde-text, .EasyMDEContainer .editor-toolbar button.mde-btn.mde-text::first-letter { text-transform: none; } |
|
| 1248 | + | /* A long hash or state address shows its two ends; the whole of it in a |
|
| 1249 | + | wrapped, square-ish tip on hover or focus (oxygenhub#198). */ |
|
| 1250 | + | .hash-tip { position: relative; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .92em; cursor: help; border-bottom: 1px dotted currentColor; white-space: nowrap; } |
|
| 1251 | + | .hash-tip::after { content: attr(data-full); position: absolute; z-index: 40; top: calc(100% + 6px); left: 0; width: min(34ch, 80vw); padding: 8px 10px; background: var(--panel); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; box-shadow: 0 8px 24px rgba(0,0,0,.15); white-space: normal; overflow-wrap: anywhere; word-break: break-all; line-height: 1.45; font-size: 12px; visibility: hidden; opacity: 0; transition: opacity .12s; pointer-events: none; text-transform: none; font-weight: 400; } |
|
| 1252 | + | .hash-tip:hover::after, .hash-tip:focus::after { visibility: visible; opacity: 1; } |