From be5d50d8d8938153c537b5a60b1d3d7c8dfac1a4 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Sun, 28 Nov 2021 21:07:37 +0100 Subject: [PATCH] webhooks: update todo tickets with git commits Allow referencing tracker tickets in git commit messages via specific trailer words: Fixes: Implements: References: 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 --- hubsrht/blueprints/webhooks.py | 58 +++++++++++++++++++++++++++++++++- hubsrht/services.py | 39 +++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/hubsrht/blueprints/webhooks.py b/hubsrht/blueprints/webhooks.py index 2b655e2..179c530 100644 --- a/hubsrht/blueprints/webhooks.py +++ b/hubsrht/blueprints/webhooks.py @@ -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~[a-z_][a-z0-9_-]+) + /(?P[\w.-]+) + /(?P\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"{commit_author} referenced this ticket in commit " + + f"{commit_sha}.") + 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/", methods=["POST"]) def hg_user(user_id): diff --git a/hubsrht/services.py b/hubsrht/services.py index 74042af..19178db 100644 --- a/hubsrht/services.py +++ b/hubsrht/services.py @@ -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", {