- Factoring out common code between crontab/reviews commands
authorJudeN <juden@webhooks.juden.cololo.co>
Wed, 15 Nov 2017 03:31:17 +0000 (22:31 -0500)
committerJudeN <juden@webhooks.juden.cololo.co>
Wed, 15 Nov 2017 03:31:17 +0000 (22:31 -0500)
- Sorting output by age / added soaktime for crontab responses
- Included age in the review description

marge.py

index 6f9f87f..548eb04 100755 (executable)
--- a/marge.py
+++ b/marge.py
@@ -1,11 +1,25 @@
 import re
 import re
+from datetime import datetime, timezone
+from dateutil import parser
+from dateutil.tz import tzutc
+from dateutil.relativedelta import *
 from errbot import BotPlugin, botcmd, webhook
 from errbot.backends import xmpp
 from errcron.bot import CrontabMixin
 from time import sleep
 import gitlab
 
 from errbot import BotPlugin, botcmd, webhook
 from errbot.backends import xmpp
 from errcron.bot import CrontabMixin
 from time import sleep
 import gitlab
 
-# TODO:  Add certificate verification to the gitlab API calls.
+def deltastr(any_delta):
+    l_delta = []
+    (days, hours, mins) = (any_delta.days, any_delta.seconds//3600, (any_delta.seconds//60)%60)
+
+    for (k,v) in {"day": days, "hour": hours, "minute": mins}.items():
+        if v == 1:
+            l_delta.append("1 " + k)
+        elif v > 1:
+            l_delta.append("{} {}s".format(v, k))
+
+    return ",".join(l_delta) + " ago"
 
 
 class Marge(BotPlugin, CrontabMixin):
 
 
 class Marge(BotPlugin, CrontabMixin):
@@ -40,12 +54,14 @@ class Marge(BotPlugin, CrontabMixin):
         CHATROOM_HOST: Chatroom host.  Usually 'chatroom' + FQDN of Jabber server
         CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
         VERIFY_SSL : True, False, or path to CA cert to verify cert
         CHATROOM_HOST: Chatroom host.  Usually 'chatroom' + FQDN of Jabber server
         CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
         VERIFY_SSL : True, False, or path to CA cert to verify cert
+        CRONTAB_SOAK_HOURS : Don't send out reminders about MRs opened less than this many hours
         """ 
         return {'GITLAB_HOST': 'gitlab.example.com',
                 'GITLAB_ADMIN_TOKEN' : 'gitlab-admin-user-private-token',
                 'CHATROOM_HOST': 'conference.jabber.example.com',
                 'CRONTAB' : '0 11,17 * * *',
         """ 
         return {'GITLAB_HOST': 'gitlab.example.com',
                 'GITLAB_ADMIN_TOKEN' : 'gitlab-admin-user-private-token',
                 'CHATROOM_HOST': 'conference.jabber.example.com',
                 'CRONTAB' : '0 11,17 * * *',
-                'VERIFY_SSL' : True}
+                'VERIFY_SSL' : True,
+                'CRONTAB_SOAK_HOURS' : 1}
 
     def check_configuration(self, configuration):
         super().check_configuration(configuration)
 
     def check_configuration(self, configuration):
         super().check_configuration(configuration)
@@ -60,10 +76,10 @@ class Marge(BotPlugin, CrontabMixin):
         self.gitlab = gitlab.Gitlab(self.git_host, self.config['GITLAB_ADMIN_TOKEN'], verify_ssl=self.config['VERIFY_SSL'])
         self.activate_crontab()
 
         self.gitlab = gitlab.Gitlab(self.git_host, self.config['GITLAB_ADMIN_TOKEN'], verify_ssl=self.config['VERIFY_SSL'])
         self.activate_crontab()
 
+        self.soak_delta = relativedelta( hours = self.config['CRONTAB_SOAK_HOURS'])
         super().activate()
 
     def deactivate(self):
         super().activate()
 
     def deactivate(self):
-        # TODO: Anything special for closing gitlab ?
         super().deactivate()
 
     @webhook('/margebot/<rooms>/')
         super().deactivate()
 
     @webhook('/margebot/<rooms>/')
@@ -75,8 +91,6 @@ class Marge(BotPlugin, CrontabMixin):
         self.log.info('margebot webhook request: {}'.format(request))
         self.log.info('margebot webhook rooms {}'.format(rooms))
 
         self.log.info('margebot webhook request: {}'.format(request))
         self.log.info('margebot webhook rooms {}'.format(rooms))
 
-        # TODO: Will errbot return a json struct or not ?
-
         # verify it's a merge request
         if request['object_kind'] != 'merge_request':
             self.log.error('expecting object_kind of merge_request but got {}'.format(request['object_kind']))
         # verify it's a merge request
         if request['object_kind'] != 'merge_request':
             self.log.error('expecting object_kind of merge_request but got {}'.format(request['object_kind']))
@@ -121,6 +135,70 @@ class Marge(BotPlugin, CrontabMixin):
 
         return "OK"
 
 
         return "OK"
 
+    def mr_status_msg(self, a_mr, author=None):
+        self.log.info("mr_status_msg: a_mr: {}".format(a_mr))
+
+        now = datetime.now(timezone.utc)
+        creation_time = parser.parse(a_mr['created_at'], tzinfos=tzutc)
+        self.log.info("times: {}, {}, {}".format(creation_time, self.soak_delta, now))
+        if creation_time + self.soak_delta > now:
+            info_msg = "skipping: MR <{},{}> was opened less than {} hours ago".format(project, iid, soak_hours)
+            self.log.info(info_msg)   
+            return None
+
+        str_open_since = deltastr(now - creation_time)
+
+        warning = ""
+        if a_mr['work_in_progress']:
+            warning = "still WIP"
+        elif a_mr['merge_status'] != 'can_be_merged':
+            warning = "there are merge conflicts"
+
+        if author:
+            authored = (a_mr['author']['id'] == author)
+        else:
+            authored = False
+
+        # TODO: Include the count of opened MR notes (does the API show resolved state ??)
+
+        # 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)
+        if upvotes >= 2:
+            msg += ": Has 2+ upvotes / Could be merge in now"
+            if warning != "":
+                msg += " except {}".format(a_mr['web_url'], str_open_since, warning)
+            else:
+                 msg += "."
+
+        elif upvotes == 1:
+            if authored:
+                msg += ": Your MR is waiting for another upvote"
+            else:
+                msg += ": Waiting for another upvote"
+            if warning != "":
+                msg += "but {}.".format(warning)
+            else:
+                msg += "."
+
+        else:
+            if authored:
+                msg += ": Your MR has no upvotes"
+            else:
+                msg += ": No upvotes, please review"
+            if warning != "":
+                msg += "but {}".format(warning)
+            else:
+                msg += "."
+
+        return((creation_time, msg))
+
+
     def crontab_hook(self, polled_time):
         """
         Send a scheduled message to the rooms margebot is watching
     def crontab_hook(self, polled_time):
         """
         Send a scheduled message to the rooms margebot is watching
@@ -129,14 +207,14 @@ class Marge(BotPlugin, CrontabMixin):
 
         self.log.info("crontab_hook triggered at {}".format(polled_time))
 
 
         self.log.info("crontab_hook triggered at {}".format(polled_time))
 
-        reminder_msg = {}  # Map of reminder_msg['roomname@domain'] = msg
+        reminder_msg = {}  # Map of reminder_msg['roomname@domain'] = [(created_at,msg)]
 
         # initialize the reminders
         rooms = self.rooms()
         for a_room in rooms:
 
         # initialize the reminders
         rooms = self.rooms()
         for a_room in rooms:
-            reminder_msg[a_room.node] = ''
+            reminder_msg[a_room.node] = []
 
 
-        msg = ""
+        msgs = ""  
         still_open_mrs = {}
 
         # Let's walk through the MRs we've seen already:
         still_open_mrs = {}
 
         # Let's walk through the MRs we've seen already:
@@ -155,32 +233,23 @@ class Marge(BotPlugin, CrontabMixin):
                 else:
                      still_open_mrs[(project, iid, notify_rooms)] = True
 
                 else:
                      still_open_mrs[(project, iid, notify_rooms)] = True
 
-                # TODO: Warn if an open MR has has conflicts (merge_status == ??)
-                # TODO: Include the count of opened MR notes (does the API show resolved state ??)
-
-                # 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']
-                if upvotes >= 2:
-                    msg = "\n{}: Has 2+ upvotes / Could be merged in now.".format(a_mr['web_url'])
-                elif upvotes == 1:
-                    msg_template = "\n{}: Waiting for another upvote."
-                    msg = msg_template.format(a_mr['web_url'])
-                else:
-                    msg = '\n{}: Noo upvotes / Please Review.'.format(a_mr['web_url'])
+                msg_tuple = self.mr_status_msg(a_mr)
+                if msg_tuple is None:
+                    continue
 
                 for a_room in notify_rooms.split(','):
 
                 for a_room in notify_rooms.split(','):
-                    reminder_msg[a_room] += msg
+                    reminder_msg[a_room].append(msg_tuple)
 
         # Remind each of the rooms about open MRs
 
         # Remind each of the rooms about open MRs
-        for a_room, room_msg in reminder_msg.items():
-            if room_msg != "":
+        for a_room, room_msg_list in reminder_msg.items():
+            if room_msg != []:
+
+                sorted_room_msg_list = sorted(room_msg_list, key=lambda x: x[0])  # sort by the creation time
+                msgs = [x[1] for x in sorted_room_msg_list]                       # extract the msgs from the tuple list
+                room_msg = "\n".join(msgs)                                        # join those msgs together.
+
                 if self.config:
                 if self.config:
-                    msg_template = "Heads up these MRs need some attention:{}\n"
+                    msg_template = "These MRs need some attention:{}\n"
                     msg_template += "You can get an updated list with the  '/msg MargeB !reviews' command."
                     msg = msg_template.format(room_msg)
                     self.send(self.build_identifier(a_room + '@' + self.config['CHATROOM_HOST']), msg)
                     msg_template += "You can get an updated list with the  '/msg MargeB !reviews' command."
                     msg = msg_template.format(room_msg)
                     self.send(self.build_identifier(a_room + '@' + self.config['CHATROOM_HOST']), msg)
@@ -215,6 +284,7 @@ class Marge(BotPlugin, CrontabMixin):
             return "Sorry, I couldn't find your gitlab account."
 
         # Walk through the MRs we've seen already:
             return "Sorry, I couldn't find your gitlab account."
 
         # Walk through the MRs we've seen already:
+        msg_list = []
         msg = ""
         still_open_mrs = {}
         with self.mutable('OPEN_MRS') as open_mrs:
         msg = ""
         still_open_mrs = {}
         with self.mutable('OPEN_MRS') as open_mrs:
@@ -230,39 +300,19 @@ class Marge(BotPlugin, CrontabMixin):
                 else:
                     still_open_mrs[(project, iid, notify_rooms)] = True
 
                 else:
                     still_open_mrs[(project, iid, notify_rooms)] = True
 
-                authored = (a_mr['author']['id'] == sender_gitlab_id)
-                already_approved = False
-
-                # getapprovals is currently only available in GitLab >= 8.9 EE (not available in the CE yet)
-                # approvals = self.gitlab.getapprovals(a_mr['id'])
-                # also_approved = ""
-                # for approved in approvals['approved_by']:
-                #     if approved['user']['id'] == sender_gitlab_id:
-                #        already_approved = True
-                #    else:
-                #        also_approved += "," + approved['user']['name']
-
-                upvotes = a_mr['upvotes']
-                if upvotes >= 2:
-                    msg += "\n{}: has 2+ upvotes and could be merged in now.".format(a_mr['web_url'])
-                elif upvotes == 1:
-                    if not authored:
-                        msg += "\n{}: is waiting for another upvote.".format(a_mr['web_url'])
-                    else:
-                        msg += "\n{}: Your MR is waiting for another upvote.".format(a_mr['web_url'])
+                msg_tuple = self.mr_status_msg(a_mr, author=sender_gitlab_id)
+                if msg_tuple is None:
+                    continue
 
 
-                else:
-                    if not authored:
-                        msg += '\n{}: Has no upvotes and needs your attention.'.format(a_mr['web_url'])
-                    else:
-                        msg += '\n{}: Your MR has no upvotes.'.format(a_mr['web_url'])
+                msg_list.append(msg_tuple)
 
 
-        if msg == "":
-            response = 'Hi {}\n{}'.format(sender, 'I found no open MRs for you.')
+        if msg_list == []:
+            response = 'Hi {}{}'.format(sender, 'I found no open MRs for you.')
         else:
         else:
-            response = 'Hi {}\n{}'.format(sender,msg)
-
-        # self.send(self.build_identifier(msg.frm), response)
+            sorted_msg_list = sorted(msg_list, key=lambda x: x[0])  # sort by the creation time
+            msgs = [x[1] for x in sorted_msg_list]                  # extract the msgs from the tuple list
+            msg = "\n".join(msgs)                                   # join those msgs together.
+            response = 'Hi {}: These MRs need some attention:\n{}'.format(sender,msg)
 
         with self.mutable('OPEN_MRS') as open_mrs:
             open_mrs = still_open_mrs
 
         with self.mutable('OPEN_MRS') as open_mrs:
             open_mrs = still_open_mrs