repoman: implemented echangelog functionality
authorFabian Groffen <grobian@gentoo.org>
Sun, 16 Oct 2011 22:31:07 +0000 (15:31 -0700)
committerZac Medico <zmedico@gentoo.org>
Sun, 16 Oct 2011 22:31:07 +0000 (15:31 -0700)
Instead of calling echangelog, which on its turn has to query the VCS
again, use the existing information on changes made to the current
directory, and update the ChangeLog from Python itself.
This avoids a call to echangelog, and avoids again retrieving the same
VCS information as repoman already did.  It makes repoman independent
from external tools it didn't install itself, and should be faster in
general.

bin/repoman
pym/repoman/utilities.py

index b1a2ac3fad11d81210a399453ee6fc582124455d..1d7d71ac382f2d46d3d08e123f17ee214f6be93c 100755 (executable)
@@ -18,7 +18,6 @@ import optparse
 import re
 import signal
 import stat
-import subprocess
 import sys
 import tempfile
 import textwrap
@@ -73,7 +72,7 @@ from portage.process import find_binary, spawn
 from portage.output import bold, create_color_func, \
        green, nocolor, red
 from portage.output import ConsoleStyleFile, StyleWriter
-from portage.util import cmp_sort_key, writemsg_level, writemsg_stdout
+from portage.util import cmp_sort_key, writemsg_level
 from portage.package.ebuild.digestgen import digestgen
 from portage.eapi import eapi_has_iuse_defaults, eapi_has_required_use
 
@@ -645,19 +644,15 @@ if options.echangelog is None and \
 if vcs is None:
        options.echangelog = 'n'
 
-if 'commit' == options.mode and \
-       options.echangelog == 'y' and \
-       find_binary('echangelog') is None:
-       logging.error("echangelog not found, and --echangelog is enabled")
-       sys.exit(1)
-
 # The --echangelog option causes automatic ChangeLog generation,
 # which invalidates changelog.ebuildadded and changelog.missing
 # checks.
-# Note: We don't use ChangeLogs in distributed SCMs.
+# Note: Some don't use ChangeLogs in distributed SCMs.
 # It will be generated on server side from scm log,
 # before package moves to the rsync server.
-# This is needed because we try to avoid merge collisions.
+# This is needed because they try to avoid merge collisions.
+# Gentoo's Council decided to always use the ChangeLog file.
+# TODO: shouldn't this just be switched on the repo, iso the VCS?
 check_changelog = options.echangelog != 'y' and vcs in ('cvs', 'svn')
 
 # Generate an appropriate PORTDIR_OVERLAY value for passing into the
@@ -2284,36 +2279,6 @@ else:
                                myautoadd+=[myunadded[x]]
                                del myunadded[x]
 
-       if myautoadd:
-               print(">>> Auto-Adding missing Manifest(s)...")
-               if options.pretend:
-                       if vcs == "cvs":
-                               print("(cvs add "+" ".join(myautoadd)+")")
-                       elif vcs == "svn":
-                               print("(svn add "+" ".join(myautoadd)+")")
-                       elif vcs == "git":
-                               print("(git add "+" ".join(myautoadd)+")")
-                       elif vcs == "bzr":
-                               print("(bzr add "+" ".join(myautoadd)+")")
-                       elif vcs == "hg":
-                               print("(hg add "+" ".join(myautoadd)+")")
-                       retval=0
-               else:
-                       if vcs == "cvs":
-                               retval=os.system("cvs add "+" ".join(myautoadd))
-                       elif vcs == "svn":
-                               retval=os.system("svn add "+" ".join(myautoadd))
-                       elif vcs == "git":
-                               retval=os.system("git add "+" ".join(myautoadd))
-                       elif vcs == "bzr":
-                               retval=os.system("bzr add "+" ".join(myautoadd))
-                       elif vcs == "hg":
-                               retval=os.system("hg add "+" ".join(myautoadd))
-               if retval:
-                       writemsg_level("!!! Exiting on %s (shell) error code: %s\n" % \
-                               (vcs, retval), level=logging.ERROR, noiselevel=-1)
-                       sys.exit(retval)
-
        if myunadded:
                print(red("!!! The following files are in your local tree but are not added to the master"))
                print(red("!!! tree. Please remove them from the local tree or add them to the master tree."))
@@ -2402,8 +2367,6 @@ else:
        myheaders = []
        mydirty = []
 
-       print("* %s files being committed..." % green(str(len(myupdates))), end=' ')
-
        commitmessage = options.commitmsg
        if options.commitmsgfile:
                try:
@@ -2473,22 +2436,60 @@ else:
                        if changelog_modified:
                                continue
 
-                       myupdates.append(changelog_path)
-                       logging.info("calling echangelog for package %s" % x)
-                       # --no-strict is required if only manifest(s) have changed
-                       echangelog_args = ["echangelog", "--no-strict",
-                               "--vcs", vcs, changelog_msg]
-                       if options.pretend:
-                               writemsg_stdout("(%s)\n" % (" ".join(echangelog_args),),
-                                       noiselevel=-1)
-                               continue
-                       echangelog_args = [_unicode_encode(arg) for arg in echangelog_args]
-                       echangelog_cwd = _unicode_encode(checkdir,
-                               encoding=_encodings['fs'], errors='strict')
-                       retcode = subprocess.call(echangelog_args, cwd=echangelog_cwd)
-                       if retcode != os.EX_OK:
-                               logging.error("echangelog exited with '%s' status" % retcode)
-                               sys.exit(retcode)
+                       # get changes for this package
+                       cdrlen = len(checkdir_relative)
+                       clnew = [elem[cdrlen:] for elem in mynew if elem.startswith(checkdir_relative)]
+                       clremoved = [elem[cdrlen:] for elem in myremoved if elem.startswith(checkdir_relative)]
+                       clchanged = [elem[cdrlen:] for elem in mychanged if elem.startswith(checkdir_relative)]
+                       new_changelog = utilities.UpdateChangeLog(checkdir_relative, \
+                               catdir, pkgdir, \
+                               clnew, clremoved, clchanged, \
+                               changelog_msg, options.pretend)
+                       if new_changelog is None:
+                               writemsg_level("!!! Updating the ChangeLog failed\n", \
+                                       level=logging.ERROR, noiselevel=-1)
+                               sys.exit(1)
+
+                       # if the ChangeLog was just created, add it to vcs
+                       if new_changelog:
+                               myautoadd.append(changelog_path)
+                               # myautoadd is appended to myupdates below
+                       else:
+                               myupdates.append(changelog_path)
+
+       if myautoadd:
+               print(">>> Auto-Adding missing Manifest/ChangeLog file(s)...")
+               if options.pretend:
+                       if vcs == "cvs":
+                               print("(cvs add "+" ".join(myautoadd)+")")
+                       elif vcs == "svn":
+                               print("(svn add "+" ".join(myautoadd)+")")
+                       elif vcs == "git":
+                               print("(git add "+" ".join(myautoadd)+")")
+                       elif vcs == "bzr":
+                               print("(bzr add "+" ".join(myautoadd)+")")
+                       elif vcs == "hg":
+                               print("(hg add "+" ".join(myautoadd)+")")
+                       retval = os.EX_OK
+               else:
+                       if vcs == "cvs":
+                               retval = os.system("cvs add "+" ".join(myautoadd))
+                       elif vcs == "svn":
+                               retval = os.system("svn add "+" ".join(myautoadd))
+                       elif vcs == "git":
+                               retval = os.system("git add "+" ".join(myautoadd))
+                       elif vcs == "bzr":
+                               retval = os.system("bzr add "+" ".join(myautoadd))
+                       elif vcs == "hg":
+                               retval = os.system("hg add "+" ".join(myautoadd))
+               if retval != os.EX_OK:
+                       writemsg_level("!!! Exiting on %s (shell) error code: %s\n" % \
+                               (vcs, retval), level=logging.ERROR, noiselevel=-1)
+                       sys.exit(retval)
+
+               myupdates += myautoadd
+
+       print("* %s files being committed..." % green(str(len(myupdates))), end=' ')
 
        if vcs not in ('cvs', 'svn'):
                # With git, bzr and hg, there's never any keyword expansion, so
index 79137036c36a68a72b84ea6faee9af270adfe1a7..8f7d5d54cb38d71b0213fc7e5411fd43d6cd41e7 100644 (file)
@@ -5,6 +5,8 @@
 """This module contains utility functions to help repoman find ebuilds to
 scan"""
 
+from __future__ import print_function
+
 __all__ = [
        "detect_vcs_conflicts",
        "editor_is_executable",
@@ -17,13 +19,22 @@ __all__ = [
        "have_profile_dir",
        "parse_metadata_use",
        "UnknownHerdsError",
-       "check_metadata"
+       "check_metadata",
+       "UpdateChangeLog"
 ]
 
 import errno
 import io
+from itertools import chain
 import logging
+import pwd
 import sys
+import time
+import textwrap
+import difflib
+import shutil
+from tempfile import mkstemp
+
 from portage import os
 from portage import subprocess_getstatusoutput
 from portage import _encodings
@@ -308,7 +319,6 @@ def get_commit_message_with_editor(editor, message=None):
        @rtype: string or None
        @returns: A string on success or None if an error occurs.
        """
-       from tempfile import mkstemp
        fd, filename = mkstemp()
        try:
                os.write(fd, _unicode_encode(_(
@@ -511,3 +521,111 @@ def FindVCS():
                outvcs = seek()
 
        return outvcs
+
+def UpdateChangeLog(pkgdir, category, package, new, removed, changed, msg, pretend):
+       """ Write an entry to an existing ChangeLog, or create a new one. """
+
+       # figure out who to write as
+       if 'GENTOO_COMMITTER_NAME' in os.environ and \
+                       'GENTOO_COMMITTER_EMAIL' in os.environ:
+               user = '%s <%s>' % (os.environ['GENTOO_COMMITTER_NAME'], \
+                               os.environ['GENTOO_COMMITTER_EMAIL'])
+       elif 'GENTOO_AUTHOR_NAME' in os.environ and \
+                       'GENTOO_AUTHOR_EMAIL' in os.environ:
+               user = '%s <%s>' % (os.environ['GENTOO_AUTHOR_NAME'], \
+                               os.environ['GENTOO_AUTHOR_EMAIL'])
+       elif 'ECHANGELOG_USER' in os.environ:
+               user = os.environ['ECHANGELOG_USER']
+       else:
+               (login, _, _, _, gecos, _, _) = pwd.getpwuid(os.getuid())
+               gecos = gecos.split(',')[0]  # bug #80011
+               user = '%s <%s@gentoo.org>' % (gecos, login)
+
+       if '<root@' in user:
+               err = 'Please set ECHANGELOG_USER or run as non-root'
+               logging.critical(err)
+               return None
+
+       cl_path = os.path.join(pkgdir, 'ChangeLog')
+       f, clnew_path = mkstemp()
+
+       # create an empty ChangeLog.new with correct header first
+       try:
+               f = os.fdopen(f, 'w+')
+               f.write('# ChangeLog for %s/%s\n' % (category, package))
+               year = time.strftime('%Y')
+               f.write('# Copyright 1999-%s Gentoo Foundation; Distributed under the GPL v2\n' % year)
+               f.write('# $Header: $\n')
+               f.write('\n')
+
+               # write new ChangeLog entry
+               date = time.strftime('%d %b %Y')
+               newebuild = False
+               for fn in new:
+                       if not fn.endswith('.ebuild'):
+                               continue
+                       ebuild = fn.split(os.sep)[-1][0:-7] 
+                       f.write('*%s (%s)\n' % (ebuild, date))
+                       newebuild = True
+               if newebuild:
+                       f.write('\n')
+               new = ['+' + elem for elem in new if elem not in ['ChangeLog', 'Manifest']]
+               removed = ['-' + elem for elem in removed]
+               changed = [elem for elem in changed if elem not in ['ChangeLog', 'Manifest']]
+               mesg = '%s; %s %s:' % (date, user, \
+                               ', '.join(chain(new,removed,changed)))
+               for line in textwrap.wrap(mesg, 80, \
+                               initial_indent='  ', subsequent_indent='  ', \
+                               break_on_hyphens=False):
+                       f.write('%s\n' % line)
+               for line in textwrap.wrap(msg, 80, \
+                               initial_indent='  ', subsequent_indent='  '):
+                       f.write('%s\n' % line)
+
+               # append stuff from old ChangeLog
+               cl_lines = []
+               if os.path.exists(cl_path):
+                       c = open(cl_path, 'r')
+                       cl_lines = c.readlines()
+                       for index, line in enumerate(cl_lines):
+                               # skip the headers
+                               if line.startswith('#'):
+                                       # normalise to $Header: $ to avoid pointless diff line
+                                       if line.startswith('# $Header:'):
+                                               cl_lines[index] = '# $Header: $\n'
+                                       continue
+                               f.write(line)
+                       c.close()
+
+               # show diff (do we want to keep on doing this, or only when
+               # pretend?)
+               f.seek(0)
+               clnew_lines = f.readlines()
+               for line in difflib.unified_diff(cl_lines, clnew_lines, \
+                               fromfile=cl_path, tofile=cl_path + '.new', n=0):
+                       print(line.rstrip())
+               print()
+
+               f.close()
+
+               if pretend:
+                       # remove what we've done
+                       os.remove(clnew_path)
+               else:
+                       # rename ChangeLog.new to ChangeLog
+                       shutil.move(clnew_path, cl_path)
+
+               if cl_lines == []:
+                       return True
+               else:
+                       return False
+       except IOError as e:
+               err = 'Repoman is unable to create/write to Changelog.new file: %s' % (e,)
+               logging.critical(err)
+               # try to remove if possible
+               try:
+                       os.remove(clnew_path)
+               except OSError:
+                       pass
+               return None
+