1---2pagination:3 data: forge.pages.reviews4---5{% extends "base.njk" %}6{% import "macros.njk" as m with context %}7{% import "ui.njk" as ui %}8{% set rv = p.review %}9{% set u = p.repo.urls %}10{% block head %}{{ m.reviewMeta(rv, p.repo, p.title, u.review(rv.id).href) }}{% endblock %}11{% block scripts %}<script src="{{ assets.diffs }}" defer></script>12<script src="{{ assets.discuss }}" defer></script>13{% endblock %}14{% block content %}15{% css %}16.review-desc .markdown > :last-child { margin-bottom: 0; }17/* Each commit of the layer: its subject on the page, then its message,18 diffstat and files in panels. */19.stacked { margin-top: var(--space-8); }20.stacked > h3 { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3); align-items: baseline; margin: 0 0 var(--space-1); font-size: var(--text-lg); }21.stacked > h3 .step { color: var(--faint); font: var(--fw-regular) var(--text-xs) var(--mono); }22.stacked > h3 a { color: var(--fg); }23.stacked > .byline { margin: 0 0 var(--space-3); font-size: var(--text-sm); }24#stack tr.this td { font-weight: var(--fw-strong); }25/* To land: a row a requirement, its mark, what it is, what it has. */26#to-land td.state { width: 1.5em; padding-right: 0; }27#to-land td.main b { font-weight: var(--fw-medium); }28#to-land .paths, #to-land .has { color: var(--muted); }29#to-land .warn { color: var(--warn); }30/* The sticky bar: layers of the stack, commits of the layer. It has a fixed31 height so file headers can stick right below it. */32.revnav { position: sticky; top: 0; z-index: 3; height: 2.75rem; display: flex; align-items: center; gap: var(--space-3); margin: var(--space-8) 0 0; background: var(--bg); border-bottom: var(--hairline) solid var(--border); white-space: nowrap; overflow-x: auto; scrollbar-width: none; font: var(--fw-regular) var(--text-xs) var(--mono); }33.revnav::-webkit-scrollbar { display: none; }34@media (max-width: 480px) {35 .revnav { gap: var(--space-2); }36 .revnav .seg > * { padding: var(--space-1) var(--space-2); }37 .revnav .n { display: none; }38}39.revnav .seg { display: inline-flex; align-items: stretch; border: var(--hairline) solid var(--border-strong); background: var(--raised); overflow: hidden; }40.revnav .seg > * { padding: var(--space-1) var(--space-3); }41.revnav .seg > .at { color: var(--fg); border-inline: var(--hairline) solid var(--border); font-variant-numeric: tabular-nums; }42.revnav .seg > a:hover { background: var(--hover); text-decoration: none; }43.revnav .seg > a:not([href]) { color: var(--faint); }44.revnav .grow { flex: 1; }45.revnav ~ .stacked .fh { top: 2.75rem; }46.revnav ~ .stacked, .revnav ~ .stacked .file { scroll-margin-top: 2.75rem; }47.revnav ~ .stacked .code .l { scroll-margin-top: 5.5rem; }48{% endcss %}49<div class="meta-head">50<h1>{{ rv.title }} <span class="desc">#{{ rv.id }}</span></h1>51<div class="byline">{{ m.status(rv.status) }} <span class="mono">{{ rv.branch }}</span> → <span class="mono">{{ rv.target }}</span>{% if rv.author %}, opened by {{ rv.author }}{% endif %}, {{ rv.revisions.length | plural('revision') }}, updated {{ rv.updated | ago }}</div>52</div>5354{% if rv.opts.description %}55{% set head %}{% if rv.author %}<b>{{ rv.author }}</b> {% endif %}<span>description</span>{% endset %}56{% call ui.panel(head, 'review-desc') %}<div class="markdown">{{ rv.opts.description | reviewMarkdown(p.repo, rv, true) | safe }}</div>{% endcall %}57{% endif %}5859{% if rv.live and (rv.status == 'open' or rv.status == 'draft') and rv.gate %}60{% set g = rv.gate %}61{% set head %}<b>to land</b><span class="grow">on <span class="mono">{{ rv.target }}</span>{% if g.required %}, which the forge holds to these{% else %}; approval is optional here{% endif %}</span>{% if g.rules %}<span class="end"><a href="{{ u.blob(rv.target, '.forge/approvals').href }}">approval rules</a></span>{% endif %}{% endset %}62{% call ui.panel(head) %}63<table class="list" id="to-land">64<tbody>65<tr><td class="state">{% if rv.ci %}<span class="ci {{ rv.ci.state }}">{{ '✓' if rv.ci.state == 'pass' else ('✗' if rv.ci.state == 'fail' else '–') }}</span>{% else %}<span class="ci partial">–</span>{% endif %}</td><td class="main"><b>ci</b><span class="has">: {% if not rv.ci %}no attestations yet, <code>forge attest --stack</code>{% elif rv.ci.state == 'pass' %}{{ 'its commit' if rv.ci.total == 1 else 'every commit' }} passed{% else %}{{ rv.ci['pass'] }} of {{ rv.ci.total }} commits passed{% endif %}</span></td></tr>66{% for n in g.needs %}67<tr><td class="state"><span class="ci {{ 'pass' if n.ok else 'partial' }}">{{ '✓' if n.ok else '–' }}</span></td><td class="main">{% if n.line %}<a href="{{ u.blob(rv.target, '.forge/approvals').href }}#l{{ n.line }}">{{ n.text }}</a>{% else %}{{ n.text }}{% endif %} <span class="paths">for <span class="mono">{{ n.paths.slice(0, 3) | join(', ') }}</span>{% if n.paths.length > 3 %} and {{ (n.paths.length - 3) | plural('more file') }}{% endif %}</span><span class="has">: {% if n.have.length %}approved by {{ n.have | join(', ') }}{% elif n.text != 'an agent review' %}not approved yet{% endif %}{% if n.agent %}{% if n.have.length or n.text != 'an agent review' %}; {% endif %}{{ 'an agent review with nothing blocking' if n.agentOk else 'no agent review with nothing blocking' }}{% endif %}</span></td></tr>68{% endfor %}69{% if g.free %}<tr><td class="state"><span class="ci pass">✓</span></td><td class="main"><b>no approval needed</b><span class="has">: every file it changes is under a <code>none</code> rule</span></td></tr>{% endif %}70{% for e in g.errors %}<tr><td class="state"><span class="ci fail">✗</span></td><td class="main warn">{{ e }}</td></tr>{% endfor %}71{% if rv.agent %}72{% set a = rv.agent %}73<tr><td class="state"><span class="ci {{ 'pass' if a.open == 0 and a.current else ('fail' if a.open else 'partial') }}">{{ '✓' if a.open == 0 and a.current else ('✗' if a.open else '–') }}</span></td><td class="main"><b>agent review</b><span class="has">: {{ a.model }}{% if a.signer %}, signed by {{ a.signer }}{% endif %}, {{ a.findings | plural('finding') }}{% if a.open %}, <b>{{ a.open }} blocking open</b>{% endif %}{% if not a.current %}, of <span class="sha">{{ a.sha | commitLink(p.repo) | safe }}</span>, an earlier tree: <code>forge critique {{ rv.id }}</code>{% endif %}</span></td></tr>74{% endif %}75</tbody>76</table>77{% endcall %}78{% endif %}7980{% if p.stack.length > 1 %}81{% set head %}<b>stack</b><span class="n">{{ p.stack.length }}</span>{% endset %}82{% call ui.panel(head) %}83<table class="list stack" id="stack">84<tbody>85{% for s in p.stack | reverse %}86<tr{% if s.review.id == rv.id %} class="this"{% endif %}><td class="main"><span class="depth">{{ s.depth | indent | safe }}</span><a href="{{ u.review(s.review.id).href }}">#{{ s.review.id }} {{ s.review.title }}</a></td><td class="mono hide-sm">{{ s.review.branch }}</td><td class="nowrap">{{ s.review.layer.length | plural('commit') }}</td><td>{{ m.reviewci(s.review.ci) }}</td><td>{% if s.review.status != 'open' %}{{ m.status(s.review.status) }}{% endif %}</td></tr>87{% endfor %}88<tr><td class="main mono"><a href="{{ u.log(p.repo.defaultBranch).href }}">{{ p.repo.defaultBranch }}</a> <span class="desc">base</span></td><td class="hide-sm"></td><td></td><td></td><td></td></tr>89</tbody>90</table>91{% endcall %}92{% endif %}9394{% set head %}<b>commits</b><span class="n">{{ rv.layer.length }}</span><span class="grow">oldest first</span><span class="end"><a href="{{ u.reviewDiff(rv.id).href }}">combined diff</a> <span>{{ rv.combinedStats.files | plural('file') }}, <span class="i">+{{ rv.combinedStats.add }}</span> <span class="d">-{{ rv.combinedStats.del }}</span></span></span>{% endset %}95{% call ui.panel(head) %}96<table class="list stack nav" id="layer">97<tbody>98{% for c in rv.layer %}99<tr><td class="num desc">{{ loop.index }}/{{ rv.layer.length }}</td><td class="main"><a href="#c-{{ c.sha }}">{{ c.subject }}</a></td><td class="sha hide-sm">{{ c.short }}</td><td class="num i">+{{ c.stats.add }}</td><td class="num d">-{{ c.stats.del }}</td><td>{{ c.ci | ciHtml | safe }}</td></tr>100{% endfor %}101</tbody>102</table>103{% endcall %}104105{% if rv.revisions.length > 1 %}106{% set lastrev = rv.revisions[rv.revisions.length - 1] %}107{% set head %}<b>revisions</b><span class="n">{{ rv.revisions.length }}</span>{% if lastrev.summary %}<span class="grow jev-sum {{ lastrev.summary.kind }}" title="What the latest revision changed, according to Jev">r{{ lastrev.n }}: {{ lastrev.summary.text }}</span>{% endif %}{% endset %}108{% call ui.panel(head, '', '', 'closed') %}109<table class="list stack">110<tbody>111{% for v in rv.revisions | reverse %}112<tr><td class="num">r{{ v.n }}</td><td class="date">{{ v.ts | date }}</td><td>{{ v.user or '' }}</td><td class="sha">{{ v.sha | commitLink(p.repo) | safe }}</td><td class="main">{% if v.view %}<a href="{{ u.revision(rv.id, v.n).href }}">what changed since r{{ v.prev }}</a>{% endif %}{% if v.summary %} <span class="jev-sum {{ v.summary.kind }}" title="What this revision changed, according to Jev">{{ v.summary.text }}</span>{% endif %}</td></tr>113{% endfor %}114</tbody>115</table>116{% endcall %}117{% endif %}118119{% if rv.status == 'open' or rv.status == 'draft' %}120{% call ui.panel('<b>check out or land</b>', '', '', 'closed') %}121<pre>git fetch {{ u.cloneSsh }} {{ rv.branch }} && git switch --detach FETCH_HEAD122forge checkout {{ rv.id }} # the same, on a local tracking branch123forge attest --stack # CI for each commit, on your machine124forge critique {{ rv.id }} # an agent review: .forge/review on {{ rv.target }}, signed125forge comment {{ rv.id }} -m "..." # add <path>:<line> to comment on a line126forge approve {{ rv.id }}127forge land {{ rv.id }} # checks CI and the approval rules, then fast-forwards128ssh {{ forge.config.ssh }} land {{ p.repo.slug }} {{ rv.id }} # the forge lands it, with the same gates: no clone needed</pre>129{% endcall %}130{% endif %}131132<div class="bar review-tools">133{% for s in rv.previews %}<span>preview <a href="{{ s.url }}">{{ s.name }}</a>{% if s.history[0].source != rv.tip %} <span class="ci partial" title="the branch has moved since this preview was built">stale</span>{% endif %}</span>{% endfor %}134<span class="grow"></span>{% if p.stack.length < 2 and rv.layer.length < 2 %}<span class="count" data-viewed="{{ p.repo.slug }}" hidden></span>{% endif %}{{ ui.checkbox('wrap lines', false, 'data-wrap') }}135</div>136137{% include "discussion.njk" %}138{# Sticks to the top while reading the diffs: [ and ] move through the stack,139 the arrows through this layer's commits (diffs.ts points them at the140 neighbours of the commit in view, and fills in the Viewed count, which141 sits in the bar above when there is nothing to move through). #}142{% if p.stack.length > 1 or rv.layer.length > 1 %}143{% set below = p.stack[p.stackAt - 1] if p.stackAt > 0 else null %}144{% set above = p.stack[p.stackAt + 1] %}145<nav class="revnav" aria-label="stack and commits">146{% if p.stack.length > 1 %}147<span class="seg"><a{% if below %} href="{{ u.review(below.review.id).href }}" data-key="[" title="layer below: #{{ below.review.id }} {{ below.review.title }}"{% endif %} aria-label="layer below">‹{% if below %}<span class="n"> #{{ below.review.id }}</span>{% endif %}</a><span class="at">layer {{ p.stackAt + 1 }}/{{ p.stack.length }}</span><a{% if above %} href="{{ u.review(above.review.id).href }}" data-key="]" title="layer above: #{{ above.review.id }} {{ above.review.title }}"{% endif %} aria-label="layer above">{% if above %}<span class="n">#{{ above.review.id }} </span>{% endif %}›</a></span>148{% endif %}149{% if rv.layer.length > 1 %}150<span class="seg commits"><a data-step="-1" aria-label="previous commit">‹</a><a class="at" href="#layer">{{ rv.layer.length }} commits</a><a data-step="1" href="#c-{{ rv.layer[0].sha }}" aria-label="next commit">›</a></span>151{% endif %}152<span class="grow"></span>153<span class="count" data-viewed="{{ p.repo.slug }}" hidden></span>154</nav>155{% endif %}156157{% for c in rv.layer %}158<section class="stacked jump" id="c-{{ c.sha }}">159<h3><span class="step">{{ loop.index }}/{{ rv.layer.length }}</span> <a href="{{ u.commit(c.sha).href }}">{{ c.subject }}</a></h3>160<p class="byline desc">{{ c.author.name }}, {{ c.author.date | ago }}, <span class="sha">{{ c.short }}</span>{% if c.ci %}. CI {{ c.ci | ciHtml(true) | safe }}{% endif %}</p>161{% if c.body %}<pre class="panel msg">{{ c.body }}</pre>{% endif %}162{{ c.files | diffstatHtml('c' ~ loop.index ~ '-') | safe }}163{{ c.files | diffHtml(p.repo, 'c' ~ loop.index ~ '-', rv.branch if rv.live else null, null, true) | safe }}164</section>165{% endfor %}166{% endblock %}