Jump to…
snowfeat: given a new redesign and emulated terminal homepagerxkspqmsoknz1mo
Matt W1//! Page bodies for the M0 surface.
Matt W2
Matt W3use chrono::{DateTime, Utc};
Matt W4use maud::{html, Markup};
Matt W5
Matt W6use df_db::models::User;
Matt W7
Matt W8use crate::views::change::change_chip;
Matt W9use crate::views::relative_time;
Matt W10
Matt W11/// One row of the public "shipping right now" feed.
Matt W12///
Matt W13/// Sourced from the event log rather than from `changes.updated_at`, so the row
Matt W14/// can say what actually happened — pushed, approved, merged, conflicted —
Matt W15/// instead of the uninformative "was touched" that a timestamp alone supports.
Matt W16pub struct FeedItem {
Matt W17 pub owner: String,
Matt W18 pub repo: String,
Matt W19 pub number: i64,
Matt W20 pub change_id: String,
Matt W21 pub synthetic: bool,
Matt W22 pub title: String,
Matt W23 /// `None` for an event by somebody with no Dogfood account — the indexer
Matt W24 /// records those, and dropping the row would misrepresent activity.
Matt W25 pub actor: Option<String>,
Matt W26 /// The name the commit itself carries, used when no account matched.
Matt W27 pub actor_name: Option<String>,
Matt W28 /// The event kind, e.g. `change.pushed`.
Matt W29 pub kind: String,
Matt W30 pub when: DateTime<Utc>,
Matt W31}
Matt W32
Matt W33/// An open change that is part of a stack — the "in flight now" list.
Matt W34pub struct StackItem {
Matt W35 pub owner: String,
Matt W36 pub repo: String,
Matt W37 pub number: i64,
Matt W38 pub change_id: String,
Matt W39 pub synthetic: bool,
Matt W40 pub title: String,
Matt W41 pub conflicted: bool,
Matt W42 pub when: DateTime<Utc>,
Matt W43}
Matt W44
Matt W45/// A bookmark and where it currently points.
Matt W46pub struct BookmarkItem {
Matt W47 pub owner: String,
Matt W48 pub repo: String,
Matt W49 pub name: String,
Matt W50 pub protected: bool,
Matt W51 pub updated_at: DateTime<Utc>,
Matt W52}
Matt W53
Matt W54/// The glyph and colour class for an event kind.
Matt W55///
Matt W56/// An unrecognised kind gets the neutral glyph rather than being dropped: an
Matt W57/// event the UI does not know about still happened.
Matt W58fn feed_glyph(kind: &str) -> (&'static str, &'static str) {
Matt W59 match kind {
Matt W60 "change.pushed" => ("↑", "is-push"),
Matt W61 "change.opened" | "change.reopened" | "change.ready" => ("○", "is-open"),
Matt W62 "change.reviewed" => ("✓", "is-review"),
Matt W63 "change.resolved" => ("✓", "is-review"),
Matt W64 "change.merged" => ("⤳", "is-merge"),
Matt W65 "change.conflicted" => ("◆", "is-conflict"),
Matt W66 "change.abandoned" => ("×", "is-abandon"),
Matt W67 _ => ("●", "is-open"),
Matt W68 }
Matt W69}
Matt W70
Matt W71/// Render whoever is responsible for something.
Matt W72///
Matt W73/// Three cases, in descending order of what we actually know:
Matt W74/// a matched account links to its profile; an unmatched commit shows the name
Matt W75/// git recorded, which is a real person even though we cannot link them; and
Matt W76/// only a commit with no author name at all falls through to "someone".
Matt W77///
Matt W78/// The middle case is the common one on a fresh instance — an account links to
Matt W79/// commits by email, and until the account records an email nothing matches —
Matt W80/// so showing the git name rather than "someone" is the difference between a
Matt W81/// readable history and an anonymous one.
Matt W82pub fn actor(handle: Option<&str>, name: Option<&str>) -> Markup {
Matt W83 html! {
Matt W84 span .feed-actor { (crate::views::person(handle, name)) }
Matt W85 }
Matt W86}
Matt W87
Matt W88/// The pitch, stated as a diff. Deliberately not configurable: it is copy.
Matt W89const COMPARISONS: &[(&str, &str)] = &[
Matt W90 ("detached HEAD, lost work", "every state is recoverable"),
Matt W91 ("rebase hell across a stack", "stacks rebase themselves"),
Matt W92 ("merge conflicts block you", "conflicts are just a state"),
Matt W93 ("force-push rewrites history", "stable ids survive rewrites"),
Matt W94];
Matt W95
Matt W96const TICKER: &[&str] = &[
Matt W97 "changes, not commits",
Matt W98 "conflicts are first-class",
Matt W99 "stacks that actually stack",
Matt W100 "stable change ids",
Matt W101 "undo anything",
Matt W102 "no staging area",
Matt W103];
Matt W104
Matt W105/// Everything the landing page renders.
Matt W106pub struct Landing<'a> {
Matt W107 /// The instance's own clone URL, e.g. `jj git clone https://host/owner/repo`.
Matt W108 pub clone_hint: &'a str,
Matt W109 /// `owner/name` of a real public repository, used in the example session so
Matt W110 /// the transcript names something a visitor can actually go and open.
Matt W111 pub sample_repo: Option<&'a str>,
Matt W112 pub feed: &'a [FeedItem],
Matt W113 pub repos: &'a [RepoSummary],
Matt W114 /// Open public changes that are part of a stack.
Matt W115 pub in_flight: &'a [StackItem],
Matt W116 /// Recently moved bookmarks across public repositories.
Matt W117 pub bookmarks: &'a [BookmarkItem],
Matt W118}
Matt W119
Matt W120/// Landing page for signed-out visitors.
Matt W121pub fn landing(l: Landing<'_>) -> Markup {
Matt W122 let now = Utc::now();
Matt W123 let first_repo = l.repos.first().map(|r| format!("/{}/{}", r.owner, r.name));
Matt W124
Matt W125 html! {
Matt W126 section .band.band-surface.hero aria-labelledby="hero-h" {
Matt W127 div .wrap {
Matt W128 div .hero-body {
Matt W129 span .hero-eyebrow.label-condensed { "code hosting on jujutsu" }
Matt W130 h1 #hero-h .display.hero-title { "A branch is a pointer. Your work is not." }
Matt W131 p .hero-lede {
Matt W132 "Push with " span .mono { "jj git push" } " and review opens itself. \
Matt W133 The " span style="color:var(--text)" { "change" } " keeps one identity \
Matt W134 through every amend, rebase, and force-push."
Matt W135 }
Matt W136 div .hero-actions {
Matt W137 a .btn.btn-primary href="/login" { "Start for free" }
Matt W138 @if let Some(href) = &first_repo {
Matt W139 a .btn href=(href) { "Browse the code" }
Matt W140 }
Matt W141 }
Matt W142 div .clone-box {
Matt W143 span .prompt aria-hidden="true" { "$" }
Matt W144 code { (l.clone_hint) }
Matt W145 }
Matt W146 }
Matt W147 (terminal(l.sample_repo))
Matt W148 }
Matt W149 }
Matt W150
Matt W151 // Decorative taglines. Static text rather than a marquee — it wraps
Matt W152 // instead of scrolling, so nothing moves for a reader who did not ask
Matt W153 // for motion.
Matt W154 div .band.ticker aria-hidden="true" {
Matt W155 @for t in TICKER {
Matt W156 span .ticker-item {
Matt W157 span .ticker-dot { "◆" }
Matt W158 (t)
Matt W159 }
Matt W160 }
Matt W161 }
Matt W162
Matt W163 div .band { div .wrap {
Matt W164 div .columns {
Matt W165 section .columns-main aria-labelledby="feed-h" {
Matt W166 div .section-head {
Matt W167 h2 #feed-h style="margin:0;font-size:var(--text-lg);line-height:28px" { "Live" }
Matt W168 span .live-indicator {
Matt W169 span .live-dot aria-hidden="true" {}
Matt W170 "public activity"
Matt W171 }
Matt W172 }
Matt W173 @if l.feed.is_empty() {
Matt W174 div .empty {
Matt W175 h2 { "Nothing public yet" }
Matt W176 p { "Activity in public repositories will show up here." }
Matt W177 }
Matt W178 } @else {
Matt W179 ul .feed {
Matt W180 @for item in l.feed { (feed_row(item, now)) }
Matt W181 }
Matt W182 div .feed-foot {
Matt W183 span {
Matt W184 "Every row is a change id. The commit behind it may be \
Matt W185 rewritten; the row will not move."
Matt W186 }
Matt W187 }
Matt W188 }
Matt W189 }
Matt W190
Matt W191 aside .columns-aside {
Matt W192 @if !l.in_flight.is_empty() {
Matt W193 section .aside-block aria-labelledby="inflight-h" {
Matt W194 h2 #inflight-h .label-condensed { "In flight now" }
Matt W195 @for s in l.in_flight { (stack_mini_row(s, now)) }
Matt W196 }
Matt W197 }
Matt W198 @if !l.bookmarks.is_empty() {
Matt W199 section .aside-block aria-labelledby="marks-h" {
Matt W200 h2 #marks-h .label-condensed { "Bookmarks" }
Matt W201 @for b in l.bookmarks { (bookmark_line(b, now)) }
Matt W202 }
Matt W203 }
Matt W204 }
Matt W205 }
Matt W206 } }
Matt W207
Matt W208 section .band.band-surface aria-labelledby="why-h" {
Matt W209 div .wrap {
Matt W210 div .band-head {
Matt W211 h2 #why-h { "Why switch" }
Matt W212 span .band-note { "Four things a branch pointer cannot represent." }
Matt W213 }
Matt W214 ul .compare {
Matt W215 @for (them, us) in COMPARISONS {
Matt W216 li .compare-item {
Matt W217 span .compare-them {
Matt W218 span .compare-sign aria-hidden="true" { "−" }
Matt W219 (them)
Matt W220 }
Matt W221 span .compare-us {
Matt W222 span .compare-sign aria-hidden="true" { "+" }
Matt W223 (us)
Matt W224 }
Matt W225 }
Matt W226 }
Matt W227 }
Matt W228 }
Matt W229 }
Matt W230
Matt W231 @if !l.repos.is_empty() {
Matt W232 section .band aria-labelledby="repos-h" {
Matt W233 div .wrap {
Matt W234 div .band-head {
Matt W235 h2 #repos-h { "Explore public repos" }
Matt W236 span .band-note {
Matt W237 "No account needed to read a repo, a change, or a stack."
Matt W238 }
Matt W239 }
Matt W240 div .repo-cards {
Matt W241 @for r in l.repos { (repo_card(r, now)) }
Matt W242 }
Matt W243 }
Matt W244 }
Matt W245 }
Matt W246
Matt W247 section .band.band-surface.cta aria-labelledby="cta-h" {
Matt W248 div .wrap {
Matt W249 div {
Matt W250 h2 #cta-h .cta-title { "Stop fighting your version control." }
Matt W251 p .cta-lede {
Matt W252 "Bring your team to a forge that matches how you already work."
Matt W253 }
Matt W254 }
Matt W255 span .spacer {}
Matt W256 div .hero-actions {
Matt W257 a .btn.btn-primary href="/login" { "Start for free" }
Matt W258 @if let Some(href) = &first_repo {
Matt W259 a .btn href=(href) { "Browse a repo first" }
Matt W260 }
Matt W261 }
Matt W262 }
Matt W263 }
Matt W264 }
Matt W265}
Matt W266
Matt W267/// The hero's terminal panel.
Matt W268///
Matt W269/// A transcript of a `jj` session that animates like a live terminal: commands
Matt W270/// type out character by character, and output lines reveal after each command
Matt W271/// finishes. The animation is driven by `terminal.js` using data attributes
Matt W272/// on each line, respects `prefers-reduced-motion`, and only plays once when
Matt W273/// the terminal scrolls into view.
Matt W274fn terminal(sample_repo: Option<&str>) -> Markup {
Matt W275 let repo = sample_repo.unwrap_or("your-org/your-repo");
Matt W276
Matt W277 html! {
Matt W278 figure .term {
Matt W279 div .term-bar {
Matt W280 span .term-dots aria-hidden="true" { span {} span {} span {} }
Matt W281 span .term-host { "~/" (repo.rsplit('/').next().unwrap_or("repo")) }
Matt W282 span .spacer {}
Matt W283 figcaption .label-condensed { "example session" }
Matt W284 }
Matt W285 pre .term-body data-term-animate="" {
Matt W286 span .term-cmd data-term-line="" data-term-cmd="" data-term-text="jj new main -m 'Cache shortest-unique prefixes per repo'" { "jj new main -m 'Cache shortest-unique prefixes per repo'" } "\n"
Matt W287 span .term-out data-term-line="" { "Working copy now at: vlrxqmpd 8f21c0ab (empty) Cache shortest-unique prefixes" } "\n"
Matt W288 span .term-out.is-faint data-term-line="" { "Parent commit : mrukztqx c40d1f8 main | Drop the legacy branch-tip fallback" } "\n"
Matt W289 span data-term-line="" data-term-blank="" { "" } "\n"
Matt W290 span .term-cmd data-term-line="" data-term-cmd="" data-term-text="jj status" { "jj status" } "\n"
Matt W291 span .term-out data-term-line="" { "Working copy changes:" } "\n"
Matt W292 span .term-out.is-add data-term-line="" { "M crates/store/prefix.rs" } "\n"
Matt W293 span .term-out.is-add data-term-line="" { "A crates/store/prefix_cache.rs" } "\n"
Matt W294 span data-term-line="" data-term-blank="" { "" } "\n"
Matt W295 span .term-cmd data-term-line="" data-term-cmd="" data-term-text="jj git push --change @" { "jj git push --change @" } "\n"
Matt W296 span .term-out data-term-line="" { "Changes to push to origin:" } "\n"
Matt W297 span .term-out.is-add data-term-line="" { " Add bookmark push-vlrxqmpd to 8f21c0ab" } "\n"
Matt W298 span .term-out.is-action data-term-line="" { "remote: change vlrx is open for review" } "\n"
Matt W299 span .term-out.is-action data-term-line="" { "remote: /" (repo) "/changes/vlrx" } "\n"
Matt W300 span data-term-line="" data-term-blank="" { "" } "\n"
Matt W301 span .term-cmd data-term-line="" data-term-cmd="" data-term-text="jj log -r 'stack(@)'" { "jj log -r 'stack(@)'" } "\n"
Matt W302 span .term-out.is-conflict data-term-line="" { "◆ zmltxruqpvks conflict in revset.rs" } "\n"
Matt W303 span .term-out.is-open data-term-line="" { "○ wpqvkrtnmzox approved" } "\n"
Matt W304 span .term-out.is-identity data-term-line="" { "@ vlrxqmpdtwzk open for review" } "\n"
Matt W305 span .term-out.is-faint data-term-line="" { "┴─ main c40d1f8" } "\n"
Matt W306 }
Matt W307 div .term-foot {
Matt W308 "Push a bookmark, the change opens for review. No web form, no PR button."
Matt W309 }
Matt W310 }
Matt W311 }
Matt W312}
Matt W313
Matt W314/// One feed row.
Matt W315///
Matt W316/// The exact timestamp goes in `title` because the visible label is coarse on
Matt W317/// purpose — see `relative_time`.
Matt W318fn feed_row(item: &FeedItem, now: DateTime<Utc>) -> Markup {
Matt W319 let href = format!("/{}/{}/changes/{}", item.owner, item.repo, item.number);
Matt W320 let (glyph, class) = feed_glyph(&item.kind);
Matt W321
Matt W322 html! {
Matt W323 li .feed-row {
Matt W324 span .feed-glyph.(class) aria-hidden="true" { (glyph) }
Matt W325 a .feed-repo href=(format!("/{}/{}", item.owner, item.repo)) {
Matt W326 span .owner { (item.owner) }
Matt W327 "/" (item.repo)
Matt W328 }
Matt W329 span .feed-verb {
Matt W330 (actor(item.actor.as_deref(), item.actor_name.as_deref()))
Matt W331 " " (activity_verb(&item.kind))
Matt W332 }
Matt W333 a .feed-title href=(href) { (item.title) }
Matt W334 (change_chip(&item.change_id, item.synthetic))
Matt W335 span .feed-age title=(item.when.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W336 (relative_time(item.when, now))
Matt W337 }
Matt W338 }
Matt W339 }
Matt W340}
Matt W341
Matt W342/// A change in an aside list, with the stack rail beside it.
Matt W343fn stack_mini_row(s: &StackItem, now: DateTime<Utc>) -> Markup {
Matt W344 let href = format!("/{}/{}/changes/{}", s.owner, s.repo, s.number);
Matt W345 let (glyph, class) = if s.conflicted {
Matt W346 ("◆", "is-conflict")
Matt W347 } else {
Matt W348 ("○", "is-open")
Matt W349 };
Matt W350
Matt W351 html! {
Matt W352 a .mini-row href=(href) {
Matt W353 span .mini-rail aria-hidden="true" {}
Matt W354 span .mini-glyph.(class) aria-hidden="true" { (glyph) }
Matt W355 (change_chip(&s.change_id, s.synthetic))
Matt W356 span .mini-title { (s.title) }
Matt W357 span .mini-age title=(s.when.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W358 (relative_time(s.when, now))
Matt W359 }
Matt W360 }
Matt W361 }
Matt W362}
Matt W363
Matt W364/// A bookmark and when it last moved.
Matt W365///
Matt W366/// Links to its repository's bookmark list rather than to the bookmark itself:
Matt W367/// a bookmark has no page of its own, because it is a pointer, not a thing —
Matt W368/// which is the distinction the whole product turns on.
Matt W369fn bookmark_line(b: &BookmarkItem, now: DateTime<Utc>) -> Markup {
Matt W370 html! {
Matt W371 a .bookmark-line href=(format!("/{}/{}/bookmarks", b.owner, b.repo)) {
Matt W372 span .chip { (b.name) }
Matt W373 @if b.protected {
Matt W374 span .bookmark-flag { "protected" }
Matt W375 }
Matt W376 span .spacer {}
Matt W377 span .mini-age title=(b.updated_at.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W378 (relative_time(b.updated_at, now))
Matt W379 }
Matt W380 }
Matt W381 }
Matt W382}
Matt W383
Matt W384fn repo_card(r: &RepoSummary, now: DateTime<Utc>) -> Markup {
Matt W385 html! {
Matt W386 a .repo-card href=(format!("/{}/{}", r.owner, r.name)) {
Matt W387 span .repo-card-name {
Matt W388 (r.owner) "/" (r.name)
Matt W389 @if r.private { " " span .chip { "private" } }
Matt W390 }
Matt W391 @if let Some(d) = &r.description {
Matt W392 span .repo-card-desc { (d) }
Matt W393 }
Matt W394 span .repo-card-meta {
Matt W395 @if r.open_changes > 0 {
Matt W396 span .is-open { "○ " (r.open_changes) }
Matt W397 }
Matt W398 @if r.conflicted > 0 {
Matt W399 span .is-conflict { "◆ " (r.conflicted) }
Matt W400 }
Matt W401 @if let Some(t) = r.pushed_at {
Matt W402 span .at-end title=(t.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W403 (relative_time(t, now))
Matt W404 }
Matt W405 }
Matt W406 }
Matt W407 }
Matt W408 }
Matt W409}
Matt W410
Matt W411/// A change as it appears in a dashboard list.
Matt W412pub struct DashChange {
Matt W413 pub owner: String,
Matt W414 pub repo: String,
Matt W415 pub number: i64,
Matt W416 pub change_id: String,
Matt W417 pub synthetic: bool,
Matt W418 pub title: String,
Matt W419 pub state: String,
Matt W420 pub conflicted: bool,
Matt W421 pub author: Option<String>,
Matt W422 /// The name the commit itself carries, used when no account matched.
Matt W423 pub author_name: Option<String>,
Matt W424 pub updated_at: DateTime<Utc>,
Matt W425}
Matt W426
Matt W427/// One row of the watched-activity feed.
Matt W428pub struct ActivityItem {
Matt W429 pub owner: String,
Matt W430 pub repo: String,
Matt W431 pub actor: Option<String>,
Matt W432 pub kind: String,
Matt W433 /// The change this event was about, when it was about one.
Matt W434 pub change_number: Option<i64>,
Matt W435 pub change_title: Option<String>,
Matt W436 pub when: DateTime<Utc>,
Matt W437}
Matt W438
Matt W439/// Everything the dashboard renders.
Matt W440pub struct Dashboard<'a> {
Matt W441 pub user: &'a User,
Matt W442 /// Open changes the viewer did not write and has not yet reviewed.
Matt W443 pub awaiting: &'a [DashChange],
Matt W444 pub mine: &'a [DashChange],
Matt W445 pub activity: &'a [ActivityItem],
Matt W446 pub repos: &'a [RepoSummary],
Matt W447}
Matt W448
Matt W449/// Signed-in dashboard.
Matt W450///
Matt W451/// The same two-column shape as the landing page, with the marketing bands
Matt W452/// removed and the personalised lists in their place. The aside answers "what
Matt W453/// is waiting on me" first, because that is the only question a dashboard is
Matt W454/// actually for.
Matt W455pub fn dashboard(d: Dashboard<'_>) -> Markup {
Matt W456 let now = Utc::now();
Matt W457
Matt W458 html! {
Matt W459 div .dash-head {
Matt W460 div {
Matt W461 h1 .dash-title {
Matt W462 "Welcome back, " span .dash-name { (d.user.label()) }
Matt W463 }
Matt W464 p .dim.section-note { "Here's what needs you across your repositories." }
Matt W465 }
Matt W466 span .spacer {}
Matt W467 a .btn.btn-primary href="/new" { "New repository" }
Matt W468 }
Matt W469
Matt W470 @if d.repos.is_empty() {
Matt W471 div .empty {
Matt W472 h2 { "No repositories yet" }
Matt W473 p { "Create one, then push to it with " code { "jj git push" } " or " code { "git push" } "." }
Matt W474 p { a .btn.btn-primary href="/new" { "New repository" } }
Matt W475 }
Matt W476 } @else {
Matt W477 div .columns {
Matt W478 div .columns-main {
Matt W479 section .dash-section aria-labelledby="awaiting-h" {
Matt W480 div .section-head {
Matt W481 h2 #awaiting-h .label-condensed { "Awaiting your review" }
Matt W482 span .live-indicator.tnum { (d.awaiting.len()) }
Matt W483 }
Matt W484 @if d.awaiting.is_empty() {
Matt W485 p .dim.section-note { "Nothing is waiting on you." }
Matt W486 } @else {
Matt W487 ul .feed {
Matt W488 @for c in d.awaiting { (dash_change_row(c, now)) }
Matt W489 }
Matt W490 }
Matt W491 }
Matt W492
Matt W493 section .dash-section aria-labelledby="mine-h" {
Matt W494 div .section-head {
Matt W495 h2 #mine-h .label-condensed { "Your open changes" }
Matt W496 }
Matt W497 @if d.mine.is_empty() {
Matt W498 p .dim.section-note { "You have no open changes." }
Matt W499 } @else {
Matt W500 ul .feed {
Matt W501 @for c in d.mine { (dash_change_row(c, now)) }
Matt W502 }
Matt W503 }
Matt W504 }
Matt W505
Matt W506 section .dash-section aria-labelledby="activity-h" {
Matt W507 div .section-head {
Matt W508 h2 #activity-h .label-condensed { "Watched activity" }
Matt W509 }
Matt W510 @if d.activity.is_empty() {
Matt W511 p .dim.section-note { "No recent activity in your repositories." }
Matt W512 } @else {
Matt W513 ul .activity {
Matt W514 @for a in d.activity { (activity_row(a, now)) }
Matt W515 }
Matt W516 }
Matt W517 }
Matt W518 }
Matt W519
Matt W520 aside .columns-aside aria-labelledby="dash-repos-h" {
Matt W521 section .aside-block {
Matt W522 h2 #dash-repos-h .label-condensed { "Your repositories" }
Matt W523 div .repo-cards style="grid-template-columns:minmax(0,1fr)" {
Matt W524 @for r in d.repos { (repo_card(r, now)) }
Matt W525 a .repo-card-new href="/new" { "new repository" }
Matt W526 }
Matt W527 }
Matt W528 }
Matt W529 }
Matt W530 }
Matt W531 }
Matt W532}
Matt W533
Matt W534/// A change in one of the dashboard's lists.
Matt W535///
Matt W536/// Same row grammar as the public feed — glyph, where, who, what, id, when —
Matt W537/// so the two lists scan identically. Here the glyph is the change's *state*
Matt W538/// rather than an event kind, because these rows are things that are still
Matt W539/// true, not things that happened.
Matt W540fn dash_change_row(c: &DashChange, now: DateTime<Utc>) -> Markup {
Matt W541 let href = format!("/{}/{}/changes/{}", c.owner, c.repo, c.number);
Matt W542 let (glyph, class) = match (c.conflicted, c.state.as_str()) {
Matt W543 (true, _) => ("◆", "is-conflict"),
Matt W544 (_, "merged") => ("⤳", "is-merge"),
Matt W545 (_, "abandoned") => ("×", "is-abandon"),
Matt W546 (_, "draft") => ("·", "is-abandon"),
Matt W547 _ => ("○", "is-review"),
Matt W548 };
Matt W549
Matt W550 html! {
Matt W551 li .feed-row {
Matt W552 span .feed-glyph.(class) aria-hidden="true" { (glyph) }
Matt W553 a .feed-repo href=(format!("/{}/{}", c.owner, c.repo)) {
Matt W554 span .owner { (c.owner) }
Matt W555 "/" (c.repo)
Matt W556 }
Matt W557 span .feed-verb {
Matt W558 (actor(c.author.as_deref(), c.author_name.as_deref()))
Matt W559 }
Matt W560 a .feed-title href=(href) { (c.title) }
Matt W561 (change_chip(&c.change_id, c.synthetic))
Matt W562 span .feed-age title=(c.updated_at.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W563 (relative_time(c.updated_at, now))
Matt W564 }
Matt W565 }
Matt W566 }
Matt W567}
Matt W568
Matt W569fn activity_row(a: &ActivityItem, now: DateTime<Utc>) -> Markup {
Matt W570 let repo_href = format!("/{}/{}", a.owner, a.repo);
Matt W571
Matt W572 html! {
Matt W573 li .activity-row {
Matt W574 @match a.actor.as_deref() {
Matt W575 Some(h) => a .activity-actor href=(format!("/{h}")) { (h) },
Matt W576 None => span .activity-actor.faint { "someone" },
Matt W577 }
Matt W578 span .dim { " " (activity_verb(&a.kind)) " " }
Matt W579 @if let (Some(n), Some(t)) = (a.change_number, &a.change_title) {
Matt W580 a href=(format!("{repo_href}/changes/{n}")) { (t) }
Matt W581 span .dim { " in " }
Matt W582 }
Matt W583 a .mono.faint href=(repo_href) { (a.owner) "/" (a.repo) }
Matt W584 span .faint.tnum.activity-when
Matt W585 title=(a.when.format("%Y-%m-%d %H:%M UTC").to_string()) {
Matt W586 (relative_time(a.when, now))
Matt W587 }
Matt W588 }
Matt W589 }
Matt W590}
Matt W591
Matt W592/// The verb phrase for an event kind.
Matt W593///
Matt W594/// Unknown kinds fall through to the raw kind rather than being dropped or
Matt W595/// silently relabelled — the same rule the change timeline follows, and for the
Matt W596/// same reason: an event the UI does not recognise still happened.
Matt W597fn activity_verb(kind: &str) -> &str {
Matt W598 match kind {
Matt W599 "change.opened" => "opened",
Matt W600 "change.pushed" => "pushed to",
Matt W601 "change.rebased" => "rewrote",
Matt W602 "change.conflicted" => "hit a conflict on",
Matt W603 "change.resolved" => "resolved conflicts on",
Matt W604 "change.merged" => "merged",
Matt W605 "change.abandoned" => "abandoned",
Matt W606 "change.reopened" => "reopened",
Matt W607 "change.drafted" => "drafted",
Matt W608 "change.ready" => "marked ready",
Matt W609 "change.reviewed" => "reviewed",
Matt W610 "comments.rebased" => "re-anchored comments on",
Matt W611 other => other,
Matt W612 }
Matt W613}
Matt W614
Matt W615pub struct RepoSummary {
Matt W616 pub owner: String,
Matt W617 pub name: String,
Matt W618 pub description: Option<String>,
Matt W619 pub private: bool,
Matt W620 pub open_changes: i64,
Matt W621 pub conflicted: i64,
Matt W622 /// `None` for a repository nobody has pushed to yet.
Matt W623 pub pushed_at: Option<DateTime<Utc>>,
Matt W624}
Matt W625
Matt W626/// The sign-in page.
Matt W627///
Matt W628/// SSO is the only method that exists. Passkeys and self-service sign-up are
Matt W629/// rendered as explicitly disabled rather than omitted: the design shows them,
Matt W630/// and a disabled control that says why is more honest than a live-looking
Matt W631/// button that does nothing. They carry `aria-disabled` and no `href`, so they
Matt W632/// are not in the tab order as if they were usable.
Matt W633pub fn signin(sso_href: &str, clone_hint: &str, sample_repo: Option<&str>) -> Markup {
Matt W634 html! {
Matt W635 div .signin {
Matt W636 div .signin-intro {
Matt W637 h1 .signin-title { "Sign in to Dogfood" }
Matt W638 p .dim.measure {
Matt W639 "Reading public repositories needs no account. Sign in to push, \
Matt W640 review, and open issues."
Matt W641 }
Matt W642 }
Matt W643
Matt W644 div .signin-card {
Matt W645 a .btn.btn-primary.btn-block href=(sso_href) {
Matt W646 span aria-hidden="true" .mono { "↗" }
Matt W647 " Continue with your SSO provider"
Matt W648 }
Matt W649
Matt W650 div .signin-or {
Matt W651 span .signin-rule aria-hidden="true" {}
Matt W652 span .label-condensed { "or" }
Matt W653 span .signin-rule aria-hidden="true" {}
Matt W654 }
Matt W655
Matt W656 // Present but not offered. A live-looking control that silently
Matt W657 // does nothing is worse than one that says why it cannot.
Matt W658 span .btn.btn-block.is-disabled aria-disabled="true"
Matt W659 title="Email sign-in links are not available on this instance" {
Matt W660 "Email me a sign-in link"
Matt W661 }
Matt W662 span .btn.btn-block.is-disabled aria-disabled="true"
Matt W663 title="Passkey sign-in is not available on this instance yet" {
Matt W664 "Continue with a passkey"
Matt W665 }
Matt W666 }
Matt W667
Matt W668 div .signin-note {
Matt W669 span .label-condensed { "No account yet" }
Matt W670 span .dim { "This instance is invitation-only — but you can read anything public first:" }
Matt W671 code .mono { (clone_hint) }
Matt W672 }
Matt W673
Matt W674 @if let Some(r) = sample_repo {
Matt W675 a href=(format!("/{r}")) { "Browse a repo instead →" }
Matt W676 }
Matt W677 }
Matt W678 }
Matt W679}
Matt W680
Matt W681/// Shown when a valid login belongs to somebody who is not admitted.
Matt W682///
Matt W683/// Deliberately says nothing about why, beyond that access is by invitation —
Matt W684/// enumerating the allowlist would be a disclosure.
Matt W685pub fn not_invited() -> Markup {
Matt W686 html! {
Matt W687 div .panel {
Matt W688 h1 { "This instance is invitation-only" }
Matt W689 p .lede {
Matt W690 "You signed in successfully, but this account has not been invited to \
Matt W691 Dogfood. Ask an administrator for an invitation."
Matt W692 }
Matt W693 form method="post" action="/logout" {
Matt W694 button .btn type="submit" { "Sign out" }
Matt W695 }
Matt W696 }
Matt W697 }
Matt W698}
Matt W699
Matt W700/// Handle chooser, shown when one cannot be derived from the OIDC claims.
Matt W701pub fn choose_handle(suggested: Option<&str>, csrf: &str, error: Option<&str>) -> Markup {
Matt W702 html! {
Matt W703 div .panel {
Matt W704 h1 { "Choose a handle" }
Matt W705 p .lede {
Matt W706 "Your handle appears in URLs, like "
Matt W707 code { "dogfood.sh/your-handle/your-repo" } "."
Matt W708 }
Matt W709 @if let Some(e) = error {
Matt W710 div .banner.banner-error role="alert" { (e) }
Matt W711 }
Matt W712 form method="post" action="/auth/handle" {
Matt W713 input type="hidden" name="_csrf" value=(csrf);
Matt W714 div .field {
Matt W715 label for="handle" { "Handle" }
Matt W716 input type="text" id="handle" name="handle"
Matt W717 value=(suggested.unwrap_or(""))
Matt W718 required
Matt W719 minlength="1" maxlength="39"
Matt W720 pattern="[a-z0-9][a-z0-9-]*"
Matt W721 autocomplete="off" autofocus;
Matt W722 p .hint { "Lowercase letters, digits and hyphens. Up to 39 characters." }
Matt W723 }
Matt W724 button .btn.btn-primary type="submit" { "Continue" }
Matt W725 }
Matt W726 }
Matt W727 }
Matt W728}
Matt W729
Matt W730/// One-time site-admin claim.
Matt W731pub fn setup(csrf: &str, error: Option<&str>) -> Markup {
Matt W732 html! {
Matt W733 div .panel {
Matt W734 h1 { "Claim site administrator" }
Matt W735 p .lede {
Matt W736 "Paste the setup token this instance printed to its logs on first boot. \
Matt W737 It can be used once."
Matt W738 }
Matt W739 @if let Some(e) = error {
Matt W740 div .banner.banner-error role="alert" { (e) }
Matt W741 }
Matt W742 form method="post" action="/setup" {
Matt W743 input type="hidden" name="_csrf" value=(csrf);
Matt W744 div .field {
Matt W745 label for="token" { "Setup token" }
Matt W746 input type="password" id="token" name="token" required autocomplete="off" autofocus;
Matt W747 }
Matt W748 button .btn.btn-primary type="submit" { "Claim" }
Matt W749 }
Matt W750 }
Matt W751 }
Matt W752}
Matt W753
Matt W754pub fn setup_done() -> Markup {
Matt W755 html! {
Matt W756 div .panel {
Matt W757 div .banner.banner-ok role="status" { "You are now a site administrator." }
Matt W758 p { a href="/" { "Go to the dashboard" } }
Matt W759 }
Matt W760 }
Matt W761}

761 lines · Rust