mirror of
https://github.com/odoo/runbot.git
synced 2025-03-25 20:35:52 +07:00

Given a batch which has been merged, and been forward-ported, to multiple branches (because skipci was set or ci passed on the repos the batch covers). There might come the need to add a PR for one of the uncovered repos. This raises the question of what to do with it, since the forward-ports for the batch already exist it's not going to get forwardported normally, nor may we want to, possibly? Options are: - don't do anything, such additions don't get ported, this is incongruous and unexpected as by default PRs are forward-ported, and if the batch wasn't an intermediate (but e.g. a conflict) it probably would be ported forward - port on merge, this allows configuring the PR properly (as it might need its own limit) but it means further batches may get unexpectedly merged (or at least retied) without the additional PR even though we likely want it in - immediately port the additional PR on creation, this makes the limit harder or impossible to configure but it makes the *batch sequence* more consistent We ended up selecting the latter, it feels closer to the updates system, and it creates more consistent batches through the sequence. It's also technically easier to ad-hoc port a PR through a bunch of branches than it is to update the "normal" forward-port process to handle partial fixups.
189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
import itertools
|
|
import re
|
|
import time
|
|
|
|
from lxml import html
|
|
|
|
MESSAGE_TEMPLATE = """{message}
|
|
|
|
closes {repo}#{number}
|
|
|
|
{headers}Signed-off-by: {name} <{email}>"""
|
|
# target branch '-' source branch '-' base64 unique '-fw'
|
|
REF_PATTERN = r'{target}-{source}-[a-zA-Z0-9_-]{{4}}-fw'
|
|
|
|
class Commit:
|
|
def __init__(self, message, *, author=None, committer=None, tree, reset=False):
|
|
self.id = None
|
|
self.message = message
|
|
self.author = author
|
|
self.committer = committer
|
|
self.tree = tree
|
|
self.reset = reset
|
|
|
|
def validate_all(repos, refs, contexts=('ci/runbot', 'legal/cla')):
|
|
""" Post a "success" status for each context on each ref of each repo
|
|
"""
|
|
for repo, branch, context in itertools.product(repos, refs, contexts):
|
|
repo.post_status(branch, 'success', context)
|
|
|
|
def get_partner(env, gh_login):
|
|
return env['res.partner'].search([('github_login', '=', gh_login)])
|
|
|
|
def _simple_init(repo):
|
|
""" Creates a very simple initialisation: a master branch with a commit,
|
|
and a PR by 'user' with two commits, targeted to the master branch
|
|
"""
|
|
m = repo.make_commit(None, 'initial', None, tree={'m': 'm'})
|
|
repo.make_ref('heads/master', m)
|
|
c1 = repo.make_commit(m, 'first', None, tree={'m': 'c1'})
|
|
c2 = repo.make_commit(c1, 'second', None, tree={'m': 'c2'})
|
|
prx = repo.make_pr(title='title', body='body', target='master', head=c2)
|
|
return prx
|
|
|
|
class re_matches:
|
|
def __init__(self, pattern, flags=0):
|
|
self._r = re.compile(pattern, flags)
|
|
|
|
def __eq__(self, text):
|
|
return self._r.match(text)
|
|
|
|
def __str__(self):
|
|
return re.sub(r'\\(.)', r'\1', self._r.pattern)
|
|
|
|
def __repr__(self):
|
|
return repr(str(self))
|
|
|
|
def seen(env, pr, users):
|
|
return users['user'], f'[Pull request status dashboard]({to_pr(env, pr).url}).'
|
|
|
|
def make_basic(
|
|
env,
|
|
config,
|
|
make_repo,
|
|
*,
|
|
project_name='myproject',
|
|
reponame='proj',
|
|
statuses='legal/cla,ci/runbot',
|
|
fp_token=True,
|
|
fp_remote=True,
|
|
):
|
|
""" Creates a project ``project_name`` **if none exists**, otherwise
|
|
retrieves the existing one and adds a new repository and its fork.
|
|
|
|
Repositories are setup with three forking branches:
|
|
|
|
::
|
|
|
|
f = 0 -- 1 -- 2 -- 3 -- 4 : a
|
|
|
|
|
g = `-- 11 -- 22 : b
|
|
|
|
|
h = `-- 111 : c
|
|
|
|
each branch just adds and modifies a file (resp. f, g and h) through the
|
|
contents sequence a b c d e
|
|
|
|
:param env: Environment, for odoo model interactions
|
|
:param config: pytest project config thingie
|
|
:param make_repo: repo maker function, normally the fixture, should be a
|
|
``Callable[[str], Repo]``
|
|
:param project_name: internal project name, can be used to recover the
|
|
project object afterward, matches exactly since it's
|
|
unique per odoo db (and thus test)
|
|
:param reponame: the base name of the repository, for identification, for
|
|
concurrency reasons the actual repository name *will* be
|
|
different
|
|
:param statuses: required statuses for the repository, stupidly default to
|
|
the old Odoo statuses, should be moved to ``default`` over
|
|
time for simplicity (unless the test specifically calls for
|
|
multiple statuses)
|
|
:param fp_token: whether to set the ``fp_github_token`` on the project if
|
|
/ when creating it
|
|
:param fp_remote: whether to create a fork repo and set it as the
|
|
repository's ``fp_remote_target``
|
|
"""
|
|
Projects = env['runbot_merge.project']
|
|
project = Projects.search([('name', '=', project_name)])
|
|
if not project:
|
|
project = env['runbot_merge.project'].create({
|
|
'name': project_name,
|
|
'github_token': config['github']['token'],
|
|
'github_prefix': 'hansen',
|
|
'fp_github_token': fp_token and config['github']['token'],
|
|
'fp_github_name': 'herbert',
|
|
'branch_ids': [
|
|
(0, 0, {'name': 'a', 'sequence': 100}),
|
|
(0, 0, {'name': 'b', 'sequence': 80}),
|
|
(0, 0, {'name': 'c', 'sequence': 60}),
|
|
],
|
|
})
|
|
|
|
prod = make_repo(reponame)
|
|
with prod:
|
|
a_0, a_1, a_2, a_3, a_4, = prod.make_commits(
|
|
None,
|
|
Commit("0", tree={'f': 'a'}),
|
|
Commit("1", tree={'f': 'b'}),
|
|
Commit("2", tree={'f': 'c'}),
|
|
Commit("3", tree={'f': 'd'}),
|
|
Commit("4", tree={'f': 'e'}),
|
|
ref='heads/a',
|
|
)
|
|
b_1, b_2 = prod.make_commits(
|
|
a_2,
|
|
Commit('11', tree={'g': 'a'}),
|
|
Commit('22', tree={'g': 'b'}),
|
|
ref='heads/b',
|
|
)
|
|
prod.make_commits(
|
|
b_1,
|
|
Commit('111', tree={'h': 'a'}),
|
|
ref='heads/c',
|
|
)
|
|
other = prod.fork() if fp_remote else None
|
|
repo = env['runbot_merge.repository'].create({
|
|
'project_id': project.id,
|
|
'name': prod.name,
|
|
'required_statuses': statuses,
|
|
'fp_remote_target': other.name if other else False,
|
|
})
|
|
env['res.partner'].search([
|
|
('github_login', '=', config['role_reviewer']['user'])
|
|
]).write({
|
|
'review_rights': [(0, 0, {'repository_id': repo.id, 'review': True})]
|
|
})
|
|
env['res.partner'].search([
|
|
('github_login', '=', config['role_self_reviewer']['user'])
|
|
]).write({
|
|
'review_rights': [(0, 0, {'repository_id': repo.id, 'self_review': True})]
|
|
})
|
|
|
|
return prod, other
|
|
|
|
def pr_page(page, pr):
|
|
return html.fromstring(page(f'/{pr.repo.name}/pull/{pr.number}'))
|
|
|
|
def to_pr(env, pr):
|
|
for _ in range(5):
|
|
pr_id = env['runbot_merge.pull_requests'].search([
|
|
('repository.name', '=', pr.repo.name),
|
|
('number', '=', pr.number),
|
|
])
|
|
if pr_id:
|
|
assert len(pr_id) == 1, f"Expected to find {pr.repo.name}#{pr.number}, got {pr_id}."
|
|
return pr_id
|
|
time.sleep(1)
|
|
|
|
raise TimeoutError(f"Unable to find {pr.repo.name}#{pr.number}")
|
|
|
|
def part_of(label, pr_id, *, separator='\n\n'):
|
|
""" Adds the "part-of" pseudo-header in the footer.
|
|
"""
|
|
return f'{label}{separator}Part-of: {pr_id.display_name}'
|
|
|
|
def ensure_one(records):
|
|
assert len(records) == 1
|
|
return records
|