Skip to content

Commit e186c48

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 14bf876 commit e186c48

File tree

3 files changed

+113
-23
lines changed

3 files changed

+113
-23
lines changed

runbot/models/bundle.py

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

6064
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.")
6165

@@ -201,19 +205,60 @@ def _compute_all_trigger_custom_ids(self):
201205
parent_bundle = self.env['runbot.bundle'].search([('name', '=', targets.pop())])
202206
bundle.all_trigger_custom_ids = parent_bundle.all_trigger_custom_ids
203207

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

218263
@api.depends('branch_ids')
219264
def _compute_description(self):
@@ -348,6 +393,7 @@ class BundleTag(models.Model):
348393

349394
_name = "runbot.bundle.tag"
350395
_description = "Bundle tag"
396+
_order = "id desc, name"
351397

352398
name = fields.Char(string='Bundle Tag')
353399
bundle_ids = fields.Many2many('runbot.bundle', string='Bundles')

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
@@ -52,12 +52,14 @@
5252
<field name="name" widget="char_frontend_url"/>
5353
<field name="description"/>
5454
<field name="tag_ids" widget="many2many_tags" options="{'not_delete': True, 'no_create': True}"/>
55+
<field name="project_id"/>
56+
<field name="team_id" string="Owning Team"/>
57+
<field name="team_ids" widget="many2many_tags"/>
58+
<field name="author_ids" widget="many2many_tags"/>
5559
</group>
5660
<group>
5761
<group string="Base options">
58-
<field name="project_id"/>
5962
<field name="priority_offset"/>
60-
<field name="team_id"/>
6163
<field name="sticky" readonly="0"/>
6264
<field name="is_base" readonly="1"/>
6365
<field name="is_staging" readonly="1"/>

0 commit comments

Comments
 (0)