Skip to content

Commit

Permalink
commit parsing and issue label modification support
Browse files Browse the repository at this point in the history
  • Loading branch information
ahebrank committed Oct 8, 2017
1 parent c87386e commit cecb9a5
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,2 +1,3 @@
repos.json
test.py
*.pyc
10 changes: 6 additions & 4 deletions README.md
Expand Up @@ -41,17 +41,19 @@ Create a JSON config file (e.g., `repos.json`) to configure repositories. Each r
"user_notify": [
"\\*\\*Sender\\*\\*\\: ([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+)",
"@gitlabuser"
]
],
"labels": []
}
}
}

```

This example handles two types of webhooks.
This example handles several types of webhooks.

1. After a push event to the repo `master` branch, it executes a couple of command in the local shell to pull in the master HEAD. The special branch key `other` will run if the pushed branch is not otherwise matched to a key in the `push` hash.
2. After a new issue event, the handler runs a regex match on the issue body and adds an issue comment to @mention the user by email.
1. `push`: After a push event to the repo `master` branch, it executes a couple of command in the local shell to pull in the master HEAD. The special branch key `other` will run if the pushed branch is not otherwise matched to a key in the `push` hash.
2. `issue.user_notify`: After a new issue event, the handler runs a regex match on the issue body and adds an issue comment to @mention the user by email.
3. `issue.labels`: Parse the commit message to handle adding or removing labels based on commit messages. For instance, "address #53, #72: carousel accessibility fixes; -browser compat, +accessibility, ~Pending" will add an "accessibility" label (if it already exists for the project) to issues 53 and 72, remove label "browser compat", and add label "Pending" (presumably a list label) while removing other list labels (effectively moving an issue on the Gitlab boards kanban).

The issue hook uses a Gitlab private token to run API commands to lookup a username by email and add an issue comment. The push hook does not require the private token because it does not use the Gitlab API.

Expand Down
33 changes: 30 additions & 3 deletions gitlab_api.py
@@ -1,7 +1,6 @@
from urlparse import urlparse
import requests
import json

class GitlabApi:
base_url = None
token = None
Expand All @@ -23,6 +22,10 @@ def post(self, endpoint, data):
r = requests.post(self.get_url(endpoint), data=data)
return r.ok

def put(self, endpoint, data):
r = requests.put(self.get_url(endpoint), data=data)
return r.ok

def get(self, endpoint):
r = requests.get(self.get_url(endpoint))
return json.loads(r.text)
Expand All @@ -36,7 +39,31 @@ def lookup_username(self, email):
def comment_on_issue(self, project_id, issue_id, comment):
data = {
'id': project_id,
'issue_id': issue_id,
'issue_iid': issue_id,
'body': comment
}
return self.post("/projects/{id}/issues/{issue_id}/notes".format(**data), data)
return self.post("/projects/{id}/issues/{issue_iid}/notes".format(**data), data)

def set_issue_labels(self, project_id, issue_id, labels):
data = {
'id': project_id,
'issue_iid': issue_id,
'labels': ','.join(labels)
}
return self.put("/projects/{id}/issues/{issue_iid}".format(**data), data)

def get_issue(self, project_id, issue_id):
data = {
'id': project_id,
'issue_iid': issue_id
}
return self.get("/projects/{id}/issues/{issue_iid}".format(**data))

def get_labels(self, project_id):
labels = self.get("/projects/{id}/labels".format(id=project_id))
return labels

def get_boards(self, project_id):
return self.get("/projects/{id}/boards".format(id=project_id))


32 changes: 24 additions & 8 deletions gwh.py
Expand Up @@ -10,6 +10,7 @@
from flask import Flask, request, abort
import argparse
from gitlab_api import GitlabApi
from helpers import Helpers

app = Flask(__name__)

Expand Down Expand Up @@ -40,7 +41,6 @@ def index():
repo_meta = {
'homepage': payload['repository']['homepage'],
}

repo = repos.get(repo_meta['homepage'], None)
private_token = repo.get('private_token', None)

Expand Down Expand Up @@ -103,23 +103,39 @@ def index():
issue_id = payload['object_attributes']['id']
gl.comment_on_issue(project_id, issue_id, "Automatic mention for %s" % (" and ".join(usernames)))

# parse commit message and manage labels on issues
if issue.get("labels")
if not private_token:
abort(403)
gl = GitlabApi(repo_meta['homepage'], private_token)
helpers = Helpers()
project_id = payload['object_attributes']['project_id']
labels = helpers.get_label_names(gl.get_labels(project_id))
list_labels = helpers.get_list_labels(gl.get_boards(project_id))
for commit in payload['commits']:
parse_commit = helpers.parse_commit_labels(commit['message'])
for issue in parse_commit['issues']:
issue_labels = gl.get_issue(project_id, issue)
updated_labels = helpers.simplify_labels(issue_labels, parse_commit['label_ops'])
gl.set_issue_labels(project_id, issue, updated_labels)
return 'OK'

# unknown event type
return json.dumps({'error': "wrong event type"})


if __name__ == "__main__":

parser = argparse.ArgumentParser(description="gitlab webhook receiver")
parser.add_argument("-c", "--config", action="store", help="path to repos configuration", required=True)
parser.add_argument("-p", "--port", action="store", help="server port", required=False, default=8080)
parser.add_argument("--allow", action="store", help="whitelist Gitlab IP block", required=False, default=None)
parser.add_argument("--debug", action="store_true", help="enable debug output", required=False, default=False)

parser.add_argument("-c", "--config", action="store",
help="path to repos configuration", required=True)
parser.add_argument("-p", "--port", action="store", help="server port",
required=False, default=8080)
parser.add_argument("--allow", action="store", help="whitelist Gitlab IP block",
required=False, default=None)
parser.add_argument("--debug", action="store_true", help="enable debug output",
required=False, default=False)

args = parser.parse_args()

port_number = int(args.port)

REPOS_JSON_PATH = args.config
Expand Down
46 changes: 46 additions & 0 deletions helpers.py
@@ -0,0 +1,46 @@
import re

class Helpers:

def get_list_labels(self, boards):
# return label IDs from all project boards
label_ids = []
for board in boards:
label_ids += [list_label['label']['name'] for list_label in board['lists']]
return label_ids

def get_label_names(self, labels):
return [label['name'] for label in labels]

def get_issue_labels(self, issue):
return issue['labels']

def parse_commit_labels(self, message, labels):
# get the issue numbers
issue_pat = re.compile('#(\d+)')
iid_match = issue_pat.findall(message)

# get the labels
label_pat = re.compile('([\~\+\-])(' + '|'.join(labels) + ')')
label_match = label_pat.findall(message)
return {
'issues': iid_match,
'label_ops': label_match
}

def simplify_labels(self, existing, from_message, board_labels = None):
labels = existing
for ops in from_message['label_ops']:
op = ops[0]
name = ops[1].decode('utf-8')
if op == '+':
labels.append(name)
if op == '-':
labels.remove(name)
if op == '~':
if name in board_labels:
# remove all board labels
labels = [label for label in labels if label not in board_labels]
# set board label
labels.append(name)
return labels

0 comments on commit cecb9a5

Please sign in to comment.