mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[IMP] runbot_merge: overridable CI
Convert overridable CI to an m2m from partners, it's significantly more convenient to manipulate as multiple users can (and likely will) have access to the same overrides, add a name_search so the override is easy to find from a partner, and provide a view for the overrides (with partners as tags). Also make the repository optional on CI overrides. Fixes #420
This commit is contained in:
parent
8abf1be278
commit
47e8b5b014
@ -1040,6 +1040,9 @@ class Model:
|
||||
ids = self._env(self._model, 'search', *args, **kwargs)
|
||||
return Model(self._env, self._model, ids)
|
||||
|
||||
def name_search(self, *args, **kwargs):
|
||||
return self._env(self._model, 'name_search', *args, **kwargs)
|
||||
|
||||
def create(self, values):
|
||||
return Model(self._env, self._model, [self._env(self._model, 'create', values)])
|
||||
|
||||
@ -1049,6 +1052,9 @@ class Model:
|
||||
def read(self, fields):
|
||||
return self._env(self._model, 'read', self._ids, fields)
|
||||
|
||||
def name_get(self):
|
||||
return self._env(self._model, 'name_get', self._ids)
|
||||
|
||||
def unlink(self):
|
||||
return self._env(self._model, 'unlink', self._ids)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'merge bot',
|
||||
'version': '1.5',
|
||||
'version': '1.6',
|
||||
'depends': ['contacts', 'website'],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
|
39
runbot_merge/migrations/13.0.1.6/pre-migration.py
Normal file
39
runbot_merge/migrations/13.0.1.6/pre-migration.py
Normal file
@ -0,0 +1,39 @@
|
||||
import collections
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
""" Status overrides: o2m -> m2m
|
||||
"""
|
||||
# create link table
|
||||
cr.execute('''
|
||||
CREATE TABLE res_partner_res_partner_override_rel (
|
||||
res_partner_id integer not null references res_partner (id) ON DELETE CASCADE,
|
||||
res_partner_override_id integer not null references res_partner_override (id) ON DELETE CASCADE,
|
||||
primary key (res_partner_id, res_partner_override_id)
|
||||
)
|
||||
''')
|
||||
cr.execute('''
|
||||
CREATE UNIQUE INDEX ON res_partner_res_partner_override_rel
|
||||
(res_partner_override_id, res_partner_id)
|
||||
''')
|
||||
|
||||
# deduplicate override rights and insert into link table
|
||||
cr.execute('SELECT array_agg(id), array_agg(partner_id)'
|
||||
' FROM res_partner_override GROUP BY repository_id, context')
|
||||
links = {}
|
||||
duplicants = set()
|
||||
for [keep, *drops], partners in cr.fetchall():
|
||||
links[keep] = partners
|
||||
duplicants.update(drops)
|
||||
for override_id, partner_ids in links.items():
|
||||
for partner_id in partner_ids:
|
||||
cr.execute('INSERT INTO res_partner_res_partner_override_rel (res_partner_override_id, res_partner_id)'
|
||||
' VALUES (%s, %s)', [override_id, partner_id])
|
||||
# drop dups
|
||||
cr.execute('DELETE FROM res_partner_override WHERE id = any(%s)', [list(duplicants)])
|
||||
|
||||
# remove old partner field
|
||||
cr.execute('ALTER TABLE res_partner_override DROP COLUMN partner_id')
|
||||
# add constraint to overrides
|
||||
cr.execute('CREATE UNIQUE INDEX res_partner_override_unique ON res_partner_override '
|
||||
'(context, coalesce(repository_id, 0))')
|
@ -991,7 +991,7 @@ class PullRequests(models.Model):
|
||||
})
|
||||
elif command == 'override':
|
||||
overridable = author.override_rights\
|
||||
.filtered(lambda r: r.repository_id == self.repository)\
|
||||
.filtered(lambda r: not r.repository_id or (r.repository_id == self.repository))\
|
||||
.mapped('context')
|
||||
if param in overridable:
|
||||
self.overrides = json.dumps({
|
||||
|
@ -13,7 +13,7 @@ class Partner(models.Model):
|
||||
delegate_reviewer = fields.Many2many('runbot_merge.pull_requests')
|
||||
formatted_email = fields.Char(string="commit email", compute='_rfc5322_formatted')
|
||||
review_rights = fields.One2many('res.partner.review', 'partner_id')
|
||||
override_rights = fields.One2many('res.partner.override', 'partner_id')
|
||||
override_rights = fields.Many2many('res.partner.override')
|
||||
|
||||
def _auto_init(self):
|
||||
res = super(Partner, self)._auto_init()
|
||||
@ -71,19 +71,34 @@ class ReviewRights(models.Model):
|
||||
for r in self
|
||||
]
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
return self.search(args + [('repository_id.name', operator, name)], limit=limit).name_get()
|
||||
return self.search((args or []) + [('repository_id.name', operator, name)], limit=limit).name_get()
|
||||
|
||||
class OverrideRights(models.Model):
|
||||
_name = 'res.partner.override'
|
||||
_description = 'lints which the partner can override'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
|
||||
repository_id = fields.Many2one('runbot_merge.repository', required=True)
|
||||
partner_ids = fields.Many2many('res.partner')
|
||||
repository_id = fields.Many2one('runbot_merge.repository')
|
||||
context = fields.Char(required=True)
|
||||
|
||||
def init(self):
|
||||
super().init()
|
||||
tools.create_unique_index(
|
||||
self.env.cr, 'res_partner_override_unique', self._table,
|
||||
['context', 'coalesce(repository_id, 0)']
|
||||
)
|
||||
|
||||
@api.model
|
||||
def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
return self.search((args or []) + [
|
||||
'|', ('context', operator, name),
|
||||
('repository_id.name', operator, name)
|
||||
], limit=limit).name_get()
|
||||
|
||||
def name_get(self):
|
||||
return [
|
||||
(r.id, f'{r.repository_id.name}: {r.context}')
|
||||
(r.id, f'{r.repository_id.name}: {r.context}' if r.repository_id else r.context)
|
||||
for r in self
|
||||
]
|
||||
|
@ -25,67 +25,3 @@ def test_partner_merge(env):
|
||||
assert not p_src.exists()
|
||||
assert p_dest.name == 'Partner P. Partnersson'
|
||||
assert p_dest.github_login == 'xxx'
|
||||
|
||||
def test_override(env, project, make_repo, users, setreviewers, config):
|
||||
"""
|
||||
Test that we can override a status on a PR:
|
||||
|
||||
* @mergebot override context=status
|
||||
* target url should be the comment (?)
|
||||
* description should be overridden by <user>
|
||||
"""
|
||||
repo = make_repo('repo')
|
||||
repo_id = env['runbot_merge.repository'].create({
|
||||
'project_id': project.id,
|
||||
'name': repo.name,
|
||||
'status_ids': [(0, 0, {'context': 'l/int'})]
|
||||
})
|
||||
setreviewers(*project.repo_ids)
|
||||
# "other" can override the lint
|
||||
env['res.partner'].create({
|
||||
'name': config['role_other'].get('name', 'Other'),
|
||||
'github_login': users['other'],
|
||||
'override_rights': [(0, 0, {
|
||||
'repository_id': repo_id.id,
|
||||
'context': 'l/int',
|
||||
})]
|
||||
})
|
||||
|
||||
with repo:
|
||||
m = repo.make_commits(None, Commit('root', tree={'a': '1'}), ref='heads/master')
|
||||
|
||||
repo.make_commits(m, Commit('pr', tree={'a': '2'}), ref='heads/change')
|
||||
pr = repo.make_pr(target='master', title='super change', head='change')
|
||||
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||
env.run_crons()
|
||||
|
||||
pr_id = env['runbot_merge.pull_requests'].search([
|
||||
('repository.name', '=', repo.name),
|
||||
('number', '=', pr.number)
|
||||
])
|
||||
assert pr_id.state == 'approved'
|
||||
|
||||
with repo:
|
||||
pr.post_comment('hansen override=l/int', config['role_reviewer']['token'])
|
||||
env.run_crons()
|
||||
assert pr_id.state == 'approved'
|
||||
|
||||
with repo:
|
||||
pr.post_comment('hansen override=l/int', config['role_other']['token'])
|
||||
env.run_crons()
|
||||
assert pr_id.state == 'ready'
|
||||
|
||||
comments = pr.comments
|
||||
assert comments == [
|
||||
(users['reviewer'], 'hansen r+'),
|
||||
(users['reviewer'], 'hansen override=l/int'),
|
||||
(users['user'], "I'm sorry, @{}. You are not allowed to override this status.".format(users['reviewer'])),
|
||||
(users['other'], "hansen override=l/int"),
|
||||
]
|
||||
assert pr_id.statuses == '{}'
|
||||
assert json.loads(pr_id.overrides) == {'l/int': {
|
||||
'state': 'success',
|
||||
'target_url': comments[-1]['html_url'],
|
||||
'description': 'Overridden by @{}'.format(users['other']),
|
||||
}}
|
||||
|
||||
|
147
runbot_merge/tests/test_status_overrides.py
Normal file
147
runbot_merge/tests/test_status_overrides.py
Normal file
@ -0,0 +1,147 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import Commit
|
||||
|
||||
|
||||
def test_no_duplicates(env):
|
||||
""" Should not have two override records for the same (context, repo)
|
||||
"""
|
||||
Overrides = env['res.partner.override']
|
||||
Overrides.create({'context': 'a'})
|
||||
with pytest.raises(Exception, match=r'already exists'):
|
||||
Overrides.create({'context': 'a'})
|
||||
|
||||
def name_search(Model, name):
|
||||
""" Convenience function to return a recordset instead of a craplist
|
||||
"""
|
||||
return Model.browse(id_ for id_, _ in Model.name_search(name))
|
||||
def test_finding(env):
|
||||
project = env['runbot_merge.project'].create({
|
||||
'name': 'test',
|
||||
'github_token': 'xxx', 'github_prefix': 'no',
|
||||
})
|
||||
repo_1 = env['runbot_merge.repository'].create({'project_id': project.id, 'name': 'r1'})
|
||||
repo_2 = env['runbot_merge.repository'].create({'project_id': project.id, 'name': 'r2'})
|
||||
|
||||
Overrides = env['res.partner.override']
|
||||
a = Overrides.create({'context': 'ctx1'})
|
||||
b = Overrides.create({'context': 'ctx1', 'repository_id': repo_1.id})
|
||||
c = Overrides.create({'context': 'ctx1', 'repository_id': repo_2.id})
|
||||
d = Overrides.create({'context': 'ctx2', 'repository_id': repo_2.id})
|
||||
|
||||
assert name_search(Overrides, 'ctx1') == a|b|c
|
||||
assert name_search(Overrides, 'ctx') == a|b|c|d
|
||||
assert name_search(Overrides, 'r2') == c|d
|
||||
|
||||
def test_basic(env, project, make_repo, users, setreviewers, config):
|
||||
"""
|
||||
Test that we can override a status on a PR:
|
||||
|
||||
* @mergebot override context=status
|
||||
* target url should be the comment (?)
|
||||
* description should be overridden by <user>
|
||||
"""
|
||||
repo = make_repo('repo')
|
||||
repo_id = env['runbot_merge.repository'].create({
|
||||
'project_id': project.id,
|
||||
'name': repo.name,
|
||||
'status_ids': [(0, 0, {'context': 'l/int'})]
|
||||
})
|
||||
setreviewers(*project.repo_ids)
|
||||
# "other" can override the lint
|
||||
env['res.partner'].create({
|
||||
'name': config['role_other'].get('name', 'Other'),
|
||||
'github_login': users['other'],
|
||||
'override_rights': [(0, 0, {
|
||||
'repository_id': repo_id.id,
|
||||
'context': 'l/int',
|
||||
})]
|
||||
})
|
||||
|
||||
with repo:
|
||||
m = repo.make_commits(None, Commit('root', tree={'a': '1'}), ref='heads/master')
|
||||
|
||||
repo.make_commits(m, Commit('pr', tree={'a': '2'}), ref='heads/change')
|
||||
pr = repo.make_pr(target='master', title='super change', head='change')
|
||||
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||
env.run_crons()
|
||||
|
||||
pr_id = env['runbot_merge.pull_requests'].search([
|
||||
('repository.name', '=', repo.name),
|
||||
('number', '=', pr.number)
|
||||
])
|
||||
assert pr_id.state == 'approved'
|
||||
|
||||
with repo:
|
||||
pr.post_comment('hansen override=l/int', config['role_reviewer']['token'])
|
||||
env.run_crons()
|
||||
assert pr_id.state == 'approved'
|
||||
|
||||
with repo:
|
||||
pr.post_comment('hansen override=l/int', config['role_other']['token'])
|
||||
env.run_crons()
|
||||
assert pr_id.state == 'ready'
|
||||
|
||||
comments = pr.comments
|
||||
assert comments == [
|
||||
(users['reviewer'], 'hansen r+'),
|
||||
(users['reviewer'], 'hansen override=l/int'),
|
||||
(users['user'], "I'm sorry, @{}. You are not allowed to override this status.".format(users['reviewer'])),
|
||||
(users['other'], "hansen override=l/int"),
|
||||
]
|
||||
assert pr_id.statuses == '{}'
|
||||
assert json.loads(pr_id.overrides) == {'l/int': {
|
||||
'state': 'success',
|
||||
'target_url': comments[-1]['html_url'],
|
||||
'description': 'Overridden by @{}'.format(users['other']),
|
||||
}}
|
||||
|
||||
def test_no_repository(env, project, make_repo, users, setreviewers, config):
|
||||
""" A repo missing from an override allows overriding the status in every repo
|
||||
"""
|
||||
repo = make_repo('repo')
|
||||
env['runbot_merge.repository'].create({
|
||||
'project_id': project.id,
|
||||
'name': repo.name,
|
||||
'status_ids': [(0, 0, {'context': 'l/int'})]
|
||||
})
|
||||
setreviewers(*project.repo_ids)
|
||||
# "other" can override the lint
|
||||
env['res.partner'].create({
|
||||
'name': config['role_other'].get('name', 'Other'),
|
||||
'github_login': users['other'],
|
||||
'override_rights': [(0, 0, {'context': 'l/int'})]
|
||||
})
|
||||
|
||||
with repo:
|
||||
m = repo.make_commits(None, Commit('root', tree={'a': '1'}), ref='heads/master')
|
||||
|
||||
repo.make_commits(m, Commit('pr', tree={'a': '2'}), ref='heads/change')
|
||||
pr = repo.make_pr(target='master', title='super change', head='change')
|
||||
pr.post_comment('hansen r+', config['role_reviewer']['token'])
|
||||
env.run_crons()
|
||||
|
||||
pr_id = env['runbot_merge.pull_requests'].search([
|
||||
('repository.name', '=', repo.name),
|
||||
('number', '=', pr.number)
|
||||
])
|
||||
assert pr_id.state == 'approved'
|
||||
|
||||
with repo:
|
||||
pr.post_comment('hansen override=l/int', config['role_other']['token'])
|
||||
env.run_crons()
|
||||
assert pr_id.state == 'ready'
|
||||
|
||||
comments = pr.comments
|
||||
assert comments == [
|
||||
(users['reviewer'], 'hansen r+'),
|
||||
(users['other'], "hansen override=l/int"),
|
||||
]
|
||||
assert pr_id.statuses == '{}'
|
||||
assert json.loads(pr_id.overrides) == {'l/int': {
|
||||
'state': 'success',
|
||||
'target_url': comments[-1]['html_url'],
|
||||
'description': 'Overridden by @{}'.format(users['other']),
|
||||
}}
|
@ -277,6 +277,22 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="runbot_merge_action_overrides" model="ir.actions.act_window">
|
||||
<field name="name">CI / statuses overrides</field>
|
||||
<field name="res_model">res.partner.override</field>
|
||||
</record>
|
||||
<record id="runot_merge_tree_overrides" model="ir.ui.view">
|
||||
<field name="name">Overrides List</field>
|
||||
<field name="model">res.partner.override</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="context"/>
|
||||
<field name="repository_id"/>
|
||||
<field name="partner_ids" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Mergebot" id="runbot_merge_menu"/>
|
||||
<menuitem name="Projects" id="runbot_merge_menu_project"
|
||||
parent="runbot_merge_menu"
|
||||
@ -290,4 +306,8 @@
|
||||
<menuitem name="Fetches" id="runbot_merge_menu_fetches"
|
||||
parent="runbot_merge_menu"
|
||||
action="runbot_merge_action_fetches"/>
|
||||
<menuitem name="Configuration" id="runbot_merge_menu_configuration" parent="runbot_merge_menu"/>
|
||||
<menuitem name="CI Overrides" id="runbot_merge_menu_configuration_overrides"
|
||||
parent="runbot_merge_menu"
|
||||
action="runbot_merge_action_overrides"/>
|
||||
</odoo>
|
||||
|
Loading…
Reference in New Issue
Block a user