initial commit
This commit is contained in:
commit
b5a05ac43a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.egg-info/
|
||||
/build/
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Gerrit link bot
|
||||
|
||||
This is a bot that polls Gerrit for changes, finds issue references in them,
|
||||
then goes and posts comments linking back to the CLs.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set FORGEJO_API_KEY in the environment. Currently the endpoints for everything
|
||||
are hardcoded to the lix.systems ones.
|
||||
|
||||
## Testing locally
|
||||
|
||||
If you want to test locally, go impersonate lix-bot on
|
||||
https://identity.lix.systems/admin, relogin to Forgejo, then issue yourself a
|
||||
new personal access token for `lix-bot` with `user:read` and `issue:write`. Set
|
||||
that in FORGEJO_API_KEY in the environment.
|
||||
|
||||
If you just want to test that it does mostly the right thing, consider either
|
||||
writing an integration test using the fake APIs, or passing `--fake` on
|
||||
startup, which will make it operate on fake data and not hit the internet at
|
||||
all.
|
230
gerrit_linkbot/__init__.py
Normal file
230
gerrit_linkbot/__init__.py
Normal file
|
@ -0,0 +1,230 @@
|
|||
import requests
|
||||
import re
|
||||
import time
|
||||
import textwrap
|
||||
import json
|
||||
import dataclasses
|
||||
import logging
|
||||
import importlib.resources
|
||||
import importlib.resources.abc
|
||||
from typing import Literal, Optional
|
||||
from . import gerrit, forgejo
|
||||
from .constants import FORGEJO_ACCOUNT, FORGEJO_SERVICE_ACCOUNT, FORGEJO_BASE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
fmt = logging.Formatter('{asctime} {levelname} {name}: {message}',
|
||||
datefmt='%b %d %H:%M:%S',
|
||||
style='{')
|
||||
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in log.handlers):
|
||||
hand = logging.StreamHandler()
|
||||
hand.setFormatter(fmt)
|
||||
log.addHandler(hand)
|
||||
|
||||
testdata = importlib.resources.files('gerrit_linkbot.testdata')
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class IssueMatch:
|
||||
"""Result of applying a RegexRule."""
|
||||
issue_num: int
|
||||
project_name: Optional[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RegexRule:
|
||||
"""Rule applied on Gerrit CL comments and commit messages to extract issue references."""
|
||||
regex: re.Pattern
|
||||
capture_num: int
|
||||
project_name_capture_num: Optional[int] = None
|
||||
|
||||
def apply(self, s: str) -> list[IssueMatch]:
|
||||
return [
|
||||
IssueMatch(
|
||||
int(m.group(self.capture_num)),
|
||||
m.group(self.project_name_capture_num)
|
||||
if self.project_name_capture_num else None)
|
||||
for m in self.regex.finditer(s)
|
||||
]
|
||||
|
||||
|
||||
RULES = [
|
||||
# #123, fj#123
|
||||
RegexRule(
|
||||
re.compile(
|
||||
r"(?:^|[\s\]\[(){},.;:!?])(fj|lix)?#(\d+)(?=$|[\s\]\[(){},.;:!?])"
|
||||
), 2),
|
||||
# URL to some issue
|
||||
RegexRule(re.compile(
|
||||
re.escape(FORGEJO_BASE) + "/" + re.escape(FORGEJO_ACCOUNT) +
|
||||
r"/([a-zA-Z0-9-]+)/issues/([0-9]+)"),
|
||||
2,
|
||||
project_name_capture_num=1)
|
||||
]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CLMention:
|
||||
"""One mention of a CL in a Forgejo comment, without mutable information about the CL itself."""
|
||||
|
||||
backlink: str
|
||||
number: int
|
||||
kind: Literal['comment'] | Literal['commit message']
|
||||
|
||||
@classmethod
|
||||
def from_messageish(cls, message_ish: gerrit.MessageIsh) -> 'CLMention':
|
||||
return cls(backlink=message_ish.backlink,
|
||||
number=message_ish.cl_number,
|
||||
kind=message_ish.kind)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj) -> 'CLMention':
|
||||
return cls(backlink=obj['backlink'],
|
||||
number=obj['number'],
|
||||
kind=obj['kind'])
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CLMeta:
|
||||
"""Mutable metadata on a Gerrit CL."""
|
||||
change_title: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj) -> 'CLMeta':
|
||||
return cls(change_title=obj['change_title'])
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CommentMeta:
|
||||
"""
|
||||
The complete set of information that is put into a Forgejo comment.
|
||||
"""
|
||||
|
||||
cls: list[CLMention]
|
||||
"""CLs mentioned, with unchanging metadata"""
|
||||
cl_meta: dict[int, CLMeta]
|
||||
"""CL metadata that might change (e.g. title)"""
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
'cls': [dataclasses.asdict(x) for x in self.cls],
|
||||
'cl_meta': {
|
||||
num: dataclasses.asdict(x)
|
||||
for num, x in self.cl_meta.items()
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj) -> 'CommentMeta':
|
||||
return cls(cls=[CLMention.parse(x) for x in obj['cls']],
|
||||
cl_meta={
|
||||
int(num): CLMeta.parse(x)
|
||||
for num, x in obj['cl_meta'].items()
|
||||
})
|
||||
|
||||
|
||||
def parse_comment(comment: str):
|
||||
LEADER = '<!-- GERRIT_LINKBOT:'
|
||||
|
||||
for line in comment.splitlines():
|
||||
if line.startswith(LEADER):
|
||||
data = json.loads(line.removeprefix(LEADER))
|
||||
return CommentMeta.parse(data)
|
||||
|
||||
|
||||
def make_comment(meta: CommentMeta):
|
||||
data_json = json.dumps(meta.serialize())
|
||||
cls = '\n'.join('* {kind} in [cl/{cl}]({backlink}) ("{cl_desc}")'.format(
|
||||
kind=cl.kind,
|
||||
cl=cl.number,
|
||||
backlink=cl.backlink,
|
||||
cl_desc=meta.cl_meta[cl.number].change_title) for cl in meta.cls)
|
||||
|
||||
return textwrap.dedent("""\
|
||||
<!-- GERRIT_LINKBOT: {data_json}
|
||||
-->
|
||||
|
||||
This issue was mentioned on Gerrit on the following CLs:
|
||||
{cls}
|
||||
""").format(data_json=data_json, cls=cls)
|
||||
|
||||
|
||||
class Service:
|
||||
"""
|
||||
The top level job in gerrit-linkbot: composes together the GerritWatcher
|
||||
and taking actual actions with the Forgejo API.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
gerrit_api: gerrit.API,
|
||||
forgejo_api: forgejo.API,
|
||||
poll_interval=30,
|
||||
gerrit_per_page=25):
|
||||
self.gerrit_api = gerrit_api
|
||||
self.forgejo_api = forgejo_api
|
||||
self.watcher = gerrit.GerritWatcher(gerrit_api,
|
||||
self.handle_comment,
|
||||
per_page=gerrit_per_page)
|
||||
self.poll_interval = poll_interval
|
||||
|
||||
def handle_hit(self, matched: IssueMatch, comment: gerrit.MessageIsh):
|
||||
target = [
|
||||
FORGEJO_ACCOUNT, matched.project_name or comment.project,
|
||||
matched.issue_num
|
||||
]
|
||||
try:
|
||||
comments = list(self.forgejo_api.all_comments(*target))
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code in (404, 500):
|
||||
# The issue doesn't exist, and it may be 500 because of a forgejo bug:
|
||||
# https://codeberg.org/forgejo/forgejo/issues/4005
|
||||
log.warn('Issue %s/%s#%d does not exist', *target)
|
||||
return
|
||||
raise
|
||||
|
||||
existing_comment = None
|
||||
comment_meta = CommentMeta([], {})
|
||||
for fj_comment in comments:
|
||||
if fj_comment.author == FORGEJO_SERVICE_ACCOUNT:
|
||||
data = parse_comment(fj_comment.text)
|
||||
if data:
|
||||
comment_meta = data
|
||||
existing_comment = (fj_comment.id, fj_comment.text)
|
||||
break
|
||||
|
||||
# construct the new comment on top of the existing one
|
||||
comment_meta.cl_meta[comment.cl_number] = CLMeta(comment.change_title)
|
||||
if not any(cl_mention.number == comment.cl_number
|
||||
and cl_mention.kind == comment.kind
|
||||
for cl_mention in comment_meta.cls):
|
||||
comment_meta.cls.append(CLMention.from_messageish(comment))
|
||||
|
||||
text = make_comment(comment_meta)
|
||||
|
||||
if existing_comment:
|
||||
# no change to the comment, no point posting it
|
||||
if existing_comment[1] == text:
|
||||
return
|
||||
|
||||
self.forgejo_api.edit_comment(
|
||||
*target,
|
||||
existing_comment[0], # type: ignore
|
||||
text) # type: ignore
|
||||
else:
|
||||
self.forgejo_api.post_comment(*target, text) # type: ignore
|
||||
|
||||
def handle_comment(self, comment: gerrit.MessageIsh):
|
||||
log.debug('Comment: %s', comment)
|
||||
for rule in RULES:
|
||||
for res in rule.apply(comment.message):
|
||||
self.handle_hit(res, comment)
|
||||
|
||||
def step(self):
|
||||
self.watcher.poll()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.step()
|
||||
time.sleep(self.poll_interval)
|
27
gerrit_linkbot/__main__.py
Normal file
27
gerrit_linkbot/__main__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from . import gerrit, forgejo
|
||||
import gerrit_linkbot
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description='Gerrit link bot')
|
||||
ap.add_argument('--fake', action='store_true', help='Fake everything')
|
||||
ap.add_argument('--gerrit-per-page', type=int, help='Number of results per page (for backfilling gerrit)')
|
||||
ap.add_argument('--poll-interval', type=int, help='How many seconds between polls of Gerrit', default=30)
|
||||
|
||||
parsed = ap.parse_args()
|
||||
|
||||
if parsed.fake:
|
||||
gerrit_api = gerrit.FakeAPI(gerrit_linkbot.testdata)
|
||||
forgejo_api = forgejo.FakeAPI(gerrit_linkbot.testdata)
|
||||
else:
|
||||
gerrit_api = gerrit.ConcreteAPI()
|
||||
forgejo_api = forgejo.ConcreteAPI()
|
||||
|
||||
service = gerrit_linkbot.Service(gerrit_api, forgejo_api, poll_interval=parsed.poll_interval, gerrit_per_page=parsed.gerrit_per_page)
|
||||
service.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
6
gerrit_linkbot/constants.py
Normal file
6
gerrit_linkbot/constants.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
FORGEJO_ACCOUNT = 'lix-project'
|
||||
FORGEJO_SERVICE_ACCOUNT = 'lix-bot'
|
||||
|
||||
GERRIT_BASE = 'https://gerrit.lix.systems'
|
||||
FORGEJO_BASE = 'https://git.lix.systems'
|
179
gerrit_linkbot/forgejo.py
Normal file
179
gerrit_linkbot/forgejo.py
Normal file
|
@ -0,0 +1,179 @@
|
|||
import dataclasses
|
||||
from importlib.resources.abc import Traversable
|
||||
import json
|
||||
import abc
|
||||
from typing import Any, Generator
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .constants import FORGEJO_SERVICE_ACCOUNT, FORGEJO_BASE
|
||||
from . import http
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
API_BASE = FORGEJO_BASE + '/api/v1'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Comment:
|
||||
"""One issue comment on Forgejo."""
|
||||
author: str
|
||||
text: str
|
||||
id: int
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj) -> 'Comment':
|
||||
return cls(
|
||||
author=obj['user']['username'],
|
||||
text=obj['body'],
|
||||
id=obj['id'],
|
||||
)
|
||||
|
||||
|
||||
class API(metaclass=abc.ABCMeta):
|
||||
"""The Forgejo API, as used by gerrit-linkbot."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def all_comments(self, owner: str, repo: str,
|
||||
issue: int) -> Generator[Comment, None, None]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def edit_comment(self, owner: str, repo: str, issue: int, id: int,
|
||||
comment: str) -> Any:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def post_comment(self, owner: str, repo: str, issue: int,
|
||||
comment: str) -> Any:
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CommentLocator:
|
||||
"""Location that a comment might appear; effectively a fully-qualified issue ID."""
|
||||
owner: str
|
||||
repo: str
|
||||
issue_id: int
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class PostedComment:
|
||||
"""Resulting posted comment in the fake API."""
|
||||
locator: CommentLocator
|
||||
comment: str
|
||||
comment_id: int
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class EditedComment:
|
||||
locator: CommentLocator
|
||||
comment: str
|
||||
edit_comment_id: int
|
||||
|
||||
|
||||
class FakeAPI(API):
|
||||
|
||||
def __init__(self, fixtures: Traversable):
|
||||
super().__init__()
|
||||
self.fixtures = fixtures
|
||||
self.posted_comments = []
|
||||
|
||||
self.mock_id = 10000
|
||||
self.comments_by_locator: dict[CommentLocator, list[Comment]] = {}
|
||||
|
||||
def fixture(self, name: str):
|
||||
return json.loads(self.fixtures.joinpath(name).read_text())
|
||||
|
||||
def init_comments(self, locator: CommentLocator):
|
||||
comments = self.comments_by_locator.get(locator, None)
|
||||
if comments is None:
|
||||
comments = [
|
||||
Comment.parse(c) for c in self.fixture(
|
||||
f'issue-comments-{locator.owner}-{locator.repo}-{locator.issue_id}.json'
|
||||
)
|
||||
]
|
||||
self.comments_by_locator[locator] = comments
|
||||
|
||||
def all_comments(self, owner: str, repo: str,
|
||||
issue: int) -> Generator[Comment, None, None]:
|
||||
locator = CommentLocator(owner, repo, issue)
|
||||
self.init_comments(locator)
|
||||
yield from self.comments_by_locator[locator]
|
||||
|
||||
def next_id(self) -> int:
|
||||
id = self.mock_id
|
||||
self.mock_id += 1
|
||||
return id
|
||||
|
||||
def edit_comment(self, owner: str, repo: str, issue: int, id: int,
|
||||
comment: str):
|
||||
log.info('Post comment on %s/%s#%d %d: %s', owner, repo, issue, id,
|
||||
comment)
|
||||
locator = CommentLocator(owner, repo, issue)
|
||||
for existing in self.comments_by_locator[locator]:
|
||||
if existing.id == id:
|
||||
existing.text = comment
|
||||
|
||||
self.posted_comments.append(
|
||||
EditedComment(locator=CommentLocator(owner, repo, issue),
|
||||
edit_comment_id=id,
|
||||
comment=comment))
|
||||
|
||||
def post_comment(self, owner: str, repo: str, issue: int, comment: str):
|
||||
log.info('Post comment on %s/%s#%d: %s', owner, repo, issue, comment)
|
||||
locator = CommentLocator(owner, repo, issue)
|
||||
|
||||
id = self.next_id()
|
||||
self.comments_by_locator[locator].append(
|
||||
Comment(FORGEJO_SERVICE_ACCOUNT, comment, id))
|
||||
|
||||
self.posted_comments.append(
|
||||
PostedComment(locator=locator, comment=comment, comment_id=id))
|
||||
|
||||
|
||||
class ConcreteAPI(API):
|
||||
|
||||
def api(self, method, endpoint: str, resp_json=True, **kwargs):
|
||||
API_KEY = os.environ['FORGEJO_API_KEY']
|
||||
|
||||
log.info('http %s %s', method, endpoint)
|
||||
if not endpoint.startswith('https'):
|
||||
endpoint = API_BASE + endpoint
|
||||
resp = http.session.request(
|
||||
method,
|
||||
endpoint,
|
||||
headers={'Authorization': f'Bearer {API_KEY}'},
|
||||
**kwargs)
|
||||
resp.raise_for_status()
|
||||
if resp_json:
|
||||
return resp.json()
|
||||
else:
|
||||
return resp
|
||||
|
||||
def paginate(self, method: str, url: str):
|
||||
while True:
|
||||
resp = self.api(method, url, resp_json=False)
|
||||
yield from resp.json()
|
||||
next_one = resp.links.get('next')
|
||||
if not next_one:
|
||||
return
|
||||
url = next_one.get('url')
|
||||
if not url:
|
||||
return
|
||||
|
||||
def all_comments(self, owner: str, repo: str,
|
||||
issue: int) -> Generator[Comment, None, None]:
|
||||
yield from (Comment.parse(c) for c in self.paginate(
|
||||
'GET', f'/repos/{owner}/{repo}/issues/{issue}/comments'))
|
||||
|
||||
def edit_comment(self, owner: str, repo: str, issue: int, id: int,
|
||||
comment: str):
|
||||
return self.api('PATCH',
|
||||
f'/repos/{owner}/{repo}/issues/comments/{id}',
|
||||
json={'body': comment})
|
||||
|
||||
def post_comment(self, owner: str, repo: str, issue: int, comment: str):
|
||||
return self.api('POST',
|
||||
f'/repos/{owner}/{repo}/issues/{issue}/comments',
|
||||
json={'body': comment})
|
179
gerrit_linkbot/gerrit.py
Normal file
179
gerrit_linkbot/gerrit.py
Normal file
|
@ -0,0 +1,179 @@
|
|||
import dataclasses
|
||||
from importlib.resources.abc import Traversable
|
||||
import json
|
||||
from typing import Callable, Literal
|
||||
import logging
|
||||
import abc
|
||||
from datetime import datetime
|
||||
|
||||
from .constants import GERRIT_BASE
|
||||
from . import http
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Change:
|
||||
"""Change metadata acquired from the changes query endpoint."""
|
||||
subject: str
|
||||
project: str
|
||||
id: str
|
||||
number: int
|
||||
updated: datetime
|
||||
commit_msg: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj: dict) -> 'Change':
|
||||
commit = obj['current_revision']
|
||||
rev_obj = obj['revisions'][commit]
|
||||
commit_msg = rev_obj['commit']['message']
|
||||
|
||||
return cls(subject=obj['subject'],
|
||||
id=obj['id'],
|
||||
project=obj['project'],
|
||||
number=obj['_number'],
|
||||
commit_msg=commit_msg,
|
||||
updated=datetime.fromisoformat(obj['updated']))
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Comment:
|
||||
"""One comment from the change comments endpoint."""
|
||||
message: str
|
||||
updated: datetime
|
||||
|
||||
@classmethod
|
||||
def parse(cls, obj) -> 'Comment':
|
||||
return Comment(message=obj['message'],
|
||||
updated=datetime.fromisoformat(obj['updated']))
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MessageIsh:
|
||||
"""
|
||||
Something kind of like a comment or a commit message. Sufficiently vague
|
||||
that we can treat both the same.
|
||||
"""
|
||||
change_title: str
|
||||
project: str
|
||||
message: str
|
||||
backlink: str
|
||||
cl_number: int
|
||||
kind: Literal['comment'] | Literal['commit message']
|
||||
|
||||
|
||||
class API(metaclass=abc.ABCMeta):
|
||||
"""A Gerrit API, as used by gerrit-linkbot."""
|
||||
|
||||
def __init__(self, base: str):
|
||||
self.base = base
|
||||
|
||||
@abc.abstractmethod
|
||||
def changes(self, per_page=25) -> list[Change]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def change_comments(self, change_id: str) -> dict[str, list[Comment]]:
|
||||
pass
|
||||
|
||||
def change_link(self, change_id: str) -> str:
|
||||
"""Assumes that change_id is of a format project~id"""
|
||||
[project, id] = change_id.split('~')
|
||||
return self.base + f'/c/{project}/+/{id}'
|
||||
|
||||
|
||||
class ConcreteAPI(API):
|
||||
"""An actual non-fake Gerrit API."""
|
||||
|
||||
def __init__(self, base=GERRIT_BASE):
|
||||
super().__init__(base)
|
||||
|
||||
def get(self, endpoint: str, **kwargs):
|
||||
log.info('GET %s params=%s', endpoint, kwargs.get('params'))
|
||||
resp = http.session.get(self.base + endpoint,
|
||||
headers={'Accept': 'application/json'},
|
||||
**kwargs)
|
||||
resp.raise_for_status()
|
||||
return json.loads(resp.content.removeprefix(b")]}'"))
|
||||
|
||||
def changes(self, per_page=25) -> list[Change]:
|
||||
resp = self.get('/changes/',
|
||||
params={
|
||||
'n': per_page,
|
||||
'o': ['CURRENT_REVISION', 'CURRENT_COMMIT']
|
||||
})
|
||||
return [Change.parse(c) for c in resp]
|
||||
|
||||
def change_comments(self, change_id: str) -> dict[str, list[Comment]]:
|
||||
resp = self.get(f'/changes/{change_id}/comments')
|
||||
return {
|
||||
filename: [Comment.parse(obj) for obj in comments]
|
||||
for (filename, comments) in resp.items()
|
||||
}
|
||||
|
||||
|
||||
class FakeAPI(API):
|
||||
"""Fake Gerrit API that returns fake data."""
|
||||
|
||||
def __init__(self, fixtures: Traversable):
|
||||
super().__init__(base=GERRIT_BASE)
|
||||
self.fixtures = fixtures
|
||||
|
||||
def fixture(self, name: str):
|
||||
return json.loads(self.fixtures.joinpath(name).read_text())
|
||||
|
||||
def changes(self, per_page=25) -> list[Change]:
|
||||
return [Change.parse(c) for c in self.fixture('changes.json')]
|
||||
|
||||
def change_comments(self, change_id: str) -> dict[str, list[Comment]]:
|
||||
return {
|
||||
filename: [Comment.parse(obj) for obj in comments]
|
||||
for filename, comments in self.fixture(
|
||||
f'change-comments-{change_id}.json').items()
|
||||
}
|
||||
|
||||
|
||||
class GerritWatcher:
|
||||
"""Watches Gerrit changes and calls a callback on commit messages and all comments."""
|
||||
HandleComment = Callable[[MessageIsh], None]
|
||||
|
||||
def __init__(self, api: API, callback: HandleComment, per_page=25):
|
||||
self.api = api
|
||||
self.per_page = per_page
|
||||
self.already_processed = datetime(1970, 1, 1)
|
||||
self.callback = callback
|
||||
|
||||
def handle_change(self, change: Change):
|
||||
self.callback(
|
||||
MessageIsh(change_title=change.subject,
|
||||
project=change.project,
|
||||
message=change.commit_msg,
|
||||
backlink=self.api.change_link(change.id),
|
||||
cl_number=change.number,
|
||||
kind='commit message'))
|
||||
comments = self.api.change_comments(change.id)
|
||||
|
||||
for _, filecomments in comments.items():
|
||||
for comment in filecomments:
|
||||
self.callback(
|
||||
MessageIsh(change_title=change.subject,
|
||||
project=change.project,
|
||||
message=comment.message,
|
||||
backlink=self.api.change_link(change.id),
|
||||
cl_number=change.number,
|
||||
kind='comment'))
|
||||
|
||||
def poll(self):
|
||||
new_already_processed = self.already_processed
|
||||
|
||||
# ordered by time descending
|
||||
for change in self.api.changes(per_page=self.per_page):
|
||||
new_already_processed = max(change.updated, new_already_processed)
|
||||
|
||||
# got past the ones we have already seen, we're done
|
||||
if change.updated <= self.already_processed:
|
||||
break
|
||||
|
||||
self.handle_change(change)
|
||||
|
||||
self.already_processed = new_already_processed
|
4
gerrit_linkbot/http.py
Normal file
4
gerrit_linkbot/http.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
import requests
|
||||
|
||||
session = requests.Session()
|
||||
session.trust_env = False
|
121
gerrit_linkbot/test_gerrit_linkbot.py
Normal file
121
gerrit_linkbot/test_gerrit_linkbot.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
import gerrit_linkbot
|
||||
from gerrit_linkbot import RULES, CLMeta, CommentMeta, CLMention, IssueMatch, Service, gerrit, forgejo, testdata
|
||||
|
||||
|
||||
def test_roundtrips():
|
||||
cls = CommentMeta(cls=[
|
||||
CLMention('https://gerrit.lix.systems/c/lix/+/1312', 1312, 'comment')
|
||||
],
|
||||
cl_meta={1312: CLMeta('meow meow')})
|
||||
result = gerrit_linkbot.parse_comment(gerrit_linkbot.make_comment(cls))
|
||||
assert result
|
||||
assert result == cls
|
||||
|
||||
|
||||
def test_reasonable_comment():
|
||||
cls = CommentMeta(cls=[
|
||||
CLMention('https://gerrit.lix.systems/c/lix/+/1312', 1312, 'comment')
|
||||
],
|
||||
cl_meta={1312: CLMeta('meow meow')})
|
||||
assert gerrit_linkbot.make_comment(cls) == \
|
||||
'<!-- GERRIT_LINKBOT: {"cls": [{"backlink": "https://gerrit.lix.systems/c/lix/+/1312", "number": 1312, "kind": "comment"}], "cl_meta": {"1312": {"change_title": "meow meow"}}}\n' \
|
||||
'-->\n' \
|
||||
'\n' \
|
||||
'This issue was mentioned on Gerrit on the following CLs:\n' \
|
||||
'* comment in [cl/1312](https://gerrit.lix.systems/c/lix/+/1312) ("meow meow")\n'
|
||||
|
||||
|
||||
def test_posts_comment():
|
||||
gerrit_api = gerrit.FakeAPI(testdata)
|
||||
forgejo_api = forgejo.FakeAPI(testdata)
|
||||
service = Service(gerrit_api, forgejo_api)
|
||||
|
||||
service.step()
|
||||
assert forgejo_api.posted_comments == [
|
||||
forgejo.PostedComment(
|
||||
locator=forgejo.CommentLocator(
|
||||
owner='lix-project',
|
||||
repo='lix',
|
||||
issue_id=361,
|
||||
),
|
||||
comment_id=10000,
|
||||
comment='<!-- GERRIT_LINKBOT: {"cls": [{"backlink": '
|
||||
'"https://gerrit.lix.systems/c/lix/+/1367", "number": 1367, '
|
||||
'"kind": "commit message"}], "cl_meta": {"1367": '
|
||||
'{"change_title": "repl: implement tab completing :colon '
|
||||
'commands"}}}\n'
|
||||
'-->\n'
|
||||
'\n'
|
||||
'This issue was mentioned on Gerrit on the following CLs:\n'
|
||||
'* commit message in '
|
||||
'[cl/1367](https://gerrit.lix.systems/c/lix/+/1367) ("repl: '
|
||||
'implement tab completing :colon commands")\n',
|
||||
),
|
||||
forgejo.EditedComment(
|
||||
locator=forgejo.CommentLocator(
|
||||
owner='lix-project',
|
||||
repo='lix',
|
||||
issue_id=361,
|
||||
),
|
||||
comment='<!-- GERRIT_LINKBOT: {"cls": [{"backlink": '
|
||||
'"https://gerrit.lix.systems/c/lix/+/1367", "number": 1367, '
|
||||
'"kind": "commit message"}, {"backlink": '
|
||||
'"https://gerrit.lix.systems/c/lix/+/1367", "number": 1367, '
|
||||
'"kind": "comment"}], "cl_meta": {"1367": {"change_title": '
|
||||
'"repl: implement tab completing :colon commands"}}}\n'
|
||||
'-->\n'
|
||||
'\n'
|
||||
'This issue was mentioned on Gerrit on the following CLs:\n'
|
||||
'* commit message in '
|
||||
'[cl/1367](https://gerrit.lix.systems/c/lix/+/1367) ("repl: '
|
||||
'implement tab completing :colon commands")\n'
|
||||
'* comment in '
|
||||
'[cl/1367](https://gerrit.lix.systems/c/lix/+/1367) ("repl: '
|
||||
'implement tab completing :colon commands")\n',
|
||||
edit_comment_id=10000,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_rules():
|
||||
|
||||
def flatten(xss):
|
||||
return [x for xs in xss for x in xs]
|
||||
|
||||
def apply_rules(text: str):
|
||||
return [x for x in flatten(rule.apply(text) for rule in RULES) if x]
|
||||
|
||||
assert apply_rules('meow #12 43') == [
|
||||
IssueMatch(
|
||||
issue_num=12,
|
||||
project_name=None,
|
||||
),
|
||||
]
|
||||
|
||||
assert apply_rules(
|
||||
'meow a#123 #123 #345 https://git.lix.systems/meow/nya/issues/86 https://git.lix.systems/lix-project/meows/issues/98'
|
||||
) == [
|
||||
IssueMatch(
|
||||
issue_num=123,
|
||||
project_name=None,
|
||||
),
|
||||
IssueMatch(
|
||||
issue_num=345,
|
||||
project_name=None,
|
||||
),
|
||||
IssueMatch(
|
||||
issue_num=98,
|
||||
project_name='meows',
|
||||
),
|
||||
]
|
||||
|
||||
assert apply_rules('lix#123 fj#583 lix#1337gj') == [
|
||||
IssueMatch(
|
||||
issue_num=123,
|
||||
project_name=None,
|
||||
),
|
||||
IssueMatch(
|
||||
issue_num=583,
|
||||
project_name=None,
|
||||
)
|
||||
]
|
20
gerrit_linkbot/testdata/change-comments-lix~1218.json
vendored
Normal file
20
gerrit_linkbot/testdata/change-comments-lix~1218.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"/PATCHSET_LEVEL": [
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"display_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "3e509ef7892725ab6a1d7d7517b18f03dd38165a",
|
||||
"unresolved": false,
|
||||
"patch_set": 1,
|
||||
"id": "48b11305_6b68a279",
|
||||
"updated": "2024-05-28 00:48:49.000000000",
|
||||
"message": "meow",
|
||||
"commit_id": "179d058214c85dd3c66eabaa102cdda1a5984e77"
|
||||
}
|
||||
]
|
||||
}
|
72
gerrit_linkbot/testdata/change-comments-lix~1367.json
vendored
Normal file
72
gerrit_linkbot/testdata/change-comments-lix~1367.json
vendored
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"src/libcmd/repl.cc": [
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "c2dca70404921f9d489a00c0724beb7e04a3b3e2",
|
||||
"unresolved": true,
|
||||
"patch_set": 3,
|
||||
"id": "63d6c6d7_cf08ec89",
|
||||
"line": 375,
|
||||
"updated": "2024-06-01 01:30:45.000000000",
|
||||
"message": "meow fj#361 filing https://git.lix.systems/lix-project/lix/issues/361",
|
||||
"commit_id": "8ab914d17ef000d374bf611e5152714bab60609e"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"display_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "24c98ffcb0815f72f625925f88d41ebf9b1c98e4",
|
||||
"unresolved": false,
|
||||
"patch_set": 3,
|
||||
"id": "c0b833ef_cd81a56e",
|
||||
"line": 375,
|
||||
"in_reply_to": "63d6c6d7_cf08ec89",
|
||||
"updated": "2024-06-01 21:56:38.000000000",
|
||||
"message": "Done",
|
||||
"commit_id": "8ab914d17ef000d374bf611e5152714bab60609e"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "c2dca70404921f9d489a00c0724beb7e04a3b3e2",
|
||||
"unresolved": true,
|
||||
"patch_set": 3,
|
||||
"id": "3a4b9716_91cddb9e",
|
||||
"line": 382,
|
||||
"updated": "2024-06-01 01:30:45.000000000",
|
||||
"message": "likewise",
|
||||
"commit_id": "8ab914d17ef000d374bf611e5152714bab60609e"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"display_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "24c98ffcb0815f72f625925f88d41ebf9b1c98e4",
|
||||
"unresolved": false,
|
||||
"patch_set": 3,
|
||||
"id": "d6459c1f_c2933d52",
|
||||
"line": 382,
|
||||
"in_reply_to": "34b31771_6369334e",
|
||||
"updated": "2024-06-01 21:56:38.000000000",
|
||||
"message": "Done",
|
||||
"commit_id": "8ab914d17ef000d374bf611e5152714bab60609e"
|
||||
}
|
||||
]
|
||||
}
|
20
gerrit_linkbot/testdata/change-comments-lix~1369.json
vendored
Normal file
20
gerrit_linkbot/testdata/change-comments-lix~1369.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"/PATCHSET_LEVEL": [
|
||||
{
|
||||
"author": {
|
||||
"_account_id": 1000000,
|
||||
"name": "REDACTED",
|
||||
"display_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"change_message_id": "23d39257c72487af7423a23c900733daabbe8956",
|
||||
"unresolved": false,
|
||||
"patch_set": 1,
|
||||
"id": "9d060685_23a11e7a",
|
||||
"updated": "2024-06-03 01:41:54.000000000",
|
||||
"message": "meow meow",
|
||||
"commit_id": "80bf7ab4a37efb61fd8a943988a086f52237fea3"
|
||||
}
|
||||
]
|
||||
}
|
374
gerrit_linkbot/testdata/changes.json
vendored
Normal file
374
gerrit_linkbot/testdata/changes.json
vendored
Normal file
|
@ -0,0 +1,374 @@
|
|||
[
|
||||
{
|
||||
"id": "lix~1369",
|
||||
"triplet_id": "lix~main~Idd8afe62f8591b5d8b70e258c5cefa09be4cab03",
|
||||
"project": "lix",
|
||||
"branch": "main",
|
||||
"attention_set": {
|
||||
"1000078": {
|
||||
"account": {
|
||||
"_account_id": 1000078
|
||||
},
|
||||
"last_update": "2024-06-01 14:38:38.000000000",
|
||||
"reason": "A robot voted negatively on a label"
|
||||
}
|
||||
},
|
||||
"removed_from_attention_set": {},
|
||||
"hashtags": [],
|
||||
"change_id": "Idd8afe62f8591b5d8b70e258c5cefa09be4cab03",
|
||||
"subject": "libmain: add progress bar with multiple status lines",
|
||||
"status": "NEW",
|
||||
"created": "2024-06-01 14:10:06.000000000",
|
||||
"updated": "2024-06-03 01:41:54.000000000",
|
||||
"submit_type": "MERGE_IF_NECESSARY",
|
||||
"mergeable": true,
|
||||
"insertions": 92,
|
||||
"deletions": 30,
|
||||
"total_comment_count": 1,
|
||||
"unresolved_comment_count": 0,
|
||||
"has_review_started": true,
|
||||
"meta_rev_id": "23d39257c72487af7423a23c900733daabbe8956",
|
||||
"_number": 1369,
|
||||
"_virtual_id_number": 1369,
|
||||
"owner": {
|
||||
"_account_id": 1000078
|
||||
},
|
||||
"current_revision": "80bf7ab4a37efb61fd8a943988a086f52237fea3",
|
||||
"revisions": {
|
||||
"80bf7ab4a37efb61fd8a943988a086f52237fea3": {
|
||||
"kind": "REWORK",
|
||||
"_number": 1,
|
||||
"created": "2024-06-01 14:10:06.000000000",
|
||||
"uploader": {
|
||||
"_account_id": 1000078
|
||||
},
|
||||
"ref": "refs/changes/69/1369/1",
|
||||
"fetch": {
|
||||
"anonymous http": {
|
||||
"url": "https://gerrit.lix.systems/lix",
|
||||
"ref": "refs/changes/69/1369/1"
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"parents": [
|
||||
{
|
||||
"commit": "5312e60be6aba84fab91e6d82af0b1aeccdfe981",
|
||||
"subject": "Merge \"libfetchers: allow fetching gitlab refs with >1 commit\" into main"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-06-01 14:06:26.000000000",
|
||||
"tz": 120
|
||||
},
|
||||
"committer": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-06-01 14:10:00.000000000",
|
||||
"tz": 120
|
||||
},
|
||||
"subject": "libmain: add progress bar with multiple status lines",
|
||||
"message": "libmain: add progress bar with multiple status lines\n\nAdd the log-formats `multiline` and `multiline-with-logs` which offer\nmultiple current active building status lines.\n\nChange-Id: Idd8afe62f8591b5d8b70e258c5cefa09be4cab03\n"
|
||||
},
|
||||
"branch": "refs/heads/main"
|
||||
}
|
||||
},
|
||||
"requirements": [],
|
||||
"submit_records": [
|
||||
{
|
||||
"rule_name": "gerrit~DefaultSubmitRule",
|
||||
"status": "NOT_READY",
|
||||
"labels": [
|
||||
{
|
||||
"label": "Code-Review",
|
||||
"status": "NEED"
|
||||
},
|
||||
{
|
||||
"label": "Verified",
|
||||
"status": "MAY",
|
||||
"applied_by": {
|
||||
"_account_id": 1000008
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Has-Tests",
|
||||
"status": "MAY"
|
||||
},
|
||||
{
|
||||
"label": "Has-Release-Notes",
|
||||
"status": "MAY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lix~1367",
|
||||
"triplet_id": "lix~main~Id159d209c537443ef5e37a975982e8e12ce1f486",
|
||||
"project": "lix",
|
||||
"branch": "main",
|
||||
"attention_set": {
|
||||
"1000003": {
|
||||
"account": {
|
||||
"_account_id": 1000003
|
||||
},
|
||||
"last_update": "2024-06-01 21:21:17.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000001> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
},
|
||||
"1000077": {
|
||||
"account": {
|
||||
"_account_id": 1000077
|
||||
},
|
||||
"last_update": "2024-06-02 17:48:45.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000001> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
}
|
||||
},
|
||||
"removed_from_attention_set": {
|
||||
"1000001": {
|
||||
"account": {
|
||||
"_account_id": 1000001
|
||||
},
|
||||
"last_update": "2024-06-02 22:21:46.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000001> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
}
|
||||
},
|
||||
"hashtags": [],
|
||||
"change_id": "Id159d209c537443ef5e37a975982e8e12ce1f486",
|
||||
"subject": "repl: implement tab completing :colon commands",
|
||||
"status": "NEW",
|
||||
"created": "2024-05-31 23:40:53.000000000",
|
||||
"updated": "2024-06-02 22:44:08.000000000",
|
||||
"submit_type": "MERGE_IF_NECESSARY",
|
||||
"mergeable": true,
|
||||
"insertions": 71,
|
||||
"deletions": 0,
|
||||
"total_comment_count": 8,
|
||||
"unresolved_comment_count": 0,
|
||||
"has_review_started": true,
|
||||
"meta_rev_id": "0b1c27db8f8854cd4bf0bc17db3ec6ee9ff07935",
|
||||
"_number": 1367,
|
||||
"_virtual_id_number": 1367,
|
||||
"owner": {
|
||||
"_account_id": 1000001
|
||||
},
|
||||
"current_revision": "5c424d24893e7f5270195074871b842e7ac7c057",
|
||||
"revisions": {
|
||||
"5c424d24893e7f5270195074871b842e7ac7c057": {
|
||||
"kind": "TRIVIAL_REBASE",
|
||||
"_number": 7,
|
||||
"created": "2024-06-02 22:16:07.000000000",
|
||||
"uploader": {
|
||||
"_account_id": 1000001
|
||||
},
|
||||
"ref": "refs/changes/67/1367/7",
|
||||
"fetch": {
|
||||
"anonymous http": {
|
||||
"url": "https://gerrit.lix.systems/lix",
|
||||
"ref": "refs/changes/67/1367/7"
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"parents": [
|
||||
{
|
||||
"commit": "c55e93ca23eae608f2ff80e940f35f5daa6b4f7f",
|
||||
"subject": "Revert \"nix3: always use the same verbosity default (info)\""
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-06-01 00:29:10.000000000",
|
||||
"tz": -360
|
||||
},
|
||||
"committer": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-06-02 22:15:57.000000000",
|
||||
"tz": -360
|
||||
},
|
||||
"subject": "repl: implement tab completing :colon commands",
|
||||
"message": "repl: implement tab completing :colon commands\n\nThis uses a minor hack in which we check the rl_line_buffer global\nvariable to workaround editline not including the colon in its\ncompletion callback.\n\nFixes #361\n\nChange-Id: Id159d209c537443ef5e37a975982e8e12ce1f486\n"
|
||||
},
|
||||
"branch": "refs/heads/main"
|
||||
}
|
||||
},
|
||||
"requirements": [],
|
||||
"submit_records": [
|
||||
{
|
||||
"rule_name": "gerrit~DefaultSubmitRule",
|
||||
"status": "NOT_READY",
|
||||
"labels": [
|
||||
{
|
||||
"label": "Code-Review",
|
||||
"status": "NEED"
|
||||
},
|
||||
{
|
||||
"label": "Verified",
|
||||
"status": "MAY",
|
||||
"applied_by": {
|
||||
"_account_id": 1000008
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Has-Tests",
|
||||
"status": "MAY"
|
||||
},
|
||||
{
|
||||
"label": "Has-Release-Notes",
|
||||
"status": "MAY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lix~1218",
|
||||
"triplet_id": "lix~main~Ibe51416d9c7a6dd635c2282990224861adf1ceab",
|
||||
"project": "lix",
|
||||
"branch": "main",
|
||||
"attention_set": {
|
||||
"1000061": {
|
||||
"account": {
|
||||
"_account_id": 1000061
|
||||
},
|
||||
"last_update": "2024-05-28 00:48:49.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000001> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
}
|
||||
},
|
||||
"removed_from_attention_set": {
|
||||
"1000003": {
|
||||
"account": {
|
||||
"_account_id": 1000003
|
||||
},
|
||||
"last_update": "2024-05-28 03:05:15.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000003> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000003
|
||||
}
|
||||
},
|
||||
"1000005": {
|
||||
"account": {
|
||||
"_account_id": 1000005
|
||||
},
|
||||
"last_update": "2024-05-28 12:18:47.000000000",
|
||||
"reason": "<GERRIT_ACCOUNT_1000005> replied on the change",
|
||||
"reason_account": {
|
||||
"_account_id": 1000005
|
||||
}
|
||||
}
|
||||
},
|
||||
"hashtags": [
|
||||
"deferred-2.91"
|
||||
],
|
||||
"change_id": "Ibe51416d9c7a6dd635c2282990224861adf1ceab",
|
||||
"subject": "libstore/build: always treat seccomp setup failures as fatal",
|
||||
"status": "NEW",
|
||||
"created": "2024-05-27 15:17:55.000000000",
|
||||
"updated": "2024-05-30 03:25:24.000000000",
|
||||
"submit_type": "MERGE_IF_NECESSARY",
|
||||
"mergeable": true,
|
||||
"insertions": 1,
|
||||
"deletions": 5,
|
||||
"total_comment_count": 3,
|
||||
"unresolved_comment_count": 0,
|
||||
"has_review_started": true,
|
||||
"meta_rev_id": "22b50cc2278823a37a81cc3b7a9a6aae2d893169",
|
||||
"_number": 1218,
|
||||
"_virtual_id_number": 1218,
|
||||
"owner": {
|
||||
"_account_id": 1000061
|
||||
},
|
||||
"current_revision": "080aa0be276d850ae19148b9e33b4e5620a87b56",
|
||||
"revisions": {
|
||||
"080aa0be276d850ae19148b9e33b4e5620a87b56": {
|
||||
"kind": "TRIVIAL_REBASE",
|
||||
"_number": 2,
|
||||
"created": "2024-05-30 02:51:46.000000000",
|
||||
"uploader": {
|
||||
"_account_id": 1000061
|
||||
},
|
||||
"real_uploader": {
|
||||
"_account_id": 1000003
|
||||
},
|
||||
"ref": "refs/changes/18/1218/2",
|
||||
"fetch": {
|
||||
"anonymous http": {
|
||||
"url": "https://gerrit.lix.systems/lix",
|
||||
"ref": "refs/changes/18/1218/2"
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"parents": [
|
||||
{
|
||||
"commit": "218630a241d71ea5c136f72e1aaaf2299df6a0b1",
|
||||
"subject": "Merge \"docs: enable non-default TOC folding\" into main"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-05-27 15:05:44.000000000",
|
||||
"tz": 120
|
||||
},
|
||||
"committer": {
|
||||
"name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"date": "2024-05-30 02:51:46.000000000",
|
||||
"tz": 0
|
||||
},
|
||||
"subject": "libstore/build: always treat seccomp setup failures as fatal",
|
||||
"message": "libstore/build: always treat seccomp setup failures as fatal\n\nIn f047e4357b4f7ad66c2e476506bf35cab82e441e, I missed the behavior that if\nbuilding without a dedicated build user (i.e. in single-user setups), seccomp\nsetup failures are silently ignored. This was introduced without explanation 7\nyears ago (ff6becafa8efc2f7e6f2b9b889ba4adf20b8d524). Hopefully the only\nuse-case nowadays is causing spurious test suite successes when messing up the\nseccomp filter during development. Let's try removing it.\n\nChange-Id: Ibe51416d9c7a6dd635c2282990224861adf1ceab\n"
|
||||
},
|
||||
"branch": "refs/heads/main",
|
||||
"description": "Rebase"
|
||||
}
|
||||
},
|
||||
"requirements": [],
|
||||
"submit_records": [
|
||||
{
|
||||
"rule_name": "gerrit~DefaultSubmitRule",
|
||||
"status": "NOT_READY",
|
||||
"labels": [
|
||||
{
|
||||
"label": "Code-Review",
|
||||
"status": "NEED"
|
||||
},
|
||||
{
|
||||
"label": "Verified",
|
||||
"status": "MAY",
|
||||
"applied_by": {
|
||||
"_account_id": 1000008
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Has-Tests",
|
||||
"status": "MAY",
|
||||
"applied_by": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Has-Release-Notes",
|
||||
"status": "MAY",
|
||||
"applied_by": {
|
||||
"_account_id": 1000001
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
111
gerrit_linkbot/testdata/issue-comments-lix-project-lix-361.json
vendored
Normal file
111
gerrit_linkbot/testdata/issue-comments-lix-project-lix-361.json
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
[
|
||||
{
|
||||
"id": 4007,
|
||||
"html_url": "https://git.lix.systems/lix-project/lix/issues/361#issuecomment-4007",
|
||||
"pull_request_url": "",
|
||||
"issue_url": "https://git.lix.systems/lix-project/lix/issues/361",
|
||||
"user": {
|
||||
"id": 3,
|
||||
"login": "REDACTED",
|
||||
"login_name": "",
|
||||
"full_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"avatar_url": "REDACTED",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2024-02-29T19:40:48Z",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "",
|
||||
"pronouns": "REDACTED",
|
||||
"website": "",
|
||||
"description": "",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 1,
|
||||
"starred_repos_count": 1,
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"original_author": "",
|
||||
"original_author_id": 0,
|
||||
"body": "This shouldn't be too hard. The `:` commands currently are hardcoded (lol) in `src/libcmd/repl.cc` in `NixRepl::processLine()`. We just need to shove the list of possible colon commands in `NixRepl::completePrefix()` in `src/libcmd/repl.cc` behind a `prefix.starts_with(\":\")`\r\n\r\nWe are currently working on https://git.lix.systems/lix-project/lix/issues/306, but can tackle this after because this should really be a thing",
|
||||
"assets": [],
|
||||
"created_at": "2024-05-30T18:45:54Z",
|
||||
"updated_at": "2024-05-30T18:45:54Z"
|
||||
},
|
||||
{
|
||||
"id": 4010,
|
||||
"html_url": "https://git.lix.systems/lix-project/lix/issues/361#issuecomment-4010",
|
||||
"pull_request_url": "",
|
||||
"issue_url": "https://git.lix.systems/lix-project/lix/issues/361",
|
||||
"user": {
|
||||
"id": 9,
|
||||
"login": "REDACTED",
|
||||
"login_name": "",
|
||||
"full_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"avatar_url": "REDACTED",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2024-03-02T00:27:52Z",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "",
|
||||
"pronouns": "",
|
||||
"website": "REDACTED",
|
||||
"description": "",
|
||||
"visibility": "limited",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 1,
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"original_author": "",
|
||||
"original_author_id": 0,
|
||||
"body": "we would love if there were a registry of repl commands, which would make this easier. ",
|
||||
"assets": [],
|
||||
"created_at": "2024-05-30T19:31:49Z",
|
||||
"updated_at": "2024-05-30T19:31:49Z"
|
||||
},
|
||||
{
|
||||
"id": 4045,
|
||||
"html_url": "https://git.lix.systems/lix-project/lix/issues/361#issuecomment-4045",
|
||||
"pull_request_url": "",
|
||||
"issue_url": "https://git.lix.systems/lix-project/lix/issues/361",
|
||||
"user": {
|
||||
"id": 3,
|
||||
"login": "REDACTED",
|
||||
"login_name": "",
|
||||
"full_name": "REDACTED",
|
||||
"email": "REDACTED",
|
||||
"avatar_url": "REDACTED",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2024-02-29T19:40:48Z",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "",
|
||||
"pronouns": "she/her",
|
||||
"website": "",
|
||||
"description": "",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 1,
|
||||
"starred_repos_count": 1,
|
||||
"username": "REDACTED"
|
||||
},
|
||||
"original_author": "",
|
||||
"original_author_id": 0,
|
||||
"body": "https://gerrit.lix.systems/c/lix/+/1367",
|
||||
"assets": [],
|
||||
"created_at": "2024-06-01T00:24:05Z",
|
||||
"updated_at": "2024-06-01T00:24:05Z"
|
||||
}
|
||||
]
|
||||
|
84
module.nix
Normal file
84
module.nix
Normal file
|
@ -0,0 +1,84 @@
|
|||
{ pkgs, config, lib, ... }:
|
||||
let
|
||||
package = pkgs.callPackage ./package.nix { };
|
||||
cfg = config.services.gerrit-linkbot;
|
||||
|
||||
inherit (lib) types;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.gerrit-linkbot = {
|
||||
enable = lib.mkEnableOption "gerrit link bot";
|
||||
# n.b. is not a mkPackageOption because we don't shove it into nixpkgs
|
||||
# via overlay.
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
description = "Package for gerrit-linkbot";
|
||||
default = package;
|
||||
};
|
||||
extraFlags = lib.mkOption {
|
||||
type = types.listOf types.str;
|
||||
};
|
||||
environmentFile = lib.mkOption {
|
||||
type = types.nullOr types.path;
|
||||
description = ''
|
||||
Environment file for gerrit-linkbot. Requires FORGEJO_API_KEY set.
|
||||
Use agenix or similar for this.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
systemd.services.gerrit-linkbot =
|
||||
let
|
||||
serviceDeps = lib.optionals config.services.gerrit.enable [ "gerrit.service" ]
|
||||
++ lib.optionals config.services.forgejo.enable [ "forgejo.service" ] ++
|
||||
[ "network-online.target" ];
|
||||
in
|
||||
{
|
||||
after = serviceDeps;
|
||||
wants = serviceDeps;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
RestartSteps = 10;
|
||||
RestartMaxDelaySec = "600s";
|
||||
|
||||
DynamicUser = true;
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
PrivateDevices = true;
|
||||
ProtectHome = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProcSubset = "pid";
|
||||
UMask = "0077";
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHostname = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RemoveIPC = true;
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" ];
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallArchitectures = "native";
|
||||
|
||||
EnvironmentFile = cfg.environmentFile;
|
||||
|
||||
ExecStart = "${lib.getExe cfg.package}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
35
package.nix
Normal file
35
package.nix
Normal file
|
@ -0,0 +1,35 @@
|
|||
{ lib, python3Packages }:
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = "gerrit-linkbot";
|
||||
version = "0.0.0";
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./gerrit_linkbot
|
||||
./pyproject.toml
|
||||
./setup.py
|
||||
];
|
||||
};
|
||||
|
||||
nativeCheckInputs = [
|
||||
python3Packages.pytestCheckHook
|
||||
];
|
||||
|
||||
build-system = with python3Packages; [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = with python3Packages; [
|
||||
requests
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Gerrit Forgejo backlink bot";
|
||||
homepage = "https://git.lix.systems/lix-project/web-services";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ lf- ];
|
||||
platforms = lib.platforms.unix;
|
||||
mainProgram = "gerrit-linkbot";
|
||||
};
|
||||
}
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[project]
|
||||
name = "gerrit-linkbot"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"requests",
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
gerrit-linkbot = "gerrit_linkbot.__main__:main"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["gerrit_linkbot", "gerrit_linkbot.testdata"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
gerrit_linkbot = ["testdata/*"]
|
Loading…
Reference in a new issue