| 1 |
{% extends "base.html" %} |
| 2 |
{% import "repo_tabs.html" as m %} |
| 3 |
{% block title %}#{{ p.number }} {{ p.title }} · {{ repo.owner }}/{{ repo.name }}{% endblock %} |
| 4 |
{% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %} |
| 5 |
{% block tabs %}{% call m::tabs(repo, "proposals") %}{% endblock %} |
| 6 |
{% block mainclass %} class="wide"{% endblock %} |
| 7 |
{% block content %} |
| 8 |
<div class="head"><h1>{{ p.title }} <small class="px">#{{ p.number }}</small> |
| 9 |
<small> |
| 10 |
<span class="tag{% if p.state == "landed" %} head{% else if p.state == "closed" %} bad{% endif %}">{{ p.state }}</span> |
| 11 |
{{ p.author }} wants <span class="lbl lbl-bm">{{ source_label }}</span> into <a class="lbl lbl-bm" href="{{ repo.url }}/tree/{{ p.target }}">{{ p.target }}</a> |
| 12 |
· {{ changes.len()|plural("change", "changes") }} · opened <time title="{{ p.created_ms|when_i }}">{{ p.created_ms|ago_i }}</time> |
| 13 |
</small> |
| 14 |
</h1></div> |
| 15 |
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %} |
| 16 |
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %} |
| 17 |
{% if conflicted %}<div class="strip bad">Landing would carry {{ conflicts.len()|plural("conflict", "conflicts") }}. Any collaborator can push a resolution change to the source; the proposal follows it. <a href="?tab=files#conflicts">See them</a></div>{% endif %} |
| 18 |
{% if landed_already && p.state != "landed" %}<div class="strip warn">Every change is already in {{ p.target }}.</div>{% endif %} |
| 19 |
<div class="bar"> |
| 20 |
<span class="modes"> |
| 21 |
<a href="?tab=conversation"{% if tab == "conversation" %} class="on"{% endif %}>conversation ({{ comments.len() + reviews.len() }})</a> |
| 22 |
<a href="?tab=changes"{% if tab == "changes" %} class="on"{% endif %}>changes ({{ changes.len() }})</a> |
| 23 |
<a href="?tab=files"{% if tab == "files" %} class="on"{% endif %}>files ({{ files.len() }})</a> |
| 24 |
</span> |
| 25 |
<span class="small mute">projected state <code title="{{ state_hash }}">{{ state_hash|short }}</code> · {{ heads.len()|plural("head", "heads") }} · <a href="{{ repo.url }}/tree/{% if let Some(h) = heads.first() %}{{ h }}{% endif %}">browse</a> · <a href="{{ api_url }}">json</a></span> |
| 26 |
</div> |
| 27 |
|
| 28 |
{% if tab == "changes" %} |
| 29 |
{% if changes.is_empty() %}<p class="empty">Nothing to add: the target already has everything.</p>{% endif %} |
| 30 |
{% for c in changes %}{% call m::change_row(repo, c) %}{% endfor %} |
| 31 |
|
| 32 |
{% else if tab == "files" %} |
| 33 |
{% if !conflicts.is_empty() %} |
| 34 |
<section class="panel" id="conflicts"><h2>Conflicts the landing would carry</h2> |
| 35 |
{% for c in conflicts %}<p><strong>{{ c.path }}</strong> <span class="tag bad">{{ c.kind }}</span></p>{% if let Some(t) = c.marked %}<pre class="marked">{{ t }}</pre>{% endif %}{% endfor %} |
| 36 |
</section> |
| 37 |
{% endif %} |
| 38 |
{% for f in files %} |
| 39 |
<section class="file-diff" id="{{ f.anchor }}"> |
| 40 |
<header><span class="kind kind-{{ f.kind }}">{{ f.kind }}</span> <strong>{{ f.path }}</strong> <span class="counts"><span class="add">+{{ f.added }}</span> <span class="del">−{{ f.removed }}</span></span></header> |
| 41 |
{% if f.lines.is_empty() %}<p class="empty">No line changes.</p>{% else %} |
| 42 |
<table class="code diff"> |
| 43 |
{% for l in f.lines %} |
| 44 |
<tr class="{{ l.kind.class() }}"> |
| 45 |
<td class="n">{% if let Some(n) = l.old_no %}{{ n }}{% endif %}</td> |
| 46 |
<td class="n">{% if let Some(n) = l.new_no %}{{ n }}{% endif %}</td> |
| 47 |
<td class="s">{{ l.kind.sign() }}</td> |
| 48 |
<td class="t"><pre>{{ l.text }}</pre>{% if let Some(n) = l.new_no %}{% if shell.user.is_some() && p.is_open() %}<details class="linecomment"><summary title="Comment on this line">+</summary> |
| 49 |
<form method="post" action="{{ repo.url }}/proposal/{{ p.number }}/comment"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="path" value="{{ f.path }}"><input type="hidden" name="line" value="{{ n }}"><textarea name="body" rows="3" required placeholder="Anchored to this line's atom; it follows the line through rebases."></textarea><button type="submit">COMMENT</button></form></details>{% endif %}{% endif %}</td> |
| 50 |
</tr> |
| 51 |
{% if let Some(n) = l.new_no %}{% if let Some(idxs) = placed.get(&crate::web::proposals::key(&f.path, n)) %} |
| 52 |
<tr class="ctx"><td colspan="4" class="thread"> |
| 53 |
{% for i in idxs %}{% let c = comments[*i] %} |
| 54 |
<div class="comment{% if c.row.resolved %} resolved{% endif %}"><span class="who">{{ c.row.author }}</span> <time class="mute" title="{{ c.row.created_ms|when_i }}">{{ c.row.created_ms|ago_i }}</time>{% if c.row.resolved %} <span class="tag">resolved</span>{% endif %} |
| 55 |
{% if shell.user.is_some() %}<form class="tiny" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/comment/{{ c.row.id }}/resolve"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button>{% if c.row.resolved %}unresolve{% else %}resolve{% endif %}</button></form>{% endif %} |
| 56 |
<div class="body">{{ c.body_html|safe }}</div></div> |
| 57 |
{% endfor %} |
| 58 |
</td></tr> |
| 59 |
{% endif %}{% endif %} |
| 60 |
{% endfor %} |
| 61 |
</table> |
| 62 |
{% endif %} |
| 63 |
</section> |
| 64 |
{% endfor %} |
| 65 |
{% if !outdated.is_empty() %} |
| 66 |
<section class="panel"><h2>Comments on lines that are gone</h2> |
| 67 |
{% for i in outdated %}{% let c = comments[*i] %}<div class="comment"><span class="who">{{ c.row.author }}</span> on <code>{{ c.row.path }}</code> <time class="mute">{{ c.row.created_ms|ago_i }}</time><div class="body">{{ c.body_html|safe }}</div></div>{% endfor %} |
| 68 |
</section> |
| 69 |
{% endif %} |
| 70 |
|
| 71 |
{% else %} |
| 72 |
<div class="side-layout"> |
| 73 |
<div> |
| 74 |
{% if !p.body.is_empty() %}<article class="doc">{{ body_html|safe }}</article>{% endif %} |
| 75 |
{% for c in comments %}{% if c.row.anchor_change.is_empty() %} |
| 76 |
<div class="comment"><span class="who">{{ c.row.author }}</span> <time class="mute" title="{{ c.row.created_ms|when_i }}">{{ c.row.created_ms|ago_i }}</time><div class="body">{{ c.body_html|safe }}</div></div> |
| 77 |
{% endif %}{% endfor %} |
| 78 |
{% for r in reviews %} |
| 79 |
<div class="comment review-{{ r.row.verdict }}"><span class="who">{{ r.row.author }}</span> {% if r.row.verdict == "approve" %}{% if r.sealed %}<span class="sig sig-valid">◈</span> approved and sealed{% else %}<span class="sig sig-unsigned">○</span> approved, seal pending{% endif %}{% else if r.row.verdict == "changes" %}<span class="sig sig-invalid">✘</span> requested changes{% else %}commented{% endif %} <time class="mute" title="{{ r.row.created_ms|when_i }}">{{ r.row.created_ms|ago_i }}</time>{% if !r.row.body.is_empty() %}<div class="body">{{ r.body_html|safe }}</div>{% endif %}</div> |
| 80 |
{% endfor %} |
| 81 |
{% for e in events %}<p class="small mute">{{ e.actor }} {{ e.event }}{% if !e.detail.is_empty() %}: {{ e.detail }}{% endif %} · {{ e.at_ms|ago_i }}</p>{% endfor %} |
| 82 |
{% if shell.user.is_some() %} |
| 83 |
<form class="card wide" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/comment"> |
| 84 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 85 |
<label for="body">Comment (Markdown)</label><textarea id="body" name="body" class="md" rows="4" required></textarea> |
| 86 |
<button type="submit">COMMENT</button> |
| 87 |
</form> |
| 88 |
{% if p.is_open() %} |
| 89 |
<form class="card wide" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/review"> |
| 90 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 91 |
<h2>Review</h2> |
| 92 |
<label for="rbody">Summary (optional)</label><textarea id="rbody" name="body" rows="3"></textarea> |
| 93 |
<div class="choice"> |
| 94 |
<label><input type="radio" name="verdict" value="comment" checked> comment</label> |
| 95 |
<label><input type="radio" name="verdict" value="approve"> approve</label> |
| 96 |
<label><input type="radio" name="verdict" value="changes"> request changes</label> |
| 97 |
</div> |
| 98 |
<p class="small mute">An approval counts once you seal the changes from a client: <code>oxy seal --proposal {{ shell.base_url }}{{ repo.url }}/proposal/{{ p.number }}</code></p> |
| 99 |
<button type="submit">SUBMIT REVIEW</button> |
| 100 |
</form> |
| 101 |
{% endif %} |
| 102 |
{% endif %} |
| 103 |
</div> |
| 104 |
<aside class="side"> |
| 105 |
<h3>Landing</h3> |
| 106 |
{% if p.state == "landed" %}<p><span class="sig sig-valid">✔</span> landed{% if !p.landed_state.is_empty() %} at <code>{{ p.landed_state|short }}</code>{% endif %}</p> |
| 107 |
{% else if p.state == "closed" %}<p class="mute">closed</p> |
| 108 |
{% else %} |
| 109 |
{% if blockers.is_empty() %}<p><span class="sig sig-valid">✔</span> ready to land</p>{% else %}<ul class="plain small">{% for b in blockers %}<li><span class="sig sig-invalid">✘</span> {{ b }}</li>{% endfor %}</ul>{% endif %} |
| 110 |
{% if can_land %} |
| 111 |
{% if hub_can_land %}<form method="post" action="{{ repo.url }}/proposal/{{ p.number }}/land"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit">LAND</button></form> |
| 112 |
{% else %}<p class="small">This repository has a trust policy, so land from a client with your key:</p><p class="secret">oxy proposal land {{ shell.base_url }}{{ repo.url }}/proposal/{{ p.number }}</p>{% endif %} |
| 113 |
{% endif %} |
| 114 |
{% endif %} |
| 115 |
<h3>Labels</h3> |
| 116 |
{% if labels.is_empty() %}<p class="mute small">None.</p>{% else %}<p>{% for l in labels %}<span class="label" style="--c: {{ l.colour }}">{{ l.name }}</span> {% endfor %}</p>{% endif %} |
| 117 |
{% if repo.can(crate::auth::Access::Write) && !all_labels.is_empty() %}<form class="tiny-form" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/labels"><input type="hidden" name="_csrf" value="{{ shell.csrf }}">{% for l in all_labels %}<label class="small"><input type="checkbox" name="label" value="{{ l.id }}"{% if labels.contains(l) %} checked{% endif %}> {{ l.name }}</label> {% endfor %}<button type="submit" class="small">SET</button></form>{% endif %} |
| 118 |
<h3>Approvals</h3> |
| 119 |
{% if approvals.is_empty() %}<p class="mute small">None yet{% if requirements.required_approvals > 0 %}; {{ requirements.required_approvals }} sealed required{% endif %}.</p>{% else %} |
| 120 |
<ul class="plain">{% for a in approvals %}<li>{% if a.sealed %}<span class="sig sig-valid">◈</span>{% else %}<span class="sig sig-unsigned">○</span>{% endif %} <a href="/{{ a.account }}">{{ a.account }}</a>{% if !a.sealed %} <span class="mute small">seal pending</span>{% endif %}</li>{% endfor %}</ul>{% endif %} |
| 121 |
{% if !owners.is_empty() %}<h3>Code owners</h3><ul class="plain">{% for o in owners %}<li>{{ o }}</li>{% endfor %}</ul>{% endif %} |
| 122 |
{% if !depends_on.is_empty() %}<h3>Depends on</h3><ul class="plain">{% for n in depends_on %}<li><a href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a></li>{% endfor %}</ul>{% endif %} |
| 123 |
{% if can_edit %} |
| 124 |
<h3>Edit</h3> |
| 125 |
<form class="card" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/edit"> |
| 126 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 127 |
<label for="etitle">Title</label><input type="text" id="etitle" name="title" value="{{ p.title }}"> |
| 128 |
<label for="etarget">Target</label><input type="text" id="etarget" name="target" value="{{ p.target }}"> |
| 129 |
<label for="ebody">Description</label><textarea id="ebody" name="body" rows="4">{{ p.body }}</textarea> |
| 130 |
<button type="submit">SAVE</button> |
| 131 |
</form> |
| 132 |
<form class="card" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/state"> |
| 133 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 134 |
<div class="choice"> |
| 135 |
{% if p.state == "draft" %}<button type="submit" name="action" value="ready">READY FOR REVIEW</button>{% endif %} |
| 136 |
{% if p.state == "open" %}<button type="submit" name="action" value="draft">BACK TO DRAFT</button>{% endif %} |
| 137 |
{% if p.is_open() %}<button type="submit" name="action" value="close">CLOSE</button>{% endif %} |
| 138 |
{% if p.state == "closed" %}<button type="submit" name="action" value="reopen">REOPEN</button>{% endif %} |
| 139 |
</div> |
| 140 |
</form> |
| 141 |
{% endif %} |
| 142 |
</aside> |
| 143 |
</div> |
| 144 |
{% endif %} |
| 145 |
{% endblock %} |