Ignore:
Timestamp:
Jun 8, 2009 3:07:47 AM (15 years ago)
Author:
retracile
Message:

Mergebot: redesigned implementation. Still has rough edges.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • mergebot/trunk/mergebot/web_ui.py

    r16 r17  
    11#!/usr/bin/env python
    22
    3 import random
     3import socket
     4import time
     5import os
     6import base64
     7from subprocess import check_call
    48
    59from trac.core import *
     
    711from trac.ticket.query import Query
    812from trac.ticket.model import Ticket
    9 from trac.config import Option
     13from trac.env import IEnvironmentSetupParticipant
    1014from trac.perm import IPermissionRequestor
    1115from trac.web.main import IRequestHandler
    12 from trac.web.chrome import INavigationContributor, ITemplateProvider
    13 
    14 from MergeActor import MergeActor
    15 from BranchActor import BranchActor
    16 from RebranchActor import RebranchActor
    17 from CheckMergeActor import CheckMergeActor
     16from trac.web.chrome import INavigationContributor, ITemplateProvider, add_warning
     17
     18from action_logic import is_branchable, is_rebranchable, is_mergeable, is_checkmergeable
    1819
    1920class MergeBotModule(Component):
    20     implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider)
     21    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider, IEnvironmentSetupParticipant)
     22
     23    # IEnvironmentSetupParticipant
     24    def environment_created(self):
     25        self._setup_config()
     26
     27    def environment_needs_upgrade(self, db):
     28        return not list(self.config.options('mergebot'))
     29
     30    def upgrade_environment(self, db):
     31        self._setup_config()
     32
     33    def _setup_config(self):
     34        self.config.set('mergebot', 'work_dir', 'mergebot')
     35        self.config.set('mergebot', 'repository_url', 'http://FIXME/svn')
     36        self.config.set('mergebot', 'listen.ip', 'localhost')
     37        self.config.set('mergebot', 'listen.port', '12345')
     38        self.config.set('mergebot', 'worker_count', '2')
     39        # Set up the needed custom field for the bot
     40        self.config.set('ticket-custom', 'mergebotstate', 'select')
     41        self.config.set('ticket-custom', 'mergebotstate.label', 'MergeBotState')
     42        self.config.set('ticket-custom', 'mergebotstate.options', '| merged | branched | conflicts')
     43        self.config.set('ticket-custom', 'mergebotstate.value', '')
     44        self.config.save()
    2145
    2246    # INavigationContributor
     
    2448    def get_active_navigation_item(self, req):
    2549        return 'mergebot'
     50
    2651    def get_navigation_items(self, req):
    2752        """Generator that yields the MergeBot tab, but only if the user has
     
    5782        fields = ['summary', 'component', 'version', 'status']
    5883        info = {}
    59         ticket = Ticket(self.env, ticketid)
    60         for field in fields:
    61             info[field] = ticket[field]
    62         info['href'] = self.env.href.ticket(ticketid)
    63         self.log.debug("id=%s, info=%r" % (ticketid, info))
     84        if ticketid:
     85            ticket = Ticket(self.env, ticketid)
     86            for field in fields:
     87                info[field] = ticket[field]
     88            info['href'] = self.env.href.ticket(ticketid)
     89            self.log.debug("id=%s, info=%r" % (ticketid, info))
    6490        return info
     91
     92    def daemon_address(self):
     93        host = self.env.config.get('mergebot', 'listen.ip')
     94        port = int(self.env.config.get('mergebot', 'listen.port'))
     95        return (host, port)
     96
     97    def start_daemon(self):
     98        check_call(['mergebotdaemon', self.env.path])
     99        time.sleep(1) # bleh
     100
     101    def _daemon_cmd(self, cmd):
     102        self.log.debug('Sending mergebotdaemon: %r' % cmd)
     103        try:
     104            info_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     105            info_socket.connect(self.daemon_address())
     106        except socket.error, e:
     107            # if we're refused, try starting the daemon and re-try
     108            if e and e[0] == 111:
     109                self.log.debug('connection to mergebotdaemon refused, trying to start mergebotdaemon')
     110                self.start_daemon()
     111            self.log.debug('Resending mergebotdaemon: %r' % cmd)
     112            info_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     113            info_socket.connect(self.daemon_address())
     114        info_socket.sendall(cmd)
     115        self.log.debug('Reading mergebotdaemon response')
     116        raw = info_socket.recv(4096)
     117        info_socket.close()
     118        self.log.debug('Reading mergebotdaemon response was %r' % raw)
     119        return raw
    65120
    66121    def process_request(self, req):
     
    68123        req.perm.assert_permission("MERGEBOT_VIEW")
    69124
    70         # 2nd redirect back to the real mergebot page To address POST
     125        # 2nd redirect back to the real mergebot page to address POST and
     126        # browser refresh
    71127        if req.path_info == "/mergebot/redir":
    72128            req.redirect(req.href.mergebot())
    73129
    74         debugs = []
    75         req.hdf["title"] = "MergeBot"
     130        data = {}
    76131
    77132        if req.method == "POST": # The user hit a button
    78             #debugs += [
    79             #    "POST",
    80             #    "Branching ticket %s" % req.args,
    81             #]
    82 
    83             ticketnum = req.args['ticket']
    84             component = req.args['component']
    85             version = req.args['version']
    86             requestor = req.authname or "anonymous"
    87             ticket = Ticket(self.env, int(ticketnum))
    88             # FIXME: check for 'todo' key?
    89             # If the request is not valid, just ignore it.
    90             actor = None
    91             if req.args['action'] == "Branch":
    92                 req.perm.assert_permission("MERGEBOT_BRANCH")
    93                 if self._is_branchable(ticket):
    94                     actor = BranchActor(self.env)
    95             elif req.args['action'] == "Rebranch":
    96                 req.perm.assert_permission("MERGEBOT_BRANCH")
    97                 if self._is_rebranchable(ticket):
    98                     actor = RebranchActor(self.env)
    99             elif req.args['action'] == "CheckMerge":
    100                 req.perm.assert_permission("MERGEBOT_VIEW")
    101                 if self._is_checkmergeable(ticket):
    102                     actor = CheckMergeActor(self.env)
    103             elif req.args['action'] == "Merge":
    104                 if version.startswith("#"):
    105                     req.perm.assert_permission("MERGEBOT_MERGE_TICKET")
    106                 else:
    107                     req.perm.assert_permission("MERGEBOT_MERGE_RELEASE")
    108                 if self._is_mergeable(ticket):
    109                     actor = MergeActor(self.env)
    110             if actor:
    111                 actor.AddTask([ticketnum, component, version, requestor])
    112                 try:
    113                     actor.Run() # Starts processing deamon.
    114                 except Exception, e:
    115                     self.log.exception(e)
     133            if req.args['action'] in ['Branch', 'Rebranch', 'CheckMerge', 'Merge']:
     134                ticketnum = req.args['ticket']
     135                component = req.args['component']
     136                version = req.args['version']
     137                requestor = req.authname or 'anonymous'
     138                ticket = Ticket(self.env, int(ticketnum))
     139                # FIXME: check for 'todo' key?
     140                # If the request is not valid, just ignore it.
     141                action = None
     142                if req.args['action'] == "Branch":
     143                    req.perm.assert_permission("MERGEBOT_BRANCH")
     144                    if is_branchable(ticket):
     145                        action = 'branch'
     146                elif req.args['action'] == "Rebranch":
     147                    req.perm.assert_permission("MERGEBOT_BRANCH")
     148                    if is_rebranchable(ticket):
     149                        action = 'rebranch'
     150                elif req.args['action'] == "CheckMerge":
     151                    req.perm.assert_permission("MERGEBOT_VIEW")
     152                    if is_checkmergeable(ticket):
     153                        action = 'checkmerge'
     154                elif req.args['action'] == "Merge":
     155                    if version.startswith("#"):
     156                        req.perm.assert_permission("MERGEBOT_MERGE_TICKET")
     157                    else:
     158                        req.perm.assert_permission("MERGEBOT_MERGE_RELEASE")
     159                    if is_mergeable(ticket):
     160                        action = 'merge'
     161                if action:
     162                    command = 'ADD %s %s %s %s %s\nQUIT\n' % (ticketnum, action, component, version, requestor)
     163                    result = self._daemon_cmd(command)
     164                    if 'OK' not in result:
     165                        add_warning(req, result)
     166            if req.args['action'] == "Cancel":
     167                command = 'CANCEL %s\nQUIT\n' % req.args['task']
     168                result = self._daemon_cmd(command)
     169                if 'OK' not in result:
     170                    add_warning(req, result)
    116171            # First half of a double-redirect to make a refresh not re-send the
    117172            # POST data.
     
    120175        # We want to fill out the information for the page unconditionally.
    121176
     177        # Connect to the daemon and read the current queue information
     178        raw_queue_info = self._daemon_cmd('LIST\nQUIT\n')
     179        # Parse the queue information into something we can display
     180        queue_info = [x.split(',') for x in raw_queue_info.split('\n') if ',' in x]
     181        status_map = {
     182            'Z':'Zombie',
     183            'R':'Running',
     184            'Q':'Queued',
     185            'W':'Waiting',
     186            'P':'Pending',
     187        }
     188        for row in queue_info:
     189            status = row[1]
     190            row[1] = status_map[status]
     191            summary = row[7]
     192            row[7] = base64.b64decode(summary)
     193
     194        data['queue'] = queue_info
     195
     196        queued_tickets = set([int(q[2]) for q in queue_info])
     197
     198        # Provide the list of tickets at the bottom of the page, along with
     199        # flags for which buttons should be enabled for each ticket.
    122200        # I need to get a list of tickets.  For non-admins, restrict the list
    123201        # to the tickets owned by that user.
    124         querystring = "status=new|assigned|reopened&version!="
    125         if not req.perm.has_permission("MERGEBOT_ADMIN"):
     202        querystring = "status!=closed&version!="
     203        required_columns = ["component", "version", "mergebotstate"]
     204        if req.perm.has_permission("MERGEBOT_ADMIN"):
     205            required_columns.append("owner")
     206        else:
    126207            querystring += "&owner=%s" % (req.authname,)
    127208        query = Query.from_string(self.env, querystring, order="id")
    128209        columns = query.get_columns()
    129         for name in ("component", "version", "mergebotstate"):
     210        for name in required_columns:
    130211            if name not in columns:
    131212                columns.append(name)
    132         #debugs.append("query.fields = %s" % str(query.fields))
    133213        db = self.env.get_db_cnx()
    134214        tickets = query.execute(req, db)
    135         #debugs += map(str, tickets)
    136         req.hdf['mergebot.ticketcount'] = str(len(tickets))
    137 
    138         # Make the tickets indexable by ticket id number:
    139         ticketinfo = {}
     215        data['unqueued'] = []
    140216        for ticket in tickets:
    141             ticketinfo[ticket['id']] = ticket
    142         #debugs.append(str(ticketinfo))
    143 
    144         availableTickets = tickets[:]
    145         queued_tickets = []
    146         # We currently have 4 queues, "branch", "rebranch", "checkmerge", and
    147         # "merge"
    148         queues = [
    149             ("branch", BranchActor),
    150             ("rebranch", RebranchActor),
    151             ("checkmerge", CheckMergeActor),
    152             ("merge", MergeActor)
    153         ]
    154         for queuename, actor in queues:
    155             status, queue = actor(self.env).GetStatus()
    156             req.hdf["mergebot.queue.%s" % (queuename, )] = queuename
    157             if status:
    158                 # status[0] is ticketnum
    159                 req.hdf["mergebot.queue.%s.current" % (queuename)] = status[0]
    160                 req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = status[3]
    161                 ticketnum = int(status[0])
    162                 queued_tickets.append(ticketnum)
    163                 ticket = self._get_ticket_info(ticketnum)
    164                 for field, value in ticket.items():
    165                     req.hdf["mergebot.queue.%s.current.%s" % (queuename, field)] = value
    166             else:
    167                 req.hdf["mergebot.queue.%s.current" % (queuename)] = ""
    168                 req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = ""
    169 
    170             for i in range(len(queue)):
    171                 ticketnum = int(queue[i][0])
    172                 queued_tickets.append(ticketnum)
    173                 req.hdf["mergebot.queue.%s.queue.%d" % (queuename, i)] = str(ticketnum)
    174                 req.hdf["mergebot.queue.%s.queue.%d.requestor" % (queuename, i)] = queue[i][3]
    175                 ticket = self._get_ticket_info(ticketnum)
    176                 for field, value in ticket.items():
    177                     req.hdf["mergebot.queue.%s.queue.%d.%s" % (queuename, i, field)] = value
    178                     #debugs.append("%s queue %d, ticket #%d, %s = %s" % (queuename, i, ticketnum, field, value))
    179 
    180         # Provide the list of tickets at the bottom of the page, along with
    181         # flags for which buttons should be enabled for each ticket.
    182         for ticket in availableTickets:
    183217            ticketnum = ticket['id']
    184218            if ticketnum in queued_tickets:
    185219                # Don't allow more actions to be taken on a ticket that is
    186220                # currently queued for something.  In the future, we may want
    187                 # to support a 'Cancel' button, though.
     221                # to support a 'Cancel' button, though.  Or present actions
     222                # based upon the expected state of the ticket
    188223                continue
    189             req.hdf["mergebot.notqueued.%d" % (ticketnum)] = str(ticketnum)
    190             for field, value in ticket.items():
    191                 req.hdf["mergebot.notqueued.%d.%s" % (ticketnum, field)] = value
     224
     225            ticket_info = {'info': ticket}
     226            data['unqueued'].append(ticket_info)
     227
    192228            # Select what actions this user may make on this ticket based on
    193229            # its current state.
     230
    194231            # MERGE
    195             req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = 0
    196             if req.perm.has_permission("MERGEBOT_MERGE_RELEASE") and \
    197                 not ticket['version'].startswith("#"):
    198                 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket)
    199             if req.perm.has_permission("MERGEBOT_MERGE_TICKET") and \
    200                 ticket['version'].startswith("#"):
    201                 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket)
     232            ticket_info['merge'] = False
     233            if ticket['version'].startswith('#'):
     234                if req.perm.has_permission("MERGEBOT_MERGE_TICKET"):
     235                    ticket_info['merge'] = is_mergeable(ticket)
     236            else:
     237                if req.perm.has_permission("MERGEBOT_MERGE_RELEASE"):
     238                    ticket_info['merge'] = is_mergeable(ticket)
    202239            # CHECK-MERGE
    203240            if req.perm.has_permission("MERGEBOT_VIEW"):
    204                 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = self._is_checkmergeable(ticket)
     241                ticket_info['checkmerge'] = is_checkmergeable(ticket)
    205242            else:
    206                 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0
     243                ticket_info['checkmerge'] = False
    207244            # BRANCH, REBRANCH
    208245            if req.perm.has_permission("MERGEBOT_BRANCH"):
    209                 req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = self._is_branchable(ticket)
    210                 req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = self._is_rebranchable(ticket)
     246                ticket_info['branch'] = is_branchable(ticket)
     247                ticket_info['rebranch'] = is_rebranchable(ticket)
    211248            else:
    212                 req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = 0
    213                 req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = 0
    214 
    215         # Add debugs:
    216         req.hdf["mergebot.debug"] = len(debugs)
    217         for i in range(len(debugs)):
    218             req.hdf["mergebot.debug.%d" % (i)] = debugs[i]
    219 
    220         return "mergebot.cs", None
    221 
    222     def _is_branchable(self, ticket):
    223         try:
    224             state = ticket['mergebotstate']
    225         except KeyError:
    226             state = ""
    227         return state == "" or state == "merged"
    228     def _is_rebranchable(self, ticket):
    229         # TODO: we should be able to tell if trunk (or version) has had commits
    230         # since we branched, and only mark it as rebranchable if there have
    231         # been.
    232         try:
    233             state = ticket['mergebotstate']
    234         except KeyError:
    235             state = ""
    236         return state in ["branched", "conflicts"]
    237     def _is_mergeable(self, ticket):
    238         try:
    239             state = ticket['mergebotstate']
    240         except KeyError:
    241             state = ""
    242         return state == "branched"
    243     def _is_checkmergeable(self, ticket):
    244         try:
    245             state = ticket['mergebotstate']
    246         except KeyError:
    247             state = ""
    248         return state == "branched" or state == "conflicts"
     249                ticket_info['branch'] = False
     250                ticket_info['rebranch'] = False
     251
     252        # proactive warnings
     253        work_dir = self.env.config.get('mergebot', 'work_dir')
     254        if not os.path.isabs(work_dir):
     255            work_dir = os.path.join(self.env.path, work_dir)
     256        if not os.path.isdir(work_dir):
     257            add_warning(req, 'The Mergebot work directory "%s" does not exist.' % work_dir)
     258
     259        return "mergebot.html", data, None
    249260
    250261    # ITemplateProvider
    251262    def get_htdocs_dirs(self):
    252263        return []
     264
    253265    def get_templates_dirs(self):
    254266        # It appears that everyone does this import here instead of at the top
Note: See TracChangeset for help on using the changeset viewer.