Edgewall Software

TracGit: post-receive-hook.py

File post-receive-hook.py, 7.3 KB (added by scorphus@…, 11 years ago)

post-receive hook for closing and referencing tickets

Line 
1#!/usr/bin/env python
2
3# post-receive-hook
4# ----------------------------------------------------------------------------
5# Copyright (c) 2004 Stephen Hansen
6# Copyright (c) 2009 Sebastian Noack
7# Copyright (c) 2013 Pablo Aguiar
8#
9# Permission is hereby granted, free of charge, to any person obtaining a copy
10# of this software and associated documentation files (the "Software"), to
11# deal in the Software without restriction, including without limitation the
12# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
13# sell copies of the Software, and to permit persons to whom the Software is
14# furnished to do so, subject to the following conditions:
15#
16# The above copyright notice and this permission notice shall be included in
17# all copies or substantial portions of the Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
25# IN THE SOFTWARE.
26# ----------------------------------------------------------------------------
27
28# This git post-receive hook script is meant to interface to the
29# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc
30# system. It is based on the Subversion post-commit hook, part of Trac 0.11.
31# Updated to conform to 1.0 changes and upcoming 1.0.1
32#
33# It can be used in-place as post-recevie hook. You only have to fill the
34# constants defined just below the imports.
35#
36# It searches commit messages for text in the form of:
37# command #1
38# command #1, #2
39# command #1 & #2
40# command #1 and #2
41#
42# Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.:
43# command ticket:1
44# command ticket:1, ticket:2
45# command ticket:1 & ticket:2
46# command ticket:1 and ticket:2
47#
48# In addition, the ':' character can be omitted and issue or bug can be used
49# instead of ticket.
50#
51# You can have more than one command in a message. The following commands
52# are supported. There is more than one spelling for each command, to make
53# this as user-friendly as possible.
54#
55# close, closed, closes, fix, fixed, fixes
56# The specified issue numbers are closed with the contents of this
57# commit message being added to it.
58# references, refs, addresses, re, see
59# The specified issue numbers are left in their current status, but
60# the contents of this commit message are added to their notes.
61#
62# A fairly complicated example of what you can do is with a commit message
63# of:
64#
65# Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
66
67import sys
68import os
69import re
70from subprocess import Popen, PIPE
71from datetime import datetime
72from operator import itemgetter
73
74TRAC_ENV = '/var/local/scm/trac'
75GIT_PATH = '/usr/bin/git'
76BRANCHES = ['master']
77COMMANDS = {'close': intern('close'),
78 'closed': intern('close'),
79 'closes': intern('close'),
80 'fix': intern('close'),
81 'fixed': intern('close'),
82 'fixes': intern('close'),
83 'addresses': intern('refs'),
84 're': intern('refs'),
85 'references': intern('refs'),
86 'refs': intern('refs'),
87 'see': intern('refs')}
88
89# Use the egg cache of the environment if not other python egg cache is given.
90if not 'PYTHON_EGG_CACHE' in os.environ:
91 os.environ['PYTHON_EGG_CACHE'] = '/tmp/.egg-cache'
92
93# Construct and compile regular expressions for finding ticket references and
94# actions in commit messages.
95ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
96ticket_reference = ticket_prefix + '[0-9]+'
97ticket_command = (r'(?P<action>[A-Za-z]*).?(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
98 (ticket_reference, ticket_reference))
99command_re = re.compile(ticket_command)
100ticket_re = re.compile(ticket_prefix + '([0-9]+)')
101
102
103def call_git(command, args):
104 return Popen([GIT_PATH, command] + args, stdout=PIPE).communicate()[0]
105
106
107def handle_commit(commit, env):
108 from trac.ticket.notification import TicketNotifyEmail
109 from trac.ticket import Ticket
110 from trac.util.text import to_unicode
111 from trac.util.datefmt import utc
112
113 msg = to_unicode(call_git('rev-list', ['-n', '1', commit, '--pretty=medium']).rstrip())
114 eml = to_unicode(call_git('rev-list', ['-n', '1', commit, '--pretty=format:%ae']).splitlines()[1])
115 now = datetime.now(utc)
116
117 tickets = {}
118 for cmd, tkts in command_re.findall(msg.split('\n\n', 1)[1]):
119 action = COMMANDS.get(cmd.lower())
120 if action:
121 for tkt_id in ticket_re.findall(tkts):
122 tickets.setdefault(tkt_id, []).append(action)
123
124 for tkt_id, actions in tickets.iteritems():
125 try:
126 db = env.get_db_cnx()
127 ticket = Ticket(env, int(tkt_id), db)
128
129 if 'close' in actions:
130 ticket['status'] = 'closed'
131 ticket['resolution'] = 'fixed'
132
133 ticket.save_changes(eml, msg, now)
134 db.commit()
135
136 tn = TicketNotifyEmail(env)
137 tn.notify(ticket, newticket=0, modtime=now)
138 except Exception, e:
139 print >>sys.stderr, 'Unexpected error while processing ticket ID %s: %s' % (tkt_id, e)
140
141
142def handle_ref(old, new, ref, env):
143 # If something else than the master branch (or whatever is contained by the
144 # constant BRANCHES) was pushed, skip this ref.
145 if not ref.startswith('refs/heads/') or ref[11:] not in BRANCHES:
146 return
147
148 # Get the list of hashs for commits in the changeset.
149 args = (old == '0' * 40) and [new] or [new, '^' + old]
150 pending_commits = call_git('rev-list', args).splitlines()
151
152 # Get the subset of pending commits that are laready seen.
153 db = env.get_db_cnx()
154 cursor = db.cursor()
155
156 try:
157 cursor.execute('SELECT sha1 FROM git_seen WHERE sha1 IN (%s)'
158 % ', '.join(['%s'] * len(pending_commits)), pending_commits)
159 seen_commits = map(itemgetter(0), cursor.fetchall())
160 except db.OperationalError:
161 # almost definitely due to git_seen missing
162 cursor = db.cursor() # in case it was closed
163 cursor.execute('CREATE TABLE git_seen (sha1 TEXT)')
164 seen_commits = []
165
166 for commit in pending_commits:
167 # If the commit was seen yet, we must skip it.
168 if commit in seen_commits:
169 continue
170
171 # Remember that have seen this commit, so each commit is only processed once.
172 try:
173 cursor.execute('INSERT INTO git_seen (sha1) VALUES (%s)', [commit])
174 except db.IntegrityError:
175 # If an integrity error occurs (e.g. because of an other process has
176 # seen the script in the meantime), skip it too.
177 continue
178
179 try:
180 handle_commit(commit, env)
181 except Exception, e:
182 print >>sys.stderr, 'Unexpected error while processing commit %s: %s' % (commit[:7], e)
183 db.rollback()
184 else:
185 db.commit()
186
187
188if __name__ == '__main__':
189 from trac.env import open_environment
190 env = open_environment(TRAC_ENV)
191
192 for line in sys.stdin:
193 handle_ref(env=env, *line.split())