diff --git a/runbot/__manifest__.py b/runbot/__manifest__.py index 5085df56..a5f3cbba 100644 --- a/runbot/__manifest__.py +++ b/runbot/__manifest__.py @@ -32,6 +32,7 @@ 'templates/bundle.xml', 'templates/commit.xml', 'templates/dashboard.xml', + 'templates/error_merge.xml', 'templates/frontend.xml', 'templates/git.xml', 'templates/nginx.xml', @@ -40,6 +41,7 @@ 'views/branch_views.xml', 'views/build_error_link_views.xml', 'views/build_error_views.xml', + 'views/build_error_merge_views.xml', 'views/build_views.xml', 'views/bundle_views.xml', 'views/codeowner_views.xml', diff --git a/runbot/controllers/__init__.py b/runbot/controllers/__init__.py index 96d149ab..23066c11 100644 --- a/runbot/controllers/__init__.py +++ b/runbot/controllers/__init__.py @@ -3,3 +3,4 @@ from . import frontend from . import hook from . import badge +from . import errors \ No newline at end of file diff --git a/runbot/controllers/errors.py b/runbot/controllers/errors.py new file mode 100644 index 00000000..dfd3441a --- /dev/null +++ b/runbot/controllers/errors.py @@ -0,0 +1,11 @@ +from odoo.http import Controller, Response, request, route + + +class ErrorControlle(Controller): + + @route('/runbot/error/merge/result/', type='http', auth='public', website=True) + def error_filter_result(self, filter_id=None, **kwargs): + merger = request.env['runbot.build.error.merge'].browse(int(filter_id)) + if not merger: + return Response('Error merge not found', status=404) + return request.render('runbot.error_merge_result', {'merger': merger, 'results': merger._get_matching_groups()}) \ No newline at end of file diff --git a/runbot/models/__init__.py b/runbot/models/__init__.py index dbd376be..fbb0f087 100644 --- a/runbot/models/__init__.py +++ b/runbot/models/__init__.py @@ -6,6 +6,7 @@ from . import build from . import build_config from . import build_config_codeowner from . import build_error +from . import build_error_merge from . import bundle from . import codeowner from . import commit diff --git a/runbot/models/build_error.py b/runbot/models/build_error.py index c2eccda4..85037aea 100644 --- a/runbot/models/build_error.py +++ b/runbot/models/build_error.py @@ -595,6 +595,7 @@ class BuildErrorContent(models.Model): _inherit = ('mail.thread', 'mail.activity.mixin', 'runbot.build.error.seen.mixin') _rec_name = "id" + active = fields.Boolean('Active', related='error_id.active') error_id = fields.Many2one('runbot.build.error', 'Linked to', index=True, required=True) error_display_id = fields.Integer(compute='_compute_error_display_id', string="Error id") content = fields.Text('Error message', required=True) diff --git a/runbot/models/build_error_merge.py b/runbot/models/build_error_merge.py new file mode 100644 index 00000000..fb2d563f --- /dev/null +++ b/runbot/models/build_error_merge.py @@ -0,0 +1,73 @@ +from odoo import models, fields, api +from odoo.osv import expression + +class BuildErrorMerge(models.Model): + _name = 'runbot.build.error.merge' + _description = 'Error Merge patterns' + _inherit = ['mail.thread'] + + name = fields.Char('Name', required=True) + merge_filter_ids = fields.One2many('runbot.build.error.merge.filters', 'error_merge_id', 'Merge Lines') + description = fields.Char('Description', compute='_compute_description', store=True, tracking=True) + oneline_description = fields.Char('One Line Description', compute='_compute_description_online') + auto_merge = fields.Boolean('Auto Merge', default=False) + + def _get_read_group_params(self, domain=None): + domain = domain if domain is not None else [('active', '=', True)] + filters_without_value = self.merge_filter_ids.filtered(lambda f: not f.value) + for filter in self.merge_filter_ids: + if filter.value: + domain = expression.AND([domain, [(filter.field_name, '=', filter.value)]]) + else: + domain = expression.AND([domain, [(filter.field_name, '!=', False)]]) + groups = filters_without_value.mapped('field_name') + assert groups + + return ( + domain, + groups, + ) + + def _get_matching_groups(self): + domain, groups = self._get_read_group_params() + result = self.env['runbot.build.error.content']._read_group( + domain, + groups, + ['id:array_agg'], + [('error_id:count_distinct', '>', 1)] + ) + return result + + def _get_similar_domain(self, error_content): + if any(f.value and error_content[f.field_name] != f.value for f in self.merge_filter_ids): + return + domain = [(f.field_name, '==', f.value | error_content[f.field_name]) for f in self.merge_filter_ids if error_content[f.field_name]] + return domain + + def action_open_candidates(self): + self.ensure_one() + return { + 'name': 'Error Candidates', + 'type': 'ir.actions.act_url', + 'url': f"/runbot/error/merge/result/{self.id}", + } + + @api.depends('merge_filter_ids.field_name', 'merge_filter_ids.value') + def _compute_description(self): + for record in self: + record.description = '\n'.join(f'{f.field_name}: {f.value or "Any"}' for f in record.merge_filter_ids) + + @api.depends('description') + def _compute_description_online(self): + for record in self: + record.oneline_description = record.description.replace('\n', ', ') + + +class BuildErrorMergeFilter(models.Model): + _name = 'runbot.build.error.merge.filters' + _description = 'Error Merge patterns filters' + + field = fields.Many2one('ir.model.fields', 'Field', domain=[('model_id.model', '=', 'runbot.build.error.content')], required=True, ondelete='cascade') + field_name = fields.Char('Field Name', related='field.name', store=True, readonly=True) + value = fields.Char('Value') + error_merge_id = fields.Many2one('runbot.build.error.merge', 'Error Merge', required=True) \ No newline at end of file diff --git a/runbot/security/ir.model.access.csv b/runbot/security/ir.model.access.csv index cd54fa04..8766494e 100644 --- a/runbot/security/ir.model.access.csv +++ b/runbot/security/ir.model.access.csv @@ -151,4 +151,9 @@ access_runbot_build_stat_regex_wizard,access_runbot_build_stat_regex_wizard,mode access_runbot_host_message,access_runbot_host_message,runbot.model_runbot_host_message,runbot.group_runbot_admin,1,0,0,0 +access_runbot_build_error_merge,access_runbot_build_error_merge,runbot.model_runbot_build_error_merge,base.group_user,1,0,0,0 +access_runbot_build_error_merge_filters,access_runbot_build_error_merge_filters,runbot.model_runbot_build_error_merge_filters,base.group_user,1,0,0,0 +access_runbot_build_error_merge,access_runbot_build_error_merge,runbot.model_runbot_build_error_merge,runbot.group_runbot_admin,1,1,1,1 +access_runbot_build_error_merge_filters,access_runbot_build_error_merge_filters,runbot.model_runbot_build_error_merge_filters,runbot.group_runbot_admin,1,1,1,1 + diff --git a/runbot/templates/error_merge.xml b/runbot/templates/error_merge.xml new file mode 100644 index 00000000..4a493e23 --- /dev/null +++ b/runbot/templates/error_merge.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/runbot/views/build_error_merge_views.xml b/runbot/views/build_error_merge_views.xml new file mode 100644 index 00000000..2712a4a5 --- /dev/null +++ b/runbot/views/build_error_merge_views.xml @@ -0,0 +1,44 @@ + + + + runbot.build.error.merge.form + runbot.build.error.merge + +
+
+
+ + + + + + + + + + + + + +
+
+
+ + runbot.build.error.merge.list + runbot.build.error.merge + + + + + + + + + + Error merge + runbot.build.error.merge + list,form + +
+
diff --git a/runbot/views/menus.xml b/runbot/views/menus.xml index 19063a9a..eb41ae5a 100644 --- a/runbot/views/menus.xml +++ b/runbot/views/menus.xml @@ -38,6 +38,8 @@ + + @@ -59,7 +61,7 @@ - +