Changeset 17 for mergebot/trunk/mergebot/web_ui.py
- Timestamp:
- Jun 8, 2009 3:07:47 AM (15 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
mergebot/trunk/mergebot/web_ui.py
r16 r17 1 1 #!/usr/bin/env python 2 2 3 import random 3 import socket 4 import time 5 import os 6 import base64 7 from subprocess import check_call 4 8 5 9 from trac.core import * … … 7 11 from trac.ticket.query import Query 8 12 from trac.ticket.model import Ticket 9 from trac. config import Option13 from trac.env import IEnvironmentSetupParticipant 10 14 from trac.perm import IPermissionRequestor 11 15 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 16 from trac.web.chrome import INavigationContributor, ITemplateProvider, add_warning 17 18 from action_logic import is_branchable, is_rebranchable, is_mergeable, is_checkmergeable 18 19 19 20 class 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() 21 45 22 46 # INavigationContributor … … 24 48 def get_active_navigation_item(self, req): 25 49 return 'mergebot' 50 26 51 def get_navigation_items(self, req): 27 52 """Generator that yields the MergeBot tab, but only if the user has … … 57 82 fields = ['summary', 'component', 'version', 'status'] 58 83 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)) 64 90 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 65 120 66 121 def process_request(self, req): … … 68 123 req.perm.assert_permission("MERGEBOT_VIEW") 69 124 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 71 127 if req.path_info == "/mergebot/redir": 72 128 req.redirect(req.href.mergebot()) 73 129 74 debugs = [] 75 req.hdf["title"] = "MergeBot" 130 data = {} 76 131 77 132 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 = None91 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) 116 171 # First half of a double-redirect to make a refresh not re-send the 117 172 # POST data. … … 120 175 # We want to fill out the information for the page unconditionally. 121 176 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. 122 200 # I need to get a list of tickets. For non-admins, restrict the list 123 201 # 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: 126 207 querystring += "&owner=%s" % (req.authname,) 127 208 query = Query.from_string(self.env, querystring, order="id") 128 209 columns = query.get_columns() 129 for name in ("component", "version", "mergebotstate"):210 for name in required_columns: 130 211 if name not in columns: 131 212 columns.append(name) 132 #debugs.append("query.fields = %s" % str(query.fields))133 213 db = self.env.get_db_cnx() 134 214 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'] = [] 140 216 for ticket in tickets: 141 ticketinfo[ticket['id']] = ticket142 #debugs.append(str(ticketinfo))143 144 availableTickets = tickets[:]145 queued_tickets = []146 # We currently have 4 queues, "branch", "rebranch", "checkmerge", and147 # "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, )] = queuename157 if status:158 # status[0] is ticketnum159 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)] = value166 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)] = value178 #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 with181 # flags for which buttons should be enabled for each ticket.182 for ticket in availableTickets:183 217 ticketnum = ticket['id'] 184 218 if ticketnum in queued_tickets: 185 219 # Don't allow more actions to be taken on a ticket that is 186 220 # 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 188 223 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 192 228 # Select what actions this user may make on this ticket based on 193 229 # its current state. 230 194 231 # MERGE 195 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = 0196 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) 202 239 # CHECK-MERGE 203 240 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) 205 242 else: 206 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0243 ticket_info['checkmerge'] = False 207 244 # BRANCH, REBRANCH 208 245 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) 211 248 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 249 260 250 261 # ITemplateProvider 251 262 def get_htdocs_dirs(self): 252 263 return [] 264 253 265 def get_templates_dirs(self): 254 266 # It appears that everyone does this import here instead of at the top
Note: See TracChangeset
for help on using the changeset viewer.