[16] | 1 | #!/usr/bin/env python |
---|
| 2 | |
---|
| 3 | import random |
---|
| 4 | |
---|
| 5 | from trac.core import * |
---|
| 6 | from trac import util |
---|
| 7 | from trac.ticket.query import Query |
---|
| 8 | from trac.ticket.model import Ticket |
---|
| 9 | from trac.config import Option |
---|
| 10 | from trac.perm import IPermissionRequestor |
---|
| 11 | from 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 |
---|
| 18 | |
---|
| 19 | class MergeBotModule(Component): |
---|
| 20 | implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider) |
---|
| 21 | |
---|
| 22 | # INavigationContributor |
---|
| 23 | # so it shows up in the main nav bar |
---|
| 24 | def get_active_navigation_item(self, req): |
---|
| 25 | return 'mergebot' |
---|
| 26 | def get_navigation_items(self, req): |
---|
| 27 | """Generator that yields the MergeBot tab, but only if the user has |
---|
| 28 | MERGEBOT_VIEW privs.""" |
---|
| 29 | if req.perm.has_permission("MERGEBOT_VIEW"): |
---|
| 30 | label = util.Markup('<a href="%s">MergeBot</a>' % \ |
---|
| 31 | req.href.mergebot()) |
---|
| 32 | yield ('mainnav', 'mergebot', label) |
---|
| 33 | |
---|
| 34 | # IPermissionRequestor methods |
---|
| 35 | # So we can control access to this functionality |
---|
| 36 | def get_permission_actions(self): |
---|
| 37 | """Returns a permission structure.""" |
---|
| 38 | actions = ["MERGEBOT_VIEW", "MERGEBOT_BRANCH", "MERGEBOT_MERGE_TICKET", |
---|
| 39 | "MERGEBOT_MERGE_RELEASE"] |
---|
| 40 | # MERGEBOT_ADMIN implies all of the above permissions |
---|
| 41 | allactions = actions + [ |
---|
| 42 | ("MERGEBOT_ADMIN", actions), |
---|
| 43 | ("MERGEBOT_BRANCH", ["MERGEBOT_VIEW"]), |
---|
| 44 | ("MERGEBOT_MERGE_TICKET", ["MERGEBOT_VIEW"]), |
---|
| 45 | ("MERGEBOT_MERGE_RELEASE", ["MERGEBOT_VIEW"]) |
---|
| 46 | ] |
---|
| 47 | return allactions |
---|
| 48 | |
---|
| 49 | # IRequestHandler |
---|
| 50 | def match_request(self, req): |
---|
| 51 | """Returns true, if the given request path is handled by this module""" |
---|
| 52 | # For now, we don't recognize any arguments... |
---|
| 53 | return req.path_info == "/mergebot" or req.path_info.startswith("/mergebot/") |
---|
| 54 | |
---|
| 55 | def _get_ticket_info(self, ticketid): |
---|
| 56 | # grab the ticket info we care about |
---|
| 57 | fields = ['summary', 'component', 'version', 'status'] |
---|
| 58 | 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)) |
---|
| 64 | return info |
---|
| 65 | |
---|
| 66 | def process_request(self, req): |
---|
| 67 | """This is the function called when a user requests a mergebot page.""" |
---|
| 68 | req.perm.assert_permission("MERGEBOT_VIEW") |
---|
| 69 | |
---|
| 70 | # 2nd redirect back to the real mergebot page To address POST |
---|
| 71 | if req.path_info == "/mergebot/redir": |
---|
| 72 | req.redirect(req.href.mergebot()) |
---|
| 73 | |
---|
| 74 | debugs = [] |
---|
| 75 | req.hdf["title"] = "MergeBot" |
---|
| 76 | |
---|
| 77 | 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) |
---|
| 116 | # First half of a double-redirect to make a refresh not re-send the |
---|
| 117 | # POST data. |
---|
| 118 | req.redirect(req.href.mergebot("redir")) |
---|
| 119 | |
---|
| 120 | # We want to fill out the information for the page unconditionally. |
---|
| 121 | |
---|
| 122 | # I need to get a list of tickets. For non-admins, restrict the list |
---|
| 123 | # to the tickets owned by that user. |
---|
| 124 | querystring = "status=new|assigned|reopened&version!=" |
---|
| 125 | if not req.perm.has_permission("MERGEBOT_ADMIN"): |
---|
| 126 | querystring += "&owner=%s" % (req.authname,) |
---|
| 127 | query = Query.from_string(self.env, querystring, order="id") |
---|
| 128 | columns = query.get_columns() |
---|
| 129 | for name in ("component", "version", "mergebotstate"): |
---|
| 130 | if name not in columns: |
---|
| 131 | columns.append(name) |
---|
| 132 | #debugs.append("query.fields = %s" % str(query.fields)) |
---|
| 133 | db = self.env.get_db_cnx() |
---|
| 134 | 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 = {} |
---|
| 140 | 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: |
---|
| 183 | ticketnum = ticket['id'] |
---|
| 184 | if ticketnum in queued_tickets: |
---|
| 185 | # Don't allow more actions to be taken on a ticket that is |
---|
| 186 | # currently queued for something. In the future, we may want |
---|
| 187 | # to support a 'Cancel' button, though. |
---|
| 188 | 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 |
---|
| 192 | # Select what actions this user may make on this ticket based on |
---|
| 193 | # its current state. |
---|
| 194 | # 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) |
---|
| 202 | # CHECK-MERGE |
---|
| 203 | if req.perm.has_permission("MERGEBOT_VIEW"): |
---|
| 204 | req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = self._is_checkmergeable(ticket) |
---|
| 205 | else: |
---|
| 206 | req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0 |
---|
| 207 | # BRANCH, REBRANCH |
---|
| 208 | 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) |
---|
| 211 | 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 | |
---|
| 250 | # ITemplateProvider |
---|
| 251 | def get_htdocs_dirs(self): |
---|
| 252 | return [] |
---|
| 253 | def get_templates_dirs(self): |
---|
| 254 | # It appears that everyone does this import here instead of at the top |
---|
| 255 | # level... I'm not sure I understand why... |
---|
| 256 | from pkg_resources import resource_filename |
---|
| 257 | return [resource_filename(__name__, 'templates')] |
---|
| 258 | |
---|
| 259 | # vim:foldmethod=indent foldcolumn=8 |
---|
| 260 | # vim:softtabstop=4 shiftwidth=4 tabstop=4 expandtab |
---|