[IMP] runbot: better search of closest branch.

Can now search for matching PR
This commit is contained in:
Christophe Simonis 2015-07-15 16:57:56 +02:00
parent 2e2c7f0c21
commit 2e01744b0d
4 changed files with 104 additions and 47 deletions

View File

@ -2,7 +2,7 @@
'name': 'Runbot', 'name': 'Runbot',
'category': 'Website', 'category': 'Website',
'summary': 'Runbot', 'summary': 'Runbot',
'version': '1.1', 'version': '1.2',
'description': "Runbot", 'description': "Runbot",
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'depends': ['website'], 'depends': ['website'],

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def migrate(cr, version):
cr.execute("""SELECT 1
FROM information_schema.columns
WHERE table_name='runbot_branch'
AND column_name='pull_head_name'
""")
if not cr.rowcount:
cr.execute('ALTER TABLE "runbot_branch" ADD COLUMN "pull_head_name" varchar')

View File

@ -413,6 +413,14 @@ class runbot_branch(osv.osv):
r[branch.id] = branch.name.split('/')[-1] r[branch.id] = branch.name.split('/')[-1]
return r return r
def _get_pull_head_name(self, cr, uid, ids, field_name, arg, context=None):
r = dict.fromkeys(ids, False)
for bid in ids:
pi = self._get_pull_info(cr, uid, [bid], context=context)
if pi:
r[bid] = pi['head']['ref']
return r
def _get_branch_url(self, cr, uid, ids, field_name, arg, context=None): def _get_branch_url(self, cr, uid, ids, field_name, arg, context=None):
r = {} r = {}
for branch in self.browse(cr, uid, ids, context=context): for branch in self.browse(cr, uid, ids, context=context):
@ -427,12 +435,22 @@ class runbot_branch(osv.osv):
'name': fields.char('Ref Name', required=True), 'name': fields.char('Ref Name', required=True),
'branch_name': fields.function(_get_branch_name, type='char', string='Branch', readonly=1, store=True), 'branch_name': fields.function(_get_branch_name, type='char', string='Branch', readonly=1, store=True),
'branch_url': fields.function(_get_branch_url, type='char', string='Branch url', readonly=1), 'branch_url': fields.function(_get_branch_url, type='char', string='Branch url', readonly=1),
'pull_head_name': fields.function(_get_pull_head_name, type='char', string='PR HEAD name', readonly=1, store=True),
'sticky': fields.boolean('Sticky', select=1), 'sticky': fields.boolean('Sticky', select=1),
'coverage': fields.boolean('Coverage'), 'coverage': fields.boolean('Coverage'),
'state': fields.char('Status'), 'state': fields.char('Status'),
'modules': fields.char("Modules to Install", help="Comma-separated list of modules to install and test."), 'modules': fields.char("Modules to Install", help="Comma-separated list of modules to install and test."),
} }
def _get_pull_info(self, cr, uid, ids, context=None):
assert len(ids) == 1
branch = self.browse(cr, uid, ids[0], context=context)
repo = branch.repo_id
if repo.token and branch.name.startswith('refs/pull/'):
pull_number = branch.name[len('refs/pull/'):]
return repo.github('/repos/:owner/:repo/pulls/%s' % pull_number)
return {}
class runbot_build(osv.osv): class runbot_build(osv.osv):
_name = "runbot.build" _name = "runbot.build"
_order = 'id desc' _order = 'id desc'
@ -552,56 +570,85 @@ class runbot_build(osv.osv):
return port return port
def get_closest_branch_name(self, cr, uid, ids, target_repo_id, hint_branches, context=None): def _get_closest_branch_name(self, cr, uid, ids, target_repo_id, context=None):
"""Return the name of the closest common branch between both repos """Return the name of the closest common branch between both repos
Rules priority for choosing the branch from the other repo is: Rules priority for choosing the branch from the other repo is:
1. Same branch name 1. Same branch name
2. Common ancestors (git merge-base) 2. A PR whose head name match
3. Match a branch which is the dashed-prefix of current branch name 3. Match a branch which is the dashed-prefix of current branch name
Note that PR numbers are replaced by the branch name from which the PR is made, 4. Common ancestors (git merge-base)
Note that PR numbers are replaced by the branch name of the PR target
to prevent the above rules to mistakenly link PR of different repos together. to prevent the above rules to mistakenly link PR of different repos together.
""" """
assert len(ids) == 1
branch_pool = self.pool['runbot.branch'] branch_pool = self.pool['runbot.branch']
for build in self.browse(cr, uid, ids, context=context):
branch, repo = build.branch_id, build.repo_id build = self.browse(cr, uid, ids[0], context=context)
name = branch.branch_name branch, repo = build.branch_id, build.repo_id
# Use github API to find name of branch on which the PR is made pi = branch._get_pull_info()
if repo.token and name.startswith('refs/pull/'): name = pi['base']['ref'] if pi else branch.branch_name
pull_number = name[len('refs/pull/'):]
pr = repo.github('/repos/:owner/:repo/pulls/%s' % pull_number) # 1. same name, not a PR
name = 'refs/heads/' + pr['base']['ref'] domain = [
# Find common branch names between repo and target repo ('repo_id', '=', target_repo_id),
branch_ids = branch_pool.search(cr, uid, [('repo_id.id', '=', repo.id)]) ('branch_name', '=', name),
target_ids = branch_pool.search(cr, uid, [('repo_id.id', '=', target_repo_id)]) ('name', '=like', 'refs/heads/%'),
branch_names = branch_pool.read(cr, uid, branch_ids, ['branch_name', 'name'], context=context) ]
target_names = branch_pool.read(cr, uid, target_ids, ['branch_name', 'name'], context=context) targets = branch_pool.search_read(cr, uid, domain, ['name'], order='id DESC',
possible_repo_branches = set([i['branch_name'] for i in branch_names if i['name'].startswith('refs/heads')]) limit=1, context=context)
possible_target_branches = set([i['branch_name'] for i in target_names if i['name'].startswith('refs/heads')]) if targets:
possible_branches = possible_repo_branches.intersection(possible_target_branches) return targets[0]['name']
if name not in possible_branches:
hinted_branches = possible_branches.intersection(hint_branches) # 2. PR with head name equals
if hinted_branches: domain = [
possible_branches = hinted_branches ('repo_id', '=', target_repo_id),
common_refs = {} ('pull_head_name', '=', name),
for target_branch_name in possible_branches: ('name', '=like', 'refs/pull/%'),
try: ]
commit = repo.git(['merge-base', branch.name, target_branch_name]).strip() pulls = branch_pool.search_read(cr, uid, domain, ['name'], order='id DESC',
cmd = ['log', '-1', '--format=%cd', '--date=iso', commit] context=context)
common_refs[target_branch_name] = repo.git(cmd).strip() for pull in pulls:
except subprocess.CalledProcessError: pi = branch_pool._get_pull_info(cr, uid, [pull['id']], context=context)
# If merge-base doesn't find any common ancestor, the command exits with a if pi.get('state') == 'open':
# non-zero return code, resulting in subprocess.check_output raising this return pull['name']
# exception. We ignore this branch as there is no common ref between us.
continue # 3. Match a branch which is the dashed-prefix of current branch name
if common_refs: branches = branch_pool.search_read(
name = sorted(common_refs.iteritems(), key=operator.itemgetter(1), reverse=True)[0][0] cr, uid,
else: [('repo_id', '=', target_repo_id), ('name', '=like', 'refs/heads/%')],
# find a branch which is a prefix of the current branch name ['name', 'branch_name'], order='id DESC', context=context
sorted_target_branches = sorted(possible_target_branches, key=len, reverse=True) )
prefixes = [b for b in sorted_target_branches if name.startswith(b + '-')]
if prefixes: for branch in branches:
return prefixes[0] if name.startswith(branch['branch_name'] + '-'):
return name return branch['name']
# 4. Common ancestors (git merge-base)
common_refs = {}
cr.execute("""
SELECT b.name
FROM runbot_branch b,
runbot_branch t
WHERE b.repo_id = %s
AND t.repo_id = %s
AND b.name = t.name
AND b.name LIKE 'refs/heads/%%'
""", [repo.id, target_repo_id])
for common_name, in cr.fetchall():
try:
commit = repo.git(['merge-base', branch.name, common_name]).strip()
cmd = ['log', '-1', '--format=%cd', '--date=iso', commit]
common_refs[common_name] = repo.git(cmd).strip()
except subprocess.CalledProcessError:
# If merge-base doesn't find any common ancestor, the command exits with a
# non-zero return code, resulting in subprocess.check_output raising this
# exception. We ignore this branch as there is no common ref between us.
continue
if common_refs:
return sorted(common_refs.iteritems(), key=operator.itemgetter(1), reverse=True)[0][0]
# 5. last-resort value
return 'master'
def path(self, cr, uid, ids, *l, **kw): def path(self, cr, uid, ids, *l, **kw):
for build in self.browse(cr, uid, ids, context=None): for build in self.browse(cr, uid, ids, context=None):
@ -656,10 +703,8 @@ class runbot_build(osv.osv):
] ]
_logger.debug("local modules_to_test for build %s: %s", build.dest, modules_to_test) _logger.debug("local modules_to_test for build %s: %s", build.dest, modules_to_test)
hint_branches = set()
for extra_repo in build.repo_id.dependency_ids: for extra_repo in build.repo_id.dependency_ids:
closest_name = build.get_closest_branch_name(extra_repo.id, hint_branches) closest_name = build._get_closest_branch_name(extra_repo.id)
hint_branches.add(closest_name)
extra_repo.git_export(closest_name, build.path()) extra_repo.git_export(closest_name, build.path())
# Finally mark all addons to move to openerp/addons # Finally mark all addons to move to openerp/addons

View File

@ -59,6 +59,7 @@
<field name="name"/> <field name="name"/>
<field name="branch_name"/> <field name="branch_name"/>
<field name="branch_url"/> <field name="branch_url"/>
<field name="pull_head_name"/>
<field name="sticky"/> <field name="sticky"/>
<field name="state"/> <field name="state"/>
<field name="modules"/> <field name="modules"/>