Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions runbot/models/bundle.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
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


VALID_BUNDLE_NAME_RE = re.compile(r'^.{3,6}-.*-.{2,5}$')
NGRAM_RE = re.compile(r'.+\(([a-z]{2,5})\)$')


class Bundle(models.Model):
Expand Down Expand Up @@ -55,7 +60,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.")

Expand Down Expand Up @@ -201,19 +210,62 @@ 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<ngram>[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', 'branch_ids.forwardport_of_id.pr_author', 'branch_ids.is_pr')
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 = {}
for bundle in bundles:
pr_branches = bundle.branch_ids.filtered('is_pr')
github_logins_by_bundle[bundle] = {
(br.forwardport_of_id.pr_author if br.forwardport_of_id else br.pr_author)
for br in pr_branches
}

github_logins = set(chain.from_iterable(github_logins_by_bundle.values()))
users = self.env['res.users'].search([('share', '=', False)])
users_with_github_login = users.filtered(lambda rec: rec.github_login in github_logins)
user_ids_by_github_login = {u.github_login: u.id for u in users_with_github_login}
for bundle, github_logins in github_logins_by_bundle.items():
if user_ids := list(filter(None, {user_ids_by_github_login.get(gl) for gl in github_logins})):
bundle.author_ids = user_ids
bundles -= bundle

bundles = bundles.filtered(lambda b: VALID_BUNDLE_NAME_RE.match(b.name))
if not bundles:
return

user_ids_by_ngram = {}
for user in users:
ngrams = NGRAM_RE.findall(user.complete_name or '')
if ngrams:
user_ids_by_ngram[ngrams[0]] = user.id

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))
if authors := list(filter(None, [user_ids_by_ngram.get(bundle_ngram)])):
bundle.author_ids = authors

@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).sorted('id')

@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):
Expand Down Expand Up @@ -348,6 +400,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')
41 changes: 32 additions & 9 deletions runbot/tests/test_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand All @@ -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"""
Expand Down Expand Up @@ -309,14 +311,16 @@ 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)
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,
Expand All @@ -332,6 +336,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({
Expand All @@ -341,3 +346,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)
6 changes: 4 additions & 2 deletions runbot/views/bundle_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@
<field name="name" widget="char_frontend_url"/>
<field name="description"/>
<field name="tag_ids" widget="many2many_tags" options="{'not_delete': True, 'no_create': True}"/>
<field name="project_id"/>
<field name="team_id" string="Owning Team"/>
<field name="team_ids" widget="many2many_tags"/>
<field name="author_ids" widget="many2many_tags"/>
</group>
<group>
<group string="Base options">
<field name="project_id"/>
<field name="priority_offset"/>
<field name="team_id"/>
<field name="sticky" readonly="0"/>
<field name="is_base" readonly="1"/>
<field name="is_staging" readonly="1"/>
Expand Down