mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
|
||||||
|
import werkzeug.exceptions
|
||||||
|
|
||||||
from odoo.http import Controller, route, request
|
from odoo.http import Controller, route, request
|
||||||
|
|
||||||
LIMIT = 20
|
LIMIT = 20
|
||||||
@ -21,3 +26,20 @@ class MergebotDashboard(Controller):
|
|||||||
'stagings': stagings[:LIMIT],
|
'stagings': stagings[:LIMIT],
|
||||||
'next': stagings[-1].staged_at if len(stagings) > LIMIT else None,
|
'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)
|
project_id = fields.Many2one('runbot_merge.project', required=True)
|
||||||
status_ids = fields.One2many('runbot_merge.repository.status', 'repo_id', string="Required Statuses")
|
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")
|
branch_filter = fields.Char(default='[(1, "=", 1)]', help="Filter branches valid for this repository")
|
||||||
substitutions = fields.Text(
|
substitutions = fields.Text(
|
||||||
"label substitutions",
|
"label substitutions",
|
||||||
@ -711,6 +713,27 @@ class PullRequests(models.Model):
|
|||||||
for p in self
|
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
|
# missing link to other PRs
|
||||||
@api.depends('priority', 'state', 'squash', 'merge_method', 'batch_id.active', 'label')
|
@api.depends('priority', 'state', 'squash', 'merge_method', 'batch_id.active', 'label')
|
||||||
def _compute_is_blocked(self):
|
def _compute_is_blocked(self):
|
||||||
@ -719,31 +742,23 @@ class PullRequests(models.Model):
|
|||||||
if pr.state in ('merged', 'closed'):
|
if pr.state in ('merged', 'closed'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
batch = pr
|
linked = pr._linked_prs
|
||||||
if not re.search(r':patch-\d+', pr.label):
|
|
||||||
batch = self.search([
|
|
||||||
('target', '=', pr.target.id),
|
|
||||||
('label', '=', pr.label),
|
|
||||||
('state', 'not in', ('merged', 'closed')),
|
|
||||||
])
|
|
||||||
|
|
||||||
# check if PRs are configured (single commit or merge method set)
|
# check if PRs are configured (single commit or merge method set)
|
||||||
if not (pr.squash or pr.merge_method):
|
if not (pr.squash or pr.merge_method):
|
||||||
pr.blocked = 'has no merge method'
|
pr.blocked = 'has no merge method'
|
||||||
continue
|
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:
|
if other_unset:
|
||||||
pr.blocked = "linked PR %s has no merge method" % other_unset.display_name
|
pr.blocked = "linked PR %s has no merge method" % other_unset.display_name
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check if any PR in the batch is p=0 and none is in error
|
# check if any PR in the batch is p=0 and none is in error
|
||||||
if any(p.priority == 0 for p in batch):
|
if any(p.priority == 0 for p in (pr | linked)):
|
||||||
in_error = next((p for p in batch if p.state == 'error'), None)
|
if pr.state == 'error':
|
||||||
if in_error:
|
pr.blocked = "in error"
|
||||||
if pr == in_error:
|
other_error = next((p for p in linked if p.state == 'error'), None)
|
||||||
pr.blocked = "in error"
|
if other_error:
|
||||||
else:
|
pr.blocked = "linked pr %s in error" % other_error.display_name
|
||||||
pr.blocked = "linked pr %s in error" % in_error.display_name
|
|
||||||
# if none is in error then none is blocked because p=0
|
# if none is in error then none is blocked because p=0
|
||||||
# "unblocks" the entire batch
|
# "unblocks" the entire batch
|
||||||
continue
|
continue
|
||||||
@ -752,7 +767,7 @@ class PullRequests(models.Model):
|
|||||||
pr.blocked = 'not ready'
|
pr.blocked = 'not ready'
|
||||||
continue
|
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:
|
if unready:
|
||||||
pr.blocked = 'linked pr %s is not ready' % unready.display_name
|
pr.blocked = 'linked pr %s is not ready' % unready.display_name
|
||||||
continue
|
continue
|
||||||
|
@ -62,3 +62,25 @@ h5 { font-size: 1em; }
|
|||||||
.pr-awaiting { opacity: 0.8; }
|
.pr-awaiting { opacity: 0.8; }
|
||||||
.pr-blocked { opacity: 0.6; }
|
.pr-blocked { opacity: 0.6; }
|
||||||
.pr-failed { opacity: 0.9; }
|
.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>
|
<h1><field name="name"/></h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="group_id" string="Accessible to"/>
|
||||||
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="branch_filter"/>
|
<field name="branch_filter"/>
|
||||||
</group>
|
</group>
|
||||||
|
@ -299,4 +299,98 @@
|
|||||||
</div></div>
|
</div></div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</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>
|
</odoo>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user