mirror of
https://github.com/odoo/runbot.git
synced 2025-03-21 10:25:44 +07:00

Before this commit, dependencies (i.e. community commit to use when testing enterprise) were computed at checkout, when the build was going from pending to testing state and were not stored. Since the duplicate detection was done at create, the get_closest_branch_name was called in a loop for each posible duplicate candidate, then a last time at checkout. The main idea of this pr is to store the build dependecies on build at create, making the duplicate detection faster (especially when the build name is matching many indirect builds). The side effect of this change is that the build dependencies won't be affected if a new commit is pushed between the build creation and the checkout. The build is fully determined at creation. get_closest_branch is only called once per build The duplicate detection will also be more precise since we are matching on the commits groups that were used to run the build, and not only the branch name. Some work has also been done to rework the closest branch detection in order to manage new corner cases. Hopefully, everything should work as before (or in a better way). In a soon future, it will also be possible to use this information to make an "exact rebuild" or to find corresponding community build. Pr: #117
229 lines
10 KiB
Python
229 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
import logging
|
|
import re
|
|
from subprocess import CalledProcessError
|
|
from odoo import models, fields, api
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_re_coverage = re.compile(r'\bcoverage\b')
|
|
_re_patch = re.compile(r'.*patch-\d+$')
|
|
|
|
class runbot_branch(models.Model):
|
|
|
|
_name = "runbot.branch"
|
|
_order = 'name'
|
|
_sql_constraints = [('branch_repo_uniq', 'unique (name,repo_id)', 'The branch must be unique per repository !')]
|
|
|
|
repo_id = fields.Many2one('runbot.repo', 'Repository', required=True, ondelete='cascade')
|
|
name = fields.Char('Ref Name', required=True)
|
|
branch_name = fields.Char(compute='_get_branch_infos', string='Branch', readonly=1, store=True)
|
|
branch_url = fields.Char(compute='_get_branch_url', string='Branch url', readonly=1)
|
|
pull_head_name = fields.Char(compute='_get_branch_infos', string='PR HEAD name', readonly=1, store=True)
|
|
target_branch_name = fields.Char(compute='_get_branch_infos', string='PR target branch', readonly=1, store=True)
|
|
sticky = fields.Boolean('Sticky')
|
|
coverage = fields.Boolean('Coverage')
|
|
coverage_result = fields.Float(compute='_get_last_coverage', type='Float', string='Last coverage', store=False)
|
|
state = fields.Char('Status')
|
|
modules = fields.Char("Modules to Install", help="Comma-separated list of modules to install and test.")
|
|
job_timeout = fields.Integer('Job Timeout (minutes)', help='For default timeout: Mark it zero')
|
|
priority = fields.Boolean('Build priority', default=False)
|
|
job_type = fields.Selection([
|
|
('testing', 'Testing jobs only'),
|
|
('running', 'Running job only'),
|
|
('all', 'All jobs'),
|
|
('none', 'Do not execute jobs')
|
|
], required=True, default='all')
|
|
|
|
@api.depends('name')
|
|
def _get_branch_infos(self):
|
|
"""compute branch_name, branch_url, pull_head_name and target_branch_name based on name"""
|
|
for branch in self:
|
|
if branch.name:
|
|
branch.branch_name = branch.name.split('/')[-1]
|
|
pi = branch._get_pull_info()
|
|
if pi:
|
|
branch.target_branch_name = pi['base']['ref']
|
|
if not _re_patch.match(pi['head']['label']):
|
|
# label is used to disambiguate PR with same branch name
|
|
branch.pull_head_name = pi['head']['label']
|
|
|
|
@api.depends('branch_name')
|
|
def _get_branch_url(self):
|
|
"""compute the branch url based on branch_name"""
|
|
for branch in self:
|
|
if branch.name:
|
|
if re.match('^[0-9]+$', branch.branch_name):
|
|
branch.branch_url = "https://%s/pull/%s" % (branch.repo_id.base, branch.branch_name)
|
|
else:
|
|
branch.branch_url = "https://%s/tree/%s" % (branch.repo_id.base, branch.branch_name)
|
|
|
|
def _get_pull_info(self):
|
|
self.ensure_one()
|
|
repo = self.repo_id
|
|
if repo.token and self.name.startswith('refs/pull/'):
|
|
pull_number = self.name[len('refs/pull/'):]
|
|
return repo._github('/repos/:owner/:repo/pulls/%s' % pull_number, ignore_errors=True) or {}
|
|
return {}
|
|
|
|
def _is_on_remote(self):
|
|
# check that a branch still exists on remote
|
|
self.ensure_one()
|
|
branch = self
|
|
repo = branch.repo_id
|
|
try:
|
|
repo._git(['ls-remote', '-q', '--exit-code', repo.name, branch.name])
|
|
except CalledProcessError:
|
|
return False
|
|
return True
|
|
|
|
def create(self, vals):
|
|
vals.setdefault('coverage', _re_coverage.search(vals.get('name') or '') is not None)
|
|
return super(runbot_branch, self).create(vals)
|
|
|
|
def _get_branch_quickconnect_url(self, fqdn, dest):
|
|
self.ensure_one()
|
|
r = {}
|
|
r[self.id] = "http://%s/web/login?db=%s-all&login=admin&redirect=/web?debug=1" % (fqdn, dest)
|
|
return r
|
|
|
|
def _get_last_coverage_build(self):
|
|
""" Return the last build with a coverage value > 0"""
|
|
self.ensure_one()
|
|
return self.env['runbot.build'].search([
|
|
('branch_id.id', '=', self.id),
|
|
('state', 'in', ['done', 'running']),
|
|
('coverage_result', '>=', 0.0),
|
|
], order='sequence desc', limit=1)
|
|
|
|
def _get_last_coverage(self):
|
|
""" Compute the coverage result of the last build in branch """
|
|
for branch in self:
|
|
last_build = branch._get_last_coverage_build()
|
|
branch.coverage_result = last_build.coverage_result or 0.0
|
|
|
|
def _get_closest_branch(self, target_repo_id):
|
|
"""
|
|
Return branch id of the closest branch based on name or pr informations.
|
|
"""
|
|
self.ensure_one()
|
|
Branch = self.env['runbot.branch']
|
|
|
|
repo = self.repo_id
|
|
name = self.pull_head_name or self.branch_name
|
|
|
|
target_repo = self.env['runbot.repo'].browse(target_repo_id)
|
|
|
|
target_repo_ids = [target_repo.id]
|
|
r = target_repo.duplicate_id
|
|
while r:
|
|
if r.id in target_repo_ids:
|
|
break
|
|
target_repo_ids.append(r.id)
|
|
r = r.duplicate_id
|
|
|
|
_logger.debug('Search closest of %s (%s) in repos %r', name, repo.name, target_repo_ids)
|
|
|
|
def sort_by_repo(branch):
|
|
return (
|
|
not branch.sticky, # sticky first
|
|
target_repo_ids.index(branch.repo_id[0].id),
|
|
-1 * len(branch.branch_name), # little change of logic here, was only sorted on branch_name in prefix matching case before
|
|
-1 * branch.id
|
|
)
|
|
|
|
# 1. same name, not a PR
|
|
if not self.pull_head_name: # not a pr
|
|
domain = [
|
|
('repo_id', 'in', target_repo_ids),
|
|
('branch_name', '=', self.branch_name),
|
|
('name', '=like', 'refs/heads/%'),
|
|
]
|
|
targets = Branch.search(domain, order='id DESC')
|
|
targets = sorted(targets, key=sort_by_repo)
|
|
if targets and targets[0]._is_on_remote():
|
|
return (targets[0], 'exact')
|
|
|
|
# 2. PR with head name equals
|
|
if self.pull_head_name:
|
|
domain = [
|
|
('repo_id', 'in', target_repo_ids),
|
|
('pull_head_name', '=', self.pull_head_name),
|
|
('name', '=like', 'refs/pull/%'),
|
|
]
|
|
pulls = Branch.search(domain, order='id DESC')
|
|
pulls = sorted(pulls, key=sort_by_repo)
|
|
for pull in Branch.browse([pu['id'] for pu in pulls]):
|
|
pi = pull._get_pull_info()
|
|
if pi.get('state') == 'open':
|
|
if ':' in self.pull_head_name:
|
|
(repo_name, pr_branch_name) = self.pull_head_name.split(':')
|
|
repo = self.env['runbot.repo'].browse(target_repo_ids).filtered(lambda r: ':%s/' % repo_name in r.name)
|
|
# most of the time repo will be pull.repo_id.duplicate_id, but it is still possible to have a pr pointing the same repo
|
|
if repo:
|
|
pr_branch_ref = 'refs/heads/%s' % pr_branch_name
|
|
pr_branch = self._get_or_create_branch(repo.id, pr_branch_ref)
|
|
# use _get_or_create_branch in case a pr is scanned before pull_head_name branch.
|
|
return (pr_branch, 'exact PR')
|
|
return (pull, 'exact PR')
|
|
|
|
# 4.Match a PR in enterprise without community PR
|
|
# Moved before 3 because it makes more sense
|
|
if self.pull_head_name:
|
|
if self.name.startswith('refs/pull'):
|
|
if ':' in self.pull_head_name:
|
|
(repo_name, pr_branch_name) = self.pull_head_name.split(':')
|
|
repos = self.env['runbot.repo'].browse(target_repo_ids).filtered(lambda r: ':%s/' % repo_name in r.name)
|
|
else:
|
|
pr_branch_name = self.pull_head_name
|
|
repos = target_repo
|
|
if repos:
|
|
duplicate_branch_name = 'refs/heads/%s' % pr_branch_name
|
|
domain = [
|
|
('repo_id', 'in', tuple(repos.ids)),
|
|
('branch_name', '=', pr_branch_name),
|
|
('pull_head_name', '=', False),
|
|
]
|
|
targets = Branch.search(domain, order='id DESC')
|
|
targets = sorted(targets, key=sort_by_repo)
|
|
if targets and targets[0]._is_on_remote():
|
|
return (targets[0], 'no PR')
|
|
|
|
# 3. Match a branch which is the dashed-prefix of current branch name
|
|
if not self.pull_head_name:
|
|
if '-' in self.branch_name:
|
|
name_start = 'refs/heads/%s' % self.branch_name.split('-')[0]
|
|
domain = [('repo_id', 'in', target_repo_ids), ('name', '=like', '%s%%' % name_start)]
|
|
branches = Branch.search(domain, order='id DESC')
|
|
branches = sorted(branches, key=sort_by_repo)
|
|
for branch in branches:
|
|
if self.branch_name.startswith('%s-' % branch.branch_name) and branch._is_on_remote():
|
|
return (branch, 'prefix')
|
|
|
|
# 5. last-resort value
|
|
if self.target_branch_name:
|
|
default_target_ref = 'refs/heads/%s' % self.target_branch_name
|
|
default_branch = self.search([('repo_id', 'in', target_repo_ids), ('name', '=', default_target_ref)], limit=1)
|
|
if default_branch:
|
|
return (default_branch, 'pr_target')
|
|
|
|
default_target_ref = 'refs/heads/master'
|
|
default_branch = self.search([('repo_id', 'in', target_repo_ids), ('name', '=', default_target_ref)], limit=1)
|
|
# we assume that master will always exists
|
|
return (default_branch, 'default')
|
|
|
|
def _branch_exists(self, branch_id):
|
|
Branch = self.env['runbot.branch']
|
|
branch = Branch.search([('id', '=', branch_id)])
|
|
if branch and branch[0]._is_on_remote():
|
|
return True
|
|
return False
|
|
|
|
def _get_or_create_branch(self, repo_id, name):
|
|
res = self.search([('repo_id', '=', repo_id), ('name', '=', name)], limit=1)
|
|
if res:
|
|
return res
|
|
_logger.warning('creating missing branch %s', name)
|
|
Branch = self.env['runbot.branch']
|
|
branch = Branch.create({'repo_id': repo_id, 'name': name})
|
|
return branch
|