Edgewall Software

Ticket #6135: workflow-post-commit-hook.patch

File workflow-post-commit-hook.patch, 11.2 KB (added by shansen@…, 12 months ago)

workflow-aware version of trac-post-commit-hook

  • trac-post-commit-hook.sh

    old new  
    5353# In addition, the ':' character can be omitted and issue or bug can be used 
    5454# instead of ticket. 
    5555# 
    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.  
    6658# 
    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: 
    69131# 
    70132#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. 
    71133# 
    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. 
    73135 
    74136import re 
    75137import os 
     
    78140 
    79141from trac.env import open_environment 
    80142from trac.ticket.notification import TicketNotifyEmail 
    81 from trac.ticket import Ticket 
     143from trac.ticket import Ticket, TicketSystem 
    82144from trac.ticket.web_ui import TicketModule 
    83145# TODO: move grouped_changelog_entries to model.py 
    84146from trac.util.text import to_unicode 
    85147from trac.util.datefmt import utc 
    86148from trac.versioncontrol.api import NoSuchChangeset 
    87  
     149from trac.perm import PermissionCache 
     150  
    88151from optparse import OptionParser 
    89152 
    90153parser = OptionParser() 
     
    123186command_re = re.compile(ticket_command) 
    124187ticket_re = re.compile(ticket_prefix + '([0-9]+)') 
    125188 
    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 
     193class 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 
    138198 
     199class 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     
    139223    def __init__(self, project=options.project, author=options.user, 
    140224                 rev=options.rev, url=options.url): 
    141225        self.env = open_environment(project) 
    142226        repos = self.env.get_repository() 
    143227        repos.sync() 
    144228         
     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 
    145244        # Instead of bothering with the encoding, we'll use unicode data 
    146245        # as provided by the Trac versioncontrol API (#1310). 
    147246        try: 
     
    157256 
    158257        tickets = {} 
    159258        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: 
    162261                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) 
    165263 
     264        req = DummyRequest(self.env, self.author) 
     265         
    166266        for tkt_id, cmds in tickets.iteritems(): 
    167267            try: 
    168268                db = self.env.get_db_cnx() 
    169269                 
    170270                ticket = Ticket(self.env, int(tkt_id), db) 
    171                 for cmd in cmds: 
    172                     cmd(ticket) 
    173271 
     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                     
    174279                # determine sequence number...  
    175280                cnum = 0 
    176281                tm = TicketModule(self.env) 
    177282                for change in tm.grouped_changelog_entries(ticket, db): 
    178283                    if change['permanent']: 
    179284                        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 
    181304                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1) 
    182305                db.commit() 
    183306                 
    184307                tn = TicketNotifyEmail(self.env) 
    185308                tn.notify(ticket, newticket=0, modtime=self.now) 
     309                                 
    186310            except Exception, e: 
    187                 # import traceback 
    188                 # traceback.print_exc(file=sys.stderr) 
    189311                print>>sys.stderr, 'Unexpected error while processing ticket ' \ 
    190312                                   'ID %s: %s' % (tkt_id, e) 
    191313             
    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 
    200321 
    201322if __name__ == "__main__": 
    202323    if len(sys.argv) < 5: