diff --git a/runbot/models/bundle.py b/runbot/models/bundle.py index 747440072..541497a71 100644 --- a/runbot/models/bundle.py +++ b/runbot/models/bundle.py @@ -1,8 +1,9 @@ import datetime import re - from collections import defaultdict -from odoo import models, fields, api, tools +from itertools import chain + +from odoo import api, fields, models, tools class Bundle(models.Model): @@ -55,7 +56,11 @@ class Bundle(models.Model): # extra_info description = fields.Char('Description', compute='_compute_description', store=True, readonly=False) tag_ids = fields.Many2many('runbot.bundle.tag', string='Tags') - team_id = fields.Many2one('runbot.team', compute='_compute_team_id', store=True, readonly=False) + author_ids = fields.Many2many('res.users', string='Involved Users', compute='_compute_author_ids', domain=[('share', '=', False)]) + team_ids = fields.Many2many('runbot.team', string='Involved Teams', compute='_compute_team_ids') + team_id = fields.Many2one('runbot.team', string='Owning Team', compute='_compute_team_id', inverse='_inverse_team_id', store=True, tracking=True) + manual_team_id = fields.Many2one('runbot.team', 'Manually set team') + auto_team_id = fields.Many2one('runbot.team', 'Automatically set team', compute='_compute_auto_team_id', readonly=True) priority_offset = fields.Integer("Priority offset", help="Offset in seconds to remove from the create date of a batch to define priority, positive value means higher priority, negative value means lower priority.") @@ -201,19 +206,51 @@ def _compute_all_trigger_custom_ids(self): parent_bundle = self.env['runbot.bundle'].search([('name', '=', targets.pop())]) bundle.all_trigger_custom_ids = parent_bundle.all_trigger_custom_ids - @api.depends('name') - def _compute_team_id(self): - ngram_re = re.compile(r'.+\((?P[a-z]{2,4})\)$') - team_by_ngram_project = dict() - for team in self.env['runbot.team'].search([('module_ownership_ids', '!=', False)]): - for user in team.user_ids: - if m := ngram_re.match(user.name.lower()): - team_by_ngram_project[m.group('ngram'), team.project_id] = team - for bundle in self: - if bundle.is_base or not bundle.name: + @api.depends('name', 'branch_ids.pr_author', 'branch_ids.forwardport_of_id') + def _compute_author_ids(self): + self.author_ids = self.env['res.users'].browse() + bundles = self.filtered(lambda b: not b.is_base and not b.is_staging) + + github_logins_by_bundle = {bundle: set(bundle.branch_ids.filtered('is_pr').mapped(lambda br: (br.forwardport_of_id and br.forwardport_of_id.pr_author) or br.pr_author)) for bundle in bundles} + github_logins = set(chain.from_iterable(github_logins_by_bundle.values())) + users = self.env['res.users'].search([('share', '=', False), ('github_login', 'in', github_logins)]) + user_ids_by_github_login = {u.github_login: u.id for u in users} + for bundle, github_logins in github_logins_by_bundle.items(): + if users_ids := list(filter(None, {user_ids_by_github_login.get(gl) for gl in github_logins})): + bundle.author_ids = users_ids + bundles -= bundle + + valid_bundle_name_re = re.compile(r'^.{3,6}-.*-.{2,5}$') + bundles = bundles.filtered(lambda b: valid_bundle_name_re.match(b.name)) + if not bundles: + return + + ngram_re = re.compile(r'.+\(([a-z]{2,5})\)$') + user_ids_by_ngram = {u[1][0]: u[0] for u in self.env['res.users'].search([('share', '=', False)]).mapped(lambda rec: (rec.id, ngram_re.findall(rec.complete_name))) if u[1]} + for bundle in bundles: + if not bundle.name: continue bundle_ngram = bundle.name.split('-')[-1].lower() - bundle.team_id = team_by_ngram_project.get((bundle_ngram, bundle.project_id)) + bundle.author_ids = list(filter(None, [user_ids_by_ngram.get(bundle_ngram)])) + + @api.depends('author_ids') + def _compute_team_ids(self): + for bundle in self: + bundle.team_ids = bundle.author_ids.runbot_team_ids.filtered(lambda rec: rec.module_ownership_ids) + + @api.depends('manual_team_id', 'auto_team_id') + def _compute_team_id(self): + for bundle in self: + bundle.team_id = bundle.manual_team_id or bundle.auto_team_id + + @api.depends('name', 'team_ids', 'author_ids') + def _compute_auto_team_id(self): + for bundle in self: + bundle.auto_team_id = bundle.team_ids and bundle.team_ids[0] + + def _inverse_team_id(self): + self.manual_team_id = self.team_id + @api.depends('branch_ids') def _compute_description(self): @@ -348,6 +385,7 @@ class BundleTag(models.Model): _name = "runbot.bundle.tag" _description = "Bundle tag" + _order = "id desc, name" name = fields.Char(string='Bundle Tag') bundle_ids = fields.Many2many('runbot.bundle', string='Bundles') diff --git a/runbot/tests/test_branch.py b/runbot/tests/test_branch.py index 304ce0d98..a4c526182 100644 --- a/runbot/tests/test_branch.py +++ b/runbot/tests/test_branch.py @@ -152,9 +152,9 @@ def test_relations_no_match(self): self.assertEqual(b.bundle_id.base_id.name, 'master') def test_relations_pr(self): - self.Branch.create({ + dev_branch = self.Branch.create({ 'remote_id': self.remote_odoo_dev.id, - 'name': 'master-test-tri', + 'name': 'master-test-tri-imp', 'is_pr': False, }) @@ -167,17 +167,19 @@ def test_relations_pr(self): 'login': 'Pr author' }, } - b = self.Branch.create({ + pr_branch = self.Branch.create({ 'remote_id': self.remote_odoo_dev.id, 'name': '100', 'is_pr': True, }) - self.assertEqual(b.bundle_id.name, 'master-test-tri-imp') - self.assertEqual(b.bundle_id.base_id.name, 'master') - self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0') - self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2']) + bundle = pr_branch.bundle_id + self.assertEqual(bundle.name, 'master-test-tri-imp') + self.assertEqual(bundle.base_id.name, 'master') + self.assertEqual(bundle.previous_major_version_base_id.name, '13.0') + self.assertEqual(sorted(bundle.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2']) + self.assertIn(dev_branch, bundle.branch_ids) class TestBranchForbidden(RunbotCase): """Test that a branch matching the repo forbidden regex, goes to dummy bundle""" @@ -309,14 +311,17 @@ def test_bundle_team_attribution(self): self.stop_patcher('isfile') self.stop_patcher('isdir') # needed to create the user avatar create_context = {'no_reset_password': True, 'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True} - test_user = new_test_user(self.env, login='testrunbot', name='testrunbot (tru)', context=create_context) + committer_user = new_test_user(self.env, login='testrunbot', name='testrunbot (tru)', email='trut@somewhere.com', context=create_context) + author_user = new_test_user(self.env, login='testrunbot_author', name='test author (aut)', email='aut@somewhere.com', context=create_context) + github_user = new_test_user(self.env, login='github_author', name='github author (gaut)', email='gaut@somewhere.com', context=create_context) + github_user.github_login = 'gaut_github' team = self.env['runbot.team'].create({ 'name': 'Test Team', 'project_id': self.project.id, }) - team.user_ids += test_user + team.user_ids += committer_user branch = self.Branch.create({ 'remote_id': self.remote_odoo_dev.id, @@ -332,6 +337,7 @@ def test_bundle_team_attribution(self): bundle = self.env['runbot.bundle'].search([('name', '=', branch.name)]) self.assertEqual(bundle.team_id, team) + self.assertEqual(bundle.author_ids, committer_user, 'The only involved author should be the one based on bundle ngram') # now test that a team can be manually set on a bundle other_team = self.env['runbot.team'].create({ @@ -341,3 +347,21 @@ def test_bundle_team_attribution(self): bundle.team_id = other_team self.assertEqual(bundle.team_id, other_team) + + self.patchers['github_patcher'].return_value = { + 'base': {'ref': 'saas-19.1'}, + 'head': {'label': 'dev:saas-19.1-test-tru', 'repo': {'full_name': 'dev/odoo'}}, + 'title': '[IMP] Title', + 'body': 'Body', + 'user': { + 'login': github_user.github_login, + }, + } + pr_branch = self.Branch.create({ + 'remote_id': self.remote_odoo_dev.id, + 'name': '100', + 'is_pr': True, + }) + + self.assertIn(pr_branch, bundle.branch_ids) + self.assertIn(github_user, bundle.author_ids) diff --git a/runbot/views/bundle_views.xml b/runbot/views/bundle_views.xml index d2804c8f3..e0120aadf 100644 --- a/runbot/views/bundle_views.xml +++ b/runbot/views/bundle_views.xml @@ -52,12 +52,14 @@ + + + + - -