]> git.rmz.io Git - dotfiles.git/blob - bin/svn-hook-postcommit-review
ef26c8e003d591f3fba61da3cb3b8b751be71e4e
[dotfiles.git] / bin / svn-hook-postcommit-review
1 #!/usr/bin/python
2
3 # svn-hook-postcommit-review
4 # This script should be invoked from the subversion post-commit hook like this:
5 #
6 # REPOS="$1"
7 # REV="$2"
8 # /usr/bin/python /some/path/svn-hook-postcommit-review "$REPOS" "$REV" || exit 1
9 #
10 # Searches the commit message for text in the form of:
11 # publish review - publishes a review request
12 # draft review - creates a draft review request
13 #
14 # The space before 'review' may be ommitted.
15 #
16 # The log message is interpreted for review request parameters:
17 # summary = up to first period+space, first new-line, or 250 chars
18 # description = entire log message
19 # existing review updated if log message includes 'update review:[0-9]+'
20 # bugs added to review if log message includes commands as defined in
21 # supported_ticket_cmds
22 #
23 # By default, the review request is created out of a diff between the current
24 # revision (M) and the previous revision (M-1).
25 #
26 # To create a diff that spans multiple revisions, include
27 # 'after revision:[0-9]+' in the log message.
28 #
29 # To limit the diff to changes in a certain path (e.g. a branch), include
30 # 'base path:"<path>"' in the log message. The path must be relative to
31 # the root of the repository and be surrounded by single or double quotes.
32 #
33 # An example commit message is:
34 #
35 # Changed blah and foo to do this or that. Publish review ticket:1
36 # update review:2 after revision:3 base path:'internal/trunk/style'.
37 #
38 # This would update the existing review 2 with a diff of changes to files under
39 # the style directory between this commit and revision 3. It would place
40 # the entire log message in the review summary and description, and put
41 # bug id 1 in the bugs field.
42 #
43 # This script may only be run from outside a working copy.
44 #
45
46 #
47 # User configurable variables
48 #
49
50 # Path to post-review script
51 POSTREVIEW_PATH = ""
52 # Username and password for Review Board user that will be connecting
53 # to create all review requests. This user must have 'submit as'
54 # privileges, since it will submit requests in the name of svn committers.
55 USERNAME = 'su_user'
56 PASSWORD = 'TYxxcGm337FtubqN'
57
58 # If true, runs post-review in debug mode and outputs its diff
59 DEBUG = False
60
61 #
62 # end user configurable variables
63 #
64
65 import sys
66 import os
67 import subprocess
68 import re
69 import svn.fs
70 import svn.core
71 import svn.repos
72
73 # list of trac commands from trac-post-commit-hook.py.
74 # numbers following these commands will be added to the bugs
75 # field of the review request.
76 supported_ticket_cmds = {'review': '_cmdReview',
77 'publishreview': '_cmdReview',
78 'publish review': '_cmdReview',
79 'draftreview': '_cmdReview',
80 'draft review': '_cmdReview'}
81
82 ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
83 ticket_reference = ticket_prefix + '[0-9]+'
84 ticket_command = (r'(?P<action>[A-Za-z]*).?'
85 '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
86 (ticket_reference, ticket_reference))
87
88 def execute(command, env=None, ignore_errors=False):
89 """
90 Utility function to execute a command and return the output.
91 Derived from Review Board's post-review script.
92 """
93 if env:
94 env.update(os.environ)
95 else:
96 env = os.environ
97
98 p = subprocess.Popen(command,
99 stdin = subprocess.PIPE,
100 stdout = subprocess.PIPE,
101 stderr = subprocess.STDOUT,
102 shell = False,
103 close_fds = sys.platform.startswith('win'),
104 universal_newlines = True,
105 env = env)
106 data = p.stdout.read()
107 rc = p.wait()
108 if rc and not ignore_errors:
109 sys.stderr.write('Failed to execute command: %s\n%s\n' % (command, data))
110 sys.exit(1)
111
112 return data
113
114 def main():
115 if len(sys.argv) != 3:
116 sys.stderr.write('Usage: %s <repos> <rev>\n' % sys.argv[0])
117 sys.exit(1)
118
119 repos = sys.argv[1]
120 rev = sys.argv[2]
121
122 # verify that rev parameter is an int
123 try:
124 int(rev)
125 except ValueError:
126 sys.stderr.write("Parameter <rev> must be an int, was given %s\n" % rev)
127 sys.exit(1)
128
129 # get the svn file system object
130 fs_ptr = svn.repos.svn_repos_fs(svn.repos.svn_repos_open(
131 svn.core.svn_path_canonicalize(repos)))
132
133 # get the log message
134 log = svn.fs.svn_fs_revision_prop(fs_ptr, int(rev),
135 svn.core.SVN_PROP_REVISION_LOG)
136
137 # error if log message is blank
138 if len(log.strip()) < 1:
139 sys.stderr.write("Log message is empty, no review request created\n")
140 sys.exit(1)
141
142 # get the author
143 author = svn.fs.svn_fs_revision_prop(fs_ptr, int(rev),
144 svn.core.SVN_PROP_REVISION_AUTHOR)
145
146 # error if author is blank
147 if len(author.strip()) < 1:
148 sys.stderr.write("Author is blank, no review request created\n")
149 sys.exit(1)
150
151 # check whether to create a review, based on presence of word
152 # 'review' with prefix
153 review = r'(?:publish|draft)(?: )?review'
154 if not re.search(review, log, re.M | re.I):
155 print 'No review requested'
156 sys.exit(0)
157
158 # check for update to existing review
159 m = re.search(r'update(?: )?review:([0-9]+)', log, re.M | re.I)
160 if m:
161 reviewid = '--review-request-id=' + m.group(1)
162 else:
163 reviewid = ''
164
165 # check whether to publish or leave review as draft
166 if re.search(r'draft(?: )?review', log, re.M | re.I):
167 publish = ''
168 else:
169 publish = '-p'
170
171 # get previous revision number -- either 1 prior, or
172 # user-specified number
173 m = re.search(r'after(?: )?revision:([0-9]+)', log, re.M | re.I)
174 if m:
175 prevrev = m.group(1)
176 else:
177 prevrev = int(rev) - 1
178
179 # check for an explicitly-provided base path (must be contained
180 # within quotes)
181 m = re.search(r'base ?path:[\'"]([^\'"]+)[\'"]', log, re.M | re.I)
182 if m:
183 base_path = m.group(1)
184 else:
185 base_path = ''
186
187 # get bug numbers referenced in this log message
188 ticket_command_re = re.compile(ticket_command)
189 ticket_re = re.compile(ticket_prefix + '([0-9]+)')
190
191 ticket_ids = []
192 ticket_cmd_groups = ticket_command_re.findall(log)
193 for cmd, tkts in ticket_cmd_groups:
194 funcname = supported_ticket_cmds.get(cmd.lower(), '')
195 if funcname:
196 for tkt_id in ticket_re.findall(tkts):
197 ticket_ids.append(tkt_id)
198
199 if ticket_ids:
200 bugs = '--bugs-closed=' + ','.join(ticket_ids)
201 else:
202 bugs = ''
203
204 # summary is log up to first period+space / first new line / first 250 chars
205 # (whichever comes first)
206 summary = '--summary=' + log[:250].splitlines().pop(0).split('. ').pop(0)
207
208 # other parameters for postreview
209 repository_url = '--repository-url=file://' + repos
210 password = '--password=' + PASSWORD
211 username = '--username=' + USERNAME
212 description = "--description=(In [%s]) %s" % (rev, log)
213 submitas = '--submit-as=' + author
214 revision = '--revision-range=%s:%s' % (prevrev, rev)
215
216 # common arguments
217 args = [repository_url, username, password, publish,
218 submitas, revision, base_path, reviewid]
219
220 # filter out any potentially blank args, which will confuse post-review
221 args = [i for i in args if len(i) > 1]
222
223 # if not updating an existing review, add extra arguments
224 if len(reviewid) == 0:
225 args += [summary, description, bugs]
226
227 if DEBUG:
228 args += ['-d', '--output-diff']
229 print [os.path.join(POSTREVIEW_PATH, 'post-review')] + args
230
231 # Run Review Board post-review script
232 data = execute([os.path.join(POSTREVIEW_PATH, 'post-review')] + args,
233 env = {'LANG': 'en_US.UTF-8'})
234
235 if DEBUG:
236 print data
237
238 if __name__ == '__main__':
239 main()
240