OXYGENHUB

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; }