[ADD] runbot_merge: PR dashboard

Provides a view of the "ongoing" state of a PR in order to more easily
know what might be blocking / problematic.
This commit is contained in:
Xavier Morel 2020-11-13 10:38:48 +01:00
parent 47e8b5b014
commit c80d8048f7
5 changed files with 173 additions and 17 deletions

View File

@ -1,4 +1,9 @@
# -*- coding: utf-8 -*-
import ast
import json
import werkzeug.exceptions
from odoo.http import Controller, route, request
LIMIT = 20
@ -21,3 +26,20 @@ class MergebotDashboard(Controller):
'stagings': stagings[:LIMIT],
'next': stagings[-1].staged_at if len(stagings) > LIMIT else None,
})
@route('/<org>/<repo>/pull/<int(min=1):pr>', auth='public', type='http', website=True)
def pr(self, org, repo, pr):
pr_id = request.env['runbot_merge.pull_requests'].sudo().search([
('repository.name', '=', f'{org}/{repo}'),
('number', '=', int(pr)),
])
if not pr_id:
raise werkzeug.exceptions.NotFound()
if not pr_id.repository.group_id <= request.env.user.groups_id:
raise werkzeug.exceptions.NotFound()
return request.render('runbot_merge.view_pull_request', {
'pr': pr_id,
'merged_head': json.loads(pr_id.commits_map).get(''),
'statuses': ast.literal_eval(pr_id.statuses) if pr_id.statuses else {}
})

View File

@ -265,6 +265,8 @@ class Repository(models.Model):
project_id = fields.Many2one('runbot_merge.project', required=True)
status_ids = fields.One2many('runbot_merge.repository.status', 'repo_id', string="Required Statuses")
group_id = fields.Many2one('res.groups', default=lambda self: self.env.ref('base.group_user'))
branch_filter = fields.Char(default='[(1, "=", 1)]', help="Filter branches valid for this repository")
substitutions = fields.Text(
"label substitutions",
@ -711,6 +713,27 @@ class PullRequests(models.Model):
for p in self
]
@property
def _approved(self):
return self.state in ('approved', 'ready') or any(
p.priority == 0
for p in (self | self._linked_prs)
)
@property
def _ready(self):
return (self.squash or self.merge_method) and self._approved and self.status == 'success'
@property
def _linked_prs(self):
if re.search(r':patch-\d+', self.label):
return self.browse(())
return self.search([
('target', '=', self.target.id),
('label', '=', self.label),
('state', 'not in', ('merged', 'closed')),
]) - self
# missing link to other PRs
@api.depends('priority', 'state', 'squash', 'merge_method', 'batch_id.active', 'label')
def _compute_is_blocked(self):
@ -719,31 +742,23 @@ class PullRequests(models.Model):
if pr.state in ('merged', 'closed'):
continue
batch = pr
if not re.search(r':patch-\d+', pr.label):
batch = self.search([
('target', '=', pr.target.id),
('label', '=', pr.label),
('state', 'not in', ('merged', 'closed')),
])
linked = pr._linked_prs
# check if PRs are configured (single commit or merge method set)
if not (pr.squash or pr.merge_method):
pr.blocked = 'has no merge method'
continue
other_unset = next((p for p in batch if not (p.squash or p.merge_method)), None)
other_unset = next((p for p in linked if not (p.squash or p.merge_method)), None)
if other_unset:
pr.blocked = "linked PR %s has no merge method" % other_unset.display_name
continue
# check if any PR in the batch is p=0 and none is in error
if any(p.priority == 0 for p in batch):
in_error = next((p for p in batch if p.state == 'error'), None)
if in_error:
if pr == in_error:
pr.blocked = "in error"
else:
pr.blocked = "linked pr %s in error" % in_error.display_name
if any(p.priority == 0 for p in (pr | linked)):
if pr.state == 'error':
pr.blocked = "in error"
other_error = next((p for p in linked if p.state == 'error'), None)
if other_error:
pr.blocked = "linked pr %s in error" % other_error.display_name
# if none is in error then none is blocked because p=0
# "unblocks" the entire batch
continue
@ -752,7 +767,7 @@ class PullRequests(models.Model):
pr.blocked = 'not ready'
continue
unready = next((p for p in batch if p.state != 'ready'), None)
unready = next((p for p in linked if p.state != 'ready'), None)
if unready:
pr.blocked = 'linked pr %s is not ready' % unready.display_name
continue

View File

@ -62,3 +62,25 @@ h5 { font-size: 1em; }
.pr-awaiting { opacity: 0.8; }
.pr-blocked { opacity: 0.6; }
.pr-failed { opacity: 0.9; }
ul.todo {
list-style-type: '';
> li.ok {
//@extend .alert-success;
list-style-type: '';
}
> li.fail {
@extend .alert-danger;
list-style-type: '';
}
}
dl.runbot-merge-fields {
@extend .row;
> dt {
@extend .col-sm-2;
}
> dd {
@extend .col-sm-10;
}
}

View File

@ -56,6 +56,9 @@
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="group_id" string="Accessible to"/>
</group>
<group>
<field name="branch_filter"/>
</group>

View File

@ -299,4 +299,98 @@
</div></div>
</t>
</template>
<template id="view_pull_request_info_merged">
<div class="alert alert-success">
Merged
<t t-if="merged_head">
at <a t-attf-href="https://github.com/{{pr.repository.name}}/commit/{{merged_head}}"><t t-esc="merged_head"/></a>
</t>
</div>
</template>
<template id="view_pull_request_info_closed">
<div class="alert alert-light">
Closed
</div>
</template>
<template id="view_pull_request_info_error">
<div class="alert alert-danger">
Error:
<p t-field="pr.staging_id.reason"/>
</div>
</template>
<template id="view_pull_request_info_staging">
<div class="alert alert-primary">
Staged <span t-field="pr.staging_id.staged_at" t-options="{'widget': 'relative'}"/>.
</div>
</template>
<template id="view_pull_request_info_open">
<!-- nb: replicates `blocked`, maybe that should be split into various criteria? -->
<div class="alert alert-info">
<p t-if="pr.blocked" class="alert-danger">Blocked</p>
<p t-else="" class="alert-success">Ready (waiting for staging)</p>
<ul class="todo">
<li t-att-class="'ok' if pr.squash or pr.merge_method else 'fail'">
Merge method
</li>
<li t-att-class="'ok' if pr._approved else 'fail'">
Review
</li>
<li t-att-class="'ok' if pr.state not in ('opened', 'approved') else ''">
CI
<t t-set="statuses" t-value="statuses"/>
<ul class="todo">
<t t-foreach="pr.repository.status_ids._for_pr(pr)" t-as="ci">
<t t-set="st" t-value="statuses.get(ci.context.strip())"/>
<t t-set="result">
<t t-if="not st or st['state'] == 'pending'"></t>
<t t-elif="st['state'] in ('error', 'failure')">fail</t>
<t t-else="">ok</t>
</t>
<li t-att-class="result">
<a t-att-href="st.get('target_url') if st else None"><t t-esc="ci.context.strip()"/></a><t t-if="st and st.get('description')">: <t t-esc="st['description']"/></t>
</li>
</t>
</ul>
</li>
<t t-set="linked_prs" t-value="pr._linked_prs"/>
<li t-if="linked_prs" t-att-class="'ok' if all(l._ready for l in linked_prs) else 'fail'">
Linked pull requests
<ul class="todo">
<t t-foreach="linked_prs" t-as="linked">
<li t-att-class="'ok' if linked._ready else 'fail'">
<a t-attf-href="/{{linked.repository.name}}/pull/{{linked.number}}" t-field="linked.display_name"/>
</li>
</t>
</ul>
</li>
</ul>
</div>
</template>
<template id="view_pull_request">
<t t-call="website.layout">
<div id="wrap"><div class="container-fluid">
<t t-set="pr_url" t-valuef="https://github.com/{{pr.repository.name}}/pull/{{pr.number}}"/>
<a t-att-href="pr_url">
<h1 t-field="pr.display_name"/>
</a>
<h6>Created by <span t-field="pr.author.display_name"/></h6>
<t t-set="tmpl">
<t t-if="pr.state in ('merged', 'closed', 'error')"><t t-esc="pr.state"/></t>
<t t-elif="pr.staging_id">staging</t>
<t t-else="">open</t>
</t>
<t t-call="runbot_merge.view_pull_request_info_{{tmpl.strip()}}"/>
<dl class="runbot-merge-fields">
<dt>label</dt>
<dd><span t-field="pr.label"/></dd>
<dt>head</dt>
<dd><a t-attf-href="{{pr_url}}/commits/{{pr.head}}"><span t-field="pr.head"/></a></dd>
</dl>
<p t-field="pr.message"/>
</div></div>
</t>
</template>
</odoo>