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 |
---|