mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[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:
parent
47e8b5b014
commit
c80d8048f7
@ -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 {}
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user