ba82f5a2d37483ef78f8c52bd125f7dfb38398ba
[update-copyright.git] / update_copyright.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of Hooke.
6 #
7 # Hooke is free software: you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation, either
10 # version 3 of the License, or (at your option) any later version.
11 #
12 # Hooke is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with Hooke.  If not, see
19 # <http://www.gnu.org/licenses/>.
20
21 """Automatically update copyright boilerplate.
22
23 This script is adapted from one written for `Bugs Everywhere`_.
24
25 .. _Bugs Everywhere: http://bugseverywhere.org/
26 """
27
28 import difflib
29 import email.utils
30 import os
31 import os.path
32 import sys
33 import textwrap
34 import time
35
36
37 PROJECT_INFO = {
38     'project': 'Hooke',
39     'vcs': 'Mercurial',
40     }
41
42 # Break "copyright" into "copy" and "right" to avoid matching the
43 # REGEXP if we decide to go back to regexps.
44 COPY_RIGHT_TEXT="""
45 This file is part of %(project)s.
46
47 %(project)s is free software: you can redistribute it and/or
48 modify it under the terms of the GNU Lesser General Public
49 License as published by the Free Software Foundation, either
50 version 3 of the License, or (at your option) any later version.
51
52 %(project)s is distributed in the hope that it will be useful,
53 but WITHOUT ANY WARRANTY; without even the implied warranty of
54 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
55 GNU Lesser General Public License for more details.
56
57 You should have received a copy of the GNU Lesser General Public
58 License along with %(project)s.  If not, see
59 <http://www.gnu.org/licenses/>.
60 """.strip()
61
62 SHORT_COPY_RIGHT_TEXT="""
63 %(project)s comes with ABSOLUTELY NO WARRANTY and is licensed
64 under the GNU Lesser General Public License.  For details,
65 %(get-details)s
66 """.strip()
67
68 COPY_RIGHT_TAG='-xyz-COPY' + '-RIGHT-zyx-' # unlikely to occur in the wild :p
69
70 # Convert author names to canonical forms.
71 # ALIASES[<canonical name>] = <list of aliases>
72 # for example,
73 # ALIASES = {
74 #     'John Doe <jdoe@a.com>':
75 #         ['John Doe', 'jdoe', 'J. Doe <j@doe.net>'],
76 #     }
77 # Git-based projects are encouraged to use .mailmap instead of
78 # ALIASES.  See git-shortlog(1) for details.
79 ALIASES = {
80     'A. Seeholzer':
81         ['A. Seeholzer'],
82     'Alberto Gomez-Casado':
83         ['albertogomcas'],
84     'Massimo Sandal <devicerandom@gmail.com>':
85         ['Massimo Sandal',
86          'devicerandom',
87          'unknown'],
88     'Fabrizio Benedetti':
89         ['fabrizio.benedetti.82'],
90     'Richard Naud <richard.naud@epfl.ch>':
91         ['Richard Naud'],
92     'Rolf Schmidt <rschmidt@alcor.concordia.ca>':
93         ['Rolf Schmidt',
94          'illysam'],
95     'Marco Brucale':
96         ['marcobrucale'],
97     'Pancaldi Paolo':
98         ['pancaldi.paolo'],
99     }
100
101 # List of paths that should not be scanned for copyright updates.
102 # IGNORED_PATHS = ['./.git/']
103 IGNORED_PATHS = ['./.hg/', './doc/img/', './test/data/',
104                  './build/', './doc/build/']
105 # List of files that should not be scanned for copyright updates.
106 # IGNORED_FILES = ['COPYING']
107 IGNORED_FILES = ['COPYING', 'COPYING.LESSER']
108
109 # Work around missing author holes in the VCS history.
110 # AUTHOR_HACKS[<path tuple>] = [<missing authors]
111 # for example, if John Doe contributed to module.py but wasn't listed
112 # in the VCS history of that file:
113 # AUTHOR_HACKS = {
114 #     ('path', 'to', 'module.py'):['John Doe'],
115 #     }
116 AUTHOR_HACKS = {
117     ('hooke','driver','hdf5.py'):['Massimo Sandal'],
118     ('hooke','driver','mcs.py'):['Allen Chen'],
119     ('hooke','driver','mfp3d.py'):['A. Seeholzer','Richard Naud','Rolf Schmidt',
120                                    'Alberto Gomez-Casado'],
121     ('hooke','util','peak.py'):['Fabrizio Benedetti'],
122     ('hooke','plugin','showconvoluted.py'):['Rolf Schmidt'],
123     ('hooke','ui','gui','formatter.py'):['Francesco Musiani','Massimo Sandal'],
124     ('hooke','ui','gui','prettyformat.py'):['Rolf Schmidt'],
125     }
126
127 # Work around missing year holes in the VCS history.
128 # YEAR_HACKS[<path tuple>] = <original year>
129 # for example, if module.py was published in 2008 but the VCS history
130 # only goes back to 2010:
131 # YEAR_HACKS = {
132 #     ('path', 'to', 'module.py'):2008,
133 #     }
134 YEAR_HACKS = {
135     ('hooke','driver','hdf5.py'):2009,
136     ('hooke','driver','mfp3d.py'):2008,
137     ('hooke','driver','picoforce.py'):2006,
138     ('hooke','driver','picoforcealt.py'):2006,
139     ('hooke','util','peak.py'):2007,
140     ('hooke','plugin','showconvoluted.py'):2009,
141     ('hooke','plugin','tutorial.py'):2007,
142     ('hooke','ui','gui','formatter.py'):2006,
143     ('hooke','ui','gui','prettyformat.py'):2009,
144     }
145
146 # Helpers for VCS-specific commands
147
148 def splitpath(path):
149     """Recursively split a path into elements.
150
151     Examples
152     --------
153
154     >>> splitpath(os.path.join('a', 'b', 'c'))
155     ('a', 'b', 'c')
156     >>> splitpath(os.path.join('.', 'a', 'b', 'c'))
157     ('a', 'b', 'c')
158     """
159     path = os.path.normpath(path)
160     elements = []
161     while True:
162         dirname,basename = os.path.split(path)
163         elements.insert(0,basename)
164         if dirname in ['', '.']:
165             break
166         path = dirname
167     return tuple(elements)
168
169 # VCS-specific commands
170
171 if PROJECT_INFO['vcs'] == 'Git':
172
173     import subprocess
174
175     _MSWINDOWS = sys.platform == 'win32'
176     _POSIX = not _MSWINDOWS
177
178     def invoke(args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, expect=(0,)):
179         """
180         expect should be a tuple of allowed exit codes.
181         """
182         try :
183             if _POSIX:
184                 q = subprocess.Popen(args, stdin=subprocess.PIPE,
185                                      stdout=stdout, stderr=stderr)
186             else:
187                 assert _MSWINDOWS == True, 'invalid platform'
188                 # win32 don't have os.execvp() so run the command in a shell
189                 q = subprocess.Popen(args, stdin=subprocess.PIPE,
190                                      stdout=stdout, stderr=stderr, shell=True)
191         except OSError, e:
192             raise ValueError([args, e])
193         stdout,stderr = q.communicate(input=stdin)
194         status = q.wait()
195         if status not in expect:
196             raise ValueError([args, status, stdout, stderr])
197         return status, stdout, stderr
198
199     def git_cmd(*args):
200         status,stdout,stderr = invoke(['git'] + list(args))
201         return stdout.rstrip('\n')
202
203     def original_year(filename=None, year_hacks=YEAR_HACKS):
204         args = [
205             '--format=format:%ad',  # Author date
206             '--date=short',         # YYYY-MM-DD
207             ]
208         if filename != None:
209             args.extend(['--follow', filename])
210         output = git_cmd('log', *args)
211         years = [int(line.split('-', 1)[0]) for line in output.splitlines()]
212         if filename == None:
213             years.extend(year_hacks.values())
214         elif splitpath(filename) in year_hacks:
215             years.append(year_hacks[splitpath(filename)])
216         years.sort()
217         return years[0]
218
219     def authors(filename, author_hacks=AUTHOR_HACKS):
220         output = git_cmd('log', '--follow', '--format=format:%aN <%aE>',
221                          filename)   # Author name <author email>
222         ret = list(set(output.splitlines()))
223         if splitpath(filename) in author_hacks:
224             ret.extend(author_hacks[splitpath(filename)])
225         return ret
226
227     def authors_list(author_hacks=AUTHOR_HACKS):
228         output = git_cmd('log', '--format=format:%aN <%aE>')
229         ret = list(set(output.splitlines()))
230         for path,authors in author_hacks.items():
231             ret.extend(authors)
232         return ret
233
234     def is_versioned(filename):
235         output = git_cmd('log', '--follow', filename)
236         if len(output) == 0:
237             return False
238         return True
239
240 elif PROJECT_INFO['vcs'] == 'Mercurial':
241
242     import StringIO
243     import mercurial
244     import mercurial.dispatch
245
246     def mercurial_cmd(*args):
247         cwd = os.getcwd()
248         stdout = sys.stdout
249         stderr = sys.stderr
250         tmp_stdout = StringIO.StringIO()
251         tmp_stderr = StringIO.StringIO()
252         sys.stdout = tmp_stdout
253         sys.stderr = tmp_stderr
254         try:
255             mercurial.dispatch.dispatch(list(args))
256         finally:
257             os.chdir(cwd)
258             sys.stdout = stdout
259             sys.stderr = stderr
260         return (tmp_stdout.getvalue().rstrip('\n'),
261                 tmp_stderr.getvalue().rstrip('\n'))
262
263     def original_year(filename=None, year_hacks=YEAR_HACKS):
264         args = [
265             '--template', '{date|shortdate}\n',
266             # shortdate filter: YEAR-MONTH-DAY
267             ]
268         if filename != None:
269             args.extend(['--follow', filename])
270         output,error = mercurial_cmd('log', *args)
271         years = [int(line.split('-', 1)[0]) for line in output.splitlines()]
272         if filename == None:
273             years.extend(year_hacks.values())
274         elif splitpath(filename) in year_hacks:
275             years.append(year_hacks[splitpath(filename)])
276         years.sort()
277         return years[0]
278
279     def authors(filename, author_hacks=AUTHOR_HACKS):
280         output,error = mercurial_cmd('log', '--follow',
281                                      '--template', '{author}\n',
282                                      filename)
283         ret = list(set(output.splitlines()))
284         if splitpath(filename) in author_hacks:
285             ret.extend(author_hacks[splitpath(filename)])
286         return ret
287
288     def authors_list(author_hacks=AUTHOR_HACKS):
289         output,error = mercurial_cmd('log', '--template', '{author}\n')
290         ret = list(set(output.splitlines()))
291         for path,authors in author_hacks.items():
292             ret.extend(authors)
293         return ret
294
295     def is_versioned(filename):
296         output,error = mercurial_cmd('log', '--follow', filename)
297         if len(error) > 0:
298             return False
299         return True
300
301 elif PROJECT_INFO['vcs'] == 'Bazaar':
302
303     import StringIO
304     import bzrlib
305     import bzrlib.builtins
306     import bzrlib.log
307
308     class LogFormatter (bzrlib.log.LogFormatter):
309         supports_merge_revisions = True
310         preferred_levels = 0
311         supports_deta = False
312         supports_tags = False
313         supports_diff = False
314
315         def log_revision(self, revision):
316             raise NotImplementedError
317
318     class YearLogFormatter (LogFormatter):
319         def log_revision(self, revision):
320             self.to_file.write(
321                 time.strftime('%Y', time.gmtime(revision.rev.timestamp))
322                 +'\n')
323
324     class AuthorLogFormatter (LogFormatter):
325         def log_revision(self, revision):
326             authors = revision.rev.get_apparent_authors()
327             self.to_file.write('\n'.join(authors)+'\n')
328
329     def original_year(filename=None, year_hacks=YEAR_HACKS):
330         cmd = bzrlib.builtins.cmd_log()
331         cmd.outf = StringIO.StringIO()
332         kwargs = {'log_format':YearLogFormatter, 'levels':0}
333         if filename != None:
334             kwargs['file_list'] = [filenme]
335         cmd.run(**kwargs)
336         years = [int(year) for year in set(cmd.outf.getvalue().splitlines())]
337         if filename == None:
338             years.append(year_hacks.values())
339         elif splitpath(filename) in year_hacks:
340             years.append(year_hacks[splitpath(filename)])
341         years.sort()
342         return years[0]
343
344     def authors(filename, author_hacks=AUTHOR_HACKS):
345         cmd = bzrlib.builtins.cmd_log()
346         cmd.outf = StringIO.StringIO()
347         cmd.run(file_list=[filename], log_format=AuthorLogFormatter, levels=0)
348         ret = list(set(cmd.outf.getvalue().splitlines()))
349         if splitpath(filename) in author_hacks:
350             ret.extend(author_hacks[splitpath(filename)])
351         return ret
352
353     def authors_list(author_hacks=AUTHOR_HACKS):
354         cmd = bzrlib.builtins.cmd_log()
355         cmd.outf = StringIO.StringIO()
356         cmd.run(log_format=AuthorLogFormatter, levels=0)
357         output = cmd.outf.getvalue()
358         ret = list(set(cmd.outf.getvalue().splitlines()))
359         for path,authors in author_hacks.items():
360             ret.extend(authors)
361         return ret
362
363     def is_versioned(filename):
364         cmd = bzrlib.builtins.cmd_log()
365         cmd.outf = StringIO.StringIO()
366         cmd.run(file_list=[filename])
367         return True
368
369 else:
370     raise NotImplementedError('Unrecognized VCS: %(vcs)s' % PROJECT_INFO)
371
372 # General utility commands
373
374 def _strip_email(*args):
375     """Remove email addresses from a series of names.
376
377     Examples
378     --------
379
380     >>> _strip_email('J Doe')
381     ['J Doe']
382     >>> _strip_email('J Doe <jdoe@a.com>')
383     ['J Doe']
384     >>> _strip_email('J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>')
385     ['J Doe', 'JJJ Smith']
386     """
387     args = list(args)
388     for i,arg in enumerate(args):
389         if arg == None:
390             continue
391         author,addr = email.utils.parseaddr(arg)
392         if author == '':
393             author = arg
394         args[i] = author
395     return args
396
397 def _reverse_aliases(aliases):
398     """Reverse an `aliases` dict.
399
400     Input:   key: canonical name,  value: list of aliases
401     Output:  key: alias,           value: canonical name
402
403     Examples
404     --------
405
406     >>> aliases = {
407     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>', 'J'],
408     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
409     ...     None:['Anonymous <a@a.com>'],
410     ...     }
411     >>> r = _reverse_aliases(aliases)
412     >>> for item in sorted(r.items()):
413     ...     print item
414     ('Anonymous <a@a.com>', None)
415     ('J', 'J Doe <jdoe@a.com>')
416     ('Jingly <jjjs@b.edu>', 'JJJ Smith <jjjs@a.com>')
417     ('Johnny <jdoe@b.edu>', 'J Doe <jdoe@a.com>')
418     """
419     output = {}
420     for canonical_name,_aliases in aliases.items():
421         for alias in _aliases:
422             output[alias] = canonical_name
423     return output
424
425 def _replace_aliases(authors, with_email=True, aliases=None):
426     """Consolidate and sort `authors`.
427
428     Make the replacements listed in the `aliases` dict (key: canonical
429     name, value: list of aliases).  If `aliases` is ``None``, default
430     to ``ALIASES``.
431
432     >>> aliases = {
433     ...     'J Doe <jdoe@a.com>':['Johnny <jdoe@b.edu>'],
434     ...     'JJJ Smith <jjjs@a.com>':['Jingly <jjjs@b.edu>'],
435     ...     None:['Anonymous <a@a.com>'],
436     ...     }
437     >>> authors = [
438     ...     'JJJ Smith <jjjs@a.com>', 'Johnny <jdoe@b.edu>',
439     ...     'Jingly <jjjs@b.edu>', 'J Doe <jdoe@a.com>', 'Anonymous <a@a.com>']
440     >>> _replace_aliases(authors, with_email=True, aliases=aliases)
441     ['J Doe <jdoe@a.com>', 'JJJ Smith <jjjs@a.com>']
442     >>> _replace_aliases(authors, with_email=False, aliases=aliases)
443     ['J Doe', 'JJJ Smith']
444     """
445     if aliases == None:
446         aliases = ALIASES
447     rev_aliases = _reverse_aliases(aliases)
448     for i,author in enumerate(authors):
449         if author in rev_aliases:
450             authors[i] = rev_aliases[author]
451     authors = sorted(list(set(authors)))
452     if None in authors:
453         authors.remove(None)
454     if with_email == False:
455         authors = _strip_email(*authors)
456     return authors
457
458 def _long_author_formatter(copyright_year_string, authors):
459     """
460     >>> print '\\n'.join(_long_author_formatter(
461     ...     copyright_year_string='Copyright (C) 1990-2010',
462     ...     authors=['Jack', 'Jill', 'John']))
463     Copyright (C) 1990-2010 Jack
464                             Jill
465                             John
466     """
467     lines = ['%s %s' % (copyright_year_string, authors[0])]
468     for author in authors[1:]:
469         lines.append(' '*(len(copyright_year_string)+1) + author)
470     return lines
471
472 def _short_author_formatter(copyright_year_string, authors, **kwargs):
473     """
474     >>> print '\\n'.join(_short_author_formatter(
475     ...     copyright_year_string='Copyright (C) 1990-2010',
476     ...     authors=['Jack', 'Jill', 'John']*5,
477     ...     width=50))
478     Copyright (C) 1990-2010 Jack, Jill, John, Jack,
479     Jill, John, Jack, Jill, John, Jack, Jill, John,
480     Jack, Jill, John
481     """
482     blurb = '%s %s' % (copyright_year_string, ', '.join(authors))
483     return textwrap.wrap(blurb, **kwargs)
484
485 def _copyright_string(original_year, final_year, authors, prefix='',
486                       text=COPY_RIGHT_TEXT, extra_info={},
487                       author_format_fn=_long_author_formatter,
488                       formatter_kwargs={}):
489     """
490     >>> print _copyright_string(original_year=2005,
491     ...                         final_year=2005,
492     ...                         authors=['A <a@a.com>', 'B <b@b.edu>'],
493     ...                         prefix='# '
494     ...                        ) # doctest: +ELLIPSIS
495     # Copyright (C) 2005 A <a@a.com>
496     #                    B <b@b.edu>
497     #
498     # This file...
499     >>> print _copyright_string(original_year=2005,
500     ...                         final_year=2009,
501     ...                         authors=['A <a@a.com>', 'B <b@b.edu>']
502     ...                        ) # doctest: +ELLIPSIS
503     Copyright (C) 2005-2009 A <a@a.com>
504                             B <b@b.edu>
505     <BLANKLINE>
506     This file...
507     >>> print _copyright_string(original_year=2005,
508     ...                         final_year=2005,
509     ...                         authors=['A <a@a.com>', 'B <b@b.edu>'],
510     ...                         prefix='',
511     ...                         text=SHORT_COPY_RIGHT_TEXT,
512     ...                         author_format_fn=_short_author_formatter,
513     ...                         extra_info={'get-details':'%(get-details)s'},
514     ...                         formatter_kwargs={'width': 50},
515     ...                        ) # doctest: +ELLIPSIS
516     Copyright (C) 2005 A <a@a.com>, B <b@b.edu>
517     <BLANKLINE>
518     Hooke comes with ABSOLUTELY NO WARRANTY and is licensed
519     under the GNU Lesser General Public License.  For details,
520     %(get-details)s
521     """
522     if original_year == final_year:
523         date_range = '%s' % original_year
524     else:
525         date_range = '%s-%s' % (original_year, final_year)
526     copyright_year_string = 'Copyright (C) %s' % date_range
527     lines = author_format_fn(copyright_year_string, authors,
528                              **formatter_kwargs)
529     lines.append('')
530     info = dict(PROJECT_INFO)
531     for key,value in extra_info.items():
532         info[key] = value
533     lines.extend((text % info).splitlines())
534     for i,line in enumerate(lines):
535         lines[i] = (prefix + line).rstrip()
536     return '\n'.join(lines)
537
538 def _tag_copyright(contents):
539     """
540     >>> contents = '''Some file
541     ... bla bla
542     ... # Copyright (copyright begins)
543     ... # (copyright continues)
544     ... # bla bla bla
545     ... (copyright ends)
546     ... bla bla bla
547     ... '''
548     >>> print _tag_copyright(contents).replace('COPY-RIGHT', 'CR')
549     Some file
550     bla bla
551     -xyz-CR-zyx-
552     (copyright ends)
553     bla bla bla
554     <BLANKLINE>
555     """
556     lines = []
557     incopy = False
558     for line in contents.splitlines():
559         if incopy == False and line.startswith('# Copyright'):
560             incopy = True
561             lines.append(COPY_RIGHT_TAG)
562         elif incopy == True and not line.startswith('#'):
563             incopy = False
564         if incopy == False:
565             lines.append(line.rstrip('\n'))
566     return '\n'.join(lines)+'\n'
567
568 def _update_copyright(contents, original_year, authors):
569     """
570     >>> contents = '''Some file
571     ... bla bla
572     ... # Copyright (copyright begins)
573     ... # (copyright continues)
574     ... # bla bla bla
575     ... (copyright ends)
576     ... bla bla bla
577     ... '''
578     >>> print _update_copyright(contents, 2008, ['Jack', 'Jill']
579     ...     ) # doctest: +ELLIPSIS, +REPORT_UDIFF
580     Some file
581     bla bla
582     # Copyright (C) 2008-... Jack
583     #                         Jill
584     #
585     # This file...
586     (copyright ends)
587     bla bla bla
588     <BLANKLINE>
589     """
590     current_year = time.gmtime()[0]
591     copyright_string = _copyright_string(
592         original_year, current_year, authors, prefix='# ')
593     contents = _tag_copyright(contents)
594     return contents.replace(COPY_RIGHT_TAG, copyright_string)
595
596 def ignored_file(filename, ignored_paths=None, ignored_files=None,
597                  check_disk=True, check_vcs=True):
598     """
599     >>> ignored_paths = ['./a/', './b/']
600     >>> ignored_files = ['x', 'y']
601     >>> ignored_file('./a/z', ignored_paths, ignored_files, False, False)
602     True
603     >>> ignored_file('./ab/z', ignored_paths, ignored_files, False, False)
604     False
605     >>> ignored_file('./ab/x', ignored_paths, ignored_files, False, False)
606     True
607     >>> ignored_file('./ab/xy', ignored_paths, ignored_files, False, False)
608     False
609     >>> ignored_file('./z', ignored_paths, ignored_files, False, False)
610     False
611     """
612     if ignored_paths == None:
613         ignored_paths = IGNORED_PATHS
614     if ignored_files == None:
615         ignored_files = IGNORED_FILES
616     if check_disk == True and os.path.isfile(filename) == False:
617         return True
618     for path in ignored_paths:
619         if filename.startswith(path):
620             return True
621     if os.path.basename(filename) in ignored_files:
622         return True
623     if check_vcs == True and is_versioned(filename) == False:
624         return True
625     return False
626
627 def _set_contents(filename, contents, original_contents=None, dry_run=False,
628                   verbose=0):
629     if original_contents == None and os.path.isfile(filename):
630         f = open(filename, 'r')
631         original_contents = f.read()
632         f.close()
633     if verbose > 0:
634         print "checking %s ... " % filename,
635     if contents != original_contents:
636         if verbose > 0:
637             if original_contents == None:
638                 print "[creating]"
639             else:
640                 print "[updating]"
641         if verbose > 1 and original_contents != None:
642             print '\n'.join(
643                 difflib.unified_diff(
644                     original_contents.splitlines(), contents.splitlines(),
645                     fromfile=os.path.normpath(os.path.join('a', filename)),
646                     tofile=os.path.normpath(os.path.join('b', filename)),
647                     n=3, lineterm=''))
648         if dry_run == False:
649             f = file(filename, 'w')
650             f.write(contents)
651             f.close()
652     elif verbose > 0:
653         print "[no change]"
654
655 # Update commands
656
657 def update_authors(authors_fn=authors_list, dry_run=False, verbose=0):
658     authors = authors_fn()
659     authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
660     new_contents = '%s was written by:\n%s\n' % (
661         PROJECT_INFO['project'],
662         '\n'.join(authors)
663         )
664     _set_contents('AUTHORS', new_contents, dry_run=dry_run, verbose=verbose)
665
666 def update_file(filename, original_year_fn=original_year, authors_fn=authors,
667                 dry_run=False, verbose=0):
668     f = file(filename, 'r')
669     contents = f.read()
670     f.close()
671
672     original_year = original_year_fn(filename)
673     authors = authors_fn(filename)
674     authors = _replace_aliases(authors, with_email=True, aliases=ALIASES)
675
676     new_contents = _update_copyright(contents, original_year, authors)
677     _set_contents(filename, contents=new_contents, original_contents=contents,
678                   dry_run=dry_run, verbose=verbose)
679
680 def update_files(files=None, dry_run=False, verbose=0):
681     if files == None or len(files) == 0:
682         files = []
683         for dirpath,dirnames,filenames in os.walk('.'):
684             for filename in filenames:
685                 files.append(os.path.join(dirpath, filename))
686
687     for filename in files:
688         if ignored_file(filename) == True:
689             continue
690         update_file(filename, dry_run=dry_run, verbose=verbose)
691
692 def update_pyfile(path, original_year_fn=original_year,
693                   authors_fn=authors_list, dry_run=False, verbose=0):
694     original_year = original_year_fn()
695     current_year = time.gmtime()[0]
696     authors = authors_fn()
697     authors = _replace_aliases(authors, with_email=False, aliases=ALIASES)
698     lines = [
699         _copyright_string(original_year, current_year, authors, prefix='# '),
700         '',
701         'LICENSE = """',
702         _copyright_string(original_year, current_year, authors, prefix=''),
703         '""".strip()',
704         '',
705         'def short_license(extra_info):',
706         '    return """',
707         _copyright_string(original_year, current_year, authors, prefix='',
708                           text=SHORT_COPY_RIGHT_TEXT,
709                           author_format_fn=_short_author_formatter,
710                           extra_info={'get-details':'%(get-details)s'}),
711         '""".strip() % extra_info',
712         ]
713     new_contents = '\n'.join(lines)+'\n'
714     _set_contents(path, new_contents, dry_run=dry_run, verbose=verbose)
715
716
717 def test():
718     import doctest
719     doctest.testmod()
720
721 if __name__ == '__main__':
722     import optparse
723     import sys
724
725     usage = """%%prog [options] [file ...]
726
727 Update copyright information in source code with information from
728 the %(vcs)s repository.  Run from the %(project)s repository root.
729
730 Replaces every line starting with '^# Copyright' and continuing with
731 '^#' with an auto-generated copyright blurb.  If you want to add
732 #-commented material after a copyright blurb, please insert a blank
733 line between the blurb and your comment, so the next run of
734 ``update_copyright.py`` doesn't clobber your comment.
735
736 If no files are given, a list of files to update is generated
737 automatically.
738 """ % PROJECT_INFO
739     p = optparse.OptionParser(usage)
740     p.add_option('--pyfile', dest='pyfile', default='hooke/license.py',
741                  metavar='PATH',
742                  help='Write project license info to a Python module at PATH')
743     p.add_option('--test', dest='test', default=False,
744                  action='store_true', help='Run internal tests and exit')
745     p.add_option('--dry-run', dest='dry_run', default=False,
746                  action='store_true', help="Don't make any changes")
747     p.add_option('-v', '--verbose', dest='verbose', default=0,
748                  action='count', help='Increment verbosity')
749     options,args = p.parse_args()
750
751     if options.test == True:
752         test()
753         sys.exit(0)
754
755     update_authors(dry_run=options.dry_run, verbose=options.verbose)
756     update_files(files=args, dry_run=options.dry_run, verbose=options.verbose)
757     if options.pyfile != None:
758         update_pyfile(path=options.pyfile,
759                       dry_run=options.dry_run, verbose=options.verbose)