Rev 1: New plugin that sets the Copyright line from bzr information. in http://bzr.arbash-meinel.com/plugins/update_copyright
John Arbash Meinel
john at arbash-meinel.com
Thu Jan 7 19:45:02 GMT 2010
At http://bzr.arbash-meinel.com/plugins/update_copyright
------------------------------------------------------------
revno: 1
revision-id: john at arbash-meinel.com-20100107194456-7chqyxethzihlcsx
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: update_copyright
timestamp: Thu 2010-01-07 13:44:56 -0600
message:
New plugin that sets the Copyright line from bzr information.
Currently it only supports exact filenames. We'd like to support recursing
and updating all files.
-------------- next part --------------
=== added file '__init__.py'
--- a/__init__.py 1970-01-01 00:00:00 +0000
+++ b/__init__.py 2010-01-07 19:44:56 +0000
@@ -0,0 +1,71 @@
+# Copyright (C) 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+"""A plugin that uses last-modified information to determine copyright dates.
+
+This looks in the bzr metadata for a given file and determines what years the
+file was actually modified. It then sets a copyright header as the first line
+of the file.
+"""
+
+
+from bzrlib import (
+ commands,
+ errors,
+ option,
+ )
+
+
+class cmd_update_copyright(commands.Command):
+ """Update the Copyright header for files using the bzr metadata."""
+
+ takes_args = ['path*']
+ takes_options = []
+
+ def run(self, path_list=None):
+ from bzrlib import builtins, osutils
+ import update_copyright
+
+ tree, relpaths = builtins.tree_files(path_list)
+ if relpaths == ['']:
+ # Selecting just the root selects everything
+ relpaths = None
+ if relpaths is not None:
+ relpaths = osutils.minimum_path_selection(relpaths)
+ tree.lock_write()
+ try:
+ basis_tree = tree.basis_tree()
+ basis_tree.lock_read()
+ try:
+ for relpath in relpaths:
+ update_copyright.update_copyright(tree, basis_tree,
+ relpath)
+ finally:
+ basis_tree.unlock()
+ finally:
+ tree.unlock()
+
+
+commands.register_command(cmd_update_copyright)
+
+
+def load_tests(standard_tests, module, loader):
+ standard_tests.addTests(loader.loadTestsFromModuleNames(
+ [__name__ + '.' + x for x in [
+ 'test_update_copyright',
+ ]]))
+ return standard_tests
=== added file 'test_update_copyright.py'
--- a/test_update_copyright.py 1970-01-01 00:00:00 +0000
+++ b/test_update_copyright.py 2010-01-07 19:44:56 +0000
@@ -0,0 +1,137 @@
+# Copyright (C) 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+import datetime
+
+from bzrlib import (
+ errors,
+ tests,
+ )
+
+from bzrlib.plugins.update_copyright import (
+ update_copyright,
+ )
+
+
+class TestUpdateCopyrightDirect(tests.TestCase):
+
+ def assertCopyrightMatch(self, prefix, dates, suffix, line):
+ m = update_copyright.copyright_re.match(line)
+ if m is None:
+ self.fail('Copyright regex did not match: %r.' % (line,))
+ self.assertEqual((prefix, dates, suffix),
+ m.group('prefix', 'dates', 'suffix'))
+
+ def test_copyright_regex(self):
+ self.assertCopyrightMatch(
+ '# Copyright (C) ', '2010 ', 'Canonical Ltd',
+ '# Copyright (C) 2010 Canonical Ltd')
+ self.assertCopyrightMatch(
+ '# Copyright ', '2010 ', 'Canonical Ltd',
+ '# Copyright 2010 Canonical Ltd')
+ self.assertCopyrightMatch(
+ 'Copyright (c) ', '2010 ', 'Canonical Ltd',
+ 'Copyright (c) 2010 Canonical Ltd')
+ self.assertCopyrightMatch(
+ '/** Copyright (c) ', '2010 ', 'Canonical Ltd',
+ '/** Copyright (c) 2010 Canonical Ltd')
+ self.assertCopyrightMatch(
+ '# Copyright (c) ', '2008, 2010 ', 'Canonical Ltd',
+ '# Copyright (c) 2008, 2010 Canonical Ltd')
+ self.assertCopyrightMatch(
+ '# Copyright (c) ', '2008-2010 ', 'Foobar Ltd',
+ '# Copyright (c) 2008-2010 Foobar Ltd')
+ self.assertCopyrightMatch(
+ '# Copyright (c) ', '2008-2010', '',
+ '# Copyright (c) 2008-2010')
+ self.assertCopyrightMatch(
+ '# Copyright (c) ', '2008-2010 ', 'Foobar',
+ '# Copyright (c) 2008-2010 Foobar\n')
+ # suffix will match '\r' so we don't have to worry about messing up
+ # line-endings
+ self.assertCopyrightMatch(
+ '# Copyright (c) ', '2008-2010 ', 'Foobar\r',
+ '# Copyright (c) 2008-2010 Foobar\r\n')
+
+
+class TestUpdateCopyrightDisk(tests.TestCaseWithTransport):
+
+ def make_old_modified_tree(self):
+ t = self.make_branch_and_tree('.')
+ t.lock_write()
+ self.addCleanup(t.unlock)
+ self.build_tree_contents([('file',
+ 'Copyright (c) 2008 Foo Bar\nsome content\nmore content\n')])
+ t.add(['file'], ['file-id'])
+ rev_ids = []
+ rev_ids.append(
+ t.commit('new file', timestamp=1199817864, timezone=0)) # 2008
+ self.build_tree_contents([('file',
+ 'Copyright (c) 2008 Foo Bar\ndifferent content\n')])
+ rev_ids.append(
+ t.commit('mod file', timestamp=1231353864, timezone=0)) # 2009
+ return t, rev_ids
+
+ def make_old_modified_but_correct_tree(self):
+ t, rev_ids = self.make_old_modified_tree()
+ self.build_tree_contents([('file',
+ 'Copyright (c) 2008, 2009 Foo Bar\ndifferent content\n')])
+ rev_ids.append(
+ t.commit('fix copyright', timestamp=1231353865, timezone=0)) # 2009
+ return t, rev_ids
+
+ def test_get_years(self):
+ t, rev_ids = self.make_old_modified_tree()
+ self.assertEqual([2008],
+ sorted(update_copyright.get_years(t.branch.repository,
+ 'file-id', rev_ids[0])))
+ self.assertEqual([2008, 2009],
+ sorted(update_copyright.get_years(t.branch.repository,
+ 'file-id', rev_ids[1])))
+
+ def test_update_copyright(self):
+ t, rev_ids = self.make_old_modified_tree()
+ bt = t.basis_tree()
+ bt.lock_read()
+ self.addCleanup(bt.unlock)
+ update_copyright.update_copyright(t, bt, 'file')
+ year = datetime.datetime.now().year
+ self.assertFileEqual('Copyright (c) 2008, 2009, %d Foo Bar\n'
+ 'different content\n' % (year,), 'file')
+
+ def test_noop_update(self):
+ t, rev_ids = self.make_old_modified_but_correct_tree()
+ bt = t.basis_tree()
+ bt.lock_read()
+ self.addCleanup(bt.unlock)
+ update_copyright.update_copyright(t, bt, 'file')
+ # Should not at *this* year, because the copyright is already correct
+ self.assertFileEqual('Copyright (c) 2008, 2009 Foo Bar\n'
+ 'different content\n', 'file')
+
+ def test_blackbox(self):
+ t, rev_ids = self.make_old_modified_tree()
+ # We need to unlock so that run_bzr can grab a lock
+ t.unlock()
+ try:
+ self.run_bzr('update-copyright file')
+ finally:
+ # leave the tree locked, because we have pending cleanup
+ t.lock_read()
+ year = datetime.datetime.now().year
+ self.assertFileEqual('Copyright (c) 2008, 2009, %d Foo Bar\n'
+ 'different content\n' % (year,), 'file')
=== added file 'update_copyright.py'
--- a/update_copyright.py 1970-01-01 00:00:00 +0000
+++ b/update_copyright.py 2010-01-07 19:44:56 +0000
@@ -0,0 +1,92 @@
+# Copyright (C) 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+"""Tools for updating the copyright of given files."""
+
+import cStringIO
+import datetime
+import re
+
+from bzrlib import (
+ graph,
+ lazy_regex,
+ )
+
+
+copyright_re = lazy_regex.lazy_compile(
+ r'(?P<prefix>.*copyright\s+(\(c\)\s+)?)'
+ r'(?P<dates>[-0-9, ]+)'
+ r'(?P<suffix>.*)',
+ re.IGNORECASE)
+
+
+def get_years(repository, file_id, last_rev_id):
+ """Determine the dates a given file was modified."""
+ tip = (file_id, last_rev_id)
+ ancestry = graph.Graph(repository.texts).iter_ancestry([tip])
+ revision_ids = set([k[1] for k,p in ancestry if p is not None])
+ # TODO: Cache Revision objects between calls
+ revisions = repository.get_revisions(revision_ids)
+ # TODO: Should we use utcfromtimestamp?
+ years = set([datetime.datetime.fromtimestamp(r.timestamp).year
+ for r in revisions])
+ return years
+
+
+def format_years(years):
+ """Format a bunch of years back into a date string."""
+ return ', '.join(map(str, sorted(years)))
+
+
+def update_copyright(tree, basis_tree, filename):
+ """Update the copyright line for a given file."""
+ file_id = tree.path2id(filename)
+ if file_id is None:
+ return # Not versioned
+ this_year = datetime.datetime.now().year
+ f = tree.get_file(file_id, path=filename)
+ try:
+ copyright_line = f.readline()
+ m = copyright_re.match(copyright_line)
+ if m is None:
+ return # No copyright line
+ # Determine the actual dates that this file was modified
+ # There really should be a better way to determine the revision of a
+ # file in a tree
+ # TODO: Handle newly added files, and consider locally modified files
+ # as having a modification including today's year
+ # Also note that running this will modify the file, causing it to
+ # be marked as modified in the current year...
+ last_mod_revision = basis_tree.inventory[file_id].revision
+ years = get_years(tree.branch.repository, file_id, last_mod_revision)
+ new_copyright_line = '%s %s %s\n' % (
+ m.group('prefix').rstrip(), format_years(years), m.group('suffix'))
+ if copyright_line == new_copyright_line:
+ return # Copyright already correct
+ if this_year not in years:
+ # We are modifying it today, so mark it as such
+ years.add(this_year)
+ new_copyright_line = '%s %s %s\n' % (
+ m.group('prefix').rstrip(), format_years(years),
+ m.group('suffix'))
+ new_content = new_copyright_line + f.read()
+ finally:
+ f.close()
+ # TODO: We could probably do this 'safer' with a big TreeTransform over
+ # all of the various content. This might also mess up
+ # 'executable' bits, which TT would get correct
+ tree.put_file_bytes_non_atomic(file_id, new_content)
More information about the bazaar-commits
mailing list