Skip to content

Commit c8d2f98

Browse files
committed
[IMP] runbot: add authors and teams on bundles
With this commit, the authors involved in a bundle are computed. Authors are found based on github logins first, then from the commit authors and commiters and finally from the ngram extracted from the bundle name. The teams are infered from the authors found. Finally an `Owning team` is automatically choosen (the first team of the teams) or can be manually set.
1 parent 26ba7fb commit c8d2f98

File tree

3 files changed

+110
-23
lines changed

3 files changed

+110
-23
lines changed

runbot/models/bundle.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ class Bundle(models.Model):
5757
# extra_info
5858
description = fields.Char('Description', compute='_compute_description', store=True, readonly=False)
5959
tag_ids = fields.Many2many('runbot.bundle.tag', string='Tags')
60-
team_id = fields.Many2one('runbot.team', compute='_compute_team_id', store=True, readonly=False)
60+
author_ids = fields.Many2many('res.users', string='Involved Users', compute='_compute_author_ids', domain=[('share', '=', False)])
61+
team_ids = fields.Many2many('runbot.team', string='Involved Teams', compute='_compute_team_ids')
62+
team_id = fields.Many2one('runbot.team', string='Owning Team', compute='_compute_team_id', inverse='_inverse_team_id', store=True, tracking=True)
63+
manual_team_id = fields.Many2one('runbot.team', 'Manually set team')
64+
auto_team_id = fields.Many2one('runbot.team', 'Automatically set team', compute='_compute_auto_team_id', readonly=True)
6165

6266
def _compute_frontend_url(self):
6367
for bundle in self:
@@ -211,19 +215,58 @@ def _compute_all_trigger_custom_ids(self):
211215
parent_bundle = self.env['runbot.bundle'].search([('name', '=', targets.pop())])
212216
bundle.all_trigger_custom_ids = parent_bundle.all_trigger_custom_ids
213217

214-
@api.depends('name')
215-
def _compute_team_id(self):
216-
ngram_re = re.compile(r'.+\((?P<ngram>[a-z]{2,4})\)$')
217-
team_by_ngram_project = dict()
218-
for team in self.env['runbot.team'].search([('module_ownership_ids', '!=', False)]):
219-
for user in team.user_ids:
220-
if m := ngram_re.match(user.name.lower()):
221-
team_by_ngram_project[m.group('ngram'), team.project_id] = team
222-
for bundle in self:
223-
if bundle.is_base or not bundle.name:
218+
@api.depends('name', 'branch_ids.head', 'branch_ids.pr_author')
219+
def _compute_author_ids(self):
220+
valid_bundle_name_re = re.compile(r'^.{3,6}-.*-.{2,5}$')
221+
self.author_ids = self.env['res.users'].browse()
222+
bundles = self.filtered(lambda b: not b.sticky and not b.is_base and not b.is_staging and valid_bundle_name_re.match(b.name))
223+
github_logins_by_bundle = {bundle: set(bundle.branch_ids.filtered('is_pr').mapped('pr_author')) for bundle in bundles}
224+
all_github_logins = set()
225+
for gl in github_logins_by_bundle.values():
226+
all_github_logins |= gl
227+
user_ids_by_github_login = {u.github_login: u.id for u in self.env['res.users'].search([('share', '=', False), ('github_login', 'in', all_github_logins)])}
228+
for bundle, github_logins in github_logins_by_bundle.items():
229+
if users_ids := list(filter(None, {user_ids_by_github_login.get(gl) for gl in github_logins})):
230+
bundle.author_ids = users_ids
231+
232+
user_ids_by_email = {u.email: u.id for u in self.env['res.users'].search([('share', '=', False)])}
233+
for bundle in bundles:
234+
emails = set()
235+
emails.update(bundle.branch_ids.head.mapped(lambda rec: rec.committer_email and rec.committer_email.strip('<>')))
236+
emails.update(bundle.branch_ids.head.mapped(lambda rec: rec.author_email and rec.author_email.strip('<>')))
237+
if users_ids := list(filter(None, {user_ids_by_email.get(e) for e in emails})):
238+
bundle.author_ids |= self.env['res.users'].browse(users_ids)
239+
bundles -= bundle
240+
241+
if not bundles:
242+
return
243+
244+
ngram_re = re.compile(r'.+\(([a-z]{2,5})\)$')
245+
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]}
246+
for bundle in bundles:
247+
if not bundle.name:
224248
continue
225249
bundle_ngram = bundle.name.split('-')[-1].lower()
226-
bundle.team_id = team_by_ngram_project.get((bundle_ngram, bundle.project_id))
250+
bundle.author_ids = list(filter(None, [user_ids_by_ngram.get(bundle_ngram)]))
251+
252+
@api.depends('author_ids')
253+
def _compute_team_ids(self):
254+
for bundle in self:
255+
bundle.team_ids = bundle.author_ids.runbot_team_ids.filtered(lambda rec: rec.module_ownership_ids)
256+
257+
@api.depends('manual_team_id', 'auto_team_id')
258+
def _compute_team_id(self):
259+
for bundle in self:
260+
bundle.team_id = bundle.manual_team_id or bundle.auto_team_id
261+
262+
@api.depends('name')
263+
def _compute_auto_team_id(self):
264+
for bundle in self:
265+
bundle.auto_team_id = bundle.team_ids and bundle.team_ids[0]
266+
267+
def _inverse_team_id(self):
268+
self.manual_team_id = self.team_id
269+
227270

228271
@api.depends('branch_ids')
229272
def _compute_description(self):

runbot/tests/test_branch.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ def test_relations_no_match(self):
152152
self.assertEqual(b.bundle_id.base_id.name, 'master')
153153

154154
def test_relations_pr(self):
155-
self.Branch.create({
155+
dev_branch = self.Branch.create({
156156
'remote_id': self.remote_odoo_dev.id,
157-
'name': 'master-test-tri',
157+
'name': 'master-test-tri-imp',
158158
'is_pr': False,
159159
})
160160

@@ -167,17 +167,19 @@ def test_relations_pr(self):
167167
'login': 'Pr author'
168168
},
169169
}
170-
b = self.Branch.create({
170+
pr_branch = self.Branch.create({
171171
'remote_id': self.remote_odoo_dev.id,
172172
'name': '100',
173173
'is_pr': True,
174174
})
175175

176-
self.assertEqual(b.bundle_id.name, 'master-test-tri-imp')
177-
self.assertEqual(b.bundle_id.base_id.name, 'master')
178-
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
179-
self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2'])
176+
bundle = pr_branch.bundle_id
180177

178+
self.assertEqual(bundle.name, 'master-test-tri-imp')
179+
self.assertEqual(bundle.base_id.name, 'master')
180+
self.assertEqual(bundle.previous_major_version_base_id.name, '13.0')
181+
self.assertEqual(sorted(bundle.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2'])
182+
self.assertIn(dev_branch, bundle.branch_ids)
181183

182184
class TestBranchForbidden(RunbotCase):
183185
"""Test that a branch matching the repo forbidden regex, goes to dummy bundle"""
@@ -309,14 +311,17 @@ def test_bundle_team_attribution(self):
309311
self.stop_patcher('isfile')
310312
self.stop_patcher('isdir') # needed to create the user avatar
311313
create_context = {'no_reset_password': True, 'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True}
312-
test_user = new_test_user(self.env, login='testrunbot', name='testrunbot (tru)', context=create_context)
314+
committer_user = new_test_user(self.env, login='testrunbot', name='testrunbot (tru)', email='trut@somewhere.com', context=create_context)
315+
author_user = new_test_user(self.env, login='testrunbot_author', name='test author (aut)', email='aut@somewhere.com', context=create_context)
316+
github_user = new_test_user(self.env, login='github_author', name='github author (gaut)', email='gaut@somewhere.com', context=create_context)
317+
github_user.github_login = 'gaut_github'
313318

314319
team = self.env['runbot.team'].create({
315320
'name': 'Test Team',
316321
'project_id': self.project.id,
317322
})
318323

319-
team.user_ids += test_user
324+
team.user_ids += committer_user
320325

321326
branch = self.Branch.create({
322327
'remote_id': self.remote_odoo_dev.id,
@@ -332,6 +337,7 @@ def test_bundle_team_attribution(self):
332337

333338
bundle = self.env['runbot.bundle'].search([('name', '=', branch.name)])
334339
self.assertEqual(bundle.team_id, team)
340+
self.assertEqual(bundle.author_ids, committer_user, 'The only involved author should be the one based on bundle ngram')
335341

336342
# now test that a team can be manually set on a bundle
337343
other_team = self.env['runbot.team'].create({
@@ -341,3 +347,39 @@ def test_bundle_team_attribution(self):
341347

342348
bundle.team_id = other_team
343349
self.assertEqual(bundle.team_id, other_team)
350+
351+
# testing the author_ids based on commit author and committers
352+
new_commit = self.Commit.create({
353+
'name': 'd0d0caca',
354+
'tree_hash': 'cacad0d0',
355+
'repo_id': self.repo_odoo.id,
356+
'author': 'aut',
357+
'author_email': author_user.email,
358+
'committer': 'test runbot',
359+
'committer_email': committer_user.email,
360+
})
361+
362+
branch.head = new_commit
363+
self.assertEqual(bundle.team_id, other_team, 'Team should be unchanged as it wa manually set')
364+
365+
self.assertIn(committer_user, bundle.author_ids)
366+
self.assertIn(author_user, bundle.author_ids)
367+
self.assertEqual(2, len(bundle.author_ids))
368+
self.patchers['github_patcher'].return_value = {
369+
'base': {'ref': 'saas-19.1'},
370+
'head': {'label': 'dev:saas-19.1-test-tru', 'repo': {'full_name': 'dev/odoo'}},
371+
'title': '[IMP] Title',
372+
'body': 'Body',
373+
'user': {
374+
'login': github_user.github_login,
375+
},
376+
}
377+
pr_branch = self.Branch.create({
378+
'remote_id': self.remote_odoo_dev.id,
379+
'name': '100',
380+
'is_pr': True,
381+
})
382+
383+
self.assertIn(pr_branch, bundle.branch_ids)
384+
self.assertIn(github_user, bundle.author_ids)
385+
self.assertEqual(3, len(bundle.author_ids))

runbot/views/bundle_views.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@
4949
<field name="name" widget="char_frontend_url"/>
5050
<field name="description"/>
5151
<field name="tag_ids" widget="many2many_tags" options="{'not_delete': True, 'no_create': True}"/>
52+
<field name="project_id"/>
53+
<field name="team_id" string="Owning Team"/>
54+
<field name="team_ids" widget="many2many_tags"/>
55+
<field name="author_ids" widget="many2many_tags"/>
5256
</group>
5357
<group>
5458
<group string="Base options">
55-
<field name="project_id"/>
56-
<field name="team_id"/>
5759
<field name="sticky" readonly="0"/>
5860
<field name="to_upgrade" readonly="0"/>
5961
<field name="to_upgrade_from" readonly="0"/>

0 commit comments

Comments
 (0)