OXYGENHUB
# e9bfea8e26db oxygenhub / templates / proposal.html
14.1 KiB · HTML · last changed Name the kind of every number; show the issue -> proposal -> bookmark chain 1 month ago source blame history raw
1
{% extends "base.html" %}
2
{% import "repo_tabs.html" as m %}
3
{% block title %}Proposal #{{ 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 kind-proposal">proposal #{{ 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 !closes.is_empty() %}<p class="chain" aria-label="The chain of work">{% for c in closes %}<a class="chain-link kind-issue st-{{ c.state }}" href="{{ c.url }}" title="{{ c.title }}"><span class="chain-kind">issue #{{ c.number }}</span> {{ c.title }} <span class="chain-state">{{ c.state }}</span></a>{% if !loop.last %} <span class="chain-and">+</span> {% endif %}{% endfor %}
16
<span class="chain-arrow">→</span> <span class="chain-link kind-proposal st-{{ p.state }}"><span class="chain-kind">proposal #{{ p.number }}</span> <span class="chain-state">{{ p.state }}</span></span>
17
<span class="chain-arrow">→</span> <a class="lbl lbl-bm" href="{{ repo.url }}/tree/{{ p.target }}">{{ p.target }}</a>{% if p.state == "landed" %} <span class="chain-state st-landed">landed</span>{% endif %}</p>{% endif %}
18
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %}
19
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %}
20
{% 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 %}
21
{% if landed_already && p.state != "landed" %}<div class="strip warn">Every change is already in {{ p.target }}.</div>{% endif %}
22
<div class="bar">
23
  <span class="modes">
24
    <a href="?tab=conversation"{% if tab == "conversation" %} class="on"{% endif %}>conversation ({{ comments.len() + reviews.len() }})</a>
25
    <a href="?tab=changes"{% if tab == "changes" %} class="on"{% endif %}>changes ({{ changes.len() }})</a>
26
    <a href="?tab=files"{% if tab == "files" %} class="on"{% endif %}>files ({{ files.len() }})</a>
27
  </span>
28
  <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>
29
</div>
30
31
{% if tab == "changes" %}
32
{% if changes.is_empty() %}<p class="empty">Nothing to add: the target already has everything.</p>{% endif %}
33
{% for c in changes %}{% call m::change_row(repo, c) %}{% endfor %}
34
35
{% else if tab == "files" %}
36
{% if !conflicts.is_empty() %}
37
<section class="panel" id="conflicts"><h2>Conflicts the landing would carry</h2>
38
{% 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 %}
39
</section>
40
{% endif %}
41
{% if !files.is_empty() %}{% call m::diff_bar(ignore_ws, "tab=files&") %}{% endif %}
42
{% for f in files %}
43
<details class="file-diff" id="{{ f.anchor }}"{% if f.lines.len() <= 400 %} open{% endif %}>
44
  <summary><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></summary>
45
  {% if f.lines.is_empty() %}<p class="empty">No line changes.</p>{% else %}
46
  <table class="code diff">
47
  {% for l in f.lines %}
48
    <tr class="{{ l.kind.class() }}">
49
      <td class="n">{% if let Some(n) = l.old_no %}{{ n }}{% endif %}</td>
50
      <td class="n">{% if let Some(n) = l.new_no %}{{ n }}{% endif %}</td>
51
      <td class="s">{{ l.kind.sign() }}</td>
52
      <td class="t">{% call m::line_text(l) %}{% 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>
53
        <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>
54
    </tr>
55
    {% if let Some(n) = l.new_no %}{% if let Some(idxs) = placed.get(&crate::web::proposals::key(&f.path, n)) %}
56
    <tr class="ctx"><td colspan="4" class="thread">
57
      {% for i in idxs %}{% let c = comments[*i] %}
58
      <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 %}
59
        {% 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 %}
60
        <div class="body">{{ c.body_html|safe }}</div></div>
61
      {% endfor %}
62
    </td></tr>
63
    {% endif %}{% endif %}
64
  {% endfor %}
65
  </table>
66
  {% endif %}
67
</details>
68
{% endfor %}
69
{% if !outdated.is_empty() %}
70
<section class="panel"><h2>Comments on lines that are gone</h2>
71
{% 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 %}
72
</section>
73
{% endif %}
74
75
{% else %}
76
<div class="side-layout">
77
<div>
78
  {% if !p.body.is_empty() %}<article class="doc">{{ body_html|safe }}</article>{% endif %}
79
  {% for c in comments %}{% if c.row.anchor_change.is_empty() %}
80
  <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>
81
  {% endif %}{% endfor %}
82
  {% for r in reviews %}
83
  <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>
84
  {% endfor %}
85
  {% for e in events %}<p class="small mute">{{ e.actor }} {{ 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 %} · {{ e.at_ms|ago_i }}</p>{% endfor %}
86
  {% if shell.user.is_some() %}
87
  <form class="card wide" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/comment">
88
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
89
    <label for="body">Comment (Markdown)</label><textarea id="body" name="body" class="md" rows="4" required></textarea>
90
    <button type="submit">COMMENT</button>
91
  </form>
92
  {% if p.is_open() %}
93
  <form class="card wide" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/review">
94
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
95
    <h2>Review</h2>
96
    <label for="rbody">Summary (optional)</label><textarea id="rbody" name="body" rows="3"></textarea>
97
    <div class="choice">
98
      <label><input type="radio" name="verdict" value="comment" checked> comment</label>
99
      <label><input type="radio" name="verdict" value="approve"> approve</label>
100
      <label><input type="radio" name="verdict" value="changes"> request changes</label>
101
    </div>
102
    <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>
103
    <button type="submit">SUBMIT REVIEW</button>
104
  </form>
105
  {% endif %}
106
  {% endif %}
107
</div>
108
<aside class="side">
109
  <h3>Landing</h3>
110
  {% 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>
111
  {% else if p.state == "closed" %}<p class="mute">closed</p>
112
  {% else %}
113
    {% 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 %}
114
    {% if can_land %}
115
      {% 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>
116
      {% 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 %}
117
    {% endif %}
118
  {% endif %}
119
  {% if !closes.is_empty() %}<h3>Closes</h3><ul class="plain">{% for c in closes %}<li><a href="{{ c.url }}" class="kind-issue">issue #{{ c.number }}</a> {{ c.title }} <span class="small mute">{{ c.state }}</span></li>{% endfor %}</ul>{% endif %}
120
  <h3>Labels</h3>
121
  {% 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 %}
122
  {% 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 %}
123
  <h3>Projects</h3>
124
  {% if projects.is_empty() %}<p class="mute small">None.</p>{% else %}<ul class="plain small">{% for (pr, col) in projects %}<li><a href="/projects/{{ pr.owner }}/{{ pr.number }}">{{ pr.title }}</a> <span class="mute">· {{ col }}</span></li>{% endfor %}</ul>{% endif %}
125
  <h3>Checks</h3>
126
  <div id="checks">
127
  {% if checks.is_empty() %}<p class="mute small">None reported on the projected state{% if !required_checks.is_empty() %}; required: {{ required_checks.join(", ") }}{% endif %}.</p>{% else %}
128
  <ul class="plain">{% for c in checks %}<li><span class="sig{% if c.status == "passed" %} sig-valid{% else if c.status == "failed" %} sig-invalid{% else %} sig-unsigned{% endif %}">{% if c.status == "passed" %}✔{% else if c.status == "failed" %}✘{% else if c.status == "running" %}…{% else %}○{% endif %}</span> {% if c.url.is_empty() %}{{ c.name }}{% else %}<a href="{{ c.url }}">{{ c.name }}</a>{% endif %} <span class="mute small">{{ c.status }}{% if !c.summary.is_empty() %} · {{ c.summary }}{% endif %}{% if !c.seal.is_empty() %} · sealed{% endif %}</span></li>{% endfor %}</ul>{% endif %}
129
  </div>
130
  <script>(function(){if(!window.EventSource)return;var es=new EventSource("{{ repo.url }}/proposal/{{ p.number }}/events");es.addEventListener("check",function(){es.close();location.reload();});})();</script>
131
  <h3>Approvals</h3>
132
  {% if approvals.is_empty() %}<p class="mute small">None yet{% if requirements.required_approvals > 0 %}; {{ requirements.required_approvals }} sealed required{% endif %}.</p>{% else %}
133
  <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 %}
134
  {% if !owners.is_empty() %}<h3>Code owners</h3><ul class="plain">{% for o in owners %}<li>{{ o }}</li>{% endfor %}</ul>{% endif %}
135
  {% 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 %}
136
  {% if can_edit %}
137
  <h3>Edit</h3>
138
  <form class="card" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/edit">
139
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
140
    <label for="etitle">Title</label><input type="text" id="etitle" name="title" value="{{ p.title }}">
141
    <label for="etarget">Target</label><input type="text" id="etarget" name="target" value="{{ p.target }}">
142
    <label for="ebody">Description</label><textarea id="ebody" name="body" rows="4">{{ p.body }}</textarea>
143
    <button type="submit">SAVE</button>
144
  </form>
145
  <form class="card" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/state">
146
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
147
    <div class="choice">
148
    {% if p.state == "draft" %}<button type="submit" name="action" value="ready">READY FOR REVIEW</button>{% endif %}
149
    {% if p.state == "open" %}<button type="submit" name="action" value="draft">BACK TO DRAFT</button>{% endif %}
150
    {% if p.is_open() %}<button type="submit" name="action" value="close">CLOSE</button>{% endif %}
151
    {% if p.state == "closed" %}<button type="submit" name="action" value="reopen">REOPEN</button>{% endif %}
152
    </div>
153
  </form>
154
  {% endif %}
155
</aside>
156
</div>
157
{% endif %}
158
{% endblock %}