#!/usr/bin/env python import random from trac.core import * from trac import util from trac.ticket.query import Query from trac.ticket.model import Ticket from trac.config import Option from trac.perm import IPermissionRequestor from trac.web.main import IRequestHandler from trac.web.chrome import INavigationContributor, ITemplateProvider from MergeActor import MergeActor from BranchActor import BranchActor from RebranchActor import RebranchActor from CheckMergeActor import CheckMergeActor class MergeBotModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider) # INavigationContributor # so it shows up in the main nav bar def get_active_navigation_item(self, req): return 'mergebot' def get_navigation_items(self, req): """Generator that yields the MergeBot tab, but only if the user has MERGEBOT_VIEW privs.""" if req.perm.has_permission("MERGEBOT_VIEW"): label = util.Markup('MergeBot' % \ req.href.mergebot()) yield ('mainnav', 'mergebot', label) # IPermissionRequestor methods # So we can control access to this functionality def get_permission_actions(self): """Returns a permission structure.""" actions = ["MERGEBOT_VIEW", "MERGEBOT_BRANCH", "MERGEBOT_MERGE_TICKET", "MERGEBOT_MERGE_RELEASE"] # MERGEBOT_ADMIN implies all of the above permissions allactions = actions + [ ("MERGEBOT_ADMIN", actions), ("MERGEBOT_BRANCH", ["MERGEBOT_VIEW"]), ("MERGEBOT_MERGE_TICKET", ["MERGEBOT_VIEW"]), ("MERGEBOT_MERGE_RELEASE", ["MERGEBOT_VIEW"]) ] return allactions # IRequestHandler def match_request(self, req): """Returns true, if the given request path is handled by this module""" # For now, we don't recognize any arguments... return req.path_info == "/mergebot" or req.path_info.startswith("/mergebot/") def _get_ticket_info(self, ticketid): # grab the ticket info we care about fields = ['summary', 'component', 'version', 'status'] info = {} ticket = Ticket(self.env, ticketid) for field in fields: info[field] = ticket[field] info['href'] = self.env.href.ticket(ticketid) self.log.debug("id=%s, info=%r" % (ticketid, info)) return info def process_request(self, req): """This is the function called when a user requests a mergebot page.""" req.perm.assert_permission("MERGEBOT_VIEW") # 2nd redirect back to the real mergebot page To address POST if req.path_info == "/mergebot/redir": req.redirect(req.href.mergebot()) debugs = [] req.hdf["title"] = "MergeBot" if req.method == "POST": # The user hit a button #debugs += [ # "POST", # "Branching ticket %s" % req.args, #] ticketnum = req.args['ticket'] component = req.args['component'] version = req.args['version'] requestor = req.authname or "anonymous" ticket = Ticket(self.env, int(ticketnum)) # FIXME: check for 'todo' key? # If the request is not valid, just ignore it. actor = None if req.args['action'] == "Branch": req.perm.assert_permission("MERGEBOT_BRANCH") if self._is_branchable(ticket): actor = BranchActor(self.env) elif req.args['action'] == "Rebranch": req.perm.assert_permission("MERGEBOT_BRANCH") if self._is_rebranchable(ticket): actor = RebranchActor(self.env) elif req.args['action'] == "CheckMerge": req.perm.assert_permission("MERGEBOT_VIEW") if self._is_checkmergeable(ticket): actor = CheckMergeActor(self.env) elif req.args['action'] == "Merge": if version.startswith("#"): req.perm.assert_permission("MERGEBOT_MERGE_TICKET") else: req.perm.assert_permission("MERGEBOT_MERGE_RELEASE") if self._is_mergeable(ticket): actor = MergeActor(self.env) if actor: actor.AddTask([ticketnum, component, version, requestor]) try: actor.Run() # Starts processing deamon. except Exception, e: self.log.exception(e) # First half of a double-redirect to make a refresh not re-send the # POST data. req.redirect(req.href.mergebot("redir")) # We want to fill out the information for the page unconditionally. # I need to get a list of tickets. For non-admins, restrict the list # to the tickets owned by that user. querystring = "status=new|assigned|reopened&version!=" if not req.perm.has_permission("MERGEBOT_ADMIN"): querystring += "&owner=%s" % (req.authname,) query = Query.from_string(self.env, querystring, order="id") columns = query.get_columns() for name in ("component", "version", "mergebotstate"): if name not in columns: columns.append(name) #debugs.append("query.fields = %s" % str(query.fields)) db = self.env.get_db_cnx() tickets = query.execute(req, db) #debugs += map(str, tickets) req.hdf['mergebot.ticketcount'] = str(len(tickets)) # Make the tickets indexable by ticket id number: ticketinfo = {} for ticket in tickets: ticketinfo[ticket['id']] = ticket #debugs.append(str(ticketinfo)) availableTickets = tickets[:] queued_tickets = [] # We currently have 4 queues, "branch", "rebranch", "checkmerge", and # "merge" queues = [ ("branch", BranchActor), ("rebranch", RebranchActor), ("checkmerge", CheckMergeActor), ("merge", MergeActor) ] for queuename, actor in queues: status, queue = actor(self.env).GetStatus() req.hdf["mergebot.queue.%s" % (queuename, )] = queuename if status: # status[0] is ticketnum req.hdf["mergebot.queue.%s.current" % (queuename)] = status[0] req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = status[3] ticketnum = int(status[0]) queued_tickets.append(ticketnum) ticket = self._get_ticket_info(ticketnum) for field, value in ticket.items(): req.hdf["mergebot.queue.%s.current.%s" % (queuename, field)] = value else: req.hdf["mergebot.queue.%s.current" % (queuename)] = "" req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = "" for i in range(len(queue)): ticketnum = int(queue[i][0]) queued_tickets.append(ticketnum) req.hdf["mergebot.queue.%s.queue.%d" % (queuename, i)] = str(ticketnum) req.hdf["mergebot.queue.%s.queue.%d.requestor" % (queuename, i)] = queue[i][3] ticket = self._get_ticket_info(ticketnum) for field, value in ticket.items(): req.hdf["mergebot.queue.%s.queue.%d.%s" % (queuename, i, field)] = value #debugs.append("%s queue %d, ticket #%d, %s = %s" % (queuename, i, ticketnum, field, value)) # Provide the list of tickets at the bottom of the page, along with # flags for which buttons should be enabled for each ticket. for ticket in availableTickets: ticketnum = ticket['id'] if ticketnum in queued_tickets: # Don't allow more actions to be taken on a ticket that is # currently queued for something. In the future, we may want # to support a 'Cancel' button, though. continue req.hdf["mergebot.notqueued.%d" % (ticketnum)] = str(ticketnum) for field, value in ticket.items(): req.hdf["mergebot.notqueued.%d.%s" % (ticketnum, field)] = value # Select what actions this user may make on this ticket based on # its current state. # MERGE req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = 0 if req.perm.has_permission("MERGEBOT_MERGE_RELEASE") and \ not ticket['version'].startswith("#"): req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket) if req.perm.has_permission("MERGEBOT_MERGE_TICKET") and \ ticket['version'].startswith("#"): req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket) # CHECK-MERGE if req.perm.has_permission("MERGEBOT_VIEW"): req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = self._is_checkmergeable(ticket) else: req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0 # BRANCH, REBRANCH if req.perm.has_permission("MERGEBOT_BRANCH"): req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = self._is_branchable(ticket) req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = self._is_rebranchable(ticket) else: req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = 0 req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = 0 # Add debugs: req.hdf["mergebot.debug"] = len(debugs) for i in range(len(debugs)): req.hdf["mergebot.debug.%d" % (i)] = debugs[i] return "mergebot.cs", None def _is_branchable(self, ticket): try: state = ticket['mergebotstate'] except KeyError: state = "" return state == "" or state == "merged" def _is_rebranchable(self, ticket): # TODO: we should be able to tell if trunk (or version) has had commits # since we branched, and only mark it as rebranchable if there have # been. try: state = ticket['mergebotstate'] except KeyError: state = "" return state in ["branched", "conflicts"] def _is_mergeable(self, ticket): try: state = ticket['mergebotstate'] except KeyError: state = "" return state == "branched" def _is_checkmergeable(self, ticket): try: state = ticket['mergebotstate'] except KeyError: state = "" return state == "branched" or state == "conflicts" # ITemplateProvider def get_htdocs_dirs(self): return [] def get_templates_dirs(self): # It appears that everyone does this import here instead of at the top # level... I'm not sure I understand why... from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] # vim:foldmethod=indent foldcolumn=8 # vim:softtabstop=4 shiftwidth=4 tabstop=4 expandtab