• R/O
  • SSH

traclightning: コミット

traclightningのリポジトリ


コミットメタ情報

リビジョンf974fa9c8b20e3c00ec110d68ae1b35a3a0a7503 (tree)
日時2011-05-30 18:37:31
作者kanu_orz
コミッターkanu_orz

ログメッセージ

update AdvancedTicketWorkflowPlugin to 0.11

変更サマリ

差分

diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/.svn/all-wcprops
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/.svn/all-wcprops Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,17 @@
1+K 25
2+svn:wc:ra_dav:version-url
3+V 52
4+/svn/!svn/ver/9962/advancedticketworkflowplugin/0.12
5+END
6+setup.py
7+K 25
8+svn:wc:ra_dav:version-url
9+V 61
10+/svn/!svn/ver/9961/advancedticketworkflowplugin/0.12/setup.py
11+END
12+setup.cfg
13+K 25
14+svn:wc:ra_dav:version-url
15+V 62
16+/svn/!svn/ver/9955/advancedticketworkflowplugin/0.12/setup.cfg
17+END
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/.svn/dir-prop-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/.svn/dir-prop-base Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,8 @@
1+K 10
2+svn:ignore
3+V 22
4+build
5+dist
6+*.egg-info
7+
8+END
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/.svn/entries
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/.svn/entries Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,99 @@
1+10
2+
3+dir
4+10247
5+http://trac-hacks.org/svn/advancedticketworkflowplugin/0.12
6+http://trac-hacks.org/svn
7+
8+
9+
10+2011-03-16T14:57:39.379328Z
11+9962
12+retracile
13+has-props
14+
15+
16+
17+
18+
19+
20+
21+
22+
23+
24+
25+
26+
27+7322e99d-02ea-0310-aa39-e9a107903beb
28+
29+advancedworkflow
30+dir
31+
32+setup.py
33+file
34+
35+
36+
37+
38+2011-03-16T14:33:31.665995Z
39+0626f7ceb43b73a4c878ef6239cfa25e
40+2011-03-16T14:33:31.665995Z
41+9961
42+retracile
43+
44+
45+
46+
47+
48+
49+
50+
51+
52+
53+
54+
55+
56+
57+
58+
59+
60+
61+
62+
63+
64+703
65+
66+setup.cfg
67+file
68+
69+
70+
71+
72+2008-05-06T03:24:37.734833Z
73+ced32e2c28d3b229c8aad7c585425ccf
74+2008-05-06T03:24:37.734833Z
75+3611
76+retracile
77+
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
89+
90+
91+
92+
93+
94+
95+
96+
97+
98+51
99+
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/.svn/text-base/setup.cfg.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/.svn/text-base/setup.cfg.svn-base Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,3 @@
1+[egg_info]
2+tag_build = dev
3+tag_svn_revision = true
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/.svn/text-base/setup.py.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/.svn/text-base/setup.py.svn-base Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,20 @@
1+#!/usr/bin/env python
2+
3+from setuptools import setup, find_packages
4+
5+setup(
6+ name='AdvancedTicketWorkflowPlugin',
7+ version='0.11',
8+ author = 'Eli Carter',
9+ author_email = 'elicarter@retracile.net',
10+ license='BSD',
11+ description = 'Advanced workflow operations Trac plugin',
12+ long_description = 'Provides more advanced workflow operations for Trac 0.12',
13+ url = 'http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin',
14+
15+ packages = find_packages(),
16+ package_data = {},
17+ entry_points = {'trac.plugins':['advancedworkflow.controller = advancedworkflow.controller']},
18+ install_requires = [],
19+ #zip_safe = False,
20+)
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/advancedworkflow/.svn/all-wcprops
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/advancedworkflow/.svn/all-wcprops Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,17 @@
1+K 25
2+svn:wc:ra_dav:version-url
3+V 69
4+/svn/!svn/ver/9962/advancedticketworkflowplugin/0.12/advancedworkflow
5+END
6+__init__.py
7+K 25
8+svn:wc:ra_dav:version-url
9+V 81
10+/svn/!svn/ver/9955/advancedticketworkflowplugin/0.12/advancedworkflow/__init__.py
11+END
12+controller.py
13+K 25
14+svn:wc:ra_dav:version-url
15+V 83
16+/svn/!svn/ver/9962/advancedticketworkflowplugin/0.12/advancedworkflow/controller.py
17+END
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/advancedworkflow/.svn/dir-prop-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/advancedworkflow/.svn/dir-prop-base Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,7 @@
1+K 10
2+svn:ignore
3+V 12
4+*.pyc
5+*.pyo
6+
7+END
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/advancedworkflow/.svn/entries
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/advancedworkflow/.svn/entries Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,96 @@
1+10
2+
3+dir
4+10247
5+http://trac-hacks.org/svn/advancedticketworkflowplugin/0.12/advancedworkflow
6+http://trac-hacks.org/svn
7+
8+
9+
10+2011-03-16T14:57:39.379328Z
11+9962
12+retracile
13+has-props
14+
15+
16+
17+
18+
19+
20+
21+
22+
23+
24+
25+
26+
27+7322e99d-02ea-0310-aa39-e9a107903beb
28+
29+__init__.py
30+file
31+
32+
33+
34+
35+2011-05-30T09:34:41.187500Z
36+d41d8cd98f00b204e9800998ecf8427e
37+2008-05-06T03:24:37.734833Z
38+3611
39+retracile
40+
41+
42+
43+
44+
45+
46+
47+
48+
49+
50+
51+
52+
53+
54+
55+
56+
57+
58+
59+
60+
61+0
62+
63+controller.py
64+file
65+
66+
67+
68+
69+2011-03-16T14:57:39.379328Z
70+831e72b9f8950209e1291d01c543ab48
71+2011-03-16T14:57:39.379328Z
72+9962
73+retracile
74+
75+
76+
77+
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
89+
90+
91+
92+
93+
94+
95+19213
96+
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/advancedworkflow/.svn/text-base/controller.py.svn-base
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/advancedticketworkflow/advancedworkflow/.svn/text-base/controller.py.svn-base Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,511 @@
1+"""Trac plugin that provides a number of advanced operations for customizable
2+workflows.
3+"""
4+
5+import os
6+import time
7+from datetime import datetime
8+from subprocess import call
9+from genshi.builder import tag
10+
11+from trac.core import implements, Component
12+from trac.ticket import model
13+from trac.ticket.api import ITicketActionController, TicketSystem
14+from trac.ticket.default_workflow import ConfigurableTicketWorkflow
15+from trac.ticket.model import Milestone
16+from trac.ticket.notification import TicketNotifyEmail
17+from trac.resource import ResourceNotFound
18+from trac.util.datefmt import utc
19+from trac.web.chrome import add_warning
20+
21+
22+class TicketWorkflowOpBase(Component):
23+ """Abstract base class for 'simple' ticket workflow operations."""
24+
25+ implements(ITicketActionController)
26+ abstract = True
27+
28+ _op_name = None # Must be specified.
29+
30+ def get_configurable_workflow(self):
31+ controllers = TicketSystem(self.env).action_controllers
32+ for controller in controllers:
33+ if isinstance(controller, ConfigurableTicketWorkflow):
34+ return controller
35+ return ConfigurableTicketWorkflow(self.env)
36+
37+ # ITicketActionController methods
38+
39+ def get_ticket_actions(self, req, ticket):
40+ """Finds the actions that use this operation"""
41+ controller = self.get_configurable_workflow()
42+ return controller.get_actions_by_operation_for_req(req, ticket,
43+ self._op_name)
44+
45+ def get_all_status(self):
46+ """Provide any additional status values"""
47+ # We don't have anything special here; the statuses will be recognized
48+ # by the default controller.
49+ return []
50+
51+ # This should most likely be overridden to be more functional
52+ def render_ticket_action_control(self, req, ticket, action):
53+ """Returns the action control"""
54+ actions = self.get_configurable_workflow().actions
55+ label = actions[action]['name']
56+ return (label, tag(''), '')
57+
58+ def get_ticket_changes(self, req, ticket, action):
59+ """Must be implemented in subclasses"""
60+ raise NotImplementedError
61+
62+ def apply_action_side_effects(self, req, ticket, action):
63+ """No side effects"""
64+ pass
65+
66+
67+class TicketWorkflowOpOwnerReporter(TicketWorkflowOpBase):
68+ """Sets the owner to the reporter of the ticket.
69+
70+ needinfo = * -> needinfo
71+ needinfo.name = Need info
72+ needinfo.operations = set_owner_to_reporter
73+
74+
75+ Don't forget to add the `TicketWorkflowOpOwnerReporter` to the workflow
76+ option in [ticket].
77+ If there is no workflow option, the line will look like this:
78+
79+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerReporter
80+ """
81+
82+ _op_name = 'set_owner_to_reporter'
83+
84+ # ITicketActionController methods
85+
86+ def render_ticket_action_control(self, req, ticket, action):
87+ """Returns the action control"""
88+ actions = self.get_configurable_workflow().actions
89+ label = actions[action]['name']
90+ hint = 'The owner will change to %s' % ticket['reporter']
91+ control = tag('')
92+ return (label, control, hint)
93+
94+ def get_ticket_changes(self, req, ticket, action):
95+ """Returns the change of owner."""
96+ return {'owner': ticket['reporter']}
97+
98+
99+class TicketWorkflowOpOwnerComponent(TicketWorkflowOpBase):
100+ """Sets the owner to the default owner for the component.
101+
102+ <someaction>.operations = set_owner_to_component_owner
103+
104+ Don't forget to add the `TicketWorkflowOpOwnerComponent` to the workflow
105+ option in [ticket].
106+ If there is no workflow option, the line will look like this:
107+
108+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerComponent
109+ """
110+
111+ _op_name = 'set_owner_to_component_owner'
112+
113+ # ITicketActionController methods
114+
115+ def render_ticket_action_control(self, req, ticket, action):
116+ """Returns the action control"""
117+ actions = self.get_configurable_workflow().actions
118+ label = actions[action]['name']
119+ hint = 'The owner will change to %s' % self._new_owner(ticket)
120+ control = tag('')
121+ return (label, control, hint)
122+
123+ def get_ticket_changes(self, req, ticket, action):
124+ """Returns the change of owner."""
125+ return {'owner': self._new_owner(ticket)}
126+
127+ def _new_owner(self, ticket):
128+ """Determines the new owner"""
129+ component = model.Component(self.env, name=ticket['component'])
130+ self.env.log.debug("component %s, owner %s" % (component, component.owner))
131+ return component.owner
132+
133+
134+class TicketWorkflowOpOwnerField(TicketWorkflowOpBase):
135+ """Sets the owner to the value of a ticket field
136+
137+ <someaction>.operations = set_owner_to_field
138+ <someaction>.set_owner_to_field = myfield
139+
140+ Don't forget to add the `TicketWorkflowOpOwnerField` to the workflow
141+ option in [ticket].
142+ If there is no workflow option, the line will look like this:
143+
144+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerField
145+ """
146+
147+ _op_name = 'set_owner_to_field'
148+
149+ # ITicketActionController methods
150+
151+ def render_ticket_action_control(self, req, ticket, action):
152+ """Returns the action control"""
153+ actions = self.get_configurable_workflow().actions
154+ label = actions[action]['name']
155+ hint = 'The owner will change to %s' % self._new_owner(action, ticket)
156+ control = tag('')
157+ return (label, control, hint)
158+
159+ def get_ticket_changes(self, req, ticket, action):
160+ """Returns the change of owner."""
161+ return {'owner': self._new_owner(action, ticket)}
162+
163+ def _new_owner(self, action, ticket):
164+ """Determines the new owner"""
165+ # Should probably do some sanity checking...
166+ field = self.config.get('ticket-workflow',
167+ action + '.' + self._op_name).strip()
168+ return ticket[field]
169+
170+
171+class TicketWorkflowOpOwnerPrevious(TicketWorkflowOpBase):
172+ """Sets the owner to the previous owner
173+
174+ Don't forget to add the `TicketWorkflowOpOwnerPrevious` to the workflow
175+ option in [ticket].
176+ If there is no workflow option, the line will look like this:
177+
178+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerPrevious
179+ """
180+
181+ _op_name = 'set_owner_to_previous'
182+
183+ # ITicketActionController methods
184+
185+ def render_ticket_action_control(self, req, ticket, action):
186+ """Returns the action control"""
187+ actions = self.get_configurable_workflow().actions
188+ label = actions[action]['name']
189+ new_owner = self._new_owner(ticket)
190+ if new_owner:
191+ hint = 'The owner will change to %s' % new_owner
192+ else:
193+ hint = 'The owner will be deleted.'
194+ control = tag('')
195+ return (label, control, hint)
196+
197+ def get_ticket_changes(self, req, ticket, action):
198+ """Returns the change of owner."""
199+ return {'owner': self._new_owner(ticket)}
200+
201+ def _new_owner(self, ticket):
202+ """Determines the new owner"""
203+ db = self.env.get_db_cnx()
204+ cursor = db.cursor()
205+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
206+ "AND field='owner' ORDER BY -time", (ticket.id, ))
207+ row = cursor.fetchone()
208+ if row:
209+ owner = row[0]
210+ else: # The owner has never changed.
211+ owner = ticket['owner']
212+ return owner
213+
214+
215+class TicketWorkflowOpStatusPrevious(TicketWorkflowOpBase):
216+ """Sets the status to the previous status
217+
218+ Don't forget to add the `TicketWorkflowOpStatusPrevious` to the workflow
219+ option in [ticket].
220+ If there is no workflow option, the line will look like this:
221+
222+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpStatusPrevious
223+ """
224+
225+ _op_name = 'set_status_to_previous'
226+
227+ # ITicketActionController methods
228+
229+ def render_ticket_action_control(self, req, ticket, action):
230+ """Returns the action control"""
231+ actions = self.get_configurable_workflow().actions
232+ label = actions[action]['name']
233+ new_status = self._new_status(ticket)
234+ if new_status != self._old_status(ticket):
235+ hint = 'The status will change to %s' % new_status
236+ else:
237+ hint = ''
238+ control = tag('')
239+ return (label, control, hint)
240+
241+ def get_ticket_changes(self, req, ticket, action):
242+ """Returns the change of status."""
243+ return {'status': self._new_status(ticket)}
244+
245+ def _old_status(self, ticket):
246+ """Determines what the ticket state was (is)"""
247+ return ticket._old.get('status', ticket['status'])
248+
249+ def _new_status(self, ticket):
250+ """Determines the new status"""
251+ db = self.env.get_db_cnx()
252+ cursor = db.cursor()
253+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
254+ "AND field='status' ORDER BY -time", (ticket.id, ))
255+ row = cursor.fetchone()
256+ if row:
257+ status = row[0]
258+ else: # The status has never changed.
259+ status = 'new'
260+ return status
261+
262+
263+class TicketWorkflowOpRunExternal(TicketWorkflowOpBase):
264+ """Action to allow running an external command as a side-effect.
265+
266+ If it is a lengthy task, it should daemonize so the webserver can get back
267+ to doing its thing. If the script exits with a non-zero return code, an
268+ error will be logged to the Trac log.
269+ The plugin will look for a script named <tracenv>/hooks/<someaction>, and
270+ will pass it 2 parameters: the ticket number, and the user.
271+
272+ <someaction>.operations = run_external
273+ <someaction>.run_external = Hint for the user
274+
275+ Don't forget to add the `TicketWorkflowOpRunExternal` to the workflow
276+ option in [ticket].
277+ If there is no workflow option, the line will look like this:
278+
279+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpRunExternal
280+ """
281+
282+ implements(ITicketActionController)
283+
284+ # ITicketActionController methods
285+
286+ def get_ticket_actions(self, req, ticket):
287+ """Finds the actions that use this operation"""
288+ controller = self.get_configurable_workflow()
289+ return controller.get_actions_by_operation_for_req(req, ticket,
290+ 'run_external')
291+
292+ def get_all_status(self):
293+ """Provide any additional status values"""
294+ # We don't have anything special here; the statuses will be recognized
295+ # by the default controller.
296+ return []
297+
298+ def render_ticket_action_control(self, req, ticket, action):
299+ """Returns the action control"""
300+ actions = self.get_configurable_workflow().actions
301+ label = actions[action]['name']
302+ hint = self.config.get('ticket-workflow',
303+ action + '.run_external').strip()
304+ if hint is None:
305+ hint = "Will run external script."
306+ return (label, tag(''), hint)
307+
308+ def get_ticket_changes(self, req, ticket, action):
309+ """No changes to the ticket"""
310+ return {}
311+
312+ def apply_action_side_effects(self, req, ticket, action):
313+ """Run the external script"""
314+ print "running external script for %s" % action
315+ script = os.path.join(self.env.path, 'hooks', action)
316+ for extension in ('', '.exe', '.cmd', '.bat'):
317+ if os.path.exists(script + extension):
318+ script += extension
319+ break
320+ else:
321+ self.env.log.error("Error in ticket workflow config; could not find external command to run for %s in %s" % (action, os.path.join(self.env.path, 'hooks')))
322+ return
323+ retval = call([script, str(ticket.id), req.authname])
324+ if retval:
325+ self.env.log.error("External script %r exited with return code %s." % (script, retval))
326+
327+
328+class TicketWorkflowOpTriage(TicketWorkflowOpBase):
329+ """Action to split a workflow based on a field
330+
331+ <someaction> = somestatus -> *
332+ <someaction>.operations = triage
333+ <someaction>.triage_field = type
334+ <someaction>.traige_split = defect -> new_defect, task -> new_task, enhancement -> new_enhancement
335+
336+ Don't forget to add the `TicketWorkflowOpTriage` to the workflow option in
337+ [ticket].
338+ If there is no workflow option, the line will look like this:
339+
340+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpTriage
341+ """
342+
343+ _op_name = 'triage'
344+
345+ # ITicketActionController methods
346+
347+ def render_ticket_action_control(self, req, ticket, action):
348+ """Returns the action control"""
349+ actions = self.get_configurable_workflow().actions
350+ label = actions[action]['name']
351+ new_status = self._new_status(ticket, action)
352+ if new_status != ticket['status']:
353+ hint = 'The status will change to %s.' % new_status
354+ else:
355+ hint = ''
356+ control = tag('')
357+ return (label, control, hint)
358+
359+ def get_ticket_changes(self, req, ticket, action):
360+ """Returns the change of status."""
361+ return {'status': self._new_status(ticket, action)}
362+
363+ def _new_status(self, ticket, action):
364+ """Determines the new status"""
365+ field = self.config.get('ticket-workflow',
366+ action + '.triage_field').strip()
367+ transitions = self.config.get('ticket-workflow',
368+ action + '.triage_split').strip()
369+ for transition in [x.strip() for x in transitions.split(',')]:
370+ value, status = [y.strip() for y in transition.split('->')]
371+ if value == ticket[field].strip():
372+ break
373+ else:
374+ self.env.log.error("Bad configuration for 'triage' operation in action '%s'" % action)
375+ status = 'new'
376+ return status
377+
378+
379+class TicketWorkflowOpXRef(TicketWorkflowOpBase):
380+ """Adds a cross reference to another ticket
381+
382+ <someaction>.operations = xref
383+ <someaction>.xref = "Ticket %s is related to this ticket"
384+ <someaction>.xref_local = "Ticket %s was marked as related to this ticket"
385+ <someaction>.xref_hint = "The specified ticket will be cross-referenced with this ticket"
386+
387+ The example values shown are the default values.
388+ Don't forget to add the `TicketWorkflowOpXRef` to the workflow
389+ option in [ticket].
390+ If there is no workflow option, the line will look like this:
391+
392+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpXRef
393+ """
394+
395+ _op_name = 'xref'
396+
397+ # ITicketActionController methods
398+
399+ def render_ticket_action_control(self, req, ticket, action):
400+ """Returns the action control"""
401+ id = 'action_%s_xref' % action
402+ ticketnum = req.args.get(id, '')
403+ actions = self.get_configurable_workflow().actions
404+ label = actions[action]['name']
405+ hint = actions[action].get('xref_hint',
406+ 'The specified ticket will be cross-referenced with this ticket')
407+ control = tag.input(type='text', id=id, name=id, value=ticketnum)
408+ return (label, control, hint)
409+
410+ def get_ticket_changes(self, req, ticket, action):
411+ # WARNING: Directly modifying the ticket in this method breaks the
412+ # intent of this method. But it does accomplish the desired goal.
413+ if not 'preview' in req.args:
414+ id = 'action_%s_xref' % action
415+ ticketnum = req.args.get(id).strip('#')
416+
417+ try:
418+ xticket = model.Ticket(self.env, ticketnum)
419+ except ValueError:
420+ req.args['preview'] = True
421+ add_warning(req, 'The cross-referenced ticket number "%s" was not a number' % ticketnum)
422+ return {}
423+ except ResourceNotFound, e:
424+ #put in preview mode to prevent ticket being saved
425+ req.args['preview'] = True
426+ add_warning(req, "Unable to cross-reference Ticket #%s (%s)." % (ticketnum, e.message))
427+ return {}
428+
429+ oldcomment = req.args.get('comment')
430+ actions = self.get_configurable_workflow().actions
431+ format_string = actions[action].get('xref_local',
432+ 'Ticket %s was marked as related to this ticket')
433+ # Add a comment to this ticket to indicate that the "remote" ticket is
434+ # related to it. (But only if <action>.xref_local was set in the
435+ # config.)
436+ if format_string:
437+ comment = format_string % ('#%s' % ticketnum)
438+ req.args['comment'] = "%s%s%s" % \
439+ (comment, oldcomment and "[[BR]]" or "", oldcomment or "")
440+
441+ """Returns no changes."""
442+ return {}
443+
444+ def apply_action_side_effects(self, req, ticket, action):
445+ """Add a cross-reference comment to the other ticket"""
446+ # TODO: This needs a lot more error checking.
447+ id = 'action_%s_xref' % action
448+ ticketnum = req.args.get(id).strip('#')
449+ actions = self.get_configurable_workflow().actions
450+ author = req.authname
451+
452+ # Add a comment to the "remote" ticket to indicate this ticket is
453+ # related to it.
454+ format_string = actions[action].get('xref',
455+ 'Ticket %s is related to this ticket')
456+ comment = format_string % ('#%s' % ticket.id)
457+ # FIXME: we need a cnum to avoid messing up
458+ xticket = model.Ticket(self.env, ticketnum)
459+ # FIXME: We _assume_ we have sufficient permissions to comment on the
460+ # other ticket.
461+ now = datetime.now(utc)
462+ xticket.save_changes(author, comment, now)
463+
464+ #Send notification on the other ticket
465+ try:
466+ tn = TicketNotifyEmail(self.env)
467+ tn.notify(xticket, newticket=False, modtime=now)
468+ except Exception, e:
469+ self.log.exception("Failure sending notification on change to "
470+ "ticket #%s: %s" % (ticketnum, e))
471+
472+
473+class TicketWorkflowOpResetMilestone(TicketWorkflowOpBase):
474+ """Resets the ticket milestone if it is assigned to a completed milestone.
475+ This is useful for reopen operations.
476+
477+ reopened = closed -> reopened
478+ reopened.name = Reopened
479+ reopened.operations = reset_milestone
480+
481+
482+ Don't forget to add the `TicketWorkflowOpResetMilestone` to the workflow
483+ option in [ticket].
484+ If there is no workflow option, the line will look like this:
485+
486+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpResetMilestone
487+ """
488+
489+ _op_name = 'reset_milestone'
490+
491+ # ITicketActionController methods
492+
493+ def render_ticket_action_control(self, req, ticket, action):
494+ """Returns the action control"""
495+ actions = self.get_configurable_workflow().actions
496+ label = actions[action]['name']
497+ # check if the assigned milestone has been completed
498+ milestone = Milestone(self.env,ticket['milestone'])
499+ if milestone.is_completed:
500+ hint = 'The milestone will be reset'
501+ else:
502+ hint = ''
503+ control = tag('')
504+ return (label, control, hint)
505+
506+ def get_ticket_changes(self, req, ticket, action):
507+ """Returns the change of milestone, if needed."""
508+ milestone = Milestone(self.env,ticket['milestone'])
509+ if milestone.is_completed:
510+ return {'milestone': ''}
511+ return {}
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/advancedworkflow/controller.py
--- a/plugins/svn/advancedticketworkflow/advancedworkflow/controller.py Mon May 30 18:32:13 2011 +0900
+++ b/plugins/svn/advancedticketworkflow/advancedworkflow/controller.py Mon May 30 18:37:31 2011 +0900
@@ -1,473 +1,511 @@
1-"""Trac plugin that provides a number of advanced operations for customizable
2-workflows.
3-"""
4-
5-import os
6-import time
7-from subprocess import call
8-from genshi.builder import tag
9-
10-from trac.core import implements, Component
11-from trac.ticket import model
12-from trac.ticket.api import ITicketActionController
13-from trac.ticket.default_workflow import ConfigurableTicketWorkflow
14-from trac.ticket.model import Milestone
15-
16-
17-class TicketWorkflowOpBase(Component):
18- """Abstract base class for 'simple' ticket workflow operations."""
19-
20- implements(ITicketActionController)
21- abstract = True
22-
23- _op_name = None # Must be specified.
24-
25- # ITicketActionController methods
26-
27- def get_ticket_actions(self, req, ticket):
28- """Finds the actions that use this operation"""
29- controller = ConfigurableTicketWorkflow(self.env)
30- return controller.get_actions_by_operation_for_req(req, ticket,
31- self._op_name)
32-
33- def get_all_status(self):
34- """Provide any additional status values"""
35- # We don't have anything special here; the statuses will be recognized
36- # by the default controller.
37- return []
38-
39- # This should most likely be overridden to be more functional
40- def render_ticket_action_control(self, req, ticket, action):
41- """Returns the action control"""
42- actions = ConfigurableTicketWorkflow(self.env).actions
43- label = actions[action]['name']
44- return (label, tag(''), '')
45-
46- def get_ticket_changes(self, req, ticket, action):
47- """Must be implemented in subclasses"""
48- raise NotImplementedError
49-
50- def apply_action_side_effects(self, req, ticket, action):
51- """No side effects"""
52- pass
53-
54-
55-class TicketWorkflowOpOwnerReporter(TicketWorkflowOpBase):
56- """Sets the owner to the reporter of the ticket.
57-
58- needinfo = * -> needinfo
59- needinfo.name = Need info
60- needinfo.operations = set_owner_to_reporter
61-
62-
63- Don't forget to add the `TicketWorkflowOpOwnerReporter` to the workflow
64- option in [ticket].
65- If there is no workflow option, the line will look like this:
66-
67- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerReporter
68- """
69-
70- _op_name = 'set_owner_to_reporter'
71-
72- # ITicketActionController methods
73-
74- def render_ticket_action_control(self, req, ticket, action):
75- """Returns the action control"""
76- actions = ConfigurableTicketWorkflow(self.env).actions
77- label = actions[action]['name']
78- hint = 'The owner will change to %s' % ticket['reporter']
79- control = tag('')
80- return (label, control, hint)
81-
82- def get_ticket_changes(self, req, ticket, action):
83- """Returns the change of owner."""
84- return {'owner': ticket['reporter']}
85-
86-
87-class TicketWorkflowOpOwnerComponent(TicketWorkflowOpBase):
88- """Sets the owner to the default owner for the component.
89-
90- <someaction>.operations = set_owner_to_component_owner
91-
92- Don't forget to add the `TicketWorkflowOpOwnerComponent` to the workflow
93- option in [ticket].
94- If there is no workflow option, the line will look like this:
95-
96- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerComponent
97- """
98-
99- _op_name = 'set_owner_to_component_owner'
100-
101- # ITicketActionController methods
102-
103- def render_ticket_action_control(self, req, ticket, action):
104- """Returns the action control"""
105- actions = ConfigurableTicketWorkflow(self.env).actions
106- label = actions[action]['name']
107- hint = 'The owner will change to %s' % self._new_owner(ticket)
108- control = tag('')
109- return (label, control, hint)
110-
111- def get_ticket_changes(self, req, ticket, action):
112- """Returns the change of owner."""
113- return {'owner': self._new_owner(ticket)}
114-
115- def _new_owner(self, ticket):
116- """Determines the new owner"""
117- component = model.Component(self.env, name=ticket['component'])
118- self.env.log.debug("component %s, owner %s" % (component, component.owner))
119- return component.owner
120-
121-
122-class TicketWorkflowOpOwnerField(TicketWorkflowOpBase):
123- """Sets the owner to the value of a ticket field
124-
125- <someaction>.operations = set_owner_to_field
126- <someaction>.set_owner_to_field = myfield
127-
128- Don't forget to add the `TicketWorkflowOpOwnerField` to the workflow
129- option in [ticket].
130- If there is no workflow option, the line will look like this:
131-
132- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerField
133- """
134-
135- _op_name = 'set_owner_to_field'
136-
137- # ITicketActionController methods
138-
139- def render_ticket_action_control(self, req, ticket, action):
140- """Returns the action control"""
141- actions = ConfigurableTicketWorkflow(self.env).actions
142- label = actions[action]['name']
143- hint = 'The owner will change to %s' % self._new_owner(action, ticket)
144- control = tag('')
145- return (label, control, hint)
146-
147- def get_ticket_changes(self, req, ticket, action):
148- """Returns the change of owner."""
149- return {'owner': self._new_owner(action, ticket)}
150-
151- def _new_owner(self, action, ticket):
152- """Determines the new owner"""
153- # Should probably do some sanity checking...
154- field = self.config.get('ticket-workflow',
155- action + '.' + self._op_name).strip()
156- return ticket[field]
157-
158-
159-class TicketWorkflowOpOwnerPrevious(TicketWorkflowOpBase):
160- """Sets the owner to the previous owner
161-
162- Don't forget to add the `TicketWorkflowOpOwnerPrevious` to the workflow
163- option in [ticket].
164- If there is no workflow option, the line will look like this:
165-
166- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerPrevious
167- """
168-
169- _op_name = 'set_owner_to_previous'
170-
171- # ITicketActionController methods
172-
173- def render_ticket_action_control(self, req, ticket, action):
174- """Returns the action control"""
175- actions = ConfigurableTicketWorkflow(self.env).actions
176- label = actions[action]['name']
177- new_owner = self._new_owner(ticket)
178- if new_owner:
179- hint = 'The owner will change to %s' % new_owner
180- else:
181- hint = 'The owner will be deleted.'
182- control = tag('')
183- return (label, control, hint)
184-
185- def get_ticket_changes(self, req, ticket, action):
186- """Returns the change of owner."""
187- return {'owner': self._new_owner(ticket)}
188-
189- def _new_owner(self, ticket):
190- """Determines the new owner"""
191- db = self.env.get_db_cnx()
192- cursor = db.cursor()
193- cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
194- "AND field='owner' ORDER BY -time", (ticket.id, ))
195- row = cursor.fetchone()
196- if row:
197- owner = row[0]
198- else: # The owner has never changed.
199- owner = ''
200- return owner
201-
202-
203-class TicketWorkflowOpStatusPrevious(TicketWorkflowOpBase):
204- """Sets the status to the previous status
205-
206- Don't forget to add the `TicketWorkflowOpStatusPrevious` to the workflow
207- option in [ticket].
208- If there is no workflow option, the line will look like this:
209-
210- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpStatusPrevious
211- """
212-
213- _op_name = 'set_status_to_previous'
214-
215- # ITicketActionController methods
216-
217- def render_ticket_action_control(self, req, ticket, action):
218- """Returns the action control"""
219- actions = ConfigurableTicketWorkflow(self.env).actions
220- label = actions[action]['name']
221- new_status = self._new_status(ticket)
222- if new_status != self._old_status(ticket):
223- hint = 'The status will change to %s' % new_status
224- else:
225- hint = ''
226- control = tag('')
227- return (label, control, hint)
228-
229- def get_ticket_changes(self, req, ticket, action):
230- """Returns the change of status."""
231- return {'status': self._new_status(ticket)}
232-
233- def _old_status(self, ticket):
234- """Determines what the ticket state was (is)"""
235- return ticket._old.get('status', ticket['status'])
236-
237- def _new_status(self, ticket):
238- """Determines the new status"""
239- db = self.env.get_db_cnx()
240- cursor = db.cursor()
241- cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
242- "AND field='status' ORDER BY -time", (ticket.id, ))
243- row = cursor.fetchone()
244- if row:
245- status = row[0]
246- else: # The status has never changed.
247- status = 'new'
248- return status
249-
250-
251-class TicketWorkflowOpRunExternal(Component):
252- """Action to allow running an external command as a side-effect.
253-
254- If it is a lengthy task, it should daemonize so the webserver can get back
255- to doing its thing. If the script exits with a non-zero return code, an
256- error will be logged to the Trac log.
257- The plugin will look for a script named <tracenv>/hooks/<someaction>, and
258- will pass it 2 parameters: the ticket number, and the user.
259-
260- <someaction>.operations = run_external
261- <someaction>.run_external = Hint for the user
262-
263- Don't forget to add the `TicketWorkflowOpRunExternal` to the workflow
264- option in [ticket].
265- If there is no workflow option, the line will look like this:
266-
267- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpRunExternal
268- """
269-
270- implements(ITicketActionController)
271-
272- # ITicketActionController methods
273-
274- def get_ticket_actions(self, req, ticket):
275- """Finds the actions that use this operation"""
276- controller = ConfigurableTicketWorkflow(self.env)
277- return controller.get_actions_by_operation_for_req(req, ticket,
278- 'run_external')
279-
280- def get_all_status(self):
281- """Provide any additional status values"""
282- # We don't have anything special here; the statuses will be recognized
283- # by the default controller.
284- return []
285-
286- def render_ticket_action_control(self, req, ticket, action):
287- """Returns the action control"""
288- actions = ConfigurableTicketWorkflow(self.env).actions
289- label = actions[action]['name']
290- hint = self.config.get('ticket-workflow',
291- action + '.run_external').strip()
292- if hint is None:
293- hint = "Will run external script."
294- return (label, tag(''), hint)
295-
296- def get_ticket_changes(self, req, ticket, action):
297- """No changes to the ticket"""
298- return {}
299-
300- def apply_action_side_effects(self, req, ticket, action):
301- """Run the external script"""
302- print "running external script for %s" % action
303- script = os.path.join(self.env.path, 'hooks', action)
304- for extension in ('', '.exe', '.cmd', '.bat'):
305- if os.path.exists(script + extension):
306- script += extension
307- break
308- else:
309- self.env.log.error("Error in ticket workflow config; could not find external command to run for %s in %s" % (action, os.path.join(self.env.path, 'hooks')))
310- return
311- retval = call([script, str(ticket.id), req.authname])
312- if retval:
313- self.env.log.error("External script %r exited with return code %s." % (script, retval))
314-
315-
316-class TicketWorkflowOpTriage(TicketWorkflowOpBase):
317- """Action to split a workflow based on a field
318-
319- <someaction> = somestatus -> *
320- <someaction>.operations = triage
321- <someaction>.triage_field = type
322- <someaction>.traige_split = defect -> new_defect, task -> new_task, enhancement -> new_enhancement
323-
324- Don't forget to add the `TicketWorkflowOpTriage` to the workflow option in
325- [ticket].
326- If there is no workflow option, the line will look like this:
327-
328- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpTriage
329- """
330-
331- _op_name = 'triage'
332-
333- # ITicketActionController methods
334-
335- def render_ticket_action_control(self, req, ticket, action):
336- """Returns the action control"""
337- actions = ConfigurableTicketWorkflow(self.env).actions
338- label = actions[action]['name']
339- new_status = self._new_status(ticket, action)
340- if new_status != ticket['status']:
341- hint = 'The status will change to %s.' % new_status
342- else:
343- hint = ''
344- control = tag('')
345- return (label, control, hint)
346-
347- def get_ticket_changes(self, req, ticket, action):
348- """Returns the change of status."""
349- return {'status': self._new_status(ticket, action)}
350-
351- def _new_status(self, ticket, action):
352- """Determines the new status"""
353- field = self.config.get('ticket-workflow',
354- action + '.triage_field').strip()
355- transitions = self.config.get('ticket-workflow',
356- action + '.triage_split').strip()
357- for transition in [x.strip() for x in transitions.split(',')]:
358- value, status = [y.strip() for y in transition.split('->')]
359- if value == ticket[field].strip():
360- break
361- else:
362- self.env.log.error("Bad configuration for 'triage' operation in action '%s'" % action)
363- status = 'new'
364- return status
365-
366-
367-class TicketWorkflowOpXRef(TicketWorkflowOpBase):
368- """Adds a cross reference to another ticket
369-
370- <someaction>.operations = xref
371- <someaction>.xref = "Ticket %s is related to this ticket"
372- <someaction>.xref_local = "Ticket %s was marked as related to this ticket"
373- <someaction>.xref_hint = "The specified ticket will be cross-referenced with this ticket"
374-
375- The example values shown are the default values.
376- Don't forget to add the `TicketWorkflowOpXRef` to the workflow
377- option in [ticket].
378- If there is no workflow option, the line will look like this:
379-
380- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpXRef
381- """
382-
383- _op_name = 'xref'
384-
385- # ITicketActionController methods
386-
387- def render_ticket_action_control(self, req, ticket, action):
388- """Returns the action control"""
389- id = 'action_%s_xref' % action
390- ticketnum = req.args.get(id, '')
391- actions = ConfigurableTicketWorkflow(self.env).actions
392- label = actions[action]['name']
393- hint = actions[action].get('xref_hint',
394- 'The specified ticket will be cross-referenced with this ticket')
395- control = tag.input(type='text', id=id, name=id, value=ticketnum)
396- return (label, control, hint)
397-
398- def get_ticket_changes(self, req, ticket, action):
399- """Returns no changes."""
400- return {}
401-
402- def apply_action_side_effects(self, req, ticket, action):
403- """Add a cross-reference comment to the other ticket"""
404- # TODO: This needs a lot more error checking.
405- id = 'action_%s_xref' % action
406- ticketnum = req.args.get(id).strip('#')
407- actions = ConfigurableTicketWorkflow(self.env).actions
408- author = req.authname
409-
410- # Add a comment to the "remote" ticket to indicate this ticket is
411- # related to it.
412- format_string = actions[action].get('xref',
413- 'Ticket %s is related to this ticket')
414- comment = format_string % ('#%s' % ticket.id)
415- # FIXME: This assumes the referenced ticket exists.
416- xticket = model.Ticket(self.env, ticketnum)
417- # FIXME: We _assume_ we have sufficient permissions to comment on the
418- # other ticket.
419- xticket.save_changes(author, comment)
420-
421- # Add a comment to this ticket to indicate that the "remote" ticket is
422- # related to it. (But only if <action>.xref_local was set in the
423- # config.)
424- format_string = actions[action].get('xref_local',
425- 'Ticket %s was marked as related to this ticket')
426- if format_string:
427- comment = format_string % ('#%s' % ticketnum)
428- time.sleep(1) # FIXME: Hack around IntegrityError
429- # HACK: Grab a new ticket object to avoid getting
430- # "OperationalError: no such column: new"
431- xticket = model.Ticket(self.env, ticket.id)
432- xticket.save_changes(author, comment)
433-
434-
435-class TicketWorkflowOpResetMilestone(TicketWorkflowOpBase):
436- """Resets the ticket milestone if it is assigned to a completed milestone.
437- This is useful for reopen operations.
438-
439- reopened = closed -> reopened
440- reopened.name = Reopened
441- reopened.operations = reset_milestone
442-
443-
444- Don't forget to add the `TicketWorkflowOpResetMilestone` to the workflow
445- option in [ticket].
446- If there is no workflow option, the line will look like this:
447-
448- workflow = ConfigurableTicketWorkflow,TicketWorkflowOpResetMilestone
449- """
450-
451- _op_name = 'reset_milestone'
452-
453- # ITicketActionController methods
454-
455- def render_ticket_action_control(self, req, ticket, action):
456- """Returns the action control"""
457- actions = ConfigurableTicketWorkflow(self.env).actions
458- label = actions[action]['name']
459- # check if the assigned milestone has been completed
460- milestone = Milestone(self.env,ticket['milestone'])
461- if milestone.is_completed:
462- hint = 'The milestone will be reset'
463- else:
464- hint = ''
465- control = tag('')
466- return (label, control, hint)
467-
468- def get_ticket_changes(self, req, ticket, action):
469- """Returns the change of milestone, if needed."""
470- milestone = Milestone(self.env,ticket['milestone'])
471- if milestone.is_completed:
472- return {'milestone': ''}
473- return {}
1+"""Trac plugin that provides a number of advanced operations for customizable
2+workflows.
3+"""
4+
5+import os
6+import time
7+from datetime import datetime
8+from subprocess import call
9+from genshi.builder import tag
10+
11+from trac.core import implements, Component
12+from trac.ticket import model
13+from trac.ticket.api import ITicketActionController, TicketSystem
14+from trac.ticket.default_workflow import ConfigurableTicketWorkflow
15+from trac.ticket.model import Milestone
16+from trac.ticket.notification import TicketNotifyEmail
17+from trac.resource import ResourceNotFound
18+from trac.util.datefmt import utc
19+from trac.web.chrome import add_warning
20+
21+
22+class TicketWorkflowOpBase(Component):
23+ """Abstract base class for 'simple' ticket workflow operations."""
24+
25+ implements(ITicketActionController)
26+ abstract = True
27+
28+ _op_name = None # Must be specified.
29+
30+ def get_configurable_workflow(self):
31+ controllers = TicketSystem(self.env).action_controllers
32+ for controller in controllers:
33+ if isinstance(controller, ConfigurableTicketWorkflow):
34+ return controller
35+ return ConfigurableTicketWorkflow(self.env)
36+
37+ # ITicketActionController methods
38+
39+ def get_ticket_actions(self, req, ticket):
40+ """Finds the actions that use this operation"""
41+ controller = self.get_configurable_workflow()
42+ return controller.get_actions_by_operation_for_req(req, ticket,
43+ self._op_name)
44+
45+ def get_all_status(self):
46+ """Provide any additional status values"""
47+ # We don't have anything special here; the statuses will be recognized
48+ # by the default controller.
49+ return []
50+
51+ # This should most likely be overridden to be more functional
52+ def render_ticket_action_control(self, req, ticket, action):
53+ """Returns the action control"""
54+ actions = self.get_configurable_workflow().actions
55+ label = actions[action]['name']
56+ return (label, tag(''), '')
57+
58+ def get_ticket_changes(self, req, ticket, action):
59+ """Must be implemented in subclasses"""
60+ raise NotImplementedError
61+
62+ def apply_action_side_effects(self, req, ticket, action):
63+ """No side effects"""
64+ pass
65+
66+
67+class TicketWorkflowOpOwnerReporter(TicketWorkflowOpBase):
68+ """Sets the owner to the reporter of the ticket.
69+
70+ needinfo = * -> needinfo
71+ needinfo.name = Need info
72+ needinfo.operations = set_owner_to_reporter
73+
74+
75+ Don't forget to add the `TicketWorkflowOpOwnerReporter` to the workflow
76+ option in [ticket].
77+ If there is no workflow option, the line will look like this:
78+
79+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerReporter
80+ """
81+
82+ _op_name = 'set_owner_to_reporter'
83+
84+ # ITicketActionController methods
85+
86+ def render_ticket_action_control(self, req, ticket, action):
87+ """Returns the action control"""
88+ actions = self.get_configurable_workflow().actions
89+ label = actions[action]['name']
90+ hint = 'The owner will change to %s' % ticket['reporter']
91+ control = tag('')
92+ return (label, control, hint)
93+
94+ def get_ticket_changes(self, req, ticket, action):
95+ """Returns the change of owner."""
96+ return {'owner': ticket['reporter']}
97+
98+
99+class TicketWorkflowOpOwnerComponent(TicketWorkflowOpBase):
100+ """Sets the owner to the default owner for the component.
101+
102+ <someaction>.operations = set_owner_to_component_owner
103+
104+ Don't forget to add the `TicketWorkflowOpOwnerComponent` to the workflow
105+ option in [ticket].
106+ If there is no workflow option, the line will look like this:
107+
108+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerComponent
109+ """
110+
111+ _op_name = 'set_owner_to_component_owner'
112+
113+ # ITicketActionController methods
114+
115+ def render_ticket_action_control(self, req, ticket, action):
116+ """Returns the action control"""
117+ actions = self.get_configurable_workflow().actions
118+ label = actions[action]['name']
119+ hint = 'The owner will change to %s' % self._new_owner(ticket)
120+ control = tag('')
121+ return (label, control, hint)
122+
123+ def get_ticket_changes(self, req, ticket, action):
124+ """Returns the change of owner."""
125+ return {'owner': self._new_owner(ticket)}
126+
127+ def _new_owner(self, ticket):
128+ """Determines the new owner"""
129+ component = model.Component(self.env, name=ticket['component'])
130+ self.env.log.debug("component %s, owner %s" % (component, component.owner))
131+ return component.owner
132+
133+
134+class TicketWorkflowOpOwnerField(TicketWorkflowOpBase):
135+ """Sets the owner to the value of a ticket field
136+
137+ <someaction>.operations = set_owner_to_field
138+ <someaction>.set_owner_to_field = myfield
139+
140+ Don't forget to add the `TicketWorkflowOpOwnerField` to the workflow
141+ option in [ticket].
142+ If there is no workflow option, the line will look like this:
143+
144+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerField
145+ """
146+
147+ _op_name = 'set_owner_to_field'
148+
149+ # ITicketActionController methods
150+
151+ def render_ticket_action_control(self, req, ticket, action):
152+ """Returns the action control"""
153+ actions = self.get_configurable_workflow().actions
154+ label = actions[action]['name']
155+ hint = 'The owner will change to %s' % self._new_owner(action, ticket)
156+ control = tag('')
157+ return (label, control, hint)
158+
159+ def get_ticket_changes(self, req, ticket, action):
160+ """Returns the change of owner."""
161+ return {'owner': self._new_owner(action, ticket)}
162+
163+ def _new_owner(self, action, ticket):
164+ """Determines the new owner"""
165+ # Should probably do some sanity checking...
166+ field = self.config.get('ticket-workflow',
167+ action + '.' + self._op_name).strip()
168+ return ticket[field]
169+
170+
171+class TicketWorkflowOpOwnerPrevious(TicketWorkflowOpBase):
172+ """Sets the owner to the previous owner
173+
174+ Don't forget to add the `TicketWorkflowOpOwnerPrevious` to the workflow
175+ option in [ticket].
176+ If there is no workflow option, the line will look like this:
177+
178+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerPrevious
179+ """
180+
181+ _op_name = 'set_owner_to_previous'
182+
183+ # ITicketActionController methods
184+
185+ def render_ticket_action_control(self, req, ticket, action):
186+ """Returns the action control"""
187+ actions = self.get_configurable_workflow().actions
188+ label = actions[action]['name']
189+ new_owner = self._new_owner(ticket)
190+ if new_owner:
191+ hint = 'The owner will change to %s' % new_owner
192+ else:
193+ hint = 'The owner will be deleted.'
194+ control = tag('')
195+ return (label, control, hint)
196+
197+ def get_ticket_changes(self, req, ticket, action):
198+ """Returns the change of owner."""
199+ return {'owner': self._new_owner(ticket)}
200+
201+ def _new_owner(self, ticket):
202+ """Determines the new owner"""
203+ db = self.env.get_db_cnx()
204+ cursor = db.cursor()
205+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
206+ "AND field='owner' ORDER BY -time", (ticket.id, ))
207+ row = cursor.fetchone()
208+ if row:
209+ owner = row[0]
210+ else: # The owner has never changed.
211+ owner = ticket['owner']
212+ return owner
213+
214+
215+class TicketWorkflowOpStatusPrevious(TicketWorkflowOpBase):
216+ """Sets the status to the previous status
217+
218+ Don't forget to add the `TicketWorkflowOpStatusPrevious` to the workflow
219+ option in [ticket].
220+ If there is no workflow option, the line will look like this:
221+
222+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpStatusPrevious
223+ """
224+
225+ _op_name = 'set_status_to_previous'
226+
227+ # ITicketActionController methods
228+
229+ def render_ticket_action_control(self, req, ticket, action):
230+ """Returns the action control"""
231+ actions = self.get_configurable_workflow().actions
232+ label = actions[action]['name']
233+ new_status = self._new_status(ticket)
234+ if new_status != self._old_status(ticket):
235+ hint = 'The status will change to %s' % new_status
236+ else:
237+ hint = ''
238+ control = tag('')
239+ return (label, control, hint)
240+
241+ def get_ticket_changes(self, req, ticket, action):
242+ """Returns the change of status."""
243+ return {'status': self._new_status(ticket)}
244+
245+ def _old_status(self, ticket):
246+ """Determines what the ticket state was (is)"""
247+ return ticket._old.get('status', ticket['status'])
248+
249+ def _new_status(self, ticket):
250+ """Determines the new status"""
251+ db = self.env.get_db_cnx()
252+ cursor = db.cursor()
253+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
254+ "AND field='status' ORDER BY -time", (ticket.id, ))
255+ row = cursor.fetchone()
256+ if row:
257+ status = row[0]
258+ else: # The status has never changed.
259+ status = 'new'
260+ return status
261+
262+
263+class TicketWorkflowOpRunExternal(TicketWorkflowOpBase):
264+ """Action to allow running an external command as a side-effect.
265+
266+ If it is a lengthy task, it should daemonize so the webserver can get back
267+ to doing its thing. If the script exits with a non-zero return code, an
268+ error will be logged to the Trac log.
269+ The plugin will look for a script named <tracenv>/hooks/<someaction>, and
270+ will pass it 2 parameters: the ticket number, and the user.
271+
272+ <someaction>.operations = run_external
273+ <someaction>.run_external = Hint for the user
274+
275+ Don't forget to add the `TicketWorkflowOpRunExternal` to the workflow
276+ option in [ticket].
277+ If there is no workflow option, the line will look like this:
278+
279+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpRunExternal
280+ """
281+
282+ implements(ITicketActionController)
283+
284+ # ITicketActionController methods
285+
286+ def get_ticket_actions(self, req, ticket):
287+ """Finds the actions that use this operation"""
288+ controller = self.get_configurable_workflow()
289+ return controller.get_actions_by_operation_for_req(req, ticket,
290+ 'run_external')
291+
292+ def get_all_status(self):
293+ """Provide any additional status values"""
294+ # We don't have anything special here; the statuses will be recognized
295+ # by the default controller.
296+ return []
297+
298+ def render_ticket_action_control(self, req, ticket, action):
299+ """Returns the action control"""
300+ actions = self.get_configurable_workflow().actions
301+ label = actions[action]['name']
302+ hint = self.config.get('ticket-workflow',
303+ action + '.run_external').strip()
304+ if hint is None:
305+ hint = "Will run external script."
306+ return (label, tag(''), hint)
307+
308+ def get_ticket_changes(self, req, ticket, action):
309+ """No changes to the ticket"""
310+ return {}
311+
312+ def apply_action_side_effects(self, req, ticket, action):
313+ """Run the external script"""
314+ print "running external script for %s" % action
315+ script = os.path.join(self.env.path, 'hooks', action)
316+ for extension in ('', '.exe', '.cmd', '.bat'):
317+ if os.path.exists(script + extension):
318+ script += extension
319+ break
320+ else:
321+ self.env.log.error("Error in ticket workflow config; could not find external command to run for %s in %s" % (action, os.path.join(self.env.path, 'hooks')))
322+ return
323+ retval = call([script, str(ticket.id), req.authname])
324+ if retval:
325+ self.env.log.error("External script %r exited with return code %s." % (script, retval))
326+
327+
328+class TicketWorkflowOpTriage(TicketWorkflowOpBase):
329+ """Action to split a workflow based on a field
330+
331+ <someaction> = somestatus -> *
332+ <someaction>.operations = triage
333+ <someaction>.triage_field = type
334+ <someaction>.traige_split = defect -> new_defect, task -> new_task, enhancement -> new_enhancement
335+
336+ Don't forget to add the `TicketWorkflowOpTriage` to the workflow option in
337+ [ticket].
338+ If there is no workflow option, the line will look like this:
339+
340+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpTriage
341+ """
342+
343+ _op_name = 'triage'
344+
345+ # ITicketActionController methods
346+
347+ def render_ticket_action_control(self, req, ticket, action):
348+ """Returns the action control"""
349+ actions = self.get_configurable_workflow().actions
350+ label = actions[action]['name']
351+ new_status = self._new_status(ticket, action)
352+ if new_status != ticket['status']:
353+ hint = 'The status will change to %s.' % new_status
354+ else:
355+ hint = ''
356+ control = tag('')
357+ return (label, control, hint)
358+
359+ def get_ticket_changes(self, req, ticket, action):
360+ """Returns the change of status."""
361+ return {'status': self._new_status(ticket, action)}
362+
363+ def _new_status(self, ticket, action):
364+ """Determines the new status"""
365+ field = self.config.get('ticket-workflow',
366+ action + '.triage_field').strip()
367+ transitions = self.config.get('ticket-workflow',
368+ action + '.triage_split').strip()
369+ for transition in [x.strip() for x in transitions.split(',')]:
370+ value, status = [y.strip() for y in transition.split('->')]
371+ if value == ticket[field].strip():
372+ break
373+ else:
374+ self.env.log.error("Bad configuration for 'triage' operation in action '%s'" % action)
375+ status = 'new'
376+ return status
377+
378+
379+class TicketWorkflowOpXRef(TicketWorkflowOpBase):
380+ """Adds a cross reference to another ticket
381+
382+ <someaction>.operations = xref
383+ <someaction>.xref = "Ticket %s is related to this ticket"
384+ <someaction>.xref_local = "Ticket %s was marked as related to this ticket"
385+ <someaction>.xref_hint = "The specified ticket will be cross-referenced with this ticket"
386+
387+ The example values shown are the default values.
388+ Don't forget to add the `TicketWorkflowOpXRef` to the workflow
389+ option in [ticket].
390+ If there is no workflow option, the line will look like this:
391+
392+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpXRef
393+ """
394+
395+ _op_name = 'xref'
396+
397+ # ITicketActionController methods
398+
399+ def render_ticket_action_control(self, req, ticket, action):
400+ """Returns the action control"""
401+ id = 'action_%s_xref' % action
402+ ticketnum = req.args.get(id, '')
403+ actions = self.get_configurable_workflow().actions
404+ label = actions[action]['name']
405+ hint = actions[action].get('xref_hint',
406+ 'The specified ticket will be cross-referenced with this ticket')
407+ control = tag.input(type='text', id=id, name=id, value=ticketnum)
408+ return (label, control, hint)
409+
410+ def get_ticket_changes(self, req, ticket, action):
411+ # WARNING: Directly modifying the ticket in this method breaks the
412+ # intent of this method. But it does accomplish the desired goal.
413+ if not 'preview' in req.args:
414+ id = 'action_%s_xref' % action
415+ ticketnum = req.args.get(id).strip('#')
416+
417+ try:
418+ xticket = model.Ticket(self.env, ticketnum)
419+ except ValueError:
420+ req.args['preview'] = True
421+ add_warning(req, 'The cross-referenced ticket number "%s" was not a number' % ticketnum)
422+ return {}
423+ except ResourceNotFound, e:
424+ #put in preview mode to prevent ticket being saved
425+ req.args['preview'] = True
426+ add_warning(req, "Unable to cross-reference Ticket #%s (%s)." % (ticketnum, e.message))
427+ return {}
428+
429+ oldcomment = req.args.get('comment')
430+ actions = self.get_configurable_workflow().actions
431+ format_string = actions[action].get('xref_local',
432+ 'Ticket %s was marked as related to this ticket')
433+ # Add a comment to this ticket to indicate that the "remote" ticket is
434+ # related to it. (But only if <action>.xref_local was set in the
435+ # config.)
436+ if format_string:
437+ comment = format_string % ('#%s' % ticketnum)
438+ req.args['comment'] = "%s%s%s" % \
439+ (comment, oldcomment and "[[BR]]" or "", oldcomment or "")
440+
441+ """Returns no changes."""
442+ return {}
443+
444+ def apply_action_side_effects(self, req, ticket, action):
445+ """Add a cross-reference comment to the other ticket"""
446+ # TODO: This needs a lot more error checking.
447+ id = 'action_%s_xref' % action
448+ ticketnum = req.args.get(id).strip('#')
449+ actions = self.get_configurable_workflow().actions
450+ author = req.authname
451+
452+ # Add a comment to the "remote" ticket to indicate this ticket is
453+ # related to it.
454+ format_string = actions[action].get('xref',
455+ 'Ticket %s is related to this ticket')
456+ comment = format_string % ('#%s' % ticket.id)
457+ # FIXME: we need a cnum to avoid messing up
458+ xticket = model.Ticket(self.env, ticketnum)
459+ # FIXME: We _assume_ we have sufficient permissions to comment on the
460+ # other ticket.
461+ now = datetime.now(utc)
462+ xticket.save_changes(author, comment, now)
463+
464+ #Send notification on the other ticket
465+ try:
466+ tn = TicketNotifyEmail(self.env)
467+ tn.notify(xticket, newticket=False, modtime=now)
468+ except Exception, e:
469+ self.log.exception("Failure sending notification on change to "
470+ "ticket #%s: %s" % (ticketnum, e))
471+
472+
473+class TicketWorkflowOpResetMilestone(TicketWorkflowOpBase):
474+ """Resets the ticket milestone if it is assigned to a completed milestone.
475+ This is useful for reopen operations.
476+
477+ reopened = closed -> reopened
478+ reopened.name = Reopened
479+ reopened.operations = reset_milestone
480+
481+
482+ Don't forget to add the `TicketWorkflowOpResetMilestone` to the workflow
483+ option in [ticket].
484+ If there is no workflow option, the line will look like this:
485+
486+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpResetMilestone
487+ """
488+
489+ _op_name = 'reset_milestone'
490+
491+ # ITicketActionController methods
492+
493+ def render_ticket_action_control(self, req, ticket, action):
494+ """Returns the action control"""
495+ actions = self.get_configurable_workflow().actions
496+ label = actions[action]['name']
497+ # check if the assigned milestone has been completed
498+ milestone = Milestone(self.env,ticket['milestone'])
499+ if milestone.is_completed:
500+ hint = 'The milestone will be reset'
501+ else:
502+ hint = ''
503+ control = tag('')
504+ return (label, control, hint)
505+
506+ def get_ticket_changes(self, req, ticket, action):
507+ """Returns the change of milestone, if needed."""
508+ milestone = Milestone(self.env,ticket['milestone'])
509+ if milestone.is_completed:
510+ return {'milestone': ''}
511+ return {}
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/setup.cfg
--- a/plugins/svn/advancedticketworkflow/setup.cfg Mon May 30 18:32:13 2011 +0900
+++ b/plugins/svn/advancedticketworkflow/setup.cfg Mon May 30 18:37:31 2011 +0900
@@ -1,3 +1,3 @@
1-[egg_info]
2-tag_build = dev
3-tag_svn_revision = true
1+[egg_info]
2+tag_build = dev
3+tag_svn_revision = true
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/advancedticketworkflow/setup.py
--- a/plugins/svn/advancedticketworkflow/setup.py Mon May 30 18:32:13 2011 +0900
+++ b/plugins/svn/advancedticketworkflow/setup.py Mon May 30 18:37:31 2011 +0900
@@ -1,20 +1,20 @@
1-#!/usr/bin/env python
2-
3-from setuptools import setup, find_packages
4-
5-setup(
6- name='AdvancedTicketWorkflowPlugin',
7- version='0.10',
8- author = 'Eli Carter',
9- author_email = 'elicarter@retracile.net',
10- license='BSD',
11- description = 'Advanced workflow operations Trac plugin',
12- long_description = 'Provides more advanced workflow operations',
13- url = 'http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin',
14-
15- packages = find_packages(),
16- package_data = {},
17- entry_points = {'trac.plugins':['advancedworkflow.controller = advancedworkflow.controller']},
18- install_requires = [],
19- #zip_safe = False,
20-)
1+#!/usr/bin/env python
2+
3+from setuptools import setup, find_packages
4+
5+setup(
6+ name='AdvancedTicketWorkflowPlugin',
7+ version='0.11',
8+ author = 'Eli Carter',
9+ author_email = 'elicarter@retracile.net',
10+ license='BSD',
11+ description = 'Advanced workflow operations Trac plugin',
12+ long_description = 'Provides more advanced workflow operations for Trac 0.12',
13+ url = 'http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin',
14+
15+ packages = find_packages(),
16+ package_data = {},
17+ entry_points = {'trac.plugins':['advancedworkflow.controller = advancedworkflow.controller']},
18+ install_requires = [],
19+ #zip_safe = False,
20+)
diff -r 51a6c1e1cd00 -r f974fa9c8b20 plugins/svn/timingandestimationplugin/scripts/git_post_receive.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/svn/timingandestimationplugin/scripts/git_post_receive.py Mon May 30 18:37:31 2011 +0900
@@ -0,0 +1,294 @@
1+#!/usr/bin/env python
2+#
3+# This script is run after receive-pack has accepted a pack and the
4+# repository has been updated. It is passed arguments in through stdin
5+# in the form
6+# <oldrev> <newrev> <refname>
7+# For example:
8+# aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
9+
10+# GOAL: Prevent patches that have appeared in one branch from
11+# reposting to trac when they are moved to another branch
12+# (this was causing duplicate comments / time from topic branches
13+# being merged into main
14+
15+# This specific script will query the repository trying to isolate what
16+# in this receive is a new commit that the repository has not yet
17+# seen. It does this by a big call to git rev-parse, including revs
18+# that are now reachable, excluding everything else (tags, heads,
19+# oldrevs).
20+
21+# http://www.kernel.org/pub/software/scm/git/docs/git-rev-list.html
22+
23+# Once it has isolated what is new it posts those to trac.
24+
25+
26+import os, os.path, sys,logging, getpass, optparse, re
27+import subprocess, threading, time, errno
28+from optparse import OptionParser
29+from subprocess import PIPE
30+
31+TRAC_POST_COMMIT = "/home/ACCELERATION/russ/trac-dev/TandE/trac0.12/scripts/trac-post-commit.py"
32+
33+
34+
35+
36+logdir=os.getenv("LOGDIR") or "/var/log/commit-hooks"
37+log = logging.getLogger('gpr')
38+
39+## Fn to easy working with remote processes
40+def capturedCall(cmd, **kwargs) :
41+ """Do the equivelent of the subprocess.call except
42+ log the stderr and stdout where appropriate."""
43+ p= capturedPopen(cmd,**kwargs)
44+ rc = p.wait()
45+ #this is a cheap attempt to make sure the monitors
46+ #are scheduled and hopefully finished.
47+ time.sleep(0.01)
48+ time.sleep(0.01)
49+ return rc
50+
51+#be warned, if you see your pipelines hanging:
52+#http://old.nabble.com/subprocess.Popen-pipeline-bug--td16026600.html
53+#close_fds=True
54+
55+## Fn to easy working with remote processes
56+def capturedPopen(cmd, stdin=None, stdout=None, stderr=None,
57+ logger=log,cd=None,
58+ stdout_level=logging.INFO,
59+ stderr_level=logging.WARNING, **kwargs) :
60+ """Equivalent to subprocess.Popen except log stdout and stderr
61+ where appropriate. Also log the command being called."""
62+ #we use None as sigil values for stdin,stdout,stderr above so we
63+ # can distinguish from the caller passing in Pipe.
64+ if(logger):
65+ #if we are logging, record the command we're running,
66+ #trying to strip out passwords.
67+ logger.debug("Running cmd: %s",
68+ isinstance(cmd,str) and cmd
69+ or subprocess.list2cmdline([i for i in cmd
70+ if not i.startswith('-p')]))
71+
72+ if cd :
73+ #subprocess does this already with the cwd arg,
74+ #convert cd over so as not to break anyone's.
75+ kwargs['cwd']=cd
76+ p = subprocess.Popen(cmd, stdin=stdin,
77+ stdout=(stdout or (logger and PIPE)),
78+ stderr=(stderr or (logger and PIPE)),
79+ **kwargs)
80+ if logger :
81+ def monitor(level, src, name) :
82+ lname = "%s.%s" % (cmd[0], name)
83+ if(hasattr(logger, 'name')) :
84+ lname = "%s.%s" % (logger.name, lname)
85+ sublog = logging.getLogger(lname)
86+
87+ def tfn() :
88+ l = src.readline()
89+ while l != "":
90+ sublog.log(level,l.strip())
91+ l = src.readline()
92+
93+ th = threading.Thread(target=tfn,name=lname)
94+ p.__setattr__("std%s_thread" % name, th)
95+ th.start()
96+
97+ if stdout == None : monitor(stdout_level, p.stdout,"out")
98+ if stderr == None : monitor(stderr_level, p.stderr,"err")
99+ return p
100+
101+
102+
103+def gitPopen(gitdir, cmd, **kwargs) :
104+ """Popen git with the given command and the git-dir given. kwargs
105+ are passed onwards to popen."""
106+ cmd = ["git","--git-dir="+gitdir] + cmd
107+ return capturedPopen(cmd, logger=log, **kwargs)
108+
109+def find_all_refs(gitdir) :
110+ "Get a list of all ref names in the git database, i.e. any head or tag name"
111+ git = gitPopen(gitdir, ["show-ref"], stdout=PIPE)
112+ return set(line.split()[1] for line in git.stdout)
113+
114+
115+def new_commits(gitdir, ref_updates) :
116+ """For the given gitdir and list of ref_updates (an array that
117+ holds [oldrev,newrev,refname] arrays) find any commit that is new
118+ to this repo.
119+
120+ This works primarily by issuing a:
121+ git rev-list new1 ^old1 new2 ^old2 ^refs/tags/foo ^refs/heads/bar
122+
123+ This function yields commits that are new in the format:
124+ [hash, author, date, message]
125+"""
126+ #the set of previously reachable roots starts as a list of all
127+ #refs currently known, which is post-receive so we will need to
128+ #remove some from here. Everything left will become ^refs.
129+ prev_roots = find_all_refs(gitdir)
130+ log.debug("Found %s named refs", len(prev_roots))
131+
132+ #open the rev-list process and make a writer function to it.
133+ grl = gitPopen(gitdir, ["rev-list","--reverse", "--stdin",
134+ "--pretty=tformat:%an <%ae>%n%ci%n%s%n%+b"],
135+ stdin=PIPE, stdout=PIPE)
136+ def w(ref) : grl.stdin.write(ref + "\n")
137+
138+ for (old,new,ref) in ref_updates :
139+ #branch deletion: newval is 00000, skip the ref, leave it in
140+ #the list of prev_roots
141+ if re.match("^0+$",new) : continue
142+
143+ #Include the newrev as now reachable.
144+ w(new)
145+
146+ #a ref that is being updated should be removed from the
147+ #previous list and ...
148+ prev_roots.discard(ref)
149+ #instead write out the negative line directly. However, if it
150+ #is a new branch (denoted by all 0s) there is no negative to
151+ #include for this ref.
152+ if re.search("[1-9]",old) :
153+ w("^" + old)
154+ else :
155+ log.info("New ref %r", ref)
156+
157+
158+ log.debug("After discarding updates, writing %s prev_roots",
159+ len(prev_roots))
160+ #write lines for (not reachable from anything else')
161+ for ref in prev_roots : w("^" + ref)
162+ grl.stdin.close()
163+
164+ ### this is a little parser for the format
165+ #commit <hash>
166+ #<Author>
167+ #<Date>
168+ #<msg>
169+ #<blank line>
170+ commit = None
171+ msg = ""
172+ def finish() :
173+ commit.append(msg[:-1]) #-1 to strip one \n from the pair.
174+ log.info("New commit: %r", commit)
175+ return commit
176+
177+ while True :
178+ line = grl.stdout.readline()
179+ #blank line and exit code set, we're done here.
180+ if line == '' and grl.poll() != None :
181+ if commit: yield finish()
182+ log.debug("Exiting loop: %s", grl.poll())
183+ break
184+
185+ m = re.match("commit ([0-9a-f]+)$", line)
186+ if m : #start of a new commit
187+ if commit: yield finish()
188+ log.debug("Starting new commit: %s", m.group(1))
189+ hash = m.group(1)
190+ author = grl.stdout.readline().strip()
191+ date = grl.stdout.readline().strip()
192+ commit = [hash,author,date]
193+ msg = grl.stdout.readline()
194+ else :
195+ msg += line
196+
197+def post(commits, gitdir, cname, trac_env) :
198+ for [rev,author,date,msg] in commits :
199+ #this subprocess uses python logging with the same formatter,
200+ #so tell it not to log, and pass through our streams and its
201+ #logging should just fall in line.
202+ log.debug("Posting %s to trac", rev)
203+ capturedCall(["python", TRAC_POST_COMMIT,
204+ "-p", trac_env or "",
205+ "-r", rev,
206+ "-u", author,
207+ "-m", msg,
208+ cname],
209+ logger=None,
210+ stdout=sys.stdout,
211+ stderr=sys.stderr)
212+
213+def process(gitdir, cname, trac_env, ref_updates) :
214+ log.info("Push by %r; CNAME: %r, TRAC_ENV: %r, updating %s refs",
215+ getpass.getuser(), cname, trac_env, len(updates))
216+
217+ post(new_commits(gitdir,ref_updates), gitdir, cname, trac_env)
218+ log.info("Finished commit hook loop, git-post-receive")
219+
220+
221+
222+#################################################################
223+#### Runtime control
224+
225+parser = OptionParser(""" """)
226+parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
227+ help="Show more verbose log messages.")
228+
229+
230+if __name__ == "__main__":
231+ (options, args) = parser.parse_args()
232+
233+ #when run as a hook the directory is the git repo.
234+ #either /var/git/ServerManagement.git
235+ #or /var/git/ServerManagement/.git
236+ gitdir = os.getcwd()
237+
238+ cname = os.getenv("CNAME")
239+ if cname == None :
240+ if len(args) >= 1 :
241+ cname = args.pop(0)
242+ else :
243+ #strip off .git if it is bare or /.git if it is a checkout.
244+ cname = re.sub("/?\.git$","", gitdir)
245+ cname = os.path.basename(cname)
246+ TRAC_ENV = os.getenv("TRAC_ENV") or os.path.join("/var/trac/",cname)
247+
248+ #### Logging configuration
249+ log.setLevel(logging.DEBUG)
250+ ## log verbosely to a file
251+ logfile=os.path.join(logdir, "%s.git-post-receive.log" % cname)
252+ fh = logging.FileHandler(logfile,mode='a')
253+ fh.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)-8s %(message)s',
254+ datefmt='%Y%m%d %H:%M:%S'))
255+
256+ ## and to standard error keep the level higher
257+ sh = logging.StreamHandler()
258+ sh.setLevel(options.verbose and logging.DEBUG or logging.INFO)
259+ sh.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)-8s %(message)s",
260+ datefmt='%H:%M:%S'))
261+
262+ log.addHandler(sh)
263+ log.addHandler(fh)
264+ log.info("----- git-post-receive.py -----")
265+
266+ #Where will we be posting to?
267+ if not os.path.exists(TRAC_ENV) :
268+ logging.warn("None existant trac_env: %s", TRAC_ENV)
269+ TRAC_ENV = None
270+ #actually read the ref updates from stdin
271+ updates = [line.split() for line in sys.stdin]
272+ process(gitdir, cname, TRAC_ENV, updates)
273+
274+# # The MIT License
275+
276+# # Copyright (c) 2010 Acceleration.net
277+
278+# # Permission is hereby granted, free of charge, to any person obtaining a copy
279+# # of this software and associated documentation files (the "Software"), to deal
280+# # in the Software without restriction, including without limitation the rights
281+# # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
282+# # copies of the Software, and to permit persons to whom the Software is
283+# # furnished to do so, subject to the following conditions:
284+
285+# # The above copyright notice and this permission notice shall be included in
286+# # all copies or substantial portions of the Software.
287+
288+# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
289+# # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
290+# # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
291+# # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
292+# # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
293+# # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
294+# # THE SOFTWARE.
旧リポジトリブラウザで表示