fix(web): a long state no longer runs across the sidebar (closes #170) 6ae3fa0af4f6
A state of many heads read as one unbroken token: Diffusion wrote it whole into its bump's description and its 'updated' event, and the proposal's timeline printed it as it was. Now a state reads as its first head and a count (02a6c9d4746a+… (14 heads)), in Diffusion's text and, for events already stored, through the timeline's state_label filter with the whole in a title; timeline text and inline code wrap inside their cards.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 22:42 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
6ae3fa0af4f6a05484806de2da4e8900cb434f25207a904dbceb675160c58b5a- Depends on
- 18e381ea88c2 1b6536f06132 9aaa88b2d9c8 a1b9869565aa c4e432da4bc9 c9aacc0e7dc1 d23464c0ac60 d7d182467382 de505df5cf8d f677d6270298
- Depended on by
- 16ee5d19d220 965b950f9b96 62cdf34daec1 3dd6582de060 addec2bd6423 00ef7b83b340 f4d6ef418a96
- 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.0 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
- 9 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified crates/hub/src/diffusion.rs +2 −2
| … | 722 unchanged lines |
||
| 723 | 723 | }; |
|
| 724 | 724 | let body = format!( |
|
| 725 | 725 | "Moves the subrepo `{path}` to [{slug}](/{slug})'s `{ub}`, {when} (`{}`). Only the pin changes; this repository's own checks test it.{amber}{flagged}\n\n**What it brings** ({count}):\n\n{}\n\nOpened and kept up to date by Diffusion (docs/diffusion.md). It never lands by itself.", |
|
| 726 | - | short.join("+"), |
|
| 726 | + | crate::web::state_label(&short.join("+")), |
|
| 727 | 727 | lines.join("\n") |
|
| 728 | 728 | ); |
|
| 729 | 729 | let open = pdb::list(&app.db, consumer.id, &["open", "draft"]).await.ok()?.into_iter().find(|p| p.source_bookmark == bookmark); |
|
| … | 1 unchanged lines |
||
| 731 | 731 | Some(p) => { |
|
| 732 | 732 | if p.title != title || p.body != body { |
|
| 733 | 733 | pdb::edit(&app.db, p.id, &title, &body, target).await.ok()?; |
|
| 734 | - | pdb::add_event(&app.db, p.id, ACCOUNT, "updated", &short.join("+")).await.ok(); |
|
| 734 | + | pdb::add_event(&app.db, p.id, ACCOUNT, "updated", &crate::web::state_label(&short.join("+"))).await.ok(); |
|
| 735 | 735 | } |
|
| 736 | 736 | (p.number, p.id) |
|
| 737 | 737 | } |
|
| … | 389 unchanged lines |
modified crates/hub/src/web/filters.rs +19 −0
| … | 43 unchanged lines |
||
| 44 | 44 | Ok(render::bytes(n.as_u64())) |
|
| 45 | 45 | } |
|
| 46 | 46 | ||
| 47 | + | /// A state written as its heads joined by `+` (oxygenhub#170), shortened to |
|
| 48 | + | /// its first head and how many there are; any other text as it is. |
|
| 49 | + | pub fn state_label(text: &str, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 50 | + | Ok(crate::web::state_label(text)) |
|
| 51 | + | } |
|
| 52 | + | ||
| 47 | 53 | pub fn short(hash: &str, _: &dyn askama::Values) -> askama::Result<String> { |
|
| 48 | 54 | Ok(hash.chars().take(12).collect()) |
|
| 49 | 55 | } |
|
| … | 6 unchanged lines |
||
| 56 | 62 | pub fn plural(n: impl Num, _: &dyn askama::Values, one: &str, many: &str) -> askama::Result<String> { |
|
| 57 | 63 | let n = n.as_u64(); |
|
| 58 | 64 | Ok(format!("{n} {}", if n == 1 { one } else { many })) |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | #[cfg(test)] |
|
| 68 | + | mod state_label_tests { |
|
| 69 | + | /// oxygenhub#170: a state of many heads reads as its first and a count. |
|
| 70 | + | #[test] |
|
| 71 | + | fn a_long_state_reads_as_its_first_head_and_a_count() { |
|
| 72 | + | let heads = ["02a6c9d4746a", "1ac2fb7fa2f8", "1c861f2583b8"].join("+"); |
|
| 73 | + | assert_eq!(crate::web::state_label(&heads), "02a6c9d4746a+… (3 heads)"); |
|
| 74 | + | assert_eq!(crate::web::state_label("02a6c9d4746a"), "02a6c9d4746a", "one head as it is"); |
|
| 75 | + | assert_eq!(crate::web::state_label("web, with no approval"), "web, with no approval"); |
|
| 76 | + | assert_eq!(crate::web::state_label("C++ is fine"), "C++ is fine", "not heads"); |
|
| 77 | + | } |
|
| 59 | 78 | } |
modified crates/hub/src/web/mod.rs +12 −0
| … | 18 unchanged lines |
||
| 19 | 19 | pub mod history; |
|
| 20 | 20 | pub mod home; |
|
| 21 | 21 | pub mod icons; |
|
| 22 | + | ||
| 23 | + | /// A state as people read it (oxygenhub#170): `02a6c9d4746a+… (14 heads)` |
|
| 24 | + | /// for heads joined by `+`, which can run to hundreds of characters; a |
|
| 25 | + | /// single head, or text that is not heads, unchanged. |
|
| 26 | + | pub fn state_label(text: &str) -> String { |
|
| 27 | + | let parts: Vec<&str> = text.split('+').collect(); |
|
| 28 | + | let heads = parts.len() > 1 && parts.iter().all(|p| p.len() >= 7 && p.chars().all(|c| c.is_ascii_hexdigit())); |
|
| 29 | + | if !heads { |
|
| 30 | + | return text.to_string(); |
|
| 31 | + | } |
|
| 32 | + | format!("{}+… ({} heads)", &parts[0][..12.min(parts[0].len())], parts.len()) |
|
| 33 | + | } |
|
| 22 | 34 | mod inbox; |
|
| 23 | 35 | pub mod issues; |
|
| 24 | 36 | mod orgs; |
|
| … | 726 unchanged lines |
modified templates/proposal.html +1 −1
| … | 115 unchanged lines |
||
| 116 | 116 | </div> |
|
| 117 | 117 | </div> |
|
| 118 | 118 | {% else %}{% let e = events[t.index] %} |
|
| 119 | - | <p class="tl-event"><span class="tl-dot"></span><img class="tl-mini" src="/avatar/{{ e.actor }}" alt="" width="18" height="18"> <strong>{{ e.actor }}</strong> {{ e.event }}{% if e.event.ends_with("by change") && !e.detail.is_empty() %}{% let hash = e.detail.split(' ').next().unwrap_or("") %} <a href="{{ repo.url }}/change/{{ hash }}"><code>{{ hash }}</code></a>{{ e.detail[hash.len()..] }}{% else if !e.detail.is_empty() %}: {{ e.detail }}{% endif %} <time class="mute" title="{{ e.at_ms|when_i }}">{{ e.at_ms|ago_i }}</time></p> |
|
| 119 | + | <p class="tl-event"><span class="tl-dot"></span><img class="tl-mini" src="/avatar/{{ e.actor }}" alt="" width="18" height="18"> <strong>{{ e.actor }}</strong> {{ e.event }}{% if e.event.ends_with("by change") && !e.detail.is_empty() %}{% let hash = e.detail.split(' ').next().unwrap_or("") %} <a href="{{ repo.url }}/change/{{ hash }}"><code>{{ hash }}</code></a>{{ e.detail[hash.len()..] }}{% else if !e.detail.is_empty() %}: <span title="{{ e.detail }}">{{ e.detail|state_label }}</span>{% endif %} <time class="mute" title="{{ e.at_ms|when_i }}">{{ e.at_ms|ago_i }}</time></p> |
|
| 120 | 120 | {% endif %} |
|
| 121 | 121 | {% endfor %} |
|
| 122 | 122 | {% if p.state == "landed" %} |
|
| … | 128 unchanged lines |
modified templates/style.css +5 −0
| … | 1168 unchanged lines |
||
| 1169 | 1169 | .backup-status.st-failed { color: var(--bad); } |
|
| 1170 | 1170 | .backup-status.st-running { color: var(--mute); } |
|
| 1171 | 1171 | .backup-what { max-width: 760px; } |
|
| 1172 | + | ||
| 1173 | + | /* Nothing runs out of its card (#170): long tokens in the timeline and in |
|
| 1174 | + | inline code wrap where they must. */ |
|
| 1175 | + | .tl-event, .tl-event code { overflow-wrap: anywhere; } |
|
| 1176 | + | article.doc :not(pre) > code, .comment :not(pre) > code, .tl-card :not(pre) > code { overflow-wrap: anywhere; } |