"""
Margebot: A Errbot Plugin for Gitlab MR reminders
"""
-import re
from datetime import datetime, timezone
-from time import sleep
from dateutil import parser
from dateutil.tz import tzutc
from dateutil.relativedelta import relativedelta
from errbot.templating import tenv
from errcron.bot import CrontabMixin
import gitlab
-import requests
-
-class MargeGitlab(gitlab.Gitlab):
- """
- Subclass gitlab.Gitlab so extra_data args can be added
- to the addprojecthook() and editprojecthook() methods
- """
-
- def __init__(self, host, token="", oauth_token="", verify_ssl=True):
- super().__init__(host, token, oauth_token, verify_ssl)
-
- def addprojecthook_extra(self, project_id, url, push=False, issues=False, merge_requests=False, tag_push=False, extra_data=None):
- """
- A copy parent addprojecthook with an extra_data field
- """
- data = {"id": project_id, "url": url}
- if extra_data:
- for ed_key, ed_value in extra_data.items():
- data[ed_key] = ed_value
- data['push_events'] = int(bool(push))
- data['issues_events'] = int(bool(issues))
- data['merge_requests_events'] = int(bool(merge_requests))
- data['tag_push_events'] = int(bool(tag_push))
- request = requests.post("{0}/{1}/hooks".format(self.projects_url, project_id),
- headers=self.headers, data=data, verify=self.verify_ssl)
- if request.status_code == 201:
- return request.json()
- return False
-
- def editprojecthook_extra(self, project_id, hook_id, url, push=False, issues=False, merge_requests=False, tag_push=False, extra_data=None):
- """
- A copy of the parent editprojecthook with an extra_data field
- """
- data = {"id": project_id, "hook_id": hook_id, "url": url}
- if extra_data:
- for ed_key, ed_value in extra_data.items():
- data[ed_key] = ed_value
- data['push_events'] = int(bool(push))
- data['issues_events'] = int(bool(issues))
- data['merge_requests_events'] = int(bool(merge_requests))
- data['tag_push_events'] = int(bool(tag_push))
- request = requests.put("{0}/{1}/hooks/{2}".format(self.projects_url, project_id, hook_id),
- headers=self.headers, data=data, verify=self.verify_ssl)
- return request.status_code == 200
def deltastr(any_delta):
self.chatroom_host = None
self.gitlab = None
self.soak_delta = None
+ self.webhook_url = None
super().__init__(*args, **kwargs)
def get_configuration_template(self):
Marge.CRONTAB = ['{} .crontab_hook'.format(self.config['CRONTAB'])]
gitlab_auth_token = self.config['GITLAB_ADMIN_TOKEN']
verify_ssl = self.config['VERIFY_SSL']
- self.gitlab = MargeGitlab(self.git_host, gitlab_auth_token, verify_ssl=verify_ssl)
+ self.gitlab = gitlab.Gitlab('https://' + self.git_host, gitlab_auth_token, ssl_verify=verify_ssl)
self.activate_crontab()
self.soak_delta = relativedelta(hours=self.config['CRONTAB_SOAK_HOURS'])
# self.log.info("state: {}".format(request['object_attributes']['state']))
# verify it's a merge request
- if request['object_kind'] != 'merge_request':
- self.log.error('unexpecting object_kind: {}'.format(request['object_kind']))
+ if request['event_type'] != 'merge_request':
+ self.log.error('unexpecting event_type: {}'.format(request['event_type']))
elif 'opened' in request['object_attributes']['state']:
if request['object_attributes']['work_in_progress']:
wip = "WIP "
else:
wip = ""
+
url = request['project']['homepage']
title = request['object_attributes']['title']
-
author_id = request['object_attributes']['author_id'] # map this to user name ...
- author = self.gitlab.getuser(author_id)
- if author:
- author_name = author['username']
- else:
- self.log.info("unexpected author_id {}".format(author_id))
+ try:
+ author = self.gitlab.users.get(author_id)
+ author_name = author.attributes['username']
+ except Exception as exp:
+ self.log.info("unexpected author_id {}, exp={}".format(author_id, exp))
author_name = author_id
target_project_id = request['object_attributes']['target_project_id']
iid = request['object_attributes']['iid']
- mr_id = request['object_attributes']['id']
+
+ # If the MR is tagged 'never-close' or 'abandoned' ignore it
+ if 'labels' in request:
+ for a_label in request['labels']:
+ if a_label['title'] in ['never-close', 'abandoned']:
+ self.log.info("Skipping {} notice for {} MR".format(a_label['title'], url))
+ return "OK"
msg_template = "Hi there ! {} has opened a new {}MR: \"{}\"\n{}/merge_requests/{}"
msg = msg_template.format(author_name, wip, title, url, iid)
open_mrs = self['OPEN_MRS']
- if (target_project_id, mr_id, rooms) not in open_mrs:
+ if (target_project_id, iid, rooms) not in open_mrs:
for a_room in rooms.split(','):
if self.config:
self.send(self.build_identifier(a_room + '@' + self.chatroom_host), msg)
- self.log.info("webhook: Saving ({}, {}, {})".format(target_project_id, mr_id, rooms))
- open_mrs[(target_project_id, mr_id, rooms)] = True
+ self.log.info("webhook: Saving ({}, {}, {})".format(target_project_id, iid, rooms))
+ open_mrs[(target_project_id, iid, rooms)] = True
self['OPEN_MRS'] = open_mrs
+
+ # TODO: Add check if an MR has toggled the WIP indicator
+ # (trigger on updates (what's that look like in request['object_attributes']['state'])
+ # Then check in request['changes']['title']['previous'] starts with 'WIP:'
+ # but not request['changes']['title']['current'], and vice versa
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53529
+
return "OK"
def mr_status_msg(self, a_mr, author=None):
Create the merge request status message
"""
self.log.info("mr_status_msg: a_mr: {}".format(a_mr))
+ mr_attrs = a_mr.attributes
# Only weed out MRs less than the soak time for the crontab output (where author==None)
now = datetime.now(timezone.utc)
- creation_time = parser.parse(a_mr['created_at'], tzinfos=tzutc)
+ creation_time = parser.parse(mr_attrs['created_at'], tzinfos=[tzutc()]).astimezone(timezone.utc)
if not author:
self.log.info("times: {}, {}, {}".format(creation_time, self.soak_delta, now))
if creation_time + self.soak_delta > now:
- project_id = a_mr['target_project_id']
- mr_id = a_mr['id']
+ project_id = mr_attrs['target_project_id']
+ mr_id = mr_attrs['id']
soak_hours = self.config['CRONTAB_SOAK_HOURS']
info_template = "skipping: MR <{},{}> was opened less than {} hours ago"
info_msg = info_template.format(project_id, mr_id, soak_hours)
str_open_since = deltastr(now - creation_time)
warning = ""
- if a_mr['work_in_progress']:
+ if mr_attrs['work_in_progress']:
warning = "still WIP"
- elif a_mr['merge_status'] != 'can_be_merged':
+ elif mr_attrs['merge_status'] != 'can_be_merged':
warning = "there are merge conflicts"
if author:
- authored = (a_mr['author']['id'] == author)
+ authored = (mr_attrs['author']['id'] == author)
else:
authored = False
- # getapprovals is only available in GitLab 8.9 EE or greater
- # (not the open source CE version)
- # approvals = self.gitlab.getapprovals(a_mr['id'])
- # also_approved = ""
- # for approved in approvals['approved_by']:
- # also_approved += "," + approved['user']['name']
-
- upvotes = a_mr['upvotes']
- msg = "{} (opened {})".format(a_mr['web_url'], str_open_since)
+ msg = "{} (opened by {} {})".format(mr_attrs['web_url'], mr_attrs['author']['username'], str_open_since)
+ upvotes = mr_attrs['upvotes']
if upvotes >= 2:
msg += ": Has 2+ upvotes and could be merged in now"
if warning != "":
msg += "."
elif upvotes == 1:
+ self.log.error("pre-award a_mr: {}".format(dir(a_mr.awardemojis)))
+ award_emoji = a_mr.awardemojis.list()
+ self.log.info("award_emoji: {}".format(award_emoji))
+ already_approved = "someone"
+ for an_emoji in award_emoji:
+ emoji_attr = an_emoji.attributes
+ if emoji_attr["name"] == "thumbsup":
+ already_approved = emoji_attr["user"]["username"]
+
if authored:
- msg += ": Your MR is waiting for another upvote"
+ msg += ": Your MR has been approved by " + already_approved + " and is waiting for another upvote"
else:
- msg += ": Waiting for another upvote"
+ msg += ": Approved by " + already_approved + " and waiting for another upvote"
if warning != "":
msg += " but {}.".format(warning)
else:
# initialize the reminders
rooms = self.rooms()
for a_room in rooms:
+ self.log.info("poller: a_room.node: {}".format(a_room.node))
reminder_msg[a_room.node] = []
still_open_mrs = {}
for (project_id, mr_id, notify_rooms) in open_mrs:
+ a_project = self.gitlab.projects.get(project_id)
+ if not a_project:
+ self.log.debug("Couldn't find project: {}".format(project_id))
+ continue
+
# Lookup the MR from the project/id
- a_mr = self.gitlab.getmergerequest(project_id, mr_id)
+ a_mr = a_project.mergerequests.get(mr_id)
+ mr_attrs = a_mr.attributes
if not a_mr:
self.log.debug("Couldn't find project: {}, id: {}".format(project_id, mr_id))
continue
- # If the MR is tagged 'never-close' ignore it
- if 'labels' in a_mr and 'never-close' in a_mr['labels']:
+ # If the MR is tagged 'never-close' or 'abandoned', ignore it
+ if 'labels' in mr_attrs and ('never-close' in mr_attrs['labels'] or 'abandoned' in mr_attrs['labels']):
continue
- self.log.info("a_mr: {} {} {} {}".format(project_id, mr_id, notify_rooms, a_mr['state']))
+ self.log.info("a_mr: {} {} {} {}".format(project_id, mr_id, notify_rooms, mr_attrs['state']))
# If the MR is no longer open, skip to the next MR,
# and don't include this MR in the next check
- if 'opened' not in a_mr['state']:
+ if 'opened' not in mr_attrs['state']:
continue
else:
still_open_mrs[(project_id, mr_id, notify_rooms)] = True
continue
for a_room in notify_rooms.split(','):
- reminder_msg[a_room].append(msg_dict)
+ if a_room in reminder_msg:
+ reminder_msg[a_room].append(msg_dict)
+ else:
+ self.log.error("{} not in reminder_msg (project_id={}, mr_id={})".format(a_room, project_id, mr_id))
self['OPEN_MRS'] = open_mrs
return "No MRs to review"
sender_gitlab_id = None
- sender_users = self.gitlab.getusers(search=(('username', sender)))
- if not sender_users:
- self.log.error('problem mapping {} to a gitlab user'.format(sender))
+ try:
+ sender_users = self.gitlab.users.list(username=sender)
+ sender_gitlab_id = sender_users[0].attributes['id']
+ except Exception as exp:
+ self.log.error('problem mapping {} to a gitlab user, exp={}'.format(sender, exp))
sender_gitlab_id = None
- else:
- sender_gitlab_id = sender_users[0]['id']
# Walk through the MRs we've seen already:
msg_list = []
msg = ""
still_open_mrs = {}
open_mrs = self['OPEN_MRS']
+ self.log.info('open_mrs: {}'.format(open_mrs))
for (project, mr_id, notify_rooms) in open_mrs:
+ try:
+ a_project = self.gitlab.projects.get(project)
+ except Exception as exp:
+ self.log.debug("Couldn't find project: {}, exp: {}".format(project, exp))
+ continue
+
# Lookup the MR from the project/id
- a_mr = self.gitlab.getmergerequest(project, mr_id)
- if not a_mr:
- self.log.debug("Couldn't find project: {}, id: {}".format(project, id))
+ try:
+ a_mr = a_project.mergerequests.get(mr_id)
+ mr_attrs = a_mr.attributes
+ except Exception as exp:
+ self.log.debug("Couldn't find project: {}, id: {}, exp: {}".format(project, mr_id, exp))
continue
self.log.info('project: {}, id: {}, a_mr: {}'.format(project, id, a_mr))
- # If the MR is tagged 'never-close' ignore it
- if 'labels' in a_mr and 'never-close' in a_mr['labels']:
+ # If the MR is tagged 'never-close' or 'abandoned', ignore it
+ if 'labels' in mr_attrs and ('never-close' in mr_attrs['labels'] or 'abandoned' in mr_attrs['labels']):
continue
# If the MR is no longer open, skip to the next MR,
# and don't include this MR in the next check
- if 'opened' not in a_mr['state']:
+ if 'opened' not in mr_attrs['state']:
+ self.log.info('state not opened: {}'.format(mr_attrs['state']))
continue
else:
still_open_mrs[(project, mr_id, notify_rooms)] = True
self.log.info("rooms={}".format(rooms))
# get the group/repo repo, error out if it doesn't exist
- project = self.gitlab.getproject(repo)
+ project = self.gitlab.projects.get(repo)
if not project:
msg = "Couldn't find repo {}".format(repo)
self.log.info("watchrepo: {}".format(msg))
self.log.info('project: {}'.format(project))
- target_project_id = project['id']
+ target_project_id = project.id
# Check is the project already includes the margebot hook
# If no hooks, will it return False or [] ?
marge_hook = None
- hooks = self.gitlab.getprojecthooks(target_project_id)
+ hooks = project.hooks.list()
self.log.error("hooks = {}".format(hooks))
if hooks is False:
msg = "Couldn't find {} hooks".format(repo)
self.log.error("watchrepo: {}".format(msg))
return msg
- else:
- for a_hook in hooks:
- self.log.info('a_hook: {}'.format(a_hook))
- if a_hook['merge_requests_events'] and a_hook['url'].startswith(self.webhook_url):
- marge_hook = a_hook
- break
+ for a_hook in hooks:
+ self.log.info('a_hook: {}'.format(a_hook))
+ hook_attributes = a_hook.attributes
+ if hook_attributes['merge_requests_events'] and hook_attributes['url'].startswith(self.webhook_url):
+ marge_hook = a_hook
+ break
# If so replace it (or error out ?)
url = "{}{}".format(self.webhook_url, rooms) # webhooks_url will end in '/'
+ hook_updated = True
if marge_hook:
-
- old_rooms = marge_hook['url'].split(self.webhook_url, 1)[1]
+ old_rooms = marge_hook.attributes['url'].split(self.webhook_url, 1)[1]
if old_rooms == rooms:
msg = "Already reporting {} MRs to the {} room(s)".format(repo, rooms)
self.log.info('watchrepo: {}'.format(msg))
return msg
- else:
- hook_updated = self.gitlab.editprojecthook_extra(target_project_id, marge_hook['id'], url, merge_requests=True, extra_data={'enable_ssl_verification': False})
- s_watch_msg = "Updating room list for {} MRs from {} to {}".format(repo, old_rooms, rooms)
+ try:
s_action = "update"
+ marge_hook.attributes['url'] = url
+ marge_hook.save()
+ s_watch_msg = "Updating room list for {} MRs from {} to {}".format(repo, old_rooms, rooms)
+ except Exception as exp:
+ hook_updated = False
+ self.log.error("watchrepo; update hook {} raised exception {}".format(repo, exp))
else:
- hook_updated = self.gitlab.addprojecthook_extra(target_project_id, url, merge_requests=True, extra_data={'enable_ssl_verification': False})
- s_watch_msg = "Now watching for new MRs in the {} repo to the {} roomi(s)".format(repo, rooms)
- s_action = "add"
+ try:
+ s_action = "add"
+ project.hooks.create({'url': url, 'merge_requests_events': 1, 'enable_ssl_verification': True})
+ s_watch_msg = "Now watching for new MRs in the {} repo to the {} room(s)".format(repo, rooms)
+ except Exception as exp:
+ hook_updated = False
+ self.log.error("watchrepo; create hook {} raised exception {}".format(repo, exp))
if not hook_updated:
msg = "Couldn't {} hook: {}".format(s_action, repo)
# pragma pylint: enable=simplifiable-if-statement
open_mrs = new_open_mrs
- # If adding a new repo, check for existing opened/reopened MRs in the repo.
+ # If adding a new repo, check for existing opened MRs in the repo.
else:
- for state in ['opened', 'reopened']:
- page = 1
- mr_list = self.gitlab.getmergerequests(target_project_id, page=page, per_page=100, state=state)
- while (mr_list is not False) and (mr_list != []):
- for an_mr in mr_list:
- mr_count += 1
- self.log.info('watchrepo: an_mr WATS THE ID\n{}'.format(an_mr))
- mr_id = an_mr['id']
- open_mrs[(target_project_id, mr_id, rooms)] = True
- # Get the next page of MRs
- page += 1
- mr_list = self.gitlab.getmergerequests(target_project_id, page=page, per_page=100)
+ for state in ['opened']:
+ a_project = self.gitlab.projects.get(target_project_id)
+ mr_list = a_project.mergerequests.list(state=state)
+ for an_mr in mr_list:
+ mr_count += 1
+ mr_id = an_mr.attributes['iid']
+ open_mrs[(target_project_id, mr_id, rooms)] = True
self['OPEN_MRS'] = open_mrs
elif mr_count == 1:
mr_msg = "1 open MR was found in the repo. Run !reviews to see the updated MR list."
else:
- mr_msg = "{} open MRs were found in the repo. Run !reviews to see the updated MR list."
-
+ mr_msg = "{} open MRs were found in the repo. Run !reviews to see the updated MR list.".format(mr_count)
return "{}\n{}".format(s_watch_msg, mr_msg)
# pragma pylint: disable=unused-argument
- @botcmd()
- def xyzzy(self, msg, args):
- """
- Don't call this command...
- """
- yield "/me whispers \"All open MRs have been merged into master.\""
- sleep(5)
- yield "(just kidding)"
-
- @re_botcmd(pattern=r"I blame marge(bot)?", prefixed=False, flags=re.IGNORECASE)
- def dont_blame_margebot(self, msg, match):
- """
- margebot is innocent.
- """
- yield u"(\u300D\uFF9F\uFF9B\uFF9F)\uFF63NOOOooooo say it ain't so."
-
- @re_botcmd(pattern=r"good bot", prefixed=False, flags=re.IGNORECASE)
- def best_bot(self, msg, match):
- """
- margebot is the best.
- """
- yield "Best bot"
+ # Check Chucklebot for the chuckles
- @re_botcmd(pattern=r"magfest", prefixed=False, flags=re.IGNORECASE)
- def margefest(self, msg, args):
- """
- margefest4ever
- """
- return "More like MargeFest, amirite ?"
-
- @re_botcmd(pattern=r"margebot sucks", prefixed=False, flags=re.IGNORECASE)
- def bring_it_up_with_the_steering_committee(self, msg, args):
+ @re_botcmd(pattern=r".*", prefixed=True)
+ def catchall(self, msg, args):
"""
- Bring it up with the committee
+ Don't have the bot complain about unknown commands if the first word in a msg is its name
"""
- return "Bring it up with the Margebot steering committee."
-
- @re_botcmd(pattern=r".*", prefixed=True)
- def catchall(self, msg,args):
- """
- Don't have the bot complain about unknown commands if the first word in a msg is its name
- """
- return
-
+ return
# pragma pylint: enable=unused-argument