~svn/ubuntu/oneiric/subversion/ppa

« back to all changes in this revision

Viewing changes to contrib/hook-scripts/enforcer/enforcer

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2006-12-13 17:57:16 UTC
  • mfrom: (1.1.6 upstream) (0.1.3 etch)
  • Revision ID: james.westby@ubuntu.com-20061213175716-2ysv6z4w5dpa2r2f
Tags: 1.4.2dfsg1-2ubuntu1
* Merge with Debian unstable; remaining changes:
  - Create pot file on build.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- coding:utf-8;mode:python;mode:font-lock -*-
 
3
 
 
4
##
 
5
# Utility for Subversion commit hook scripts
 
6
# This script enforces certain coding guidelines
 
7
##
 
8
# Copyright (c) 2005 Wilfredo Sanchez Vega <wsanchez@wsanchez.net>.
 
9
# All rights reserved.
 
10
#
 
11
# Permission to use, copy, modify, and distribute this software for any
 
12
# purpose with or without fee is hereby granted, provided that the above
 
13
# copyright notice and this permission notice appear in all copies.
 
14
#
 
15
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL
 
16
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
 
17
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
 
18
# AUTHORS BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 
19
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
 
20
# PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 
21
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 
22
# PERFORMANCE OF THIS SOFTWARE.
 
23
##
 
24
 
 
25
import sys
 
26
import os
 
27
import getopt
 
28
import popen2
 
29
 
 
30
#
 
31
# FIXME: Should probably retool this using python bindings, not svnlook
 
32
#
 
33
 
 
34
__doc__ = '''
 
35
Enforcer is a utility which can be used in a Subversion pre-commit
 
36
hook script to enforce various requirements which a repository
 
37
administrator would like to impose on data coming into the repository.
 
38
 
 
39
A couple of example scenarios:
 
40
 
 
41
 - In a Java project I work on, we use log4j extensively.  Use of
 
42
   System.out.println() bypasses the control that we get from log4j,
 
43
   so we would like to discourage the addition of println calls in our
 
44
   code.
 
45
 
 
46
   We want to deny any commits that add a println into the code.  The
 
47
   world being full of exceptions, we do need a way to allow some uses
 
48
   of println, so we will allow it if the line of code that calls
 
49
   println ends in a comment that says it is ok:
 
50
 
 
51
       System.out.println("No log4j here"); // (authorized)
 
52
 
 
53
   We also do not (presently) want to refuse a commit to a file which
 
54
   already has a println in it.  There are too many already in the
 
55
   code and a given developer may not have time to fix them up before
 
56
   commiting an unrelated change to a file.
 
57
 
 
58
 - The above project uses WebObjects, and you can enable debugging in
 
59
   a WebObjects component by turning on the WODebug flag in the
 
60
   component WOD file.  That is great for debugging, but massively
 
61
   bloats the log files when the application is deployed.
 
62
 
 
63
   We want to disable any commit of a file enabling WODebug,
 
64
   regardless of whether the committer made the change or not; these
 
65
   have to be cleaned up before any successful commit.
 
66
 
 
67
What this script does is it uses svnlook to peek into the transaction
 
68
is progress.  As it sifts through the transaction, it calls out to a
 
69
set of hooks which allow the repository administrator to examine what
 
70
is going on and decide whether it is acceptable.  Hooks may be written
 
71
(in Python) into a configuration file.  If the hook raises an
 
72
exception, enforcer will exit with an error status (and presumably the
 
73
commit will be denied by th pre-commit hook). The following hooks are
 
74
available:
 
75
 
 
76
 verify_file_added(filename)
 
77
  - called when a file is added.
 
78
 
 
79
 verify_file_removed(fielname)
 
80
  - called when a file is removed.
 
81
 
 
82
 verify_file_copied(destination_filename, source_filename)
 
83
  - called when a file is copied.
 
84
 
 
85
 verify_file_modified(filename)
 
86
  - called when a file is modified.
 
87
 
 
88
 verify_line_added(filename, line)
 
89
  - called for each line that is added to a file.
 
90
    (verify_file_modified() will have been called on the file
 
91
    beforehand)
 
92
 
 
93
 verify_line_removed(filename, line)
 
94
  - called for each line that is removed from a file.
 
95
    (verify_file_modified() will have been called on the file
 
96
    beforehand)
 
97
 
 
98
 verify_property_line_added(filename, property, line)
 
99
  - called for each line that is added to a property on a file.
 
100
 
 
101
 verify_property_line_removed(filename, property, line)
 
102
  - called for each line that is removed from a property on a file.
 
103
 
 
104
In addition, these functions are available to be called from within a
 
105
hook routine:
 
106
 
 
107
 open_file(filename)
 
108
  - Returns an open file-like object from which the data of the given
 
109
    file (as available in the transaction being processed) can be
 
110
    read.
 
111
 
 
112
In our example scenarios, we can deny the addition of println calls by
 
113
hooking into verify_line_added(): if the file is a Java file, and the
 
114
added line calls println, raise an exception.
 
115
 
 
116
Similarly, we can deny the commit of any WOD file enabling WODebug by
 
117
hooking into verify_file_modified(): open the file using open_file(),
 
118
then raise if WODebug is enabled anywhere in the file.
 
119
 
 
120
Note that verify_file_modified() is called once per modified file,
 
121
whereas verify_line_added() and verify_line_removed() may each be
 
122
called zero or many times for each modified file, depending on the
 
123
change.  This makes verify_file_modified() appropriate for checking
 
124
the entire file and the other two appropriate for checking specific
 
125
changes to files.
 
126
 
 
127
These example scenarios are implemented in the provided example
 
128
configuration file "enforcer.conf".
 
129
 
 
130
When writing hooks, it is usually easier to test the hooks on commited
 
131
transactions already in the repository, rather than installing the
 
132
hook and making commits to test the them.  Enforcer allows you to
 
133
specify either a transaction ID (for use in a hook script) or a
 
134
revision number (for testing).  You can then, for example, find a
 
135
revision that you would like to have blocked (or not) and test your
 
136
hooks against that revision.
 
137
'''
 
138
__author__ = "Wilfredo Sanchez Vega <wsanchez@wsanchez.net>"
 
139
 
 
140
##
 
141
# Handle command line
 
142
##
 
143
 
 
144
program     = os.path.split(sys.argv[0])[1]
 
145
debug       = 0
 
146
transaction = None
 
147
revision    = None
 
148
 
 
149
def usage(e=None):
 
150
    if e:
 
151
        print e
 
152
        print ""
 
153
 
 
154
    print "usage: %s [options] repository config" % program
 
155
    print "options:"
 
156
    print "\t-d, --debug             Print debugging output; use twice for more"
 
157
    print "\t-r, --revision    rev   Specify revision to check"
 
158
    print "\t-t, --transaction txn   Specify transaction to check"
 
159
    print "Exactly one of --revision or --transaction is required"
 
160
 
 
161
    sys.exit(1)
 
162
 
 
163
# Read options
 
164
try:
 
165
    (optargs, args) = getopt.getopt(sys.argv[1:], "dt:r:", ["debug", "transaction=", "revision="])
 
166
except getopt.GetoptError, e:
 
167
    usage(e)
 
168
 
 
169
for optarg in optargs:
 
170
    (opt, arg) = optarg
 
171
    if   opt in ("-d", "--debug"      ): debug += 1
 
172
    elif opt in ("-t", "--transaction"): transaction = arg
 
173
    elif opt in ("-r", "--revision"   ): revision    = arg
 
174
 
 
175
if transaction and revision:
 
176
    usage("Cannot specify both transaction and revision to check")
 
177
if not transaction and not revision:
 
178
    usage("Must specify transaction or revision to check")
 
179
 
 
180
if not len(args): usage("No repository")
 
181
repository = args.pop(0)
 
182
 
 
183
if not len(args): usage("No config")
 
184
configuration_filename = args.pop(0)
 
185
 
 
186
if len(args): usage("Too many arguments")
 
187
 
 
188
##
 
189
# Validation
 
190
# All rule enforcement goes in these routines
 
191
##
 
192
 
 
193
def open_file(filename):
 
194
    """
 
195
    Retrieves the contents of the given file.
 
196
    """
 
197
    cat_cmd = [ "svnlook", "cat", None, repository, filename ]
 
198
 
 
199
    if   transaction: cat_cmd[2] = "--transaction=" + transaction
 
200
    elif revision:    cat_cmd[2] = "--revision="    + revision
 
201
    else: raise ValueError("No transaction or revision")
 
202
 
 
203
    cat_out, cat_in = popen2.popen2(cat_cmd)
 
204
    cat_in.close()
 
205
 
 
206
    return cat_out
 
207
 
 
208
def verify_file_added(filename):
 
209
    """
 
210
    Here we verify file additions which may not meet our requirements.
 
211
    """
 
212
    if debug: print "Added file %r" % filename
 
213
    if configuration.has_key("verify_file_added"):
 
214
        configuration["verify_file_added"](filename)
 
215
 
 
216
def verify_file_removed(filename):
 
217
    """
 
218
    Here we verify file removals which may not meet our requirements.
 
219
    """
 
220
    if debug: print "Removed file %r" % filename
 
221
    if configuration.has_key("verify_file_removed"):
 
222
        configuration["verify_file_removed"](filename)
 
223
 
 
224
def verify_file_copied(destination_filename, source_filename):
 
225
    """
 
226
    Here we verify file copies which may not meet our requirements.
 
227
    """
 
228
    if debug: print "Copied %r to %r" % (source_filename, destination_filename)
 
229
    if configuration.has_key("verify_file_copied"):
 
230
        configuration["verify_file_copied"](destination_filename, source_filename)
 
231
 
 
232
def verify_file_modified(filename):
 
233
    """
 
234
    Here we verify files which may not meet our requirements.
 
235
    Any failure, even if not due to the specific changes in the commit
 
236
    will raise an error.
 
237
    """
 
238
    if debug: print "Modified file %r" % filename
 
239
    if configuration.has_key("verify_file_modified"):
 
240
        configuration["verify_file_modified"](filename)
 
241
 
 
242
def verify_line_added(filename, line):
 
243
    """
 
244
    Here we verify new lines of code which may not meet our requirements.
 
245
    Code not changed as part of this commit is not verified.
 
246
    """
 
247
    if configuration.has_key("verify_line_added"):
 
248
        configuration["verify_line_added"](filename, line)
 
249
 
 
250
def verify_line_removed(filename, line):
 
251
    """
 
252
    Here we verify removed lines of code which may not meet our requirements.
 
253
    Code not changed as part of this commit is not verified.
 
254
    """
 
255
    if configuration.has_key("verify_line_removed"):
 
256
        configuration["verify_line_removed"](filename, line)
 
257
 
 
258
def verify_property_line_added(filename, property, line):
 
259
    """
 
260
    Here we verify added property lines which may not meet our requirements.
 
261
    Code not changed as part of this commit is not verified.
 
262
    """
 
263
    if debug: print "Add %s::%s: %s" % (filename, property, line)
 
264
    if configuration.has_key("verify_property_line_added"):
 
265
        configuration["verify_property_line_added"](filename, property, line)
 
266
 
 
267
def verify_property_line_removed(filename, property, line):
 
268
    """
 
269
    Here we verify removed property lines which may not meet our requirements.
 
270
    Code not changed as part of this commit is not verified.
 
271
    """
 
272
    if debug: print "Del %s::%s: %s" % (filename, property, line)
 
273
    if configuration.has_key("verify_property_line_removed"):
 
274
        configuration["verify_property_line_removed"](filename, property, line)
 
275
 
 
276
##
 
277
# Do the Right Thing
 
278
##
 
279
 
 
280
configuration = {"open_file": open_file}
 
281
execfile(configuration_filename, configuration, configuration)
 
282
 
 
283
diff_cmd = [ "svnlook", "diff", None, repository ]
 
284
 
 
285
if   transaction: diff_cmd[2] = "--transaction=" + transaction
 
286
elif revision:    diff_cmd[2] = "--revision="    + revision
 
287
else: raise ValueError("No transaction or revision")
 
288
 
 
289
diff_out, diff_in = popen2.popen2(diff_cmd)
 
290
diff_in.close()
 
291
 
 
292
try:
 
293
    state = 0
 
294
 
 
295
    #
 
296
    # This is the svnlook output parser
 
297
    #
 
298
    for line in diff_out:
 
299
        if line[-1] == "\n": line = line[:-1] # Zap trailing newline
 
300
 
 
301
        # Test cases:
 
302
        #   r2266:  Added text files, property changes
 
303
        #  r18923: Added, deleted, modified text files
 
304
        #  r25692: Copied files
 
305
        #   r7758: Added binary files
 
306
 
 
307
        if debug > 1: print "%4d: %s" % (state, line) # Useful for testing parser problems
 
308
 
 
309
        if state is -1: # Used for testing new states: print whatever is left
 
310
            print line
 
311
            continue
 
312
 
 
313
        if state in (0, 100, 300): # Initial state or in a state that may return to initial state
 
314
            if state is 0 and not line: continue
 
315
 
 
316
            colon = line.find(":")
 
317
 
 
318
            if colon != -1 and len(line) > colon + 2:
 
319
                action   = line[:colon]
 
320
                filename = line[colon+2:]
 
321
 
 
322
                if action in (
 
323
                    "Modified",
 
324
                    "Added", "Deleted", "Copied",
 
325
                    "Property changes on",
 
326
                ):
 
327
                    if   action == "Modified": verify_file_modified(filename)
 
328
                    elif action == "Added"   : verify_file_added   (filename)
 
329
                    elif action == "Deleted" : verify_file_removed (filename)
 
330
                    elif action == "Copied":
 
331
                        i = filename.find(" (from rev ")
 
332
                        destination_filename = filename[:i]
 
333
                        filename = filename[i:]
 
334
 
 
335
                        i = filename.find(", ")
 
336
                        assert filename[-1] == ")"
 
337
                        source_filename = filename[i+2:-1]
 
338
 
 
339
                        verify_file_copied(destination_filename, source_filename)
 
340
 
 
341
                        filename = destination_filename
 
342
 
 
343
                    if   action == "Modified"           : state = 10
 
344
                    elif action == "Added"              : state = 10
 
345
                    elif action == "Deleted"            : state = 10
 
346
                    elif action == "Copied"             : state = 20
 
347
                    elif action == "Property changes on": state = 30
 
348
                    else: raise AssertionError("Unknown action")
 
349
 
 
350
                    current_filename = filename
 
351
                    current_property = None
 
352
 
 
353
                    continue
 
354
 
 
355
            assert state in (100, 300)
 
356
 
 
357
        if state is 10: # Expecting a bar (follows "Modified:" line)
 
358
            assert line == "=" * 67
 
359
            state = 11
 
360
            continue
 
361
 
 
362
        if state is 11: # Expecting left file info (follows bar)
 
363
            if   line == "":                      state =  0
 
364
            elif line == "(Binary files differ)": state =  0
 
365
            elif line.startswith("--- "):         state = 12
 
366
            else: raise AssertionError("Expected left file info, got: %r" % line)
 
367
 
 
368
            continue
 
369
 
 
370
        if state is 12: # Expecting right file info (follows left file info)
 
371
            assert line.startswith("+++ " + current_filename)
 
372
            state = 100
 
373
            continue
 
374
 
 
375
        if state is 20: # Expecting a bar or blank (follows "Copied:" line)
 
376
            # Test cases:
 
377
            # r25692: Copied and not modified (blank)
 
378
            # r26613: Copied and modified (bar)
 
379
            if not line:
 
380
                state = 0
 
381
            elif line == "=" * 67:
 
382
                state = 11
 
383
            else:
 
384
                raise AssertionError("After Copied: line, neither bar nor blank: %r" % line)
 
385
            continue
 
386
 
 
387
        if state is 100: # Expecting diff data
 
388
            for c, verify in (("-", verify_line_removed), ("+", verify_line_added)):
 
389
                if len(line) >= 1 and line[0] == c:
 
390
                    try: verify(current_filename, line[1:])
 
391
                    except Exception, e:
 
392
                        sys.stderr.write(str(e))
 
393
                        sys.stderr.write("\n")
 
394
                        sys.exit(1)
 
395
                    break
 
396
            else:
 
397
                if (
 
398
                    not line or
 
399
                    (len(line) >= 4 and line[:2] == "@@" == line[-2:]) or
 
400
                    (len(line) >= 1 and line[0]  == " ") or
 
401
                    line == "\\ No newline at end of file"
 
402
                ):
 
403
                    continue
 
404
 
 
405
                raise AssertionError("Expected diff data, got: %r" % line)
 
406
 
 
407
            continue
 
408
 
 
409
        if state is 30: # Expecting a bar (follows "Property changes on:" line)
 
410
            assert line == "_" * 67
 
411
            state = 31
 
412
            continue
 
413
 
 
414
        if state is 31: # Expecting property name (follows bar)
 
415
            assert line.startswith("Name: ")
 
416
            state = 300
 
417
            # Fall through to state 300
 
418
 
 
419
        if state is 300:
 
420
            if line.startswith("Name: "):
 
421
                current_property = line[6:]
 
422
                continue
 
423
 
 
424
            for prefix, verify in (
 
425
                ("   - ", verify_property_line_removed),
 
426
                ("   + ", verify_property_line_added)
 
427
            ):
 
428
                if line.startswith(prefix):
 
429
                    try: verify(current_filename, current_property, line[5:])
 
430
                    except Exception, e:
 
431
                        sys.stderr.write(str(e))
 
432
                        sys.stderr.write("\n")
 
433
                        sys.exit(1)
 
434
                    break
 
435
            else:
 
436
                if not line: continue
 
437
 
 
438
                raise AssertionError("Expected property diff data, got: %r" % line)
 
439
 
 
440
            continue
 
441
 
 
442
        raise AssertionError("Unparsed line: %r" % line)
 
443
 
 
444
    if debug: print "Commit is OK"
 
445
 
 
446
finally:
 
447
    while diff_out.read(1024 * 1024 * 64): pass
 
448
    diff_out.close()