drop IMAGE -- people have had years to catch up (trunk r14694)
[portage.git] / bin / repoman
index fd5d8a68d1203e86e2fd1f0ffeee4101672a9dab..9b1fd461e5be0627ea77dd117172c1f9ac475ea1 100755 (executable)
@@ -7,13 +7,17 @@
 # Then, check to make sure deps are satisfiable (to avoid "can't find match for" problems)
 # that last one is tricky because multiple profiles need to be checked.
 
+from __future__ import print_function
+
 import codecs
-import commands
+try:
+       from subprocess import getstatusoutput as subprocess_getstatusoutput
+except ImportError:
+       from commands import getstatusoutput as subprocess_getstatusoutput
 import errno
 import formatter
 import logging
 import optparse
-import os
 import re
 import signal
 import stat
@@ -22,19 +26,10 @@ import tempfile
 import time
 import platform
 
-from itertools import chain, izip
+from io import StringIO
+from itertools import chain
 from stat import S_ISDIR, ST_CTIME
 
-try:
-       import cPickle as pickle
-except ImportError:
-       import pickle
-
-try:
-       import cStringIO as StringIO
-except ImportError:
-       import StringIO
-
 if not hasattr(__builtins__, "set"):
        from sets import Set as set
 
@@ -45,6 +40,9 @@ except ImportError:
        sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
        import portage
 portage._disable_legacy_globals()
+from portage import os
+from portage import _encodings
+from portage import _unicode_encode
 
 try:
        from repoman.checks import run_checks
@@ -55,8 +53,9 @@ except ImportError:
        from repoman.checks import run_checks
        from repoman import utilities
 
-from _emerge import Package, RootConfig
-from portage.sets import load_default_config
+from _emerge.Package import Package
+from _emerge.RootConfig import RootConfig
+from portage._sets import load_default_config
 
 import portage.checksum
 import portage.const
@@ -71,21 +70,21 @@ from portage.process import find_binary, spawn
 from portage.output import bold, create_color_func, darkgreen, \
        green, nocolor, red, turquoise, yellow
 from portage.output import ConsoleStyleFile, StyleWriter
+from portage.util import cmp_sort_key, writemsg_level
+
+if sys.hexversion >= 0x3000000:
+       basestring = str
 
 util.initialize_logger()
 
 # 14 is the length of DESCRIPTION=""
 max_desc_len = 100
 allowed_filename_chars="a-zA-Z0-9._-+:"
-allowed_filename_chars_set = {}
-map(allowed_filename_chars_set.setdefault, map(chr, range(ord('a'), ord('z')+1)))
-map(allowed_filename_chars_set.setdefault, map(chr, range(ord('A'), ord('Z')+1)))
-map(allowed_filename_chars_set.setdefault, map(chr, range(ord('0'), ord('9')+1)))
-map(allowed_filename_chars_set.setdefault, map(chr, map(ord, [".", "-", "_", "+", ":"])))
+disallowed_filename_chars_re = re.compile(r'[^a-zA-Z0-9._\-+:]')
 bad = create_color_func("BAD")
 
 # A sane umask is needed for files that portage creates.
-os.umask(022)
+os.umask(0o22)
 repoman_settings = portage.config(local_config=False,
        config_incrementals=portage.const.INCREMENTALS)
 repoman_settings.lock()
@@ -95,7 +94,7 @@ if repoman_settings.get("NOCOLOR", "").lower() in ("yes", "true") or \
        nocolor()
 
 def warn(txt):
-       print "repoman: " + txt
+       print("repoman: " + txt)
 
 def err(txt):
        warn(txt)
@@ -150,13 +149,11 @@ def ParseArgs(args, qahelp):
                'fix' : 'Fix simple QA issues (stray digests, missing digests)',
                'full' : 'Scan directory tree and print all issues (not a summary)',
                'help' : 'Show this screen',
-               'last' : 'Remember report from last run',
-               'lfull' : 'Remember report from last run (full listing)',
                'manifest' : 'Generate a Manifest (fetches files if necessary)',
                'scan' : 'Scan directory tree for QA issues' 
        }
 
-       mode_keys = modes.keys()
+       mode_keys = list(modes)
        mode_keys.sort()
 
        parser = RepomanOptionParser(formatter=RepomanHelpFormatter(), usage="%prog [options] [mode]")
@@ -183,6 +180,9 @@ def ParseArgs(args, qahelp):
        parser.add_option('-v', '--verbose', dest="verbosity", action='count',
                help='be very verbose in output', default=0)
 
+       parser.add_option('-V', '--version', dest='version', action='store_true',
+               help='show version info')
+
        parser.add_option('-x', '--xmlparse', dest='xml_parse', action='store_true',
                default=False, help='forces the metadata.xml parse check to be carried out')
 
@@ -192,10 +192,13 @@ def ParseArgs(args, qahelp):
        parser.add_option('-I', '--ignore-masked', dest='ignore_masked', action='store_true',
                default=False, help='ignore masked packages (not allowed with commit mode)')
 
+       parser.add_option('-d', '--include-dev', dest='include_dev', action='store_true',
+               default=False, help='include dev profiles in dependency checks')
+
        parser.add_option('--without-mask', dest='without_mask', action='store_true',
                default=False, help='behave as if no package.mask entries exist (not allowed with commit mode)')
 
-       parser.add_option('--mode', type='choice', dest='mode', choices=modes.keys(), 
+       parser.add_option('--mode', type='choice', dest='mode', choices=list(modes), 
                help='specify which mode repoman will run in (default=full)')
 
        parser.on_tail("\n " + green("Modes".ljust(20) + " Description\n"))
@@ -205,7 +208,7 @@ def ParseArgs(args, qahelp):
 
        parser.on_tail("\n " + green("QA keyword".ljust(20) + " Description\n"))
 
-       sorted_qa = qahelp.keys()
+       sorted_qa = list(qahelp)
        sorted_qa.sort()
        for k in sorted_qa:
                parser.on_tail(" %s %s\n" % (k.ljust(20), qahelp[k]))
@@ -224,7 +227,7 @@ def ParseArgs(args, qahelp):
                                break
 
        if not opts.mode:
-               opts.mode = 'full'      #default to full
+               opts.mode = 'full'
        
        if opts.mode == 'ci':
                opts.mode = 'commit'  # backwards compat shortcut
@@ -258,7 +261,8 @@ qahelp={
        "changelog.notadded":"ChangeLogs that exist but have not been added to cvs",
        "filedir.missing":"Package lacks a files directory",
        "file.executable":"Ebuilds, digests, metadata.xml, Manifest, and ChangeLog do note need the executable bit",
-       "file.size":"Files in the files directory must be under 20k",
+       "file.size":"Files in the files directory must be under 20 KiB",
+       "file.size.fatal":"Files in the files directory must be under 60 KiB",
        "file.name":"File/dir name must be composed of only the following chars: %s " % allowed_filename_chars,
        "file.UTF8":"File is not UTF8 compliant",
        "inherit.autotools":"Ebuild inherits autotools but does not call eautomake, eautoconf or eautoreconf",
@@ -270,6 +274,7 @@ qahelp={
        "LICENSE.missing":"Ebuilds that have a missing or empty LICENSE variable",
        "DESCRIPTION.missing":"Ebuilds that have a missing or empty DESCRIPTION variable",
        "DESCRIPTION.toolong":"DESCRIPTION is over %d characters" % max_desc_len,
+       "EAPI.definition":"EAPI is defined after an inherit call (must be defined before)",
        "EAPI.incompatible":"Ebuilds that use features that are only available with a different EAPI",
        "EAPI.unsupported":"Ebuilds that have an unsupported EAPI version (you must upgrade portage)",
        "SLOT.missing":"Ebuilds that have a missing or empty SLOT variable",
@@ -286,9 +291,13 @@ qahelp={
        "DEPEND.badmaskedindev":"Masked ebuilds with bad DEPEND settings (matched against *all* ebuilds) in developing arch",
        "RDEPEND.badmaskedindev":"Masked ebuilds with RDEPEND settings (matched against *all* ebuilds) in developing arch",
        "PDEPEND.badmaskedindev":"Masked ebuilds with PDEPEND settings (matched against *all* ebuilds) in developing arch",
+       "PDEPEND.suspect":"PDEPEND contains a package that usually only belongs in DEPEND.",
        "DEPEND.syntax":"Syntax error in DEPEND (usually an extra/missing space/parenthesis)",
        "RDEPEND.syntax":"Syntax error in RDEPEND (usually an extra/missing space/parenthesis)",
        "PDEPEND.syntax":"Syntax error in PDEPEND (usually an extra/missing space/parenthesis)",
+       "DEPEND.badtilde":"DEPEND uses the ~ dep operator with a non-zero revision part, which is useless (the revision is ignored)",
+       "RDEPEND.badtilde":"RDEPEND uses the ~ dep operator with a non-zero revision part, which is useless (the revision is ignored)",
+       "PDEPEND.badtilde":"PDEPEND uses the ~ dep operator with a non-zero revision part, which is useless (the revision is ignored)",
        "LICENSE.syntax":"Syntax error in LICENSE (usually an extra/missing space/parenthesis)",
        "PROVIDE.syntax":"Syntax error in PROVIDE (usually an extra/missing space/parenthesis)",
        "PROPERTIES.syntax":"Syntax error in PROPERTIES (usually an extra/missing space/parenthesis)",
@@ -298,15 +307,20 @@ qahelp={
        "ebuild.syntax":"Error generating cache entry for ebuild; typically caused by ebuild syntax error or digest verification failure",
        "ebuild.output":"A simple sourcing of the ebuild produces output; this breaks ebuild policy.",
        "ebuild.nesteddie":"Placing 'die' inside ( ) prints an error, but doesn't stop the ebuild.",
+       "variable.invalidchar":"A variable contains an invalid character that is not part of the ASCII character set",
        "variable.readonly":"Assigning a readonly variable",
        "LIVEVCS.stable":"This ebuild is a live checkout from a VCS but has stable keywords.",
-       "IUSE.invalid":"This ebuild has a variable in IUSE that is not in the use.desc or use.local.desc file",
+       "LIVEVCS.unmasked":"This ebuild is a live checkout from a VCS but has keywords and is not masked in the global package.mask.",
+       "IUSE.invalid":"This ebuild has a variable in IUSE that is not in the use.desc or its metadata.xml file",
        "IUSE.undefined":"This ebuild does not define IUSE (style guideline says to define IUSE even when empty)",
        "LICENSE.invalid":"This ebuild is listing a license that doesnt exist in portages license/ dir.",
        "KEYWORDS.invalid":"This ebuild contains KEYWORDS that are not listed in profiles/arch.list or for which no valid profile was found",
+       "RDEPEND.implicit":"RDEPEND is unset in the ebuild which triggers implicit RDEPEND=$DEPEND assignment",
        "RDEPEND.suspect":"RDEPEND contains a package that usually only belongs in DEPEND.",
        "RESTRICT.invalid":"This ebuild contains invalid RESTRICT values.",
-       "digestentry.unused":"Some files listed in the Manifest aren't referenced in SRC_URI",
+       "digest.assumed":"Existing digest must be assumed correct (Package level only)",
+       "digest.missing":"Some files listed in SRC_URI aren't referenced in the Manifest",
+       "digest.unused":"Some files listed in the Manifest aren't referenced in SRC_URI",
        "ebuild.nostable":"There are no ebuilds that are marked as stable for your ARCH",
        "ebuild.allmasked":"All ebuilds are masked for this package (Package level only)",
        "ebuild.majorsyn":"This ebuild has a major syntax error that may cause the ebuild to fail partially or fully",
@@ -314,19 +328,22 @@ qahelp={
        "ebuild.badheader":"This ebuild has a malformed header",
        "metadata.missing":"Missing metadata.xml files",
        "metadata.bad":"Bad metadata.xml files",
+       "metadata.warning":"Warnings in metadata.xml files",
        "virtual.versioned":"PROVIDE contains virtuals with versions",
        "virtual.exists":"PROVIDE contains existing package names",
        "virtual.unavailable":"PROVIDE contains a virtual which contains no profile default",
-       "usage.obsolete":"The ebuild makes use of an obsolete construct"
+       "usage.obsolete":"The ebuild makes use of an obsolete construct",
+       "upstream.workaround":"The ebuild works around an upstream bug, an upstream bug should be filed and tracked in bugs.gentoo.org"
 }
 
-qacats = qahelp.keys()
+qacats = list(qahelp)
 qacats.sort()
 
 qawarnings = set((
 "changelog.missing",
 "changelog.notadded",
-"digestentry.unused",
+"digest.assumed",
+"digest.unused",
 "ebuild.notadded",
 "ebuild.nostable",
 "ebuild.allmasked",
@@ -335,11 +352,14 @@ qawarnings = set((
 "DEPEND.badmasked","RDEPEND.badmasked","PDEPEND.badmasked",
 "DEPEND.badindev","RDEPEND.badindev","PDEPEND.badindev",
 "DEPEND.badmaskedindev","RDEPEND.badmaskedindev","PDEPEND.badmaskedindev",
+"DEPEND.badtilde", "RDEPEND.badtilde", "PDEPEND.badtilde",
 "DESCRIPTION.toolong",
 "KEYWORDS.dropped",
 "KEYWORDS.stupid",
 "KEYWORDS.missing",
 "IUSE.undefined",
+"PDEPEND.suspect",
+"RDEPEND.implicit",
 "RDEPEND.suspect",
 "RESTRICT.invalid",
 "SRC_URI.mirror",
@@ -349,18 +369,20 @@ qawarnings = set((
 "file.size",
 "inherit.autotools",
 "java.eclassesnotused",
-"metadata.missing",
-"metadata.bad",
+"metadata.warning",
 "virtual.versioned",
 "virtual.exists",
 "virtual.unavailable",
 "usage.obsolete",
-"LIVEVCS.stable"
+"upstream.workaround",
+"LIVEVCS.stable",
+"LIVEVCS.unmasked",
 ))
 
+non_ascii_re = re.compile(r'[^\x00-\x7f]')
+
 missingvars=["KEYWORDS","LICENSE","DESCRIPTION","HOMEPAGE","SLOT"]
 allvars = set(x for x in portage.auxdbkeys if not x.startswith("UNUSED_"))
-allvars.discard("CDEPEND")
 allvars.update(Package.metadata_keys)
 allvars = sorted(allvars)
 commitmessage=None
@@ -392,10 +414,10 @@ suspect_rdepend = frozenset([
        "dev-lang/swig",
        "dev-lang/yasm",
        "dev-perl/extutils-pkgconfig",
-       "dev-python/setuptools",
        "dev-util/byacc",
        "dev-util/cmake",
        "dev-util/ftjam",
+       "dev-util/gperf",
        "dev-util/gtk-doc",
        "dev-util/gtk-doc-am",
        "dev-util/intltool",
@@ -412,7 +434,6 @@ suspect_rdepend = frozenset([
        "sys-devel/bison",
        "sys-devel/dev86",
        "sys-devel/flex",
-       "sys-devel/libtool",
        "sys-devel/m4",
        "sys-devel/pmake",
        "x11-misc/bdftopcf",
@@ -422,72 +443,40 @@ suspect_rdepend = frozenset([
 # file.executable
 no_exec = frozenset(["Manifest","ChangeLog","metadata.xml"])
 
-def last(full=False):
-       """Print the results of the last repoman run
-       Args:
-               full - Print the complete results, if false, print a summary
-       Returns:
-               Doesn't return (invokes sys.exit()
-       """
-       #Retrieve and unpickle stats and fails from saved files
-       savedf=open(os.path.join(portage.const.CACHE_PATH, 'repo.stats'),'r')
-       stats = pickle.load(savedf)
-       savedf.close()
-       savedf=open(os.path.join(portage.const.CACHE_PATH, 'repo.fails'),'r')
-       fails = pickle.load(savedf)
-       savedf.close()
-
-       #dofail will be set to 1 if we have failed in at least one non-warning category
-       dofail=0
-       #dowarn will be set to 1 if we tripped any warnings
-       dowarn=0
-       #dofull will be set if we should print a "repoman full" informational message
-       dofull=0
-
-       dofull = options.mode not in ("full", "lfull")
-       
-       for x in qacats:
-               if not stats[x]:
-                       continue
-               dowarn = 1
-               if x not in qawarnings:
-                       dofail = 1
-
-       print
-       print green("RepoMan remembers...")
-       print
-       style_file = ConsoleStyleFile(sys.stdout)
-       console_writer = StyleWriter(file=style_file, maxcol=9999)
-       console_writer.style_listener = style_file.new_styles
-       f = formatter.AbstractFormatter(console_writer)
-       utilities.format_qa_output(f, stats, fails, dofull, dofail, options, qawarnings)
-       print
-       if dofull:
-               print bold("Note: type \"repoman lfull\" for a complete listing of repomans last run.")
-               print
-       if dowarn and not dofail:
-               print green("RepoMan sez:"),"\"You only gave me a partial QA payment last time?\n              I took it, but I wasn't happy.\""
-       elif not dofail:
-               print green("RepoMan sez:"),"\"If everyone were like you, I'd be out of business!\""
-       print
-       sys.exit(0)
-
 options, arguments = ParseArgs(sys.argv, qahelp)
 
-if options.mode in ('last', 'lfull'):
-       last('lfull' in options.mode)
+if options.version:
+       print("Portage", portage.VERSION)
+       sys.exit(0)
 
 # Set this to False when an extraordinary issue (generally
 # something other than a QA issue) makes it impossible to
 # commit (like if Manifest generation fails).
 can_force = True
 
+portdir, portdir_overlay, mydir = utilities.FindPortdir(repoman_settings)
+if portdir is None:
+       sys.exit(1)
+
+myreporoot = os.path.basename(portdir_overlay)
+myreporoot += mydir[len(portdir_overlay):]
 
 vcs = None
 if os.path.isdir("CVS"):
        vcs = "cvs"
 if os.path.isdir(".svn"):
        vcs = "svn"
+elif os.path.isdir(os.path.join(portdir_overlay, ".git")):
+       vcs = "git"
+
+vcs_local_opts = repoman_settings.get("REPOMAN_VCS_LOCAL_OPTS", "").split()
+vcs_global_opts = repoman_settings.get("REPOMAN_VCS_GLOBAL_OPTS")
+if vcs_global_opts is None:
+       if vcs != "git":
+               vcs_global_opts = "-q"
+       else:
+               vcs_global_opts = ""
+vcs_global_opts = vcs_global_opts.split()
 
 if vcs == "cvs" and \
        "commit" == options.mode and \
@@ -503,7 +492,7 @@ if vcs == "cvs" and \
                prefix = bad(" * ")
                from textwrap import wrap
                for line in wrap(msg, 70):
-                       print prefix + line
+                       print(prefix + line)
                sys.exit(1)
        del repo_lines
 
@@ -511,39 +500,49 @@ if options.mode == 'commit' and not options.pretend and not vcs:
        logging.info("Not in a version controlled repository; enabling pretend mode.")
        options.pretend = True
 
-try:
-       portdir, portdir_overlay, mydir = utilities.FindPortdir(repoman_settings)
-except ValueError:
-       sys.exit(1)
+# Ensure that PORTDIR_OVERLAY contains the repository corresponding to $PWD.
+repoman_settings = portage.config(local_config=False)
+repoman_settings['PORTDIR_OVERLAY'] = "%s %s" % \
+       (repoman_settings.get('PORTDIR_OVERLAY', ''), portdir_overlay)
+repoman_settings.backup_changes('PORTDIR_OVERLAY')
 
-os.environ["PORTDIR"] = portdir
-if portdir_overlay != portdir:
-       os.environ["PORTDIR_OVERLAY"] = portdir_overlay
-else:
-       os.environ["PORTDIR_OVERLAY"] = ""
+root = '/'
+trees = {
+       root : {'porttree' : portage.portagetree(root, settings=repoman_settings)}
+}
+portdb = trees[root]['porttree'].dbapi
+
+# Constrain dependency resolution to the master(s)
+# that are specified in layout.conf.
+portdir_overlay = os.path.realpath(portdir_overlay)
+repo_info = portdb._repo_info[portdir_overlay]
+portdb.porttrees = list(repo_info.eclass_db.porttrees)
+portdir = portdb.porttrees[0]
+
+# Generate an appropriate PORTDIR_OVERLAY value for passing into the
+# profile-specific config constructor calls.
+env = os.environ.copy()
+env['PORTDIR'] = portdir
+env['PORTDIR_OVERLAY'] = ' '.join(portdb.porttrees[1:])
 
 logging.info('Setting paths:')
-logging.info('PORTDIR = "' + os.environ['PORTDIR'] + '"')
-logging.info('PORTDIR_OVERLAY = "' + os.environ['PORTDIR_OVERLAY']+'"')
+logging.info('PORTDIR = "' + portdir + '"')
+logging.info('PORTDIR_OVERLAY = "%s"' % env['PORTDIR_OVERLAY'])
+
+categories = []
+for path in set([portdir, portdir_overlay]):
+       categories.extend(portage.util.grabfile(
+               os.path.join(path, 'profiles', 'categories')))
+repoman_settings.categories = tuple(sorted(
+       portage.util.stack_lists([categories], incremental=1)))
+del categories
 
-# Now that PORTDIR_OVERLAY is properly overridden, create the portdb.
-repoman_settings = portage.config(local_config=False,
-       config_incrementals=portage.const.INCREMENTALS)
-trees = portage.create_trees()
-trees["/"]["porttree"].settings = repoman_settings
-portdb = trees["/"]["porttree"].dbapi
 portdb.mysettings = repoman_settings
-setconfig = load_default_config(repoman_settings, trees["/"])
-root_config = RootConfig(repoman_settings, trees["/"], setconfig)
+root_config = RootConfig(repoman_settings, trees[root], None)
 # We really only need to cache the metadata that's necessary for visibility
 # filtering. Anything else can be discarded to reduce memory consumption.
 portdb._aux_cache_keys.clear()
 portdb._aux_cache_keys.update(["EAPI", "KEYWORDS", "SLOT"])
-# dep_zapdeps looks at the vardbapi, but it shouldn't for repoman.
-del trees["/"]["vartree"]
-
-myreporoot = os.path.basename(portdir_overlay)
-myreporoot += mydir[len(portdir_overlay):]
 
 reposplit = myreporoot.split(os.path.sep)
 repolevel = len(reposplit)
@@ -552,51 +551,153 @@ repolevel = len(reposplit)
 # Reason for this is if they're trying to commit in just $FILESDIR/*, the Manifest needs updating.
 # this check ensures that repoman knows where it is, and the manifest recommit is at least possible.
 if options.mode == 'commit' and repolevel not in [1,2,3]:
-       print red("***")+" Commit attempts *must* be from within a vcs co, category, or package directory."
-       print red("***")+" Attempting to commit from a packages files directory will be blocked for instance."
-       print red("***")+" This is intended behaviour, to ensure the manifest is recommited for a package."
-       print red("***")
+       print(red("***")+" Commit attempts *must* be from within a vcs co, category, or package directory.")
+       print(red("***")+" Attempting to commit from a packages files directory will be blocked for instance.")
+       print(red("***")+" This is intended behaviour, to ensure the manifest is recommited for a package.")
+       print(red("***"))
        err("Unable to identify level we're commiting from for %s" % '/'.join(reposplit))
 
 startdir = normalize_path(mydir)
 repodir = startdir
 for x in range(0, repolevel - 1):
        repodir = os.path.dirname(repodir)
+repodir = os.path.realpath(repodir)
 
 def caterror(mycat):
        err(mycat+" is not an official category.  Skipping QA checks in this directory.\nPlease ensure that you add "+catdir+" to "+repodir+"/profiles/categories\nif it is a new category.")
 
-# setup a uselist from portage
-uselist=[]
-try:
-       uselist=portage.grabfile(portdir+"/profiles/use.desc")
-       for l in range(0,len(uselist)):
-               uselist[l]=uselist[l].split()[0]
-       for var in repoman_settings["USE_EXPAND"].split():
-               vardescs = portage.grabfile(portdir+"/profiles/desc/"+var.lower()+".desc")
-               for l in range(0, len(vardescs)):
-                       uselist.append(var.lower() + "_" + vardescs[l].split()[0])
-except (IOError, OSError, ParseError), e:
-       logging.exception("Couldn't read USE flags from use.desc")
-       sys.exit(1)
+class ProfileDesc(object):
+       __slots__ = ('abs_path', 'arch', 'status', 'sub_path', 'tree_path',)
+       def __init__(self, arch, status, sub_path, tree_path):
+               self.arch = arch
+               self.status = status
+               self.sub_path = normalize_path(sub_path.lstrip(os.sep))
+               self.tree_path = tree_path
+               self.abs_path = os.path.join(tree_path, 'profiles', self.sub_path)
+
+profile_list = []
+valid_profile_types = frozenset(['dev', 'exp', 'stable'])
+
+# get lists of valid keywords, licenses, and use
+kwlist = set()
+liclist = set()
+uselist = set()
+global_pmasklines = []
+
+for path in portdb.porttrees:
+       try:
+               liclist.update(os.listdir(os.path.join(path, "licenses")))
+       except OSError:
+               pass
+       kwlist.update(portage.grabfile(os.path.join(path,
+               "profiles", "arch.list")))
+
+       use_desc = portage.grabfile(os.path.join(path, 'profiles', 'use.desc'))
+       for x in use_desc:
+               x = x.split()
+               if x:
+                       uselist.add(x[0])
+
+       expand_desc_dir = os.path.join(path, 'profiles', 'desc')
+       try:
+               expand_list = os.listdir(expand_desc_dir)
+       except OSError:
+               pass
+       else:
+               for fn in expand_list:
+                       if not fn[-5:] == '.desc':
+                               continue
+                       use_prefix = fn[:-5].lower() + '_'
+                       for x in portage.grabfile(os.path.join(expand_desc_dir, fn)):
+                               x = x.split()
+                               if x:
+                                       uselist.add(use_prefix + x[0])
+
+       global_pmasklines.append(portage.util.grabfile_package(
+               os.path.join(path, 'profiles', 'package.mask'), recursive=1))
+
+       desc_path = os.path.join(path, 'profiles', 'profiles.desc')
+       try:
+               desc_file = codecs.open(_unicode_encode(desc_path,
+                       encoding=_encodings['fs'], errors='strict'),
+                       mode='r', encoding=_encodings['repo.content'], errors='replace')
+       except EnvironmentError:
+               pass
+       else:
+               for i, x in enumerate(desc_file):
+                       if x[0] == "#":
+                               continue
+                       arch = x.split()
+                       if len(arch) == 0:
+                               continue
+                       if len(arch) != 3:
+                               err("wrong format: \"" + bad(x.strip()) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       elif arch[0] not in kwlist:
+                               err("invalid arch: \"" + bad(arch[0]) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       elif arch[2] not in valid_profile_types:
+                               err("invalid profile type: \"" + bad(arch[2]) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       profile_desc = ProfileDesc(arch[0], arch[2], arch[1], portdir)
+                       if not os.path.isdir(profile_desc.abs_path):
+                               logging.error(
+                                       "Invalid %s profile (%s) for arch %s in %s line %d",
+                                       arch[2], arch[1], arch[0], desc_path, i+1)
+                               continue
+                       profile_list.append(profile_desc)
+               desc_file.close()
+
+repoman_settings['PORTAGE_ARCHLIST'] = ' '.join(sorted(kwlist))
+repoman_settings.backup_changes('PORTAGE_ARCHLIST')
+
+global_pmasklines = portage.util.stack_lists(global_pmasklines, incremental=1)
+global_pmaskdict = {}
+for x in global_pmasklines:
+       global_pmaskdict.setdefault(portage.dep_getkey(x), []).append(x)
+del global_pmasklines
+
+def has_global_mask(pkg):
+       mask_atoms = global_pmaskdict.get(pkg.cp)
+       if mask_atoms:
+               pkg_list = [pkg]
+               for x in mask_atoms:
+                       if portage.dep.match_from_list(x, pkg_list):
+                               return x
+       return None
+
+# Ensure that profile sub_path attributes are unique. Process in reverse order
+# so that profiles with duplicate sub_path from overlays will override
+# profiles with the same sub_path from parent repos.
+profiles = {}
+profile_list.reverse()
+profile_sub_paths = set()
+for prof in profile_list:
+       if prof.sub_path in profile_sub_paths:
+               continue
+       profile_sub_paths.add(prof.sub_path)
+       profiles.setdefault(prof.arch, []).append(prof)
+
+for x in repoman_settings.archlist():
+       if x[0] == "~":
+               continue
+       if x not in profiles:
+               print(red("\""+x+"\" doesn't have a valid profile listed in profiles.desc."))
+               print(red("You need to either \"cvs update\" your profiles dir or follow this"))
+               print(red("up with the "+x+" team."))
+               print()
 
-# retrieve a list of current licenses in portage
-liclist = set(portage.listdir(os.path.join(portdir, "licenses")))
 if not liclist:
        logging.fatal("Couldn't find licenses?")
        sys.exit(1)
-if portdir_overlay != portdir:
-       liclist.update(portage.listdir(os.path.join(portdir_overlay, "licenses")))
 
-# retrieve list of offical keywords
-kwlist = set(portage.grabfile(os.path.join(portdir, "profiles", "arch.list")))
 if not kwlist:
        logging.fatal("Couldn't read KEYWORDS from arch.list")
        sys.exit(1)
 
-if portdir_overlay != portdir:
-       kwlist.update(portage.grabfile(
-               os.path.join(portdir_overlay, "profiles", "arch.list")))
+if not uselist:
+       logging.fatal("Couldn't find use.desc?")
+       sys.exit(1)
 
 scanlist=[]
 if repolevel==2:
@@ -610,6 +711,7 @@ if repolevel==2:
                        continue
                if os.path.isdir(startdir+"/"+x):
                        scanlist.append(catdir+"/"+x)
+       repo_subdir = catdir + os.sep
 elif repolevel==1:
        for x in repoman_settings.categories:
                if not os.path.isdir(startdir+"/"+x):
@@ -619,56 +721,39 @@ elif repolevel==1:
                                continue
                        if os.path.isdir(startdir+"/"+x+"/"+y):
                                scanlist.append(x+"/"+y)
+       repo_subdir = ""
 elif repolevel==3:
        catdir = reposplit[-2]
        if catdir not in repoman_settings.categories:
                caterror(catdir)
        scanlist.append(catdir+"/"+reposplit[-1])
+       repo_subdir = scanlist[-1] + os.sep
+repo_subdir_len = len(repo_subdir)
 scanlist.sort()
 
 logging.debug("Found the following packages to scan:\n%s" % '\n'.join(scanlist))
 
-profiles={}
-valid_profile_types = frozenset(["dev", "exp", "stable"])
-descfile=portdir+"/profiles/profiles.desc"
-if os.path.exists(descfile):
-       for i, x in enumerate(open(descfile, 'rb')):
-               if x[0]=="#":
-                       continue
-               arch=x.split()
-               if len(arch) == 0:
-                       continue
-               if len(arch)!=3:
-                       err("wrong format: \"" + bad(x.strip()) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               elif arch[0] not in kwlist:
-                       err("invalid arch: \"" + bad(arch[0]) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               elif arch[2] not in valid_profile_types:
-                       err("invalid profile type: \"" + bad(arch[2]) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               if not os.path.isdir(portdir+"/profiles/"+arch[1]):
-                       print "Invalid "+arch[2]+" profile ("+arch[1]+") for arch "+arch[0]
-                       continue
-               if arch[0] in profiles:
-                       profiles[arch[0]]+= [[arch[1], arch[2]]]
-               else:
-                       profiles[arch[0]] = [[arch[1], arch[2]]]
+def dev_keywords(profiles):
+       """
+       Create a set of KEYWORDS values that exist in 'dev'
+       profiles. These are used
+       to trigger a message notifying the user when they might
+       want to add the --include-dev option.
+       """
+       type_arch_map = {}
+       for arch, arch_profiles in profiles.items():
+               for prof in arch_profiles:
+                       arch_set = type_arch_map.get(prof.status)
+                       if arch_set is None:
+                               arch_set = set()
+                               type_arch_map[prof.status] = arch_set
+                       arch_set.add(arch)
 
-       for x in repoman_settings.archlist():
-               if x[0] == "~":
-                       continue
-               if x not in profiles:
-                       print red("\""+x+"\" doesn't have a valid profile listed in profiles.desc.")
-                       print red("You need to either \"cvs update\" your profiles dir or follow this")
-                       print red("up with the "+x+" team.")
-                       print
-else:
-       print red("profiles.desc does not exist: "+descfile)
-       print red("You need to do \"cvs update\" in profiles dir.")
-       print
-       sys.exit(1)
+       dev_keywords = type_arch_map.get('dev', set())
+       dev_keywords.update(['~' + arch for arch in dev_keywords])
+       return frozenset(dev_keywords)
 
+dev_keywords = dev_keywords(profiles)
 
 stats={}
 fails={}
@@ -685,9 +770,9 @@ metadata_dtd = os.path.join(repoman_settings["DISTDIR"], 'metadata.dtd')
 if options.mode == "manifest":
        pass
 elif not find_binary('xmllint'):
-       print red("!!! xmllint not found. Can't check metadata.xml.\n")
+       print(red("!!! xmllint not found. Can't check metadata.xml.\n"))
        if options.xml_parse or repolevel==3:
-               print red("!!!")+" sorry, xmllint is needed.  failing\n"
+               print(red("!!!")+" sorry, xmllint is needed.  failing\n")
                sys.exit(1)
 else:
        #hardcoded paths/urls suck. :-/
@@ -703,35 +788,35 @@ else:
                else:
                        must_fetch=0
 
-       except (OSError,IOError), e:
+       except (OSError,IOError) as e:
                if e.errno != 2:
-                       print red("!!!")+" caught exception '%s' for %s/metadata.dtd, bailing" % (str(e), portage.CACHE_PATH)
+                       print(red("!!!")+" caught exception '%s' for %s/metadata.dtd, bailing" % (str(e), portage.CACHE_PATH))
                        sys.exit(1)
 
        if must_fetch:
-               print 
-               print green("***")+" the local copy of metadata.dtd needs to be refetched, doing that now"
-               print
+               print() 
+               print(green("***")+" the local copy of metadata.dtd needs to be refetched, doing that now")
+               print()
                val = 0
                try:
                        try:
                                os.unlink(metadata_dtd)
-                       except OSError, e:
+                       except OSError as e:
                                if e.errno != errno.ENOENT:
                                        raise
                                del e
                        val=portage.fetch(['http://www.gentoo.org/dtd/metadata.dtd'],repoman_settings,fetchonly=0, \
                                try_mirrors=0)
 
-               except SystemExit, e:
+               except SystemExit as e:
                        raise  # Need to propogate this
-               except Exception,e:
-                       print
-                       print red("!!!")+" attempting to fetch 'http://www.gentoo.org/dtd/metadata.dtd', caught"
-                       print red("!!!")+" exception '%s' though." % str(e)
+               except Exception as e:
+                       print()
+                       print(red("!!!")+" attempting to fetch 'http://www.gentoo.org/dtd/metadata.dtd', caught")
+                       print(red("!!!")+" exception '%s' though." % str(e))
                        val=0
                if not val:
-                       print red("!!!")+" fetching new metadata.dtd failed, aborting"
+                       print(red("!!!")+" fetching new metadata.dtd failed, aborting")
                        sys.exit(1)
        #this can be problematic if xmllint changes their output
        xmllint_capable=True
@@ -742,9 +827,9 @@ if options.mode == 'commit' and vcs:
 if options.mode == "manifest":
        pass
 elif options.pretend:
-       print green("\nRepoMan does a once-over of the neighborhood...")
+       print(green("\nRepoMan does a once-over of the neighborhood..."))
 else:
-       print green("\nRepoMan scours the neighborhood...")
+       print(green("\nRepoMan scours the neighborhood..."))
 
 new_ebuilds = set()
 modified_changelogs = set()
@@ -756,18 +841,31 @@ if vcs == "cvs":
        mycvstree = cvstree.getentries("./", recursive=1)
        mychanged = cvstree.findchanged(mycvstree, recursive=1, basedir="./")
        mynew = cvstree.findnew(mycvstree, recursive=1, basedir="./")
-
 if vcs == "svn":
        svnstatus = os.popen("svn status").readlines()
        mychanged = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem and elem[:1] in "MR" ]
        mynew     = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem.startswith("A") ]
-
+elif vcs == "git":
+       strip_levels = repolevel - 1
+
+       mychanged = os.popen("git diff-index --name-only --diff-filter=M HEAD").readlines()
+       if strip_levels:
+               mychanged = [elem[repo_subdir_len:] for elem in mychanged \
+                       if elem[:repo_subdir_len] == repo_subdir]
+       mychanged = ["./" + elem[:-1] for elem in mychanged]
+
+       mynew = os.popen("git diff-index --name-only --diff-filter=A HEAD").readlines()
+       if strip_levels:
+               mynew = [elem[repo_subdir_len:] for elem in mynew \
+                       if elem[:repo_subdir_len] == repo_subdir]
+       mynew = ["./" + elem[:-1] for elem in mynew]
 if vcs:
        new_ebuilds.update(x for x in mynew if x.endswith(".ebuild"))
        modified_changelogs.update(x for x in chain(mychanged, mynew) \
                if os.path.basename(x) == "ChangeLog")
 
-have_masked = False
+have_pmasked = False
+have_dev_keywords = False
 dofail = 0
 arch_caches={}
 arch_xmatch_caches = {}
@@ -780,7 +878,7 @@ check_ebuild_notadded = not \
        (vcs == "svn" and repolevel < 3 and options.mode != "commit")
 
 # Build a regex from thirdpartymirrors for the SRC_URI.mirror check.
-thirdpartymirrors = portage.flatten(repoman_settings.thirdpartymirrors().values())
+thirdpartymirrors = portage.flatten(list(repoman_settings.thirdpartymirrors().values()))
 
 for x in scanlist:
        #ebuilds and digests added to cvs respectively.
@@ -788,14 +886,59 @@ for x in scanlist:
        eadded=[]
        catdir,pkgdir=x.split("/")
        checkdir=repodir+"/"+x
+       checkdir_relative = ""
+       if repolevel < 3:
+               checkdir_relative = os.path.join(pkgdir, checkdir_relative)
+       if repolevel < 2:
+               checkdir_relative = os.path.join(catdir, checkdir_relative)
+       checkdir_relative = os.path.join(".", checkdir_relative)
 
        if options.mode == "manifest" or \
          options.mode in ('commit', 'fix') and not options.pretend:
+               auto_assumed = set()
+               fetchlist_dict = portage.FetchlistDict(checkdir,
+                       repoman_settings, portdb)
+               if options.mode == 'manifest' and options.force:
+                       portage._doebuild_manifest_exempt_depend += 1
+                       try:
+                               distdir = repoman_settings['DISTDIR']
+                               mf = portage.manifest.Manifest(checkdir, distdir,
+                                       fetchlist_dict=fetchlist_dict)
+                               mf.create(requiredDistfiles=None,
+                                       assumeDistHashesAlways=True)
+                               for distfiles in fetchlist_dict.values():
+                                       for distfile in distfiles:
+                                               if os.path.isfile(os.path.join(distdir, distfile)):
+                                                       mf.fhashdict['DIST'].pop(distfile, None)
+                                               else:
+                                                       auto_assumed.add(distfile)
+                               mf.write()
+                       finally:
+                               portage._doebuild_manifest_exempt_depend -= 1
+
                repoman_settings["O"] = checkdir
                if not portage.digestgen([], repoman_settings, myportdb=portdb):
-                       print "Unable to generate manifest."
+                       print("Unable to generate manifest.")
                        dofail = 1
                if options.mode == "manifest":
+                       if not dofail and options.force and auto_assumed and \
+                               'assume-digests' in repoman_settings.features:
+                               # Show which digests were assumed despite the --force option
+                               # being given. This output will already have been shown by
+                               # digestgen() if assume-digests is not enabled, so only show
+                               # it here if assume-digests is enabled.
+                               pkgs = list(fetchlist_dict)
+                               pkgs.sort()
+                               portage.writemsg_stdout("  digest.assumed" + \
+                                       portage.output.colorize("WARN",
+                                       str(len(auto_assumed)).rjust(18)) + "\n")
+                               for cpv in pkgs:
+                                       fetchmap = fetchlist_dict[cpv]
+                                       pf = portage.catsplit(cpv)[1]
+                                       for distfile in sorted(fetchmap):
+                                               if distfile in auto_assumed:
+                                                       portage.writemsg_stdout(
+                                                               "   %s::%s\n" % (pf, distfile))
                        continue
                elif dofail:
                        sys.exit(1)
@@ -805,7 +948,7 @@ for x in scanlist:
        pkgs = {}
        for y in checkdirlist:
                if y in no_exec and \
-                       stat.S_IMODE(os.stat(os.path.join(checkdir, y)).st_mode) & 0111:
+                       stat.S_IMODE(os.stat(os.path.join(checkdir, y)).st_mode) & 0o111:
                                stats["file.executable"] += 1
                                fails["file.executable"].append(os.path.join(checkdir, y))
                if y.endswith(".ebuild"):
@@ -813,7 +956,7 @@ for x in scanlist:
                        ebuildlist.append(pf)
                        cpv = "%s/%s" % (catdir, pf)
                        try:
-                               myaux = dict(izip(allvars, portdb.aux_get(cpv, allvars)))
+                               myaux = dict(zip(allvars, portdb.aux_get(cpv, allvars)))
                        except KeyError:
                                stats["ebuild.syntax"] += 1
                                fails["ebuild.syntax"].append(os.path.join(x, y))
@@ -831,12 +974,12 @@ for x in scanlist:
 
        # Sort ebuilds in ascending order for the KEYWORDS.dropped check.
        pkgsplits = {}
-       for i in xrange(len(ebuildlist)):
+       for i in range(len(ebuildlist)):
                ebuild_split = portage.pkgsplit(ebuildlist[i])
                pkgsplits[ebuild_split] = ebuildlist[i]
                ebuildlist[i] = ebuild_split
-       ebuildlist.sort(portage.pkgcmp)
-       for i in xrange(len(ebuildlist)):
+       ebuildlist.sort(key=cmp_sort_key(portage.pkgcmp))
+       for i in range(len(ebuildlist)):
                ebuildlist[i] = pkgsplits[ebuildlist[i]]
        del pkgsplits
 
@@ -852,19 +995,21 @@ for x in scanlist:
                continue
 
        for y in checkdirlist:
-               for c in y.strip(os.path.sep):
-                       if c not in allowed_filename_chars_set:
-                               stats["file.name"] += 1
-                               fails["file.name"].append("%s/%s: char '%s'" % (checkdir, y, c))
-                               break
+               m = disallowed_filename_chars_re.search(y.strip(os.sep))
+               if m is not None:
+                       stats["file.name"] += 1
+                       fails["file.name"].append("%s/%s: char '%s'" % \
+                               (checkdir, y, m.group(0)))
 
                if not (y in ("ChangeLog", "metadata.xml") or y.endswith(".ebuild")):
                        continue
                try:
                        line = 1
-                       for l in codecs.open(checkdir+"/"+y, "r", "utf8"):
+                       for l in codecs.open(_unicode_encode(os.path.join(checkdir, y),
+                               encoding=_encodings['fs'], errors='strict'),
+                               mode='r', encoding=_encodings['repo.content']):
                                line +=1
-               except UnicodeDecodeError, ue:
+               except UnicodeDecodeError as ue:
                        stats["file.UTF8"] += 1
                        s = ue.object[:ue.start]
                        l2 = s.count("\n")
@@ -873,7 +1018,17 @@ for x in scanlist:
                                s = s[s.rfind("\n") + 1:]
                        fails["file.UTF8"].append("%s/%s: line %i, just after: '%s'" % (checkdir, y, line, s))
 
-       if vcs and check_ebuild_notadded:
+       if vcs == "git" and check_ebuild_notadded:
+               myf = os.popen("git ls-files --others %s" % \
+                       (portage._shell_quote(checkdir_relative),))
+               for l in myf:
+                       if l[:-1][-7:] == ".ebuild":
+                               stats["ebuild.notadded"] += 1
+                               fails["ebuild.notadded"].append(
+                                       os.path.join(x, os.path.basename(l[:-1])))
+               myf.close()
+
+       if vcs in ("cvs", "svn") and check_ebuild_notadded:
                try:
                        if vcs == "cvs":
                                myf=open(checkdir+"/CVS/Entries","r")
@@ -893,6 +1048,9 @@ for x in scanlist:
                                if vcs == "svn":
                                        if l[:1] == "?":
                                                continue
+                                       if l[:7] == '      >':
+                                               # tree conflict, new in subversion 1.6
+                                               continue
                                        l = l.split()[-1]
                                        if l[-7:] == ".ebuild":
                                                eadded.append(os.path.basename(l[:-7]))
@@ -923,7 +1081,7 @@ for x in scanlist:
        for mykey in fetchlist_dict:
                try:
                        myfiles_all.extend(fetchlist_dict[mykey])
-               except portage.exception.InvalidDependString, e:
+               except portage.exception.InvalidDependString as e:
                        src_uri_error = True
                        try:
                                portdb.aux_get(mykey, ["SRC_URI"])
@@ -944,8 +1102,12 @@ for x in scanlist:
                myfiles_all = set(myfiles_all)
                for entry in mydigests:
                        if entry not in myfiles_all:
-                               stats["digestentry.unused"] += 1
-                               fails["digestentry.unused"].append(checkdir+"::"+entry)
+                               stats["digest.unused"] += 1
+                               fails["digest.unused"].append(checkdir+"::"+entry)
+               for entry in myfiles_all:
+                       if entry not in mydigests:
+                               stats["digest.missing"] += 1
+                               fails["digest.missing"].append(checkdir+"::"+entry)
        del myfiles_all
 
        if os.path.exists(checkdir+"/files"):
@@ -959,7 +1121,7 @@ for x in scanlist:
                        full_path = os.path.join(repodir, relative_path)
                        try:
                                mystat = os.stat(full_path)
-                       except OSError, oe:
+                       except OSError as oe:
                                if oe.errno == 2:
                                        # don't worry about it.  it likely was removed via fix above.
                                        continue
@@ -973,19 +1135,24 @@ for x in scanlist:
                                        if z == "CVS" or z == ".svn":
                                                continue
                                        filesdirlist.append(y+"/"+z)
-                       # current policy is no files over 20k, this is the check.
+                       # Current policy is no files over 20 KiB, these are the checks. File size between
+                       # 20 KiB and 60 KiB causes a warning, while file size over 60 KiB causes an error.
+                       elif mystat.st_size > 61440:
+                               stats["file.size.fatal"] += 1
+                               fails["file.size.fatal"].append("("+ str(mystat.st_size//1024) + " KiB) "+x+"/files/"+y)
                        elif mystat.st_size > 20480:
                                stats["file.size"] += 1
-                               fails["file.size"].append("("+ str(mystat.st_size/1024) + "K) "+x+"/files/"+y)
+                               fails["file.size"].append("("+ str(mystat.st_size//1024) + " KiB) "+x+"/files/"+y)
 
-                       for c in os.path.basename(y.rstrip(os.path.sep)):
-                               if c not in allowed_filename_chars_set:
-                                       stats["file.name"] += 1
-                                       fails["file.name"].append("%s/files/%s: char '%s'" % (checkdir, y, c))
-                                       break
+                       m = disallowed_filename_chars_re.search(
+                               os.path.basename(y.rstrip(os.sep)))
+                       if m is not None:
+                               stats["file.name"] += 1
+                               fails["file.name"].append("%s/files/%s: char '%s'" % \
+                                       (checkdir, y, m.group(0)))
 
                        if desktop_file_validate and desktop_pattern.match(y):
-                               status, cmd_output = commands.getstatusoutput(
+                               status, cmd_output = subprocess_getstatusoutput(
                                        "'%s' '%s'" % (desktop_file_validate, full_path))
                                if os.WIFEXITED(status) and os.WEXITSTATUS(status) != os.EX_OK:
                                        # Note: in the future we may want to grab the
@@ -1002,8 +1169,11 @@ for x in scanlist:
                                                        relative_path + ': %s' % error_match.group(1))
 
        del mydigests
-
-       if "ChangeLog" not in checkdirlist:
+       # Note: We 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.
+       if vcs not in ( "git", ) and "ChangeLog" not in checkdirlist:
                stats["changelog.missing"]+=1
                fails["changelog.missing"].append(x+"/ChangeLog")
        
@@ -1022,7 +1192,7 @@ for x in scanlist:
                        f = open(os.path.join(checkdir, "metadata.xml"))
                        utilities.parse_metadata_use(f, muselist)
                        f.close()
-               except (EnvironmentError, ParseError), e:
+               except (EnvironmentError, ParseError) as e:
                        metadata_bad = True
                        stats["metadata.bad"] += 1
                        fails["metadata.bad"].append("%s/metadata.xml: %s" % (x, e))
@@ -1032,27 +1202,25 @@ for x in scanlist:
                if xmllint_capable and not metadata_bad:
                        # xmlint can produce garbage output even on success, so only dump
                        # the ouput when it fails.
-                       st, out = commands.getstatusoutput(
+                       st, out = subprocess_getstatusoutput(
                                "xmllint --nonet --noout --dtdvalid '%s' '%s'" % \
                                 (metadata_dtd, os.path.join(checkdir, "metadata.xml")))
                        if st != os.EX_OK:
-                               print red("!!!") + " metadata.xml is invalid:"
+                               print(red("!!!") + " metadata.xml is invalid:")
                                for z in out.splitlines():
-                                       print red("!!! ")+z
+                                       print(red("!!! ")+z)
                                stats["metadata.bad"]+=1
                                fails["metadata.bad"].append(x+"/metadata.xml")
 
                del metadata_bad
+       muselist = frozenset(muselist)
 
-       changelog_path = "ChangeLog"
-       if repolevel < 3:
-               changelog_path = os.path.join(pkgdir, changelog_path)
-       if repolevel < 2:
-               changelog_path = os.path.join(catdir, changelog_path)
-       changelog_path = os.path.join(".", changelog_path)
+       changelog_path = os.path.join(checkdir_relative, "ChangeLog")
        changelog_modified = changelog_path in modified_changelogs
 
        allmasked = True
+       # detect unused local USE-descriptions
+       used_useflags = set()
 
        for y in ebuildlist:
                relative_path = os.path.join(x, y + ".ebuild")
@@ -1067,29 +1235,48 @@ for x in scanlist:
                        stats['changelog.ebuildadded'] += 1
                        fails['changelog.ebuildadded'].append(relative_path)
 
-               if stat.S_IMODE(os.stat(full_path).st_mode) & 0111:
+               if stat.S_IMODE(os.stat(full_path).st_mode) & 0o111:
                        stats["file.executable"] += 1
                        fails["file.executable"].append(x+"/"+y+".ebuild")
-               if vcs and check_ebuild_notadded and y not in eadded:
+               if vcs in ("cvs", "svn") and check_ebuild_notadded and y not in eadded:
                        #ebuild not added to vcs
                        stats["ebuild.notadded"]=stats["ebuild.notadded"]+1
                        fails["ebuild.notadded"].append(x+"/"+y+".ebuild")
-
                myesplit=portage.pkgsplit(y)
                if myesplit is None or myesplit[0] != x.split("/")[-1]:
                        stats["ebuild.invalidname"]=stats["ebuild.invalidname"]+1
                        fails["ebuild.invalidname"].append(x+"/"+y+".ebuild")
                        continue
                elif myesplit[0]!=pkgdir:
-                       print pkgdir,myesplit[0]
+                       print(pkgdir,myesplit[0])
                        stats["ebuild.namenomatch"]=stats["ebuild.namenomatch"]+1
                        fails["ebuild.namenomatch"].append(x+"/"+y+".ebuild")
                        continue
 
                pkg = pkgs[y]
+
+               if pkg.invalid:
+                       for k, msgs in pkg.invalid.items():
+                               for msg in msgs:
+                                       stats[k] = stats[k] + 1
+                                       fails[k].append("%s %s" % (relative_path, msg))
+                       continue
+
                myaux = pkg.metadata
                eapi = myaux["EAPI"]
                inherited = pkg.inherited
+               live_ebuild = live_eclasses.intersection(inherited)
+
+               for k, v in myaux.items():
+                       if not isinstance(v, basestring):
+                               continue
+                       m = non_ascii_re.search(v)
+                       if m is not None:
+                               stats["variable.invalidchar"] += 1
+                               fails["variable.invalidchar"].append(
+                                       ("%s: %s variable contains non-ASCII " + \
+                                       "character at position %s") % \
+                                       (relative_path, k, m.start() + 1))
 
                if not src_uri_error:
                        # Check that URIs don't reference a server from thirdpartymirrors.
@@ -1108,20 +1295,9 @@ for x in scanlist:
                                        "%s: '%s' found in thirdpartymirrors" % \
                                        (relative_path, mirror))
 
-               # Test for negative logic and bad words in the RESTRICT var.
-               #for x in myaux[allvars.index("RESTRICT")].split():
-               #       if x.startswith("no"):
-               #               print "Bad RESTRICT value: %s" % x
-               try:
-                       myaux["PROVIDE"] = portage.dep.use_reduce(
-                               portage.dep.paren_reduce(myaux["PROVIDE"]), matchall=1)
-               except portage.exception.InvalidDependString, e:
-                       stats["PROVIDE.syntax"] = stats["PROVIDE.syntax"] + 1
-                       fails["PROVIDE.syntax"].append(mykey+".ebuild PROVIDE: "+str(e))
-                       del e
-                       continue
-               myaux["PROVIDE"] = " ".join(portage.flatten(myaux["PROVIDE"]))
-               for myprovide in myaux["PROVIDE"].split():
+               # The Package class automatically evaluates USE conditionals.
+               for myprovide in portage.flatten(portage.dep.use_reduce(
+                       portage.dep.paren_reduce(pkg.metadata['PROVIDE']), matchall=1)):
                        prov_cp = portage.dep_getkey(myprovide)
                        if prov_cp != myprovide:
                                stats["virtual.versioned"]+=1
@@ -1137,6 +1313,8 @@ for x in scanlist:
                                if catdir == "virtual" and \
                                        missing_var in ("HOMEPAGE", "LICENSE"):
                                        continue
+                               if live_ebuild and missing_var == "KEYWORDS":
+                                       continue
                                myqakey=missingvars[pos]+".missing"
                                stats[myqakey]=stats[myqakey]+1
                                fails[myqakey].append(x+"/"+y+".ebuild")
@@ -1168,7 +1346,7 @@ for x in scanlist:
                previous_keywords = slot_keywords.get(myaux["SLOT"])
                if previous_keywords is None:
                        slot_keywords[myaux["SLOT"]] = set()
-               else:
+               elif not live_ebuild:
                        dropped_keywords = previous_keywords.difference(ebuild_archs)
                        if dropped_keywords:
                                stats["KEYWORDS.dropped"] += 1
@@ -1194,7 +1372,7 @@ for x in scanlist:
                Ebuilds that inherit a "Live" eclass (darcs,subversion,git,cvs,etc..) should
                not be allowed to be marked stable
                """
-               if live_eclasses.intersection(pkg.inherited):
+               if live_ebuild:
                        bad_stable_keywords = []
                        for keyword in keywords:
                                if not keyword.startswith("~") and \
@@ -1208,6 +1386,10 @@ for x in scanlist:
                                                bad_stable_keywords)
                        del bad_stable_keywords
 
+                       if keywords and not has_global_mask(pkg):
+                               stats["LIVEVCS.unmasked"] += 1
+                               fails["LIVEVCS.unmasked"].append(relative_path)
+
                if options.ignore_arches:
                        arches = [[repoman_settings["ARCH"], repoman_settings["ARCH"],
                                repoman_settings["ACCEPT_KEYWORDS"].split()]]
@@ -1252,14 +1434,14 @@ for x in scanlist:
                        except ValueError:
                                badsyntax.append("parenthesis mismatch")
                                mydeplist = []
-                       except portage.exception.InvalidDependString, e:
+                       except portage.exception.InvalidDependString as e:
                                badsyntax.append(str(e))
                                del e
                                mydeplist = []
 
                        try:
                                portage.dep.use_reduce(mydeplist, matchall=1)
-                       except portage.exception.InvalidDependString, e:
+                       except portage.exception.InvalidDependString as e:
                                badsyntax.append(str(e))
 
                        for token in operator_tokens:
@@ -1277,7 +1459,12 @@ for x in scanlist:
                        if mytype in ("DEPEND", "RDEPEND", "PDEPEND"):
                                for token in mydepstr.split():
                                        if token in operator_tokens or \
-                                               token.endswith("?"):
+                                               token[-1:] == "?":
+                                               if token == "test?" and \
+                                                       mytype in ("RDEPEND", "PDEPEND"):
+                                                       stats[mytype + '.suspect'] += 1
+                                                       fails[mytype + '.suspect'].append(relative_path + \
+                                                               ": 'test?' USE conditional in %s" % mytype)
                                                continue
                                        try:
                                                atom = portage.dep.Atom(token)
@@ -1292,11 +1479,11 @@ for x in scanlist:
                                                        portage.dep_getkey(atom) == "virtual/jdk":
                                                        stats['java.eclassesnotused'] += 1
                                                        fails['java.eclassesnotused'].append(relative_path)
-                                               elif mytype == "RDEPEND":
+                                               elif mytype in ("PDEPEND", "RDEPEND"):
                                                        if not is_blocker and \
                                                                portage.dep_getkey(atom) in suspect_rdepend:
-                                                               stats['RDEPEND.suspect'] += 1
-                                                               fails['RDEPEND.suspect'].append(
+                                                               stats[mytype + '.suspect'] += 1
+                                                               fails[mytype + '.suspect'].append(
                                                                        relative_path + ": '%s'" % atom)
                                                if eapi == "0":
                                                        if portage.dep.dep_getslot(atom):
@@ -1319,14 +1506,22 @@ for x in scanlist:
                                                                " not supported with EAPI='%s':" + \
                                                                " '%s'") % (mytype, eapi, atom))
 
+                                               if atom.operator == "~" and \
+                                                       portage.versions.catpkgsplit(atom.cpv)[3] != "r0":
+                                                       stats[mytype + '.badtilde'] += 1
+                                                       fails[mytype + '.badtilde'].append(
+                                                               (relative_path + ": %s uses the ~ operator"
+                                                                " with a non-zero revision:" + \
+                                                                " '%s'") % (mytype, atom))
+
                        type_list.extend([mytype] * (len(badsyntax) - len(type_list)))
 
                for m,b in zip(type_list, badsyntax):
                        stats[m+".syntax"] += 1
                        fails[m+".syntax"].append(catpkg+".ebuild "+m+": "+b)
 
-               badlicsyntax = len(filter(lambda x:x=="LICENSE", type_list))
-               badprovsyntax = len(filter(lambda x:x=="PROVIDE", type_list))
+               badlicsyntax = len([z for z in type_list if z == "LICENSE"])
+               badprovsyntax = len([z for z in type_list if z == "PROVIDE"])
                baddepsyntax = len(type_list) != badlicsyntax + badprovsyntax 
                badlicsyntax = badlicsyntax > 0
                badprovsyntax = badprovsyntax > 0
@@ -1336,6 +1531,7 @@ for x in scanlist:
                default_use = []
                for myflag in myaux["IUSE"].split():
                        flag_name = myflag.lstrip("+-")
+                       used_useflags.add(flag_name)
                        if myflag != flag_name:
                                default_use.append(myflag)
                        if flag_name not in uselist:
@@ -1395,7 +1591,7 @@ for x in scanlist:
                try:
                        myrestrict = portage.dep.use_reduce(
                                portage.dep.paren_reduce(myaux["RESTRICT"]), matchall=1)
-               except portage.exception.InvalidDependString, e:
+               except portage.exception.InvalidDependString as e:
                        stats["RESTRICT.syntax"] = stats["RESTRICT.syntax"] + 1
                        fails["RESTRICT.syntax"].append(
                                "%s: RESTRICT: %s" % (relative_path, e))
@@ -1410,14 +1606,20 @@ for x in scanlist:
                # Syntax Checks
                relative_path = os.path.join(x, y + ".ebuild")
                full_path = os.path.join(repodir, relative_path)
-               f = open(full_path, 'rb')
                try:
-                       for check_name, e in run_checks(f, pkg):
-                               stats[check_name] += 1
-                               fails[check_name].append(relative_path + ': %s' % e)
-               finally:
-                       f.close()
-                       del f
+                       # All ebuilds should have utf_8 encoding.
+                       f = codecs.open(_unicode_encode(full_path,
+                               encoding=_encodings['fs'], errors='strict'),
+                               mode='r', encoding=_encodings['repo.content'])
+                       try:
+                               for check_name, e in run_checks(f, pkg):
+                                       stats[check_name] += 1
+                                       fails[check_name].append(relative_path + ': %s' % e)
+                       finally:
+                               f.close()
+               except UnicodeDecodeError:
+                       # A file.UTF8 failure will have already been recorded above.
+                       pass
 
                if options.force:
                        # The dep_check() calls are the most expensive QA test. If --force
@@ -1434,21 +1636,20 @@ for x in scanlist:
                                
                        for prof in profiles[arch]:
 
-                               if prof[1] not in ("stable", "dev"):
+                               if prof.status not in ("stable", "dev") or \
+                                       prof.status == "dev" and not options.include_dev:
                                        continue
 
-                               profdir = portdir+"/profiles/"+prof[0]
-       
-                               if prof[0] in arch_caches:
-                                       dep_settings = arch_caches[prof[0]]
-                               else:
+                               dep_settings = arch_caches.get(prof.sub_path)
+                               if dep_settings is None:
                                        dep_settings = portage.config(
-                                               config_profile_path=profdir,
+                                               config_profile_path=prof.abs_path,
                                                config_incrementals=portage.const.INCREMENTALS,
-                                               local_config=False)
+                                               local_config=False,
+                                               env=env)
                                        if options.without_mask:
                                                dep_settings.pmaskdict.clear()
-                                       arch_caches[prof[0]] = dep_settings
+                                       arch_caches[prof.sub_path] = dep_settings
                                        while True:
                                                try:
                                                        # Protect ACCEPT_KEYWORDS from config.regenerate()
@@ -1457,7 +1658,7 @@ for x in scanlist:
                                                except ValueError:
                                                        break
 
-                               xmatch_cache_key = (prof[0], tuple(groups))
+                               xmatch_cache_key = (prof.sub_path, tuple(groups))
                                xcache = arch_xmatch_caches.get(xmatch_cache_key)
                                if xcache is None:
                                        portdb.melt()
@@ -1479,13 +1680,16 @@ for x in scanlist:
                                        prov_cp = portage.dep_getkey(myprovide)
                                        if prov_cp not in dep_settings.getvirtuals():
                                                stats["virtual.unavailable"]+=1
-                                               fails["virtual.unavailable"].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+prov_cp)
+                                               fails["virtual.unavailable"].append("%s: %s(%s) %s" % \
+                                                       (relative_path, keyword, prof.sub_path, prov_cp))
 
                                if not baddepsyntax:
                                        ismasked = os.path.join(catdir, y) not in \
                                                portdb.xmatch("list-visible", x)
                                        if ismasked:
-                                               have_masked = True
+                                               if not have_pmasked:
+                                                       have_pmasked = bool(dep_settings._getMaskAtom(
+                                                               pkg.cpv, pkg.metadata))
                                                if options.ignore_masked:
                                                        continue
                                                #we are testing deps for a masked package; give it some lee-way
@@ -1494,8 +1698,12 @@ for x in scanlist:
                                        else:
                                                suffix=""
                                                matchmode = "minimum-visible"
-       
-                                       if prof[1] == "dev":
+
+                                       if not have_dev_keywords:
+                                               have_dev_keywords = \
+                                                       bool(dev_keywords.intersection(keywords))
+
+                                       if prof.status == "dev":
                                                suffix=suffix+"indev"
 
                                        for mytype,mypos in [["DEPEND",len(missingvars)],["RDEPEND",len(missingvars)+1],["PDEPEND",len(missingvars)+2]]:
@@ -1504,33 +1712,28 @@ for x in scanlist:
                                                myvalue = myaux[mytype]
                                                if not myvalue:
                                                        continue
-                                               try:
-                                                       mydep = portage.dep_check(myvalue, portdb,
-                                                               dep_settings, use="all", mode=matchmode,
-                                                               trees=trees)
-                                               except KeyError, e:
-                                                       stats[mykey]=stats[mykey]+1
-                                                       fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(e[0]))
-                                                       continue
-       
-                                               if mydep[0]==1:
-                                                       if mydep[1]!=[]:
+
+                                               success, atoms = portage.dep_check(myvalue, portdb,
+                                                       dep_settings, use="all", mode=matchmode,
+                                                       trees=trees)
+
+                                               if success:
+                                                       if atoms:
                                                                #we have some unsolvable deps
                                                                #remove ! deps, which always show up as unsatisfiable
-                                                               d=0
-                                                               while d<len(mydep[1]):
-                                                                       if mydep[1][d][0]=="!":
-                                                                               del mydep[1][d]
-                                                                       else:
-                                                                               d += 1
+                                                               atoms = [str(atom) for atom in atoms if not atom.blocker]
                                                                #if we emptied out our list, continue:
-                                                               if not mydep[1]:
+                                                               if not atoms:
                                                                        continue
                                                                stats[mykey]=stats[mykey]+1
-                                                               fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
+                                                               fails[mykey].append("%s: %s(%s) %s" % \
+                                                                       (relative_path, keyword,
+                                                                       prof.sub_path, repr(atoms)))
                                                else:
                                                        stats[mykey]=stats[mykey]+1
-                                                       fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
+                                                       fails[mykey].append("%s: %s(%s) %s" % \
+                                                               (relative_path, keyword,
+                                                               prof.sub_path, repr(atoms)))
 
        # Check for 'all unstable' or 'all masked' -- ACCEPT_KEYWORDS is stripped
        # XXX -- Needs to be implemented in dep code. Can't determine ~arch nicely.
@@ -1541,27 +1744,22 @@ for x in scanlist:
                stats["ebuild.allmasked"]+=1
                fails["ebuild.allmasked"].append(x)
 
+       # check if there are unused local USE-descriptions in metadata.xml
+       for myflag in muselist.difference(used_useflags):
+               stats["metadata.warning"] += 1
+               fails["metadata.warning"].append(
+                       "%s/metadata.xml: unused local USE-description: '%s'" % \
+                       (x, myflag))
+
 if options.mode == "manifest":
        sys.exit(dofail)
 
-#Pickle and save results for instant reuse in last and lfull
-if os.access(portage.const.CACHE_PATH, os.W_OK):
-       for myobj, fname in (stats, "repo.stats"), (fails, "repo.fails"):
-               fpath = os.path.join(portage.const.CACHE_PATH, fname)
-               savef = open(fpath, 'w')
-               pickle.dump(myobj, savef)
-               savef.close()
-               portage.apply_secpass_permissions(fpath, gid=portage.portage_gid,
-                       mode=0664)
-
-# TODO(antarus) This function and last () look familiar ;)
-
 #dofail will be set to 1 if we have failed in at least one non-warning category
 dofail=0
 #dowarn will be set to 1 if we tripped any warnings
 dowarn=0
 #dofull will be set if we should print a "repoman full" informational message
-dofull = options.mode not in ("full", "lfull")
+dofull = options.mode != 'full'
 
 for x in qacats:
        if not stats[x]:
@@ -1578,7 +1776,7 @@ if dofail or \
 # in $EDITOR while the user creates a commit message.
 # Otherwise, the user would not be able to see this output
 # once the editor has taken over the screen.
-qa_output = StringIO.StringIO()
+qa_output = StringIO()
 style_file = ConsoleStyleFile(sys.stdout)
 if options.mode == 'commit' and \
        (not commitmessage or not commitmessage.strip()):
@@ -1611,60 +1809,80 @@ def grouplist(mylist,seperator="/"):
                        mygroups[xs[0]]+=[seperator.join(xs[1:])]
        return mygroups
 
-if have_masked and not (options.without_mask or options.ignore_masked):
-       print bold("Note: use --without-mask to check " + \
-               "KEYWORDS on dependencies of masked packages")
+suggest_ignore_masked = False
+suggest_include_dev = False
+
+if have_pmasked and not (options.without_mask or options.ignore_masked):
+       suggest_ignore_masked = True
+if have_dev_keywords and not options.include_dev:
+       suggest_include_dev = True
+
+if suggest_ignore_masked or suggest_include_dev:
+       print()
+       if suggest_ignore_masked:
+               print(bold("Note: use --without-mask to check " + \
+                       "KEYWORDS on dependencies of masked packages"))
+
+       if suggest_include_dev:
+               print(bold("Note: use --include-dev (-d) to check " + \
+                       "dependencies for 'dev' profiles"))
+       print()
 
 if options.mode != 'commit':
        if dofull:
-               print bold("Note: type \"repoman full\" for a complete listing.")
+               print(bold("Note: type \"repoman full\" for a complete listing."))
        if dowarn and not dofail:
-               print green("RepoMan sez:"),"\"You're only giving me a partial QA payment?\n              I'll take it this time, but I'm not happy.\""
+               print(green("RepoMan sez:"),"\"You're only giving me a partial QA payment?\n              I'll take it this time, but I'm not happy.\"")
        elif not dofail:
-               print green("RepoMan sez:"),"\"If everyone were like you, I'd be out of business!\""
+               print(green("RepoMan sez:"),"\"If everyone were like you, I'd be out of business!\"")
        elif dofail:
-               print turquoise("Please fix these important QA issues first.")
-               print green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n"
+               print(turquoise("Please fix these important QA issues first."))
+               print(green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n")
                sys.exit(1)
 else:
        if dofail and can_force and options.force and not options.pretend:
-               print green("RepoMan sez:") + \
+               print(green("RepoMan sez:") + \
                        " \"You want to commit even with these QA issues?\n" + \
-                       "              I'll take it this time, but I'm not happy.\"\n"
+                       "              I'll take it this time, but I'm not happy.\"\n")
        elif dofail:
                if options.force and not can_force:
-                       print bad("The --force option has been disabled due to extraordinary issues.")
-               print turquoise("Please fix these important QA issues first.")
-               print green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n"
+                       print(bad("The --force option has been disabled due to extraordinary issues."))
+               print(turquoise("Please fix these important QA issues first."))
+               print(green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n")
                sys.exit(1)
 
        if options.pretend:
-               print green("RepoMan sez:"), "\"So, you want to play it safe. Good call.\"\n"
+               print(green("RepoMan sez:"), "\"So, you want to play it safe. Good call.\"\n")
 
        myunadded = []
        if vcs == "cvs":
                try:
                        myvcstree=portage.cvstree.getentries("./",recursive=1)
                        myunadded=portage.cvstree.findunadded(myvcstree,recursive=1,basedir="./")
-               except SystemExit, e:
+               except SystemExit as e:
                        raise  # TODO propogate this
                except:
                        err("Error retrieving CVS tree; exiting.")
-
        if vcs == "svn":
                try:
                        svnstatus=os.popen("svn status --no-ignore").readlines()
                        myunadded = [ "./"+elem.rstrip().split()[1] for elem in svnstatus if elem.startswith("?") or elem.startswith("I") ]
-               except SystemExit, e:
+               except SystemExit as e:
                        raise  # TODO propogate this
                except:
                        err("Error retrieving SVN info; exiting.")
+       if vcs == "git":
+               # get list of files not under version control or missing
+               myf = os.popen("git ls-files --others")
+               myunadded = [ "./" + elem[:-1] for elem in myf ]
+               myf.close()
+
        myautoadd=[]
        if myunadded:
                for x in range(len(myunadded)-1,-1,-1):
                        xs=myunadded[x].split("/")
                        if xs[-1]=="files":
-                               print "!!! files dir is not added! Please correct this."
+                               print("!!! files dir is not added! Please correct this.")
                                sys.exit(-1)
                        elif xs[-1]=="Manifest":
                                # It's a manifest... auto add
@@ -1672,29 +1890,34 @@ else:
                                del myunadded[x]
 
        if myautoadd:
-               print ">>> Auto-Adding missing Manifest(s)..."
+               print(">>> Auto-Adding missing Manifest(s)...")
                if options.pretend:
                        if vcs == "cvs":
-                               print "(cvs add "+" ".join(myautoadd)+")"
+                               print("(cvs add "+" ".join(myautoadd)+")")
                        if vcs == "svn":
-                               print "(svn add "+" ".join(myautoadd)+")"
+                               print("(svn add "+" ".join(myautoadd)+")")
+                       elif vcs == "git":
+                               print("(git add "+" ".join(myautoadd)+")")
                        retval=0
                else:
                        if vcs == "cvs":
                                retval=os.system("cvs add "+" ".join(myautoadd))
                        if vcs == "svn":
                                retval=os.system("svn add "+" ".join(myautoadd))
+                       elif vcs == "git":
+                               retval=os.system("git add "+" ".join(myautoadd))
                if retval:
-                       print "!!! Exiting on vcs (shell) error code:",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.")
+               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."))
                for x in myunadded:
-                       print "   ",x
-               print
-               print
+                       print("   ",x)
+               print()
+               print()
                sys.exit(1)
 
        if vcs == "cvs":
@@ -1726,34 +1949,48 @@ else:
                expansion = set("./" + prop.split(" - ")[0] \
                        for prop in props if " - " in prop)
 
+       elif vcs == "git":
+               strip_levels = repolevel - 1
+
+               mychanged = os.popen("git diff-index --name-only --diff-filter=M HEAD").readlines()
+               if strip_levels:
+                       mychanged = [elem[repo_subdir_len:] for elem in mychanged \
+                               if elem[:repo_subdir_len] == repo_subdir]
+               mychanged = ["./" + elem[:-1] for elem in mychanged]
+
+               mynew = os.popen("git diff-index --name-only --diff-filter=A HEAD").readlines()
+               if strip_levels:
+                       mynew = [elem[repo_subdir_len:] for elem in mynew \
+                               if elem[:repo_subdir_len] == repo_subdir]
+               mynew = ["./" + elem[:-1] for elem in mynew]
+
+               myremoved = os.popen("git diff-index --name-only --diff-filter=D HEAD").readlines()
+               if strip_levels:
+                       myremoved = [elem[repo_subdir_len:] for elem in myremoved \
+                               if elem[:repo_subdir_len] == repo_subdir]
+               myremoved = ["./" + elem[:-1] for elem in myremoved]
+
        if vcs:
                if not (mychanged or mynew or myremoved):
-                       print green("RepoMan sez:"), "\"Doing nothing is not always good for QA.\""
-                       print
-                       print "(Didn't find any changed files...)"
-                       print
-                       sys.exit(0)
+                       print(green("RepoMan sez:"), "\"Doing nothing is not always good for QA.\"")
+                       print()
+                       print("(Didn't find any changed files...)")
+                       print()
+                       sys.exit(1)
 
        # Manifests need to be regenerated after all other commits, so don't commit
        # them now even if they have changed.
        mymanifests = set()
-       changed_set = set()
-       new_set = set()
-       for f in mychanged:
-               if "Manifest" == os.path.basename(f):
-                       mymanifests.add(f)
-               else:
-                       changed_set.add(f)
-       for f in mynew:
+       myupdates = set()
+       for f in mychanged + mynew:
                if "Manifest" == os.path.basename(f):
                        mymanifests.add(f)
                else:
-                       new_set.add(f)
-       mychanged = list(changed_set)
-       mynew =  list(new_set)
+                       myupdates.add(f)
+       if vcs == 'git':
+               myupdates.difference_update(myremoved)
+       myupdates = list(myupdates)
        mymanifests = list(mymanifests)
-       del changed_set, new_set
-       myupdates = mychanged + mynew
        myheaders = []
        mydirty = []
        headerstring = "'\$(Header|Id)"
@@ -1770,14 +2007,22 @@ else:
                        if myfile not in expansion:
                                continue
 
-               myout = commands.getstatusoutput("egrep -q "+headerstring+" "+myfile)
+               myout = subprocess_getstatusoutput("egrep -q "+headerstring+" "+myfile)
                if myout[0] == 0:
                        myheaders.append(myfile)
 
-       print "*",green(str(len(myupdates))),"files being committed...",green(str(len(myheaders))),"have headers that will change."
-       print "*","Files with headers will cause the manifests to be made and recommited."
-       logging.info("myupdates:", str(myupdates))
-       logging.info("myheaders:", str(myheaders))
+       print("* %s files being committed..." % green(str(len(myupdates))), end=' ')
+       if vcs == 'git':
+               # With git, there's never any keyword expansion, so there's
+               # no need to regenerate manifests and all files will be
+               # committed in one big commit at the end.
+               print()
+       else:
+               print("%s have headers that will change." % green(str(len(myheaders))))
+               print("* Files with headers will cause the " + \
+                       "manifests to be made and recommited.")
+       logging.info("myupdates: %s", myupdates)
+       logging.info("myheaders: %s", myheaders)
 
        commitmessage = options.commitmsg
        if options.commitmsgfile:
@@ -1786,7 +2031,7 @@ else:
                        commitmessage = f.read()
                        f.close()
                        del f
-               except (IOError, OSError), e:
+               except (IOError, OSError) as e:
                        if e.errno == errno.ENOENT:
                                portage.writemsg("!!! File Not Found: --commitmsgfile='%s'\n" % options.commitmsgfile)
                        else:
@@ -1804,7 +2049,7 @@ else:
                except KeyboardInterrupt:
                        exithandler()
                if not commitmessage or not commitmessage.strip():
-                       print "* no commit message?  aborting commit."
+                       print("* no commit message?  aborting commit.")
                        sys.exit(1)
        commitmessage = commitmessage.rstrip()
        portage_version = getattr(portage, "VERSION", None)
@@ -1812,7 +2057,7 @@ else:
                sys.stderr.write("Failed to insert portage version in message!\n")
                sys.stderr.flush()
                portage_version = "Unknown"
-       unameout = platform.system() + " " + platform.release() + " "
+       unameout = platform.system() + " "
        if platform.system() in ["Darwin", "SunOS"]:
                unameout += platform.processor()
        else:
@@ -1823,7 +2068,7 @@ else:
                commitmessage += ", RepoMan options: --force"
        commitmessage += ")"
 
-       if myupdates or myremoved:
+       if vcs != 'git' and (myupdates or myremoved):
                myfiles = myupdates + myremoved
                if not myheaders and "sign" not in repoman_settings.features:
                        myfiles += mymanifests
@@ -1832,37 +2077,40 @@ else:
                mymsg.write(commitmessage)
                mymsg.close()
 
-               print
-               print green("Using commit message:")
-               print green("------------------------------------------------------------------------------")
-               print commitmessage
-               print green("------------------------------------------------------------------------------")
-               print
+               print()
+               print(green("Using commit message:"))
+               print(green("------------------------------------------------------------------------------"))
+               print(commitmessage)
+               print(green("------------------------------------------------------------------------------"))
+               print()
+
+               # Having a leading ./ prefix on file paths can trigger a bug in
+               # the cvs server when committing files to multiple directories,
+               # so strip the prefix.
+               myfiles = [f.lstrip("./") for f in myfiles]
+
+               commit_cmd = [vcs]
+               commit_cmd.extend(vcs_global_opts)
+               commit_cmd.append("commit")
+               commit_cmd.extend(vcs_local_opts)
+               commit_cmd.extend(["-F", commitmessagefile])
+               commit_cmd.extend(myfiles)
 
-               retval = None
-               if options.pretend:
-                       if vcs == "cvs":
-                               print "(cvs -q commit -F %s %s)" % \
-                                       (commitmessagefile, " ".join(myfiles))
-                       if vcs == "svn":
-                               print "(svn commit -F %s %s)" % \
-                                       (commitmessagefile, " ".join(myfiles))
-               else:
-                       if vcs == "cvs":
-                               retval = spawn(["cvs", "-q", "commit",
-                                       "-F", commitmessagefile] + myfiles,
-                                       env=os.environ)
-                       if vcs == "svn":
-                               retval = spawn(["svn", "commit",
-                                       "-F", commitmessagefile] + myfiles,
-                                       env=os.environ)
                try:
-                       os.unlink(commitmessagefile)
-               except OSError:
-                       pass
-               if retval:
-                       print "!!! Exiting on cvs (shell) error code:",retval
-                       sys.exit(retval)
+                       if options.pretend:
+                               print("(%s)" % (" ".join(commit_cmd),))
+                       else:
+                               retval = spawn(commit_cmd, env=os.environ)
+                               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)
+               finally:
+                       try:
+                               os.unlink(commitmessagefile)
+                       except OSError:
+                               pass
 
        # Setup the GPG commands
        def gpgsign(filename):
@@ -1887,7 +2135,7 @@ else:
                if "PORTAGE_GPG_DIR" in repoman_settings:
                        gpgcmd += " --homedir "+repoman_settings["PORTAGE_GPG_DIR"]
                if options.pretend:
-                       print "("+gpgcmd+" "+filename+")"
+                       print("("+gpgcmd+" "+filename+")")
                else:
                        rValue = os.system(gpgcmd+" "+filename)
                        if rValue == os.EX_OK:
@@ -1913,8 +2161,8 @@ else:
                        write_atomic(x, "".join(mylines))
 
        manifest_commit_required = True
-       if myupdates or myremoved or mynew:
-               myfiles=myupdates+myremoved+mynew
+       if vcs != 'git' and (myupdates or myremoved):
+               myfiles = myupdates + myremoved
                for x in range(len(myfiles)-1, -1, -1):
                        if myfiles[x].count("/") < 4-repolevel:
                                del myfiles[x]
@@ -1939,7 +2187,7 @@ else:
                                portage.digestgen([], repoman_settings, manifestonly=1,
                                        myportdb=portdb)
                elif repolevel==1: # repo-cvsroot
-                       print green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n"
+                       print(green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n")
                        for x in myfiles:
                                xs=x.split("/")
                                if len(xs) < 4-repolevel:
@@ -1955,33 +2203,40 @@ else:
                                portage.digestgen([], repoman_settings, manifestonly=1,
                                        myportdb=portdb)
                else:
-                       print red("I'm confused... I don't know where I am!")
+                       print(red("I'm confused... I don't know where I am!"))
                        sys.exit(1)
 
                # Force an unsigned commit when more than one Manifest needs to be signed.
                if repolevel < 3 and "sign" in repoman_settings.features:
-                       if options.pretend:
-                               if vcs == "cvs":
-                                       print "(cvs -q commit -F commitmessagefile)"
-                               if vcs == "svn":
-                                       print "(svn -q commit -F commitmessagefile)"
-                       else:
-                               fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
-                               mymsg = os.fdopen(fd, "w")
-                               mymsg.write(commitmessage)
-                               mymsg.write("\n (Unsigned Manifest commit)")
-                               mymsg.close()
-                               if vcs == "cvs":
-                                       retval=os.system("cvs -q commit -F "+commitmessagefile)
-                               if vcs == "svn":
-                                       retval=os.system("svn -q commit -F "+commitmessagefile)
+
+                       fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
+                       mymsg = os.fdopen(fd, "w")
+                       mymsg.write(commitmessage)
+                       mymsg.write("\n (Unsigned Manifest commit)")
+                       mymsg.close()
+
+                       commit_cmd = [vcs]
+                       commit_cmd.extend(vcs_global_opts)
+                       commit_cmd.append("commit")
+                       commit_cmd.extend(vcs_local_opts)
+                       commit_cmd.extend(["-F", commitmessagefile])
+                       commit_cmd.extend(f.lstrip("./") for f in mymanifests)
+
+                       try:
+                               if options.pretend:
+                                       print("(%s)" % (" ".join(commit_cmd),))
+                               else:
+                                       retval = spawn(commit_cmd, env=os.environ)
+                                       if retval:
+                                               writemsg_level(("!!! Exiting on %s (shell) " + \
+                                                       "error code: %s\n") % (vcs, retval),
+                                                       level=logging.ERROR, noiselevel=-1)
+                                               sys.exit(retval)
+                       finally:
                                try:
                                        os.unlink(commitmessagefile)
                                except OSError:
                                        pass
-                               if retval:
-                                       print "!!! Exiting on cvs (shell) error code:",retval
-                                       sys.exit(retval)
                        manifest_commit_required = False
 
        signed = False
@@ -2008,7 +2263,7 @@ else:
                                                continue
                                        gpgsign(os.path.join(repoman_settings["O"], "Manifest"))
                        elif repolevel==1: # repo-cvsroot
-                               print green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n"
+                               print(green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n")
                                mydone=[]
                                for x in myfiles:
                                        xs=x.split("/")
@@ -2023,43 +2278,82 @@ else:
                                        if not os.path.isdir(repoman_settings["O"]):
                                                continue
                                        gpgsign(os.path.join(repoman_settings["O"], "Manifest"))
-               except portage.exception.PortageException, e:
+               except portage.exception.PortageException as e:
                        portage.writemsg("!!! %s\n" % str(e))
                        portage.writemsg("!!! Disabled FEATURES='sign'\n")
                        signed = False
 
-       if manifest_commit_required or signed:
+       if vcs == 'git':
+               # It's not safe to use the git commit -a option since there might
+               # be some modified files elsewhere in the working tree that the
+               # user doesn't want to commit. Therefore, call git update-index
+               # in order to ensure that the index is updated with the latest
+               # versions of all new and modified files in the relevant portion
+               # of the working tree.
+               myfiles = mymanifests + myupdates
+               myfiles.sort()
+               update_index_cmd = ["git", "update-index"]
+               update_index_cmd.extend(f.lstrip("./") for f in myfiles)
                if options.pretend:
-                       if vcs == "cvs":
-                               print "(cvs -q commit -F commitmessagefile)"
-                       if vcs == "svn":
-                               print "(svn -q commit -F commitmessagefile)"
+                       print("(%s)" % (" ".join(update_index_cmd),))
                else:
-                       fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
-                       mymsg = os.fdopen(fd, "w")
-                       mymsg.write(commitmessage)
-                       if signed:
-                               mymsg.write("\n (Signed Manifest commit)")
+                       retval = spawn(update_index_cmd, env=os.environ)
+                       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)
+
+       if vcs == 'git' or manifest_commit_required or signed:
+
+               myfiles = mymanifests[:]
+               if vcs == 'git':
+                       myfiles += myupdates
+                       myfiles += myremoved
+               myfiles.sort()
+
+               fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
+               mymsg = os.fdopen(fd, "w")
+               mymsg.write(commitmessage)
+               if signed:
+                       mymsg.write("\n (Signed Manifest commit)")
+               else:
+                       mymsg.write("\n (Unsigned Manifest commit)")
+               mymsg.close()
+
+               commit_cmd = []
+               if options.pretend and vcs is None:
+                       # substitute a bogus value for pretend output
+                       commit_cmd.append("cvs")
+               else:
+                       commit_cmd.append(vcs)
+               commit_cmd.extend(vcs_global_opts)
+               commit_cmd.append("commit")
+               commit_cmd.extend(vcs_local_opts)
+               commit_cmd.extend(["-F", commitmessagefile])
+               commit_cmd.extend(f.lstrip("./") for f in myfiles)
+
+               try:
+                       if options.pretend:
+                               print("(%s)" % (" ".join(commit_cmd),))
                        else:
-                               mymsg.write("\n (Unsigned Manifest commit)")
-                       mymsg.close()
-                       if vcs == "cvs":
-                               retval=os.system("cvs -q commit -F "+commitmessagefile)
-                       if vcs == "svn":
-                               retval=os.system("svn -q commit -F "+commitmessagefile)
+                               retval = spawn(commit_cmd, env=os.environ)
+                               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)
+               finally:
                        try:
                                os.unlink(commitmessagefile)
                        except OSError:
                                pass
-                       if retval:
-                               print "!!! Exiting on cvs (shell) error code:",retval
-                               sys.exit(retval)
 
-       print
+       print()
        if vcs:
-               print "Commit complete."
+               print("Commit complete.")
        else:
-               print "repoman was too scared by not seeing any familiar version control file that he forgot to commit anything"
-       print green("RepoMan sez:"), "\"If everyone were like you, I'd be out of business!\"\n"
+               print("repoman was too scared by not seeing any familiar version control file that he forgot to commit anything")
+       print(green("RepoMan sez:"), "\"If everyone were like you, I'd be out of business!\"\n")
 sys.exit(0)