From c80d8048f7508aaf97af57f4f978431ea488f04a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 13 Nov 2020 10:38:48 +0100 Subject: [PATCH] [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. --- runbot_merge/controllers/dashboard.py | 22 +++++ runbot_merge/models/pull_requests.py | 49 +++++++---- runbot_merge/static/scss/runbot_merge.scss | 22 +++++ runbot_merge/views/mergebot.xml | 3 + runbot_merge/views/templates.xml | 94 ++++++++++++++++++++++ 5 files changed, 173 insertions(+), 17 deletions(-) diff --git a/runbot_merge/controllers/dashboard.py b/runbot_merge/controllers/dashboard.py index 061ad5c7..65078021 100644 --- a/runbot_merge/controllers/dashboard.py +++ b/runbot_merge/controllers/dashboard.py @@ -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('///pull/', 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 {} + }) diff --git a/runbot_merge/models/pull_requests.py b/runbot_merge/models/pull_requests.py index f571e99f..27c79f7b 100644 --- a/runbot_merge/models/pull_requests.py +++ b/runbot_merge/models/pull_requests.py @@ -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 diff --git a/runbot_merge/static/scss/runbot_merge.scss b/runbot_merge/static/scss/runbot_merge.scss index 97ce8693..f1cb7beb 100644 --- a/runbot_merge/static/scss/runbot_merge.scss +++ b/runbot_merge/static/scss/runbot_merge.scss @@ -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; + } +} diff --git a/runbot_merge/views/mergebot.xml b/runbot_merge/views/mergebot.xml index e3fe9fbb..98b760ee 100644 --- a/runbot_merge/views/mergebot.xml +++ b/runbot_merge/views/mergebot.xml @@ -56,6 +56,9 @@

+ + + diff --git a/runbot_merge/views/templates.xml b/runbot_merge/views/templates.xml index 3879c6f3..6ea86076 100644 --- a/runbot_merge/views/templates.xml +++ b/runbot_merge/views/templates.xml @@ -299,4 +299,98 @@ + + + + + + + + +