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 email
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Blueprint, request, current_app
|
from flask import Blueprint, request, current_app
|
||||||
from hubsrht.builds import submit_patchset
|
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 Event, EventType, MailingList, SourceRepo, RepoType
|
||||||
from hubsrht.types import Tracker, User, Visibility
|
from hubsrht.types import Tracker, User, Visibility
|
||||||
from srht.config import get_origin
|
from srht.config import get_origin
|
||||||
|
@ -99,10 +101,64 @@ def git_repo(repo_id):
|
||||||
repo.project.updated = datetime.utcnow()
|
repo.project.updated = datetime.utcnow()
|
||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
db.session.commit()
|
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!"
|
return "Thanks!"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError()
|
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
|
@csrf_bypass
|
||||||
@webhooks.route("/webhooks/hg-user/<int:user_id>", methods=["POST"])
|
@webhooks.route("/webhooks/hg-user/<int:user_id>", methods=["POST"])
|
||||||
def hg_user(user_id):
|
def hg_user(user_id):
|
||||||
|
|
|
@ -179,6 +179,37 @@ class GitService(SrhtService):
|
||||||
return None
|
return None
|
||||||
return manifests
|
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):
|
def create_repo(self, user, valid, visibility):
|
||||||
query = """
|
query = """
|
||||||
mutation CreateRepo(
|
mutation CreateRepo(
|
||||||
|
@ -452,6 +483,14 @@ class TodoService(SrhtService):
|
||||||
except:
|
except:
|
||||||
pass # nbd, upstream was presumably deleted
|
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):
|
class BuildService(SrhtService):
|
||||||
def submit_build(self, user, manifest, note, tags, execute=True):
|
def submit_build(self, user, manifest, note, tags, execute=True):
|
||||||
return self.post(user, None, f"{_buildsrht}/api/jobs", {
|
return self.post(user, None, f"{_buildsrht}/api/jobs", {
|
||||||
|
|
Loading…
Reference in New Issue