mirror of https://git.sr.ht/~sircmpwn/hub.sr.ht
webhooks: update todo tickets with git commits
Allow referencing tracker tickets in git commit messages via specific trailer words: Fixes: <ticket url> Implements: <ticket url> References: <ticket url> These must follow standard git trailer syntax. The trailers are extracted from commit messages with the function added in previous commit. The ticket url must point to a valid ticket. A comment will be inserted in the ticket with a back reference to the git commit and its original author. The comment will be made by the user who pushed the commit. E.g.: ~arkanoid REPORTED -> FIXED 9 seconds ago John Doe referenced this ticket in commit b4dc4c40. Open tickets referenced by a Fixes trailer will be resolved with the FIXED resolution. Open tickets referenced by an Implements trailer will be resolved with the IMPLEMENTED resolution. Caveats: * Only the 25 most recent commit messages will be considered when pushing long series. This should be a fairly sane limitation. * If the user pushing commits does not have triage/comment permissions on the bug tracker, nothing will happen. * Invalid/non-existent ticket urls are ignored. * When a git repository is part of more than one project, the webhook will run once per project and update the same ticket(s) once per project as well. * If an already resolved ticket is referenced by a Fixes or Implements trailer, only a comment will be added. Link: https://git-scm.com/docs/git-interpret-trailers Implements: https://todo.sr.ht/~sircmpwn/hub.sr.ht/55 Signed-off-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
a9f72b5a59
commit
be5d50d8d8
|
@ -1,10 +1,12 @@
|
|||
import email
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, current_app
|
||||
from hubsrht.builds import submit_patchset
|
||||
from hubsrht.services import todo, lists
|
||||
from hubsrht.services import git, todo, lists
|
||||
from hubsrht.trailers import commit_trailers
|
||||
from hubsrht.types import Event, EventType, MailingList, SourceRepo, RepoType
|
||||
from hubsrht.types import Tracker, User, Visibility
|
||||
from srht.config import get_origin
|
||||
|
@ -99,10 +101,64 @@ def git_repo(repo_id):
|
|||
repo.project.updated = datetime.utcnow()
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
for ref in payload["refs"]:
|
||||
old = (ref["old"] or {}).get("id")
|
||||
new = (ref["new"] or {}).get("id")
|
||||
for commit in reversed(git.log(pusher, repo, old, new)):
|
||||
for trailer, value in commit_trailers(commit["message"]):
|
||||
_handle_commit_trailer(trailer, value, pusher, repo, commit)
|
||||
|
||||
return "Thanks!"
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
_ticket_url_re = re.compile(
|
||||
rf"""
|
||||
^
|
||||
{re.escape(_todosrht)}
|
||||
/(?P<owner>~[a-z_][a-z0-9_-]+)
|
||||
/(?P<tracker>[\w.-]+)
|
||||
/(?P<ticket>\d+)
|
||||
$
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
def _handle_commit_trailer(trailer, value, pusher, repo, commit):
|
||||
if trailer == "Fixes":
|
||||
resolution = "fixed"
|
||||
elif trailer == "Implements":
|
||||
resolution = "implemented"
|
||||
elif trailer == "References":
|
||||
resolution = None
|
||||
else:
|
||||
return
|
||||
|
||||
match = _ticket_url_re.match(value.strip())
|
||||
if not match:
|
||||
return
|
||||
|
||||
commit_message = html.escape(commit["message"].split("\n")[0])
|
||||
commit_author = html.escape(commit["author"]["name"].strip())
|
||||
commit_sha = commit["id"][:7]
|
||||
commit_url = repo.url() + f"/commit/{commit_sha}"
|
||||
comment = (
|
||||
f"<i>{commit_author} referenced this ticket in commit " +
|
||||
f"<a href='{commit_url}' title='{commit_message}'>{commit_sha}</a>.</i>")
|
||||
try:
|
||||
todo.update_ticket(
|
||||
user=pusher,
|
||||
owner=match["owner"],
|
||||
tracker=match["tracker"],
|
||||
ticket=int(match["ticket"]),
|
||||
comment=" ".join(comment.split()).strip(),
|
||||
resolution=resolution,
|
||||
)
|
||||
except Exception:
|
||||
# invalid ticket or pusher does not have triage access, ignore
|
||||
pass
|
||||
|
||||
@csrf_bypass
|
||||
@webhooks.route("/webhooks/hg-user/<int:user_id>", methods=["POST"])
|
||||
def hg_user(user_id):
|
||||
|
|
|
@ -179,6 +179,37 @@ class GitService(SrhtService):
|
|||
return None
|
||||
return manifests
|
||||
|
||||
def log(self, user, repo, old, new):
|
||||
query = """
|
||||
query Log($owner: String!, $repo: String!, $from: String!) {
|
||||
repositoryByOwner(owner: $owner, repo: $repo) {
|
||||
log(from: $from) {
|
||||
results {
|
||||
id
|
||||
message
|
||||
author {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
r = self.post(user, None, f"{_gitsrht}/query", {
|
||||
"query": query,
|
||||
"variables": {
|
||||
"owner": repo.owner.canonical_name,
|
||||
"repo": repo.name,
|
||||
"from": new,
|
||||
}
|
||||
})
|
||||
commits = []
|
||||
for c in r["data"]["repositoryByOwner"]["log"]["results"]:
|
||||
if c["id"] == old:
|
||||
break
|
||||
commits.append(c)
|
||||
return commits
|
||||
|
||||
def create_repo(self, user, valid, visibility):
|
||||
query = """
|
||||
mutation CreateRepo(
|
||||
|
@ -452,6 +483,14 @@ class TodoService(SrhtService):
|
|||
except:
|
||||
pass # nbd, upstream was presumably deleted
|
||||
|
||||
def update_ticket(self, user, owner, tracker, ticket, comment, resolution=None):
|
||||
url = f"{_todosrht}/api/user/{owner}/trackers/{tracker}/tickets/{ticket}"
|
||||
payload = {"comment": comment}
|
||||
if resolution is not None:
|
||||
payload["resolution"] = resolution
|
||||
payload["status"] = "resolved"
|
||||
self.put(user, None, url, payload)
|
||||
|
||||
class BuildService(SrhtService):
|
||||
def submit_build(self, user, manifest, note, tags, execute=True):
|
||||
return self.post(user, None, f"{_buildsrht}/api/jobs", {
|
||||
|
|
Loading…
Reference in New Issue