Add support for 'egencache --update-changelogs'.
authorMichał Górny <mgorny@gentoo.org>
Sat, 2 Oct 2010 20:57:16 +0000 (22:57 +0200)
committerZac Medico <zmedico@gentoo.org>
Wed, 6 Oct 2010 22:43:28 +0000 (15:43 -0700)
bin/egencache

index a16ac4f9304a62d4370f601adb881639d49f7adb..48350e7130b84d43737bad9a9dff2fde2002ff08 100755 (executable)
@@ -23,6 +23,9 @@ except KeyboardInterrupt:
 import codecs
 import logging
 import optparse
+import subprocess
+import time
+import textwrap
 
 try:
        import portage
@@ -31,7 +34,7 @@ except ImportError:
        sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
        import portage
 
-from portage import os, _encodings, _unicode_encode
+from portage import os, _encodings, _unicode_encode, _unicode_decode
 from _emerge.MetadataRegen import MetadataRegen
 from portage.cache.cache_errors import CacheError, StatCollision
 from portage.util import cmp_sort_key, writemsg_level
@@ -47,6 +50,8 @@ else:
        from repoman.utilities import parse_metadata_use
        from xml.parsers.expat import ExpatError
 
+from repoman.utilities import FindVCS
+
 if sys.hexversion >= 0x3000000:
        long = int
 
@@ -61,6 +66,9 @@ def parse_args(args):
        actions.add_option("--update-use-local-desc",
                action="store_true",
                help="update the use.local.desc file from metadata.xml")
+       actions.add_option("--update-changelogs",
+               action="store_true",
+               help="update the ChangeLog files from SCM logs")
        parser.add_option_group(actions)
 
        common = optparse.OptionGroup(parser, 'Common options')
@@ -454,6 +462,130 @@ class GenUseLocalDesc(object):
 
                output.close()
 
+class GenChangeLogs(object):
+       def __init__(self, portdb):
+               self.returncode = os.EX_OK
+               self._portdb = portdb
+               self._wrapper = textwrap.TextWrapper(
+                               width = 78,
+                               initial_indent = '  ',
+                               subsequent_indent = '  '
+                       )
+
+       def generate_changelog(self, cp):
+               try:
+                       output = codecs.open('ChangeLog',
+                               mode='w', encoding=_encodings['repo.content'],
+                               errors='replace')
+               except IOError as e:
+                       writemsg_level(
+                               "ERROR: failed to open ChangeLog for %s: %s\n" % (cp,e,),
+                               level=logging.ERROR, noiselevel=-1)
+                       self.returncode |= 2
+                       return
+
+               output.write(('''
+# ChangeLog for %s
+# Copyright 1999-%s Gentoo Foundation; Distributed under the GPL v2
+# $Header: $
+
+''' % (cp, time.strftime('%Y'))).lstrip())
+
+               def grab(cmd):
+                       p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+                       return _unicode_decode(p.communicate()[0],
+                                       encoding=_encodings['stdio'], errors='strict')
+
+               # now grab all the commits
+               commits = grab(['git', 'rev-list', 'HEAD', '--', '.']).split()
+
+               for c in commits:
+                       # Explaining the arguments:
+                       # --name-status to get a list of added/removed files
+                       # --no-renames to avoid getting more complex records on the list
+                       # --format to get the timestamp, author and commit description
+                       # --root to make it work fine even with the initial commit
+                       # --relative to get paths relative to ebuilddir
+                       # -r (recursive) to get per-file changes
+                       # then the commit-id and path.
+
+                       cinfo = grab(['git', 'diff-tree', '--name-status', '--no-renames',
+                                       '--format=%ct %cN <%cE>%n%B', '--root', '--relative', '-r',
+                                       c, '--', '.']).rstrip('\n').split('\n')
+
+                       # Expected output:
+                       # timestamp Author Name <author@email>
+                       # commit message l1
+                       # ...
+                       # commit message ln
+                       #
+                       # status1       filename1
+                       # ...
+                       # statusn       filenamen
+
+                       changed = []
+                       for n, l in enumerate(reversed(cinfo)):
+                               if not l:
+                                       body = cinfo[1:-n]
+                                       break
+                               else:
+                                       f = l.split()
+                                       if f[1] == 'Manifest':
+                                               pass # XXX: remanifest commits?
+                                       elif f[1] == 'ChangeLog':
+                                               pass
+                                       elif f[0].startswith('A'):
+                                               changed.append('+%s' % f[1])
+                                       elif f[0].startswith('D'):
+                                               changed.append('-%s' % f[1])
+                                       elif f[0].startswith('M'):
+                                               changed.append(f[1])
+                                       else:
+                                               writemsg_level(
+                                                       "ERROR: unexpected git file status for %s: %s\n" % (cp,f,),
+                                                       level=logging.ERROR, noiselevel=-1)
+                                               self.returncode |= 1
+
+                       if not changed:
+                               continue
+
+                       (ts, author) = cinfo[0].split(' ', 1)
+                       date = time.strftime('%d %b %Y', time.gmtime(float(ts)))
+
+                       # XXX: sort changes (ebuilds should go by PV)
+
+                       wroteheader = False
+                       for c in changed:
+                               if c.startswith('+') and c.endswith('.ebuild'):
+                                       output.write('*%s (%s)\n' % (c[1:-7], date))
+                                       wroteheader = True
+                       if wroteheader:
+                               output.write('\n')
+
+                       # XXX: strip '<CP>: ' and '[<CP>] ' from the commit message
+                       # XXX: strip the repoman suffix note from the commit message
+
+                       body.insert(0, '%s; %s %s:' % (date, author, ', '.join(changed)))
+                       output.write('\n'.join([self._wrapper.fill(x) for x in body]))
+
+               output.close()
+       
+       def run(self):
+               repo_path = self._portdb.porttrees[0]
+               os.chdir(repo_path)
+               if 'git' not in FindVCS():
+                       writemsg_level(
+                               "ERROR: --update-changelogs supported only in git repos\n",
+                               level=logging.ERROR, noiselevel=-1)
+                       self.returncode = 127
+                       return
+
+               for cp in self._portdb.cp_all():
+                       os.chdir(os.path.join(repo_path, cp))
+                       # XXX: support checking somehow whether the ChangeLog is up-to-date.
+                       if 1:
+                               self.generate_changelog(cp)
+
 def egencache_main(args):
        parser, options, atoms = parse_args(args)
 
@@ -493,9 +625,9 @@ def egencache_main(args):
                settings = portage.config(config_root=config_root,
                        target_root='/', local_config=False, env=env)
 
-       if not options.update and not options.update_use_local_desc:
-               parser.error('No action specified (--update ' + \
-                       'and/or --update-use-local-desc)')
+       if not options.update and not options.update_use_local_desc \
+                       and not options.update_changelogs:
+               parser.error('No action specified')
                return 1
 
        if options.update and 'metadata-transfer' not in settings.features:
@@ -538,6 +670,11 @@ def egencache_main(args):
                gen_desc.run()
                ret.append(gen_desc.returncode)
 
+       if options.update_changelogs:
+               gen_clogs = GenChangeLogs(portdb)
+               gen_clogs.run()
+               ret.append(gen_clogs.returncode)
+
        if options.tolerant:
                return ret[0]
        return max(ret)