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