mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00
parent
f3a0a5c27c
commit
dc90a207d6
11
conftest.py
11
conftest.py
@ -481,6 +481,17 @@ def env(request, port, server, db, default_crons):
|
|||||||
if b"Traceback (most recent call last):" in server[1]:
|
if b"Traceback (most recent call last):" in server[1]:
|
||||||
pytest.fail("unexpected error in logs, fix, or mark function as `expect_log_errors` to require.")
|
pytest.fail("unexpected error in logs, fix, or mark function as `expect_log_errors` to require.")
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def reviewer_admin(env, partners):
|
||||||
|
env['res.users'].create({
|
||||||
|
'partner_id': partners['reviewer'].id,
|
||||||
|
'login': 'reviewer',
|
||||||
|
'groups_id': [
|
||||||
|
(4, env.ref("base.group_user").id, 0),
|
||||||
|
(4, env.ref("runbot_merge.group_admin").id, 0),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
def check(response):
|
def check(response):
|
||||||
assert response.ok, response.text or response.reason
|
assert response.ok, response.text or response.reason
|
||||||
return response
|
return response
|
||||||
|
@ -131,7 +131,36 @@ def test_disable(env, config, make_repo, users):
|
|||||||
assert set(pr.comments) == {
|
assert set(pr.comments) == {
|
||||||
seen(env, pr, users),
|
seen(env, pr, users),
|
||||||
(users['reviewer'], "hansen r+ up to"),
|
(users['reviewer'], "hansen r+ up to"),
|
||||||
(users['user'], "@{reviewer} please provide a branch to forward-port to.\n\nFor your own safety I've ignored *everything in your entire comment*.".format_map(users)),
|
(users['user'], """\
|
||||||
|
@{reviewer} please provide a branch to forward-port to.
|
||||||
|
|
||||||
|
For your own safety I've ignored *everything in your entire comment*.
|
||||||
|
|
||||||
|
Currently available commands:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|`r(eview)+`|approves the PR, if it's a forwardport also approves all non-detached parents|
|
||||||
|
|`r(eview)=<number>`|only approves the specified parents|
|
||||||
|
|`fw=no`|does not forward-port this PR|
|
||||||
|
|`fw=default`|forward-ports this PR normally|
|
||||||
|
|`fw=skipci`|does not wait for a forward-port's statuses to succeed before creating the next one|
|
||||||
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
||||||
|
|`merge`|integrate the PR with a simple merge commit, using the PR description as message|
|
||||||
|
|`rebase-merge`|rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message|
|
||||||
|
|`rebase-ff`|rebases the PR on top of the target branch, then fast-forwards|
|
||||||
|
|`squash`|squashes the PR as a single commit on the target branch, using the PR description as message|
|
||||||
|
|`delegate+`|grants approval rights to the PR author|
|
||||||
|
|`delegate=<...>`|grants approval rights on this PR to the specified github users|
|
||||||
|
|`default`|stages the PR normally|
|
||||||
|
|`priority`|tries to stage this PR first, then adds `default` PRs if the staging has room|
|
||||||
|
|`alone`|stages this PR only with other PRs of the same priority|
|
||||||
|
|`cancel=staging`|automatically cancels the current staging when this PR becomes ready|
|
||||||
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.
|
||||||
|
""".format_map(users)),
|
||||||
(users['reviewer'], "hansen up to b"),
|
(users['reviewer'], "hansen up to b"),
|
||||||
(users['user'], "@{reviewer} branch 'b' is disabled, it can't be used as a forward port target.".format_map(users)),
|
(users['user'], "@{reviewer} branch 'b' is disabled, it can't be used as a forward port target.".format_map(users)),
|
||||||
(users['reviewer'], "hansen up to foo"),
|
(users['reviewer'], "hansen up to foo"),
|
||||||
|
@ -3,7 +3,7 @@ from collections.abc import Iterator
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from operator import contains
|
from operator import contains
|
||||||
from typing import Callable, List, Optional, Union
|
from typing import Callable, List, Optional, Union, Tuple
|
||||||
|
|
||||||
|
|
||||||
def tokenize(line: str) -> Iterator[str]:
|
def tokenize(line: str) -> Iterator[str]:
|
||||||
@ -72,11 +72,18 @@ class Approve:
|
|||||||
return f"r={ids}"
|
return f"r={ids}"
|
||||||
return 'review+'
|
return 'review+'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "r(eview)+", "approves the PR, if it's a forwardport also approves all non-detached parents"
|
||||||
|
yield "r(eview)=<number>", "only approves the specified parents"
|
||||||
|
|
||||||
class Reject:
|
class Reject:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'review-'
|
return 'review-'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "r(eview)-", "removes approval of a previously approved PR, if the PR is staged the staging will be cancelled"
|
||||||
|
|
||||||
class MergeMethod(enum.Enum):
|
class MergeMethod(enum.Enum):
|
||||||
SQUASH = 'squash'
|
SQUASH = 'squash'
|
||||||
@ -87,16 +94,31 @@ class MergeMethod(enum.Enum):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield str(cls.MERGE), "integrate the PR with a simple merge commit, using the PR description as message"
|
||||||
|
yield str(cls.REBASE_MERGE), "rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message"
|
||||||
|
yield str(cls.REBASE_FF), "rebases the PR on top of the target branch, then fast-forwards"
|
||||||
|
yield str(cls.SQUASH), "squashes the PR as a single commit on the target branch, using the PR description as message"
|
||||||
|
|
||||||
|
|
||||||
class Retry:
|
class Retry:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'retry'
|
return 'retry'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "retry", 're-tries staging a PR in the "error" state'
|
||||||
|
|
||||||
|
|
||||||
class Check:
|
class Check:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'check'
|
return 'check'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "check", "fetches or refreshes PR metadata, resets mergebot state"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Override:
|
class Override:
|
||||||
@ -105,6 +127,10 @@ class Override:
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"override={','.join(self.statuses)}"
|
return f"override={','.join(self.statuses)}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "override=<...>", "marks overridable statuses as successful"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Delegate:
|
class Delegate:
|
||||||
@ -115,6 +141,11 @@ class Delegate:
|
|||||||
return 'delegate+'
|
return 'delegate+'
|
||||||
return f"delegate={','.join(self.users)}"
|
return f"delegate={','.join(self.users)}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "delegate+", "grants approval rights to the PR author"
|
||||||
|
yield "delegate=<...>", "grants approval rights on this PR to the specified github users"
|
||||||
|
|
||||||
|
|
||||||
class Priority(enum.Enum):
|
class Priority(enum.Enum):
|
||||||
DEFAULT = enum.auto()
|
DEFAULT = enum.auto()
|
||||||
@ -124,16 +155,30 @@ class Priority(enum.Enum):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name.lower()
|
return self.name.lower()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield str(cls.DEFAULT), "stages the PR normally"
|
||||||
|
yield str(cls.PRIORITY), "tries to stage this PR first, then adds `default` PRs if the staging has room"
|
||||||
|
yield str(cls.ALONE), "stages this PR only with other PRs of the same priority"
|
||||||
|
|
||||||
|
|
||||||
class CancelStaging:
|
class CancelStaging:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "cancel=staging"
|
return "cancel=staging"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "cancel=staging", "automatically cancels the current staging when this PR becomes ready"
|
||||||
|
|
||||||
|
|
||||||
class SkipChecks:
|
class SkipChecks:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'skipchecks'
|
return 'skipchecks'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "skipchecks", "bypasses both statuses and review"
|
||||||
|
|
||||||
|
|
||||||
class FW(enum.Enum):
|
class FW(enum.Enum):
|
||||||
DEFAULT = enum.auto()
|
DEFAULT = enum.auto()
|
||||||
@ -144,6 +189,13 @@ class FW(enum.Enum):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f'fw={self.name.lower()}'
|
return f'fw={self.name.lower()}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, is_reviewer: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield str(cls.NO), "does not forward-port this PR"
|
||||||
|
if is_reviewer:
|
||||||
|
yield str(cls.DEFAULT), "forward-ports this PR normally"
|
||||||
|
yield str(cls.SKIPCI), "does not wait for a forward-port's statuses to succeed before creating the next one"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Limit:
|
class Limit:
|
||||||
@ -154,11 +206,28 @@ class Limit:
|
|||||||
return 'ignore'
|
return 'ignore'
|
||||||
return f'up to {self.branch}'
|
return f'up to {self.branch}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield "up to <branch>", "only ports this PR forward to the specified branch (included)"
|
||||||
|
|
||||||
|
|
||||||
class Close:
|
class Close:
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'close'
|
return 'close'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield str(cls()), "closes this forward-port"
|
||||||
|
|
||||||
|
|
||||||
|
class Help:
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return 'help'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls, _: bool) -> Iterator[Tuple[str, str]]:
|
||||||
|
yield str(cls()), "displays this help"
|
||||||
|
|
||||||
|
|
||||||
Command = Union[
|
Command = Union[
|
||||||
Approve,
|
Approve,
|
||||||
@ -167,6 +236,7 @@ Command = Union[
|
|||||||
Check,
|
Check,
|
||||||
Delegate,
|
Delegate,
|
||||||
FW,
|
FW,
|
||||||
|
Help,
|
||||||
Limit,
|
Limit,
|
||||||
MergeMethod,
|
MergeMethod,
|
||||||
Override,
|
Override,
|
||||||
@ -309,3 +379,6 @@ class Parser:
|
|||||||
|
|
||||||
def parse_close(self) -> Close:
|
def parse_close(self) -> Close:
|
||||||
return Close()
|
return Close()
|
||||||
|
|
||||||
|
def parse_help(self) -> Help:
|
||||||
|
return Help()
|
||||||
|
@ -642,6 +642,51 @@ class PullRequests(models.Model):
|
|||||||
'message': message,
|
'message': message,
|
||||||
'close': close,
|
'close': close,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
is_admin, is_reviewer, is_author = self._pr_acl(author)
|
||||||
|
_source_admin, source_reviewer, source_author = self.source_id._pr_acl(author)
|
||||||
|
# nota: 15.0 `has_group` completely doesn't work if the recordset is empty
|
||||||
|
super_admin = is_admin and author.user_ids and author.user_ids.has_group('runbot_merge.group_admin')
|
||||||
|
|
||||||
|
help_list: list[type(commands.Command)] = list(filter(None, [
|
||||||
|
commands.Help,
|
||||||
|
|
||||||
|
(self.source_id and (source_author or source_reviewer) or is_reviewer) and not self.reviewed_by and commands.Approve,
|
||||||
|
is_author and self.reviewed_by and commands.Reject,
|
||||||
|
(is_author or source_author) and self.error and commands.Retry,
|
||||||
|
|
||||||
|
is_author and not self.source_id and commands.FW,
|
||||||
|
is_author and commands.Limit,
|
||||||
|
source_author and self.source_id and commands.Close,
|
||||||
|
|
||||||
|
is_reviewer and commands.MergeMethod,
|
||||||
|
is_reviewer and commands.Delegate,
|
||||||
|
|
||||||
|
is_admin and commands.Priority,
|
||||||
|
super_admin and commands.SkipChecks,
|
||||||
|
is_admin and commands.CancelStaging,
|
||||||
|
|
||||||
|
author.override_rights and commands.Override,
|
||||||
|
is_author and commands.Check,
|
||||||
|
]))
|
||||||
|
def format_help(warn_ignore: bool, address: bool = True) -> str:
|
||||||
|
s = [
|
||||||
|
'Currently available commands{}:'.format(
|
||||||
|
f" for @{login}" if address else ""
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
'|command||',
|
||||||
|
'|-|-|',
|
||||||
|
]
|
||||||
|
for command_type in help_list:
|
||||||
|
for cmd, text in command_type.help(is_reviewer):
|
||||||
|
s.append(f"|`{cmd}`|{text}|")
|
||||||
|
|
||||||
|
s.extend(['', 'Note: this help text is dynamic and will change with the state of the PR.'])
|
||||||
|
if warn_ignore:
|
||||||
|
s.extend(["", "Warning: in invoking help, every other command has been ignored."])
|
||||||
|
return "\n".join(s)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmds: List[commands.Command] = [
|
cmds: List[commands.Command] = [
|
||||||
ps
|
ps
|
||||||
@ -656,11 +701,21 @@ class PullRequests(models.Model):
|
|||||||
utils.shorten(comment['body'] or '', 50),
|
utils.shorten(comment['body'] or '', 50),
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
feedback(message=f"@{login} {e.args[0]}.\n\nFor your own safety I've ignored *everything in your entire comment*.")
|
feedback(message=f"""@{login} {e.args[0]}.
|
||||||
|
|
||||||
|
For your own safety I've ignored *everything in your entire comment*.
|
||||||
|
|
||||||
|
{format_help(False, address=False)}
|
||||||
|
""")
|
||||||
return 'error'
|
return 'error'
|
||||||
|
|
||||||
is_admin, is_reviewer, is_author = self._pr_acl(author)
|
if any(isinstance(cmd, commands.Help) for cmd in cmds):
|
||||||
_source_admin, source_reviewer, source_author = self.source_id._pr_acl(author)
|
self.env['runbot_merge.pull_requests.feedback'].create({
|
||||||
|
'repository': self.repository.id,
|
||||||
|
'pull_request': self.number,
|
||||||
|
'message': format_help(len(cmds) != 1),
|
||||||
|
})
|
||||||
|
return "help"
|
||||||
|
|
||||||
if not (is_author or self.source_id or (any(isinstance(cmd, commands.Override) for cmd in cmds) and author.override_rights)):
|
if not (is_author or self.source_id or (any(isinstance(cmd, commands.Override) for cmd in cmds) and author.override_rights)):
|
||||||
# no point even parsing commands
|
# no point even parsing commands
|
||||||
@ -775,7 +830,7 @@ class PullRequests(models.Model):
|
|||||||
delegates.write({'delegate_reviewer': [(4, self.id, 0)]})
|
delegates.write({'delegate_reviewer': [(4, self.id, 0)]})
|
||||||
case commands.Priority() if is_admin:
|
case commands.Priority() if is_admin:
|
||||||
self.batch_id.priority = str(command)
|
self.batch_id.priority = str(command)
|
||||||
case commands.SkipChecks() if is_admin:
|
case commands.SkipChecks() if super_admin:
|
||||||
self.batch_id.skipchecks = True
|
self.batch_id.skipchecks = True
|
||||||
self.reviewed_by = author
|
self.reviewed_by = author
|
||||||
if not (self.squash or self.merge_method):
|
if not (self.squash or self.merge_method):
|
||||||
|
@ -2801,6 +2801,7 @@ class TestBatching(object):
|
|||||||
]
|
]
|
||||||
assert not pr22.staging_id
|
assert not pr22.staging_id
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_batching_urgent(self, env, repo, config):
|
def test_batching_urgent(self, env, repo, config):
|
||||||
with repo:
|
with repo:
|
||||||
m = repo.make_commit(None, 'initial', None, tree={'a': 'some content'})
|
m = repo.make_commit(None, 'initial', None, tree={'a': 'some content'})
|
||||||
@ -2920,6 +2921,7 @@ class TestBatching(object):
|
|||||||
assert p_01.skipchecks == False
|
assert p_01.skipchecks == False
|
||||||
assert p_01.cancel_staging == True
|
assert p_01.cancel_staging == True
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_batching_urgenter_than_split(self, env, repo, config):
|
def test_batching_urgenter_than_split(self, env, repo, config):
|
||||||
""" p=alone PRs should take priority over split stagings (processing
|
""" p=alone PRs should take priority over split stagings (processing
|
||||||
of a staging having CI-failed and being split into sub-stagings)
|
of a staging having CI-failed and being split into sub-stagings)
|
||||||
@ -2959,6 +2961,7 @@ class TestBatching(object):
|
|||||||
assert not p_1.staging_id
|
assert not p_1.staging_id
|
||||||
assert to_pr(env, pr0).staging_id
|
assert to_pr(env, pr0).staging_id
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_urgent_failed(self, env, repo, config):
|
def test_urgent_failed(self, env, repo, config):
|
||||||
""" Ensure pr[p=0,state=failed] don't get picked up
|
""" Ensure pr[p=0,state=failed] don't get picked up
|
||||||
"""
|
"""
|
||||||
@ -3306,6 +3309,7 @@ class TestReviewing:
|
|||||||
env.run_crons()
|
env.run_crons()
|
||||||
assert to_pr(env, pr).state == 'approved'
|
assert to_pr(env, pr).state == 'approved'
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_skipchecks(self, env, repo, users, config):
|
def test_skipchecks(self, env, repo, users, config):
|
||||||
"""Skipcheck makes the PR immediately ready (if it's not in error or
|
"""Skipcheck makes the PR immediately ready (if it's not in error or
|
||||||
something)
|
something)
|
||||||
@ -3622,8 +3626,66 @@ class TestRecognizeCommands:
|
|||||||
(users['reviewer'], "hansen do the thing"),
|
(users['reviewer'], "hansen do the thing"),
|
||||||
(users['reviewer'], "hansen @bobby-b r+ :+1:"),
|
(users['reviewer'], "hansen @bobby-b r+ :+1:"),
|
||||||
seen(env, pr, users),
|
seen(env, pr, users),
|
||||||
(users['user'], "@{reviewer} unknown command 'do'.\n\nFor your own safety I've ignored *everything in your entire comment*.".format_map(users)),
|
(users['user'], """\
|
||||||
(users['user'], "@{reviewer} unknown command '@bobby-b'.\n\nFor your own safety I've ignored *everything in your entire comment*.".format_map(users)),
|
@{reviewer} unknown command 'do'.
|
||||||
|
|
||||||
|
For your own safety I've ignored *everything in your entire comment*.
|
||||||
|
|
||||||
|
Currently available commands:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|`r(eview)+`|approves the PR, if it's a forwardport also approves all non-detached parents|
|
||||||
|
|`r(eview)=<number>`|only approves the specified parents|
|
||||||
|
|`fw=no`|does not forward-port this PR|
|
||||||
|
|`fw=default`|forward-ports this PR normally|
|
||||||
|
|`fw=skipci`|does not wait for a forward-port's statuses to succeed before creating the next one|
|
||||||
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
||||||
|
|`merge`|integrate the PR with a simple merge commit, using the PR description as message|
|
||||||
|
|`rebase-merge`|rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message|
|
||||||
|
|`rebase-ff`|rebases the PR on top of the target branch, then fast-forwards|
|
||||||
|
|`squash`|squashes the PR as a single commit on the target branch, using the PR description as message|
|
||||||
|
|`delegate+`|grants approval rights to the PR author|
|
||||||
|
|`delegate=<...>`|grants approval rights on this PR to the specified github users|
|
||||||
|
|`default`|stages the PR normally|
|
||||||
|
|`priority`|tries to stage this PR first, then adds `default` PRs if the staging has room|
|
||||||
|
|`alone`|stages this PR only with other PRs of the same priority|
|
||||||
|
|`cancel=staging`|automatically cancels the current staging when this PR becomes ready|
|
||||||
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.
|
||||||
|
""".format_map(users)),
|
||||||
|
(users['user'], """\
|
||||||
|
@{reviewer} unknown command '@bobby-b'.
|
||||||
|
|
||||||
|
For your own safety I've ignored *everything in your entire comment*.
|
||||||
|
|
||||||
|
Currently available commands:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|`r(eview)+`|approves the PR, if it's a forwardport also approves all non-detached parents|
|
||||||
|
|`r(eview)=<number>`|only approves the specified parents|
|
||||||
|
|`fw=no`|does not forward-port this PR|
|
||||||
|
|`fw=default`|forward-ports this PR normally|
|
||||||
|
|`fw=skipci`|does not wait for a forward-port's statuses to succeed before creating the next one|
|
||||||
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
||||||
|
|`merge`|integrate the PR with a simple merge commit, using the PR description as message|
|
||||||
|
|`rebase-merge`|rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message|
|
||||||
|
|`rebase-ff`|rebases the PR on top of the target branch, then fast-forwards|
|
||||||
|
|`squash`|squashes the PR as a single commit on the target branch, using the PR description as message|
|
||||||
|
|`delegate+`|grants approval rights to the PR author|
|
||||||
|
|`delegate=<...>`|grants approval rights on this PR to the specified github users|
|
||||||
|
|`default`|stages the PR normally|
|
||||||
|
|`priority`|tries to stage this PR first, then adds `default` PRs if the staging has room|
|
||||||
|
|`alone`|stages this PR only with other PRs of the same priority|
|
||||||
|
|`cancel=staging`|automatically cancels the current staging when this PR becomes ready|
|
||||||
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.
|
||||||
|
""".format_map(users)),
|
||||||
]
|
]
|
||||||
|
|
||||||
class TestRMinus:
|
class TestRMinus:
|
||||||
|
@ -761,6 +761,7 @@ class TestMultiBatches:
|
|||||||
assert sp.mapped('batch_ids.prs') == \
|
assert sp.mapped('batch_ids.prs') == \
|
||||||
prs[2][0] | prs[2][1] | prs[3][0] | prs[3][1] | prs[4][0]
|
prs[2][0] | prs[2][1] | prs[3][0] | prs[3][1] | prs[4][0]
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_urgent(env, repo_a, repo_b, config):
|
def test_urgent(env, repo_a, repo_b, config):
|
||||||
""" Either PR of a co-dependent pair being prioritised leads to the entire
|
""" Either PR of a co-dependent pair being prioritised leads to the entire
|
||||||
pair being prioritized
|
pair being prioritized
|
||||||
@ -872,6 +873,7 @@ class TestBlocked:
|
|||||||
p = to_pr(env, pr)
|
p = to_pr(env, pr)
|
||||||
assert not p.blocked
|
assert not p.blocked
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("reviewer_admin")
|
||||||
def test_linked_unready(self, env, repo_a, repo_b, config):
|
def test_linked_unready(self, env, repo_a, repo_b, config):
|
||||||
""" Create a PR A linked to a non-ready PR B,
|
""" Create a PR A linked to a non-ready PR B,
|
||||||
* A is blocked by default
|
* A is blocked by default
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from utils import Commit, to_pr
|
from utils import Commit, to_pr, seen
|
||||||
|
|
||||||
|
|
||||||
def test_partner_merge(env):
|
def test_partner_merge(env):
|
||||||
@ -253,8 +253,8 @@ def test_force_ready(env, repo, config):
|
|||||||
with repo:
|
with repo:
|
||||||
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref="heads/master")
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref="heads/master")
|
||||||
|
|
||||||
[c] = repo.make_commits(m, Commit('first', tree={'m': 'c1'}), ref="heads/other")
|
repo.make_commits(m, Commit('first', tree={'m': 'c1'}), ref="heads/other")
|
||||||
pr = repo.make_pr(title='title', body='body', target='master', head=c)
|
pr = repo.make_pr(target='master', head='other')
|
||||||
env.run_crons()
|
env.run_crons()
|
||||||
|
|
||||||
pr_id = to_pr(env, pr)
|
pr_id = to_pr(env, pr)
|
||||||
@ -264,3 +264,102 @@ def test_force_ready(env, repo, config):
|
|||||||
assert pr_id.status == 'pending'
|
assert pr_id.status == 'pending'
|
||||||
reviewer = env['res.users'].browse([env._uid]).partner_id
|
reviewer = env['res.users'].browse([env._uid]).partner_id
|
||||||
assert pr_id.reviewed_by == reviewer
|
assert pr_id.reviewed_by == reviewer
|
||||||
|
|
||||||
|
def test_help(env, repo, config, users, partners):
|
||||||
|
with repo:
|
||||||
|
[m] = repo.make_commits(None, Commit('initial', tree={'m': 'm'}), ref="heads/master")
|
||||||
|
|
||||||
|
repo.make_commits(m, Commit('first', tree={'m': 'c1'}), ref="heads/other")
|
||||||
|
pr = repo.make_pr(target='master', head='other')
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
for role in ['reviewer', 'self_reviewer', 'user', 'other']:
|
||||||
|
v = config[f'role_{role}']
|
||||||
|
with repo:
|
||||||
|
pr.post_comment("hansen help", v['token'])
|
||||||
|
with repo:
|
||||||
|
pr.post_comment("hansen r+ help", config['role_reviewer']['token'])
|
||||||
|
|
||||||
|
assert not partners['reviewer'].user_ids, "the reviewer should not be an internal user"
|
||||||
|
|
||||||
|
group_internal = env.ref("base.group_user")
|
||||||
|
group_admin = env.ref("runbot_merge.group_admin")
|
||||||
|
env['res.users'].create({
|
||||||
|
'partner_id': partners['reviewer'].id,
|
||||||
|
'login': 'reviewer',
|
||||||
|
'groups_id': [(4, group_internal.id, 0), (4, group_admin.id, 0)],
|
||||||
|
})
|
||||||
|
|
||||||
|
with repo:
|
||||||
|
pr.post_comment("hansen help", config['role_reviewer']['token'])
|
||||||
|
env.run_crons()
|
||||||
|
|
||||||
|
assert pr.comments == [
|
||||||
|
seen(env, pr, users),
|
||||||
|
(users['reviewer'], "hansen help"),
|
||||||
|
(users['self_reviewer'], "hansen help"),
|
||||||
|
(users['user'], "hansen help"),
|
||||||
|
(users['other'], "hansen help"),
|
||||||
|
(users['reviewer'], "hansen r+ help"),
|
||||||
|
(users['reviewer'], "hansen help"),
|
||||||
|
(users['user'], REVIEWER.format(user=users['reviewer'], skip="")),
|
||||||
|
(users['user'], RANDO.format(user=users['self_reviewer'])),
|
||||||
|
(users['user'], AUTHOR.format(user=users['user'])),
|
||||||
|
(users['user'], RANDO.format(user=users['other'])),
|
||||||
|
(users['user'],
|
||||||
|
REVIEWER.format(user=users['reviewer'], skip='')
|
||||||
|
+ "\n\nWarning: in invoking help, every other command has been ignored."),
|
||||||
|
(users['user'], REVIEWER.format(
|
||||||
|
user=users['reviewer'],
|
||||||
|
skip='|`skipchecks`|bypasses both statuses and review|\n',
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|
||||||
|
REVIEWER = """\
|
||||||
|
Currently available commands for @{user}:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|`r(eview)+`|approves the PR, if it's a forwardport also approves all non-detached parents|
|
||||||
|
|`r(eview)=<number>`|only approves the specified parents|
|
||||||
|
|`fw=no`|does not forward-port this PR|
|
||||||
|
|`fw=default`|forward-ports this PR normally|
|
||||||
|
|`fw=skipci`|does not wait for a forward-port's statuses to succeed before creating the next one|
|
||||||
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
||||||
|
|`merge`|integrate the PR with a simple merge commit, using the PR description as message|
|
||||||
|
|`rebase-merge`|rebases the PR on top of the target branch the integrates with a merge commit, using the PR description as message|
|
||||||
|
|`rebase-ff`|rebases the PR on top of the target branch, then fast-forwards|
|
||||||
|
|`squash`|squashes the PR as a single commit on the target branch, using the PR description as message|
|
||||||
|
|`delegate+`|grants approval rights to the PR author|
|
||||||
|
|`delegate=<...>`|grants approval rights on this PR to the specified github users|
|
||||||
|
|`default`|stages the PR normally|
|
||||||
|
|`priority`|tries to stage this PR first, then adds `default` PRs if the staging has room|
|
||||||
|
|`alone`|stages this PR only with other PRs of the same priority|
|
||||||
|
{skip}\
|
||||||
|
|`cancel=staging`|automatically cancels the current staging when this PR becomes ready|
|
||||||
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.\
|
||||||
|
"""
|
||||||
|
AUTHOR = """\
|
||||||
|
Currently available commands for @{user}:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|`fw=no`|does not forward-port this PR|
|
||||||
|
|`up to <branch>`|only ports this PR forward to the specified branch (included)|
|
||||||
|
|`check`|fetches or refreshes PR metadata, resets mergebot state|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.\
|
||||||
|
"""
|
||||||
|
RANDO = """\
|
||||||
|
Currently available commands for @{user}:
|
||||||
|
|
||||||
|
|command||
|
||||||
|
|-|-|
|
||||||
|
|`help`|displays this help|
|
||||||
|
|
||||||
|
Note: this help text is dynamic and will change with the state of the PR.\
|
||||||
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user