Ticket #6135: workflow-post-commit-hook.patch
| File workflow-post-commit-hook.patch, 11.2 KB (added by shansen@…, 12 months ago) |
|---|
-
trac-post-commit-hook.sh
old new 53 53 # In addition, the ':' character can be omitted and issue or bug can be used 54 54 # instead of ticket. 55 55 # 56 # You can have more then one command in a message. The following commands 57 # are supported. There is more then one spelling for each command, to make 58 # this as user-friendly as possible. 59 # 60 # close, closed, closes, fix, fixed, fixes 61 # The specified issue numbers are closed with the contents of this 62 # commit message being added to it. 63 # references, refs, addresses, re, see 64 # The specified issue numbers are left in their current status, but 65 # the contents of this commit message are added to their notes. 56 # The hook is configured in the environment's trac.ini's svn-post-commit-hook 57 # section. 66 58 # 67 # A fairly complicated example of what you can do is with a commit message 68 # of: 59 # You specify commands in terms of groups. An example set of commands would 60 # be: 61 # 62 # [svn-post-commit-hook] 63 # command_groups = close, refs 64 # close_commands = close, closed, closes, fix, fixes, fixed 65 # refs_commands = references, refs, addresses, re, see 66 # 67 # For the purpose of the above settings, 'close' and 'closes' and 'fixes' 68 # are all considered identical. Many options are provided to make it as 69 # user-friendly as possible to use. 70 # 71 # As long as at least one command is included in the commit message, the 72 # entire contents of the message will be added to the specified ticket. 73 # 74 # You can have more then one command in a message. 75 # 76 # In addition to this, you may specify a search list of workflow actions 77 # that should be performed if available on the ticket. The first matching 78 # action found will be executed; if none of the actions are found in the 79 # ticket, the hook will simply give up quietly. Its not considered an 80 # error. 81 # 82 # Actions are specified as a list named <group>_actions. So the actions 83 # that you want to be executed for commands in close_commands you would 84 # specify in close_actions. This is optional; if there are no actions 85 # then the command will cause the commit message to be added, and all is 86 # fine. 87 # 88 # 89 # A more complete example that can be used in the basic workflow would be: 90 # 91 # [svn-post-commit-hook] 92 # command_groups = close, refs 93 # close_commands = close, closed, closes, fix, fixes, fixed 94 # refs_commands = references, refs, addresses, re, see 95 # close_actions = resolve 96 # 97 # TODO: How to handle the action 'operations', and in particular from 98 # above set_resolution? Perhaps allow "close_actions = resolve=fixed" 99 # I don't know the API to pass such into the workflow API. Must dig. 100 # 101 # A fairly complicated example of what you can do is in an enterprise or 102 # more complicated workflow would be: 103 # 104 # [svn-post-commit-hook] 105 # command_groups = close, answer, refs 106 # close_commands = close, closed, closes, fix, fixes, fixed 107 # refs_commands = references, refs, addresses, re, see 108 # answer_commands = answer, answers 109 # answer_actions = provideinfo, provideinfo_new 110 # close_actions = test, resolve 111 # 112 # In the above example, we assume that there are two statuses, "infoneeded" 113 # and "infoneeded_new". The former has an action called "provideinfo" that 114 # will send the ticket back to Assigned; the latter has an action called 115 # "provideinfo_new" that will set the status back to New. 116 # 117 # If a commit message has "answers #4", the hook will see if ticket #4 has 118 # a provideinfo action; if so it performs it and the ticket becomes Assigned. 119 # If it has provideinfo_new instead, the ticket becomes New. Otherwise the 120 # ticket is left alone as nothing was specified that is understood in terms 121 # of the workflow. 122 # 123 # The close action will first try to see if the ticket is in a position to 124 # be sent to QA via the test action; failing that it'll see if it can be 125 # resolved. 126 # 127 # As with the previous examples, the reference commands do nothing special. 128 # 129 # Given the above, the following more complicated message shows how the 130 # hook will try to easily parse through what you intend: 69 131 # 70 132 # Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. 71 133 # 72 # This will close #10 and #12, and add a note to #12.134 # This will send #10 and #12 to QA, and add a note to #12. 73 135 74 136 import re 75 137 import os … … 78 140 79 141 from trac.env import open_environment 80 142 from trac.ticket.notification import TicketNotifyEmail 81 from trac.ticket import Ticket 143 from trac.ticket import Ticket, TicketSystem 82 144 from trac.ticket.web_ui import TicketModule 83 145 # TODO: move grouped_changelog_entries to model.py 84 146 from trac.util.text import to_unicode 85 147 from trac.util.datefmt import utc 86 148 from trac.versioncontrol.api import NoSuchChangeset 87 149 from trac.perm import PermissionCache 150 88 151 from optparse import OptionParser 89 152 90 153 parser = OptionParser() … … 123 186 command_re = re.compile(ticket_command) 124 187 ticket_re = re.compile(ticket_prefix + '([0-9]+)') 125 188 126 class CommitHook: 127 _supported_cmds = {'close': '_cmdClose', 128 'closed': '_cmdClose', 129 'closes': '_cmdClose', 130 'fix': '_cmdClose', 131 'fixed': '_cmdClose', 132 'fixes': '_cmdClose', 133 'addresses': '_cmdRefs', 134 're': '_cmdRefs', 135 'references': '_cmdRefs', 136 'refs': '_cmdRefs', 137 'see': '_cmdRefs'} 189 # TODO: This is ugly but the workflow API expects a Request object with a PermissionCache 190 # on it to check for the allowability of actions, and I can't see a better way to get 191 # about it. 192 193 class DummyRequest: 194 def __init__(self, env, username): 195 self.perm = PermissionCache(env, username) 196 # close_commands = close, closed, closes, fix, fixes, fixed 197 # refs_commands = references, refs, addresses, re, see 138 198 199 class CommitHook: 200 # The defaults are suitable to mimic previous behavior in an environment that is 201 # using the 'basic' workflow. 202 203 default_supported_commands = { 204 "close": "close", 205 "closes": "close", 206 "closed": "close", 207 "fix": "close", 208 "fixes": "close", 209 "fixed": "close", 210 211 "references": "refs", 212 "refs": "refs", 213 "addresses": "refs", 214 "re": "refs", 215 "see": "refs" 216 } 217 218 default_command_actions = { 219 "close": ["resolve"], 220 "refs": [] 221 } 222 139 223 def __init__(self, project=options.project, author=options.user, 140 224 rev=options.rev, url=options.url): 141 225 self.env = open_environment(project) 142 226 repos = self.env.get_repository() 143 227 repos.sync() 144 228 229 supported_commands = {} 230 command_actions = {} 231 for group in self.env.config.get('svn-post-commit-hook', 'command_groups').split(','): 232 group = group.strip() 233 234 if not group: 235 continue 236 237 for command in self.env.config.get('svn-post-commit-hook', '%s_commands' % group).split(','): 238 supported_commands[command.strip()] = group 239 command_actions[group] = self.env.config.get('svn-post-commit-hook', '%s_actions' % group, '').split(',') 240 241 supported_commands = supported_commands or self.default_supported_commands 242 command_actions = command_actions or self.default_command_actions 243 145 244 # Instead of bothering with the encoding, we'll use unicode data 146 245 # as provided by the Trac versioncontrol API (#1310). 147 246 try: … … 157 256 158 257 tickets = {} 159 258 for cmd, tkts in cmd_groups: 160 funcname = CommitHook._supported_cmds.get(cmd.lower(), '')161 if funcname:259 command_group = supported_commands.get(cmd.lower(), '') 260 if command_group: 162 261 for tkt_id in ticket_re.findall(tkts): 163 func = getattr(self, funcname) 164 tickets.setdefault(tkt_id, []).append(func) 262 tickets.setdefault(tkt_id, []).append(command_group) 165 263 264 req = DummyRequest(self.env, self.author) 265 166 266 for tkt_id, cmds in tickets.iteritems(): 167 267 try: 168 268 db = self.env.get_db_cnx() 169 269 170 270 ticket = Ticket(self.env, int(tkt_id), db) 171 for cmd in cmds:172 cmd(ticket)173 271 272 # If multiple commands are given for a particular ticket, then the 273 # first one that has actions set are the actions that will be 274 # executed. 275 desiredActions = [] 276 for cmd in cmds: 277 desiredActions = desiredActions or command_actions.get(cmd, None) 278 174 279 # determine sequence number... 175 280 cnum = 0 176 281 tm = TicketModule(self.env) 177 282 for change in tm.grouped_changelog_entries(ticket, db): 178 283 if change['permanent']: 179 284 cnum += 1 180 285 286 # Determine all the actions that the ticket has available 287 availableActions = TicketSystem(self.env).get_available_actions(req, ticket) 288 289 # See if any actions we want are actually available 290 chosenAction = None 291 for action in availableActions: 292 if action in desiredActions: 293 chosenAction = action 294 break 295 296 if chosenAction: 297 controllers = self._get_action_controllers(req, ticket, chosenAction) 298 for controller in controllers: 299 changes = controller.get_ticket_changes(req, ticket, action) 300 301 for key, value in changes.items(): 302 ticket[key] = value 303 181 304 ticket.save_changes(self.author, self.msg, self.now, db, cnum+1) 182 305 db.commit() 183 306 184 307 tn = TicketNotifyEmail(self.env) 185 308 tn.notify(ticket, newticket=0, modtime=self.now) 309 186 310 except Exception, e: 187 # import traceback188 # traceback.print_exc(file=sys.stderr)189 311 print>>sys.stderr, 'Unexpected error while processing ticket ' \ 190 312 'ID %s: %s' % (tkt_id, e) 191 313 192 193 def _cmdClose(self, ticket): 194 ticket['status'] = 'closed' 195 ticket['resolution'] = 'fixed' 196 197 def _cmdRefs(self, ticket): 198 pass 199 314 def _get_action_controllers(self, req, ticket, action): 315 """Generator yielding the controllers handling the given `action`""" 316 for controller in TicketSystem(self.env).action_controllers: 317 actions = [a for w,a in 318 controller.get_ticket_actions(req, ticket)] 319 if action in actions: 320 yield controller 200 321 201 322 if __name__ == "__main__": 202 323 if len(sys.argv) < 5:
