diff --git a/runbot_merge/models/pull_requests.py b/runbot_merge/models/pull_requests.py index 267c1de2..c7a165bb 100644 --- a/runbot_merge/models/pull_requests.py +++ b/runbot_merge/models/pull_requests.py @@ -220,6 +220,12 @@ class Repository(models.Model): default='legal/cla,ci/runbot' ) branch_filter = fields.Char(default='[(1, "=", 1)]', help="Filter branches valid for this repository") + substitutions = fields.Text( + "label substitutions", + help="""sed-style substitution patterns applied to the label on input, one per line. + +All substitutions are tentatively applied sequentially to the input. +""") def github(self, token_field='github_token'): return github.GH(self.project_id[token_field], self.name) @@ -291,6 +297,20 @@ class Repository(models.Model): branches = self.env['runbot_merge.branch'].search return self.filtered(lambda r: branch in branches(ast.literal_eval(r.branch_filter))) + def _remap_label(self, label): + print(self.substitutions) + for line in filter(None, (self.substitutions or '').splitlines()): + print(line) + sep = line[0] + _, pattern, repl, flags = line.split(sep) + label = re.sub( + pattern, repl, label, + count=0 if 'g' in flags else 1, + flags=(re.MULTILINE if 'm' in flags.lower() else 0) + | (re.IGNORECASE if 'i' in flags.lower() else 0) + ) + return label + class Branch(models.Model): _name = _description = 'runbot_merge.branch' _order = 'sequence, name' @@ -1004,7 +1024,7 @@ class PullRequests(models.Model): message += '\n\n' + body return self.env['runbot_merge.pull_requests'].create({ 'number': description['number'], - 'label': description['head']['label'], + 'label': repo._remap_label(description['head']['label']), 'author': author.id, 'target': branch.id, 'repository': repo.id, diff --git a/runbot_merge/tests/test_multirepo.py b/runbot_merge/tests/test_multirepo.py index 5c10fd86..f4b83b71 100644 --- a/runbot_merge/tests/test_multirepo.py +++ b/runbot_merge/tests/test_multirepo.py @@ -8,6 +8,7 @@ are staged concurrently in all repos import json import pytest +import requests from test_utils import re_matches, get_partner @@ -758,3 +759,109 @@ def test_remove_acl(env, partners, repo_a, repo_b, repo_c): assert r.mapped('review_rights.repository_id') == repo_a | repo_b | repo_c r.write({'review_rights': [(5, 0, 0)]}) assert r.mapped('review_rights.repository_id') == env['runbot_merge.repository'] + +class TestSubstitutions: + def test_substitution_patterns(self, env, port): + p = env['runbot_merge.project'].create({ + 'name': 'proj', + 'github_token': 'wheeee', + 'repo_ids': [(0, 0, {'name': 'xxx/xxx'})], + 'branch_ids': [(0, 0, {'name': 'master'})] + }) + r = p.repo_ids + # replacement pattern, pr label, stored label + cases = [ + ('/^foo:/foo-dev:/', 'foo:bar', 'foo-dev:bar'), + ('/^foo:/foo-dev:/', 'foox:bar', 'foox:bar'), + ('/^foo:/foo-dev:/i', 'FOO:bar', 'foo-dev:bar'), + ('/o/x/g', 'foo:bar', 'fxx:bar'), + ('@foo:@bar:@', 'foo:bar', 'bar:bar'), + ('/foo:/bar:/\n/bar:/baz:/', 'foo:bar', 'baz:bar'), + ] + for pr_number, (pattern, original, target) in enumerate(cases, start=1): + r.substitutions = pattern + requests.post( + 'http://localhost:{}/runbot_merge/hooks'.format(port), + headers={'X-Github-Event': 'pull_request'}, + json={ + 'action': 'opened', + 'repository': { + 'full_name': r.name, + }, + 'pull_request': { + 'user': {'login': 'bob'}, + 'base': { + 'repo': {'full_name': r.name}, + 'ref': p.branch_ids.name, + }, + 'number': pr_number, + 'title': "a pr", + 'body': None, + 'commits': 1, + 'head': { + 'label': original, + 'sha': format(pr_number, 'x')*40, + } + } + } + ) + pr = env['runbot_merge.pull_requests'].search([ + ('repository', '=', r.id), + ('number', '=', pr_number) + ]) + assert pr.label == target + + + def test_substitutions_staging(self, env, repo_a, repo_b, config): + """ Different repos from the same project may have different policies for + sourcing PRs. So allow for remapping labels on input in order to match. + """ + repo_b_id = env['runbot_merge.repository'].search([ + ('name', '=', repo_b.name) + ]) + # in repo b, replace owner part by repo_a's owner + repo_b_id.substitutions = r"/.+:/%s:/" % repo_a.owner + + with repo_a: + make_branch(repo_a, 'master', 'initial', {'a': '0'}) + with repo_b: + make_branch(repo_b, 'master', 'initial', {'b': '0'}) + + # policy is that repo_a PRs are created in the same repo while repo_b PRs + # are created in personal forks + with repo_a: + repo_a.make_commits('master', repo_a.Commit('bop', tree={'a': '1'}), ref='heads/abranch') + pra = repo_a.make_pr(target='master', head='abranch') + b_fork = repo_b.fork() + with b_fork, repo_b: + b_fork.make_commits('master', b_fork.Commit('pob', tree={'b': '1'}), ref='heads/abranch') + prb = repo_b.make_pr( + title="a pr", + target='master', head='%s:abranch' % b_fork.owner + ) + + pra_id = env['runbot_merge.pull_requests'].search([ + ('repository.name', '=', repo_a.name), + ('number', '=', pra.number) + ]) + prb_id = env['runbot_merge.pull_requests'].search([ + ('repository.name', '=', repo_b.name), + ('number', '=', prb.number) + ]) + assert pra_id.label.endswith(':abranch') + assert prb_id.label.endswith(':abranch') + + with repo_a, repo_b: + repo_a.post_status(pra.head, 'success', 'legal/cla') + repo_a.post_status(pra.head, 'success', 'ci/runbot') + pra.post_comment('hansen r+', config['role_reviewer']['token']) + + repo_b.post_status(prb.head, 'success', 'legal/cla') + repo_b.post_status(prb.head, 'success', 'ci/runbot') + prb.post_comment('hansen r+', config['role_reviewer']['token']) + env.run_crons() + + assert pra_id.staging_id, 'PR A should be staged' + assert prb_id.staging_id, "PR B should be staged" + assert pra_id.staging_id == prb_id.staging_id, "both prs should be staged together" + assert pra_id.batch_id == prb_id.batch_id, "both prs should be part of the same batch"