Bug #250212 - Add a new 'upstream.workaround' qa category, and use it for
[portage.git] / bin / repoman
1 #!/usr/bin/python -O
2 # Copyright 1999-2006 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6 # Next to do: dep syntax checking in mask files
7 # Then, check to make sure deps are satisfiable (to avoid "can't find match for" problems)
8 # that last one is tricky because multiple profiles need to be checked.
9
10 import codecs
11 import commands
12 import errno
13 import formatter
14 import logging
15 import optparse
16 import os
17 import re
18 import signal
19 import stat
20 import sys
21 import tempfile
22 import time
23 import platform
24
25 from itertools import chain, izip
26 from stat import S_ISDIR, ST_CTIME
27
28 try:
29         import cStringIO as StringIO
30 except ImportError:
31         import StringIO
32
33 if not hasattr(__builtins__, "set"):
34         from sets import Set as set
35
36 try:
37         import portage
38 except ImportError:
39         from os import path as osp
40         sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
41         import portage
42 portage._disable_legacy_globals()
43
44 try:
45         from repoman.checks import run_checks
46         from repoman import utilities
47 except ImportError:
48         from os import path as osp
49         sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), 'pym'))
50         from repoman.checks import run_checks
51         from repoman import utilities
52
53 from _emerge import Package, RootConfig
54 from portage._sets import load_default_config
55
56 import portage.checksum
57 import portage.const
58 import portage.dep
59 portage.dep._dep_check_strict = True
60 import portage.exception
61 from portage import cvstree, normalize_path
62 from portage import util
63 from portage.exception import ParseError
64 from portage.manifest import Manifest
65 from portage.process import find_binary, spawn
66 from portage.output import bold, create_color_func, darkgreen, \
67         green, nocolor, red, turquoise, yellow
68 from portage.output import ConsoleStyleFile, StyleWriter
69 from portage.util import writemsg_level
70
71 util.initialize_logger()
72
73 # 14 is the length of DESCRIPTION=""
74 max_desc_len = 100
75 allowed_filename_chars="a-zA-Z0-9._-+:"
76 allowed_filename_chars_set = {}
77 map(allowed_filename_chars_set.setdefault, map(chr, range(ord('a'), ord('z')+1)))
78 map(allowed_filename_chars_set.setdefault, map(chr, range(ord('A'), ord('Z')+1)))
79 map(allowed_filename_chars_set.setdefault, map(chr, range(ord('0'), ord('9')+1)))
80 map(allowed_filename_chars_set.setdefault, map(chr, map(ord, [".", "-", "_", "+", ":"])))
81 bad = create_color_func("BAD")
82
83 # A sane umask is needed for files that portage creates.
84 os.umask(022)
85 repoman_settings = portage.config(local_config=False,
86         config_incrementals=portage.const.INCREMENTALS)
87 repoman_settings.lock()
88
89 if repoman_settings.get("NOCOLOR", "").lower() in ("yes", "true") or \
90         not sys.stdout.isatty():
91         nocolor()
92
93 def warn(txt):
94         print "repoman: " + txt
95
96 def err(txt):
97         warn(txt)
98         sys.exit(1)
99
100 def exithandler(signum=None, frame=None):
101         logging.fatal("Interrupted; exiting...")
102         sys.exit(1)
103         os.kill(0, signal.SIGKILL)
104
105 signal.signal(signal.SIGINT,exithandler)
106
107 class RepomanHelpFormatter(optparse.IndentedHelpFormatter):
108         """Repoman needs it's own HelpFormatter for now, because the default ones
109         murder the help text."""
110         
111         def __init__(self, indent_increment=1, max_help_position=24, width=150, short_first=1):
112                 optparse.HelpFormatter.__init__(self, indent_increment, max_help_position, width, short_first)
113
114         def format_description(self, description):
115                 return description
116
117 class RepomanOptionParser(optparse.OptionParser):
118         """Add the on_tail function, ruby has it, optionParser should too
119         """
120         
121         def __init__(self, *args, **kwargs):
122                 optparse.OptionParser.__init__(self, *args, **kwargs)
123                 self.tail = ""
124
125         def on_tail(self, description):
126                 self.tail += description
127
128         def format_help(self, formatter=None):
129                 result = optparse.OptionParser.format_help(self, formatter)
130                 result += self.tail
131                 return result
132
133
134 def ParseArgs(args, qahelp):
135         """This function uses a customized optionParser to parse command line arguments for repoman
136         Args:
137           args - a sequence of command line arguments
138                 qahelp - a dict of qa warning to help message
139         Returns:
140           (opts, args), just like a call to parser.parse_args()
141         """
142
143         modes = {
144                 'commit' : 'Run a scan then commit changes',
145                 'ci' : 'Run a scan then commit changes',
146                 'fix' : 'Fix simple QA issues (stray digests, missing digests)',
147                 'full' : 'Scan directory tree and print all issues (not a summary)',
148                 'help' : 'Show this screen',
149                 'manifest' : 'Generate a Manifest (fetches files if necessary)',
150                 'scan' : 'Scan directory tree for QA issues' 
151         }
152
153         mode_keys = modes.keys()
154         mode_keys.sort()
155
156         parser = RepomanOptionParser(formatter=RepomanHelpFormatter(), usage="%prog [options] [mode]")
157         parser.description = green(" ".join((os.path.basename(args[0]), "1.2")))
158         parser.description += "\nCopyright 1999-2007 Gentoo Foundation"
159         parser.description += "\nDistributed under the terms of the GNU General Public License v2"
160         parser.description += "\nmodes: " + " | ".join(map(green,mode_keys))
161
162         parser.add_option('-m', '--commitmsg', dest='commitmsg',
163                 help='specify a commit message on the command line')
164
165         parser.add_option('-M', '--commitmsgfile', dest='commitmsgfile',
166                 help='specify a path to a file that contains a commit message')
167
168         parser.add_option('-p', '--pretend', dest='pretend', default=False,
169                 action='store_true', help='don\'t commit or fix anything; just show what would be done')
170         
171         parser.add_option('-q', '--quiet', dest="quiet", action="count", default=0,
172                 help='do not print unnecessary messages')
173
174         parser.add_option('-f', '--force', dest='force', default=False, action='store_true',
175                 help='Commit with QA violations')
176
177         parser.add_option('-v', '--verbose', dest="verbosity", action='count',
178                 help='be very verbose in output', default=0)
179
180         parser.add_option('-x', '--xmlparse', dest='xml_parse', action='store_true',
181                 default=False, help='forces the metadata.xml parse check to be carried out')
182
183         parser.add_option('-i', '--ignore-arches', dest='ignore_arches', action='store_true',
184                 default=False, help='ignore arch-specific failures (where arch != host)')
185
186         parser.add_option('-I', '--ignore-masked', dest='ignore_masked', action='store_true',
187                 default=False, help='ignore masked packages (not allowed with commit mode)')
188
189         parser.add_option('-d', '--include-dev', dest='include_dev', action='store_true',
190                 default=False, help='include dev profiles in dependency checks')
191
192         parser.add_option('--without-mask', dest='without_mask', action='store_true',
193                 default=False, help='behave as if no package.mask entries exist (not allowed with commit mode)')
194
195         parser.add_option('--mode', type='choice', dest='mode', choices=modes.keys(), 
196                 help='specify which mode repoman will run in (default=full)')
197
198         parser.on_tail("\n " + green("Modes".ljust(20) + " Description\n"))
199
200         for k in mode_keys:
201                 parser.on_tail(" %s %s\n" % (k.ljust(20), modes[k]))
202
203         parser.on_tail("\n " + green("QA keyword".ljust(20) + " Description\n"))
204
205         sorted_qa = qahelp.keys()
206         sorted_qa.sort()
207         for k in sorted_qa:
208                 parser.on_tail(" %s %s\n" % (k.ljust(20), qahelp[k]))
209
210         if not args:
211                 args = sys.argv
212         opts, args = parser.parse_args(args)
213
214         if opts.mode == 'help':
215                 parser.print_help(short=False)
216
217         for arg in args:
218                 if arg in modes:
219                         if not opts.mode:
220                                 opts.mode = arg
221                                 break
222
223         if not opts.mode:
224                 opts.mode = 'full'
225         
226         if opts.mode == 'ci':
227                 opts.mode = 'commit'  # backwards compat shortcut
228
229         if opts.mode == 'commit' and not (opts.force or opts.pretend):
230                 if opts.ignore_masked:
231                         parser.error('Commit mode and --ignore-masked are not compatible')
232                 if opts.without_mask:
233                         parser.error('Commit mode and --without-mask are not compatible')
234
235         # Use the verbosity and quiet options to fiddle with the loglevel appropriately
236         for val in range(opts.verbosity):
237                 logger = logging.getLogger()
238                 logger.setLevel(logger.getEffectiveLevel() - 10)
239
240         for val in range(opts.quiet):
241                 logger = logging.getLogger()
242                 logger.setLevel(logger.getEffectiveLevel() + 10)
243
244         return (opts, args)
245
246 qahelp={
247         "CVS/Entries.IO_error":"Attempting to commit, and an IO error was encountered access the Entries file",
248         "desktop.invalid":"desktop-file-validate reports errors in a *.desktop file",
249         "ebuild.invalidname":"Ebuild files with a non-parseable or syntactically incorrect name (or using 2.1 versioning extensions)",
250         "ebuild.namenomatch":"Ebuild files that do not have the same name as their parent directory",
251         "changelog.ebuildadded":"An ebuild was added but the ChangeLog was not modified",
252         "changelog.missing":"Missing ChangeLog files",
253         "ebuild.notadded":"Ebuilds that exist but have not been added to cvs",
254         "ebuild.patches":"PATCHES variable should be a bash array to ensure white space safety",
255         "changelog.notadded":"ChangeLogs that exist but have not been added to cvs",
256         "filedir.missing":"Package lacks a files directory",
257         "file.executable":"Ebuilds, digests, metadata.xml, Manifest, and ChangeLog do note need the executable bit",
258         "file.size":"Files in the files directory must be under 20k",
259         "file.name":"File/dir name must be composed of only the following chars: %s " % allowed_filename_chars,
260         "file.UTF8":"File is not UTF8 compliant",
261         "inherit.autotools":"Ebuild inherits autotools but does not call eautomake, eautoconf or eautoreconf",
262         "java.eclassesnotused":"With virtual/jdk in DEPEND you must inherit a java eclass",
263         "KEYWORDS.dropped":"Ebuilds that appear to have dropped KEYWORDS for some arch",
264         "KEYWORDS.missing":"Ebuilds that have a missing or empty KEYWORDS variable",
265         "KEYWORDS.stable":"Ebuilds that have been added directly with stable KEYWORDS",
266         "KEYWORDS.stupid":"Ebuilds that use KEYWORDS=-* instead of package.mask", 
267         "LICENSE.missing":"Ebuilds that have a missing or empty LICENSE variable",
268         "DESCRIPTION.missing":"Ebuilds that have a missing or empty DESCRIPTION variable",
269         "DESCRIPTION.toolong":"DESCRIPTION is over %d characters" % max_desc_len,
270         "EAPI.incompatible":"Ebuilds that use features that are only available with a different EAPI",
271         "EAPI.unsupported":"Ebuilds that have an unsupported EAPI version (you must upgrade portage)",
272         "SLOT.missing":"Ebuilds that have a missing or empty SLOT variable",
273         "HOMEPAGE.missing":"Ebuilds that have a missing or empty HOMEPAGE variable",
274         "DEPEND.bad":"User-visible ebuilds with bad DEPEND settings (matched against *visible* ebuilds)",
275         "RDEPEND.bad":"User-visible ebuilds with bad RDEPEND settings (matched against *visible* ebuilds)",
276         "PDEPEND.bad":"User-visible ebuilds with bad PDEPEND settings (matched against *visible* ebuilds)",
277         "DEPEND.badmasked":"Masked ebuilds with bad DEPEND settings (matched against *all* ebuilds)",
278         "RDEPEND.badmasked":"Masked ebuilds with RDEPEND settings (matched against *all* ebuilds)",
279         "PDEPEND.badmasked":"Masked ebuilds with PDEPEND settings (matched against *all* ebuilds)",
280         "DEPEND.badindev":"User-visible ebuilds with bad DEPEND settings (matched against *visible* ebuilds) in developing arch",
281         "RDEPEND.badindev":"User-visible ebuilds with bad RDEPEND settings (matched against *visible* ebuilds) in developing arch",
282         "PDEPEND.badindev":"User-visible ebuilds with bad PDEPEND settings (matched against *visible* ebuilds) in developing arch",
283         "DEPEND.badmaskedindev":"Masked ebuilds with bad DEPEND settings (matched against *all* ebuilds) in developing arch",
284         "RDEPEND.badmaskedindev":"Masked ebuilds with RDEPEND settings (matched against *all* ebuilds) in developing arch",
285         "PDEPEND.badmaskedindev":"Masked ebuilds with PDEPEND settings (matched against *all* ebuilds) in developing arch",
286         "DEPEND.syntax":"Syntax error in DEPEND (usually an extra/missing space/parenthesis)",
287         "RDEPEND.syntax":"Syntax error in RDEPEND (usually an extra/missing space/parenthesis)",
288         "PDEPEND.syntax":"Syntax error in PDEPEND (usually an extra/missing space/parenthesis)",
289         "LICENSE.syntax":"Syntax error in LICENSE (usually an extra/missing space/parenthesis)",
290         "PROVIDE.syntax":"Syntax error in PROVIDE (usually an extra/missing space/parenthesis)",
291         "PROPERTIES.syntax":"Syntax error in PROPERTIES (usually an extra/missing space/parenthesis)",
292         "RESTRICT.syntax":"Syntax error in RESTRICT (usually an extra/missing space/parenthesis)",
293         "SRC_URI.syntax":"Syntax error in SRC_URI (usually an extra/missing space/parenthesis)",
294         "SRC_URI.mirror":"A uri listed in profiles/thirdpartymirrors is found in SRC_URI",
295         "ebuild.syntax":"Error generating cache entry for ebuild; typically caused by ebuild syntax error or digest verification failure",
296         "ebuild.output":"A simple sourcing of the ebuild produces output; this breaks ebuild policy.",
297         "ebuild.nesteddie":"Placing 'die' inside ( ) prints an error, but doesn't stop the ebuild.",
298         "variable.readonly":"Assigning a readonly variable",
299         "LIVEVCS.stable":"This ebuild is a live checkout from a VCS but has stable keywords.",
300         "IUSE.invalid":"This ebuild has a variable in IUSE that is not in the use.desc or use.local.desc file",
301         "IUSE.undefined":"This ebuild does not define IUSE (style guideline says to define IUSE even when empty)",
302         "LICENSE.invalid":"This ebuild is listing a license that doesnt exist in portages license/ dir.",
303         "KEYWORDS.invalid":"This ebuild contains KEYWORDS that are not listed in profiles/arch.list or for which no valid profile was found",
304         "RDEPEND.suspect":"RDEPEND contains a package that usually only belongs in DEPEND.",
305         "RESTRICT.invalid":"This ebuild contains invalid RESTRICT values.",
306         "digestentry.unused":"Some files listed in the Manifest aren't referenced in SRC_URI",
307         "ebuild.nostable":"There are no ebuilds that are marked as stable for your ARCH",
308         "ebuild.allmasked":"All ebuilds are masked for this package (Package level only)",
309         "ebuild.majorsyn":"This ebuild has a major syntax error that may cause the ebuild to fail partially or fully",
310         "ebuild.minorsyn":"This ebuild has a minor syntax error that contravenes gentoo coding style",
311         "ebuild.badheader":"This ebuild has a malformed header",
312         "metadata.missing":"Missing metadata.xml files",
313         "metadata.bad":"Bad metadata.xml files",
314         "virtual.versioned":"PROVIDE contains virtuals with versions",
315         "virtual.exists":"PROVIDE contains existing package names",
316         "virtual.unavailable":"PROVIDE contains a virtual which contains no profile default",
317         "usage.obsolete":"The ebuild makes use of an obsolete construct",
318         "upstream.workaround":"The ebuild works around an upstream bug, an upstream bug should be filed and tracked in bugs.gentoo.org"
319 }
320
321 qacats = qahelp.keys()
322 qacats.sort()
323
324 qawarnings = set((
325 "changelog.missing",
326 "changelog.notadded",
327 "digestentry.unused",
328 "ebuild.notadded",
329 "ebuild.nostable",
330 "ebuild.allmasked",
331 "ebuild.nesteddie",
332 "desktop.invalid",
333 "DEPEND.badmasked","RDEPEND.badmasked","PDEPEND.badmasked",
334 "DEPEND.badindev","RDEPEND.badindev","PDEPEND.badindev",
335 "DEPEND.badmaskedindev","RDEPEND.badmaskedindev","PDEPEND.badmaskedindev",
336 "DESCRIPTION.toolong",
337 "KEYWORDS.dropped",
338 "KEYWORDS.stupid",
339 "KEYWORDS.missing",
340 "IUSE.undefined",
341 "RDEPEND.suspect",
342 "RESTRICT.invalid",
343 "SRC_URI.mirror",
344 "ebuild.minorsyn",
345 "ebuild.badheader",
346 "ebuild.patches",
347 "file.size",
348 "inherit.autotools",
349 "java.eclassesnotused",
350 "virtual.versioned",
351 "virtual.exists",
352 "virtual.unavailable",
353 "usage.obsolete",
354 "upstream.workaround",
355 "LIVEVCS.stable"
356 ))
357
358 missingvars=["KEYWORDS","LICENSE","DESCRIPTION","HOMEPAGE","SLOT"]
359 allvars = set(x for x in portage.auxdbkeys if not x.startswith("UNUSED_"))
360 allvars.discard("CDEPEND")
361 allvars.update(Package.metadata_keys)
362 allvars = sorted(allvars)
363 commitmessage=None
364 for x in missingvars:
365         x += ".missing"
366         if x not in qacats:
367                 logging.warn('* missingvars values need to be added to qahelp ("%s")' % x)
368                 qacats.append(x)
369                 qawarnings.add(x)
370
371 valid_restrict = frozenset(["binchecks", "bindist",
372         "fetch", "installsources", "mirror",
373         "primaryuri", "strip", "test", "userpriv"])
374
375 live_eclasses = frozenset([
376         "bzr",
377         "cvs",
378         "darcs",
379         "git",
380         "mercurial",
381         "subversion"
382 ])
383
384 suspect_rdepend = frozenset([
385         "app-arch/cabextract",
386         "app-arch/rpm2targz",
387         "app-doc/doxygen",
388         "dev-lang/nasm",
389         "dev-lang/swig",
390         "dev-lang/yasm",
391         "dev-perl/extutils-pkgconfig",
392         "dev-python/setuptools",
393         "dev-util/byacc",
394         "dev-util/cmake",
395         "dev-util/ftjam",
396         "dev-util/gtk-doc",
397         "dev-util/gtk-doc-am",
398         "dev-util/intltool",
399         "dev-util/jam",
400         "dev-util/pkgconfig",
401         "dev-util/scons",
402         "dev-util/unifdef",
403         "dev-util/yacc",
404         "media-gfx/ebdftopcf",
405         "sys-apps/help2man",
406         "sys-devel/autoconf",
407         "sys-devel/automake",
408         "sys-devel/bin86",
409         "sys-devel/bison",
410         "sys-devel/dev86",
411         "sys-devel/flex",
412         "sys-devel/libtool",
413         "sys-devel/m4",
414         "sys-devel/pmake",
415         "x11-misc/bdftopcf",
416         "x11-misc/imake",
417 ])
418
419 # file.executable
420 no_exec = frozenset(["Manifest","ChangeLog","metadata.xml"])
421
422 options, arguments = ParseArgs(sys.argv, qahelp)
423
424 # Set this to False when an extraordinary issue (generally
425 # something other than a QA issue) makes it impossible to
426 # commit (like if Manifest generation fails).
427 can_force = True
428
429 portdir, portdir_overlay, mydir = utilities.FindPortdir(repoman_settings)
430 if portdir is None:
431         sys.exit(1)
432
433 vcs = None
434 if os.path.isdir("CVS"):
435         vcs = "cvs"
436 if os.path.isdir(".svn"):
437         vcs = "svn"
438 elif os.path.isdir(os.path.join(portdir_overlay, ".git")):
439         vcs = "git"
440
441 vcs_local_opts = repoman_settings.get("REPOMAN_VCS_LOCAL_OPTS", "").split()
442 vcs_global_opts = repoman_settings.get("REPOMAN_VCS_GLOBAL_OPTS")
443 if vcs_global_opts is None:
444         if vcs != "git":
445                 vcs_global_opts = "-q"
446         else:
447                 vcs_global_opts = ""
448 vcs_global_opts = vcs_global_opts.split()
449
450 if vcs == "cvs" and \
451         "commit" == options.mode and \
452         "RMD160" not in portage.checksum.hashorigin_map:
453         from portage.util import grablines
454         repo_lines = grablines("./CVS/Repository")
455         if repo_lines and \
456                 "gentoo-x86" == repo_lines[0].strip().split(os.path.sep)[0]:
457                 msg = "Please install " \
458                 "pycrypto or enable python's ssl USE flag in order " \
459                 "to enable RMD160 hash support. See bug #198398 for " \
460                 "more information."
461                 prefix = bad(" * ")
462                 from textwrap import wrap
463                 for line in wrap(msg, 70):
464                         print prefix + line
465                 sys.exit(1)
466         del repo_lines
467
468 if options.mode == 'commit' and not options.pretend and not vcs:
469         logging.info("Not in a version controlled repository; enabling pretend mode.")
470         options.pretend = True
471
472 os.environ["PORTDIR"] = portdir
473 if portdir_overlay != portdir:
474         os.environ["PORTDIR_OVERLAY"] = portdir_overlay
475 else:
476         os.environ["PORTDIR_OVERLAY"] = ""
477
478 logging.info('Setting paths:')
479 logging.info('PORTDIR = "' + os.environ['PORTDIR'] + '"')
480 logging.info('PORTDIR_OVERLAY = "' + os.environ['PORTDIR_OVERLAY']+'"')
481
482 # Now that PORTDIR_OVERLAY is properly overridden, create the portdb.
483 repoman_settings = portage.config(local_config=False,
484         config_incrementals=portage.const.INCREMENTALS)
485 trees = portage.create_trees()
486 trees["/"]["porttree"].settings = repoman_settings
487 portdb = trees["/"]["porttree"].dbapi
488 portdb.mysettings = repoman_settings
489 setconfig = load_default_config(repoman_settings, trees["/"])
490 root_config = RootConfig(repoman_settings, trees["/"], setconfig)
491 # We really only need to cache the metadata that's necessary for visibility
492 # filtering. Anything else can be discarded to reduce memory consumption.
493 portdb._aux_cache_keys.clear()
494 portdb._aux_cache_keys.update(["EAPI", "KEYWORDS", "SLOT"])
495 # dep_zapdeps looks at the vardbapi, but it shouldn't for repoman.
496 del trees["/"]["vartree"]
497
498 myreporoot = os.path.basename(portdir_overlay)
499 myreporoot += mydir[len(portdir_overlay):]
500
501 reposplit = myreporoot.split(os.path.sep)
502 repolevel = len(reposplit)
503
504 # check if it's in $PORTDIR/$CATEGORY/$PN , otherwise bail if commiting.
505 # Reason for this is if they're trying to commit in just $FILESDIR/*, the Manifest needs updating.
506 # this check ensures that repoman knows where it is, and the manifest recommit is at least possible.
507 if options.mode == 'commit' and repolevel not in [1,2,3]:
508         print red("***")+" Commit attempts *must* be from within a vcs co, category, or package directory."
509         print red("***")+" Attempting to commit from a packages files directory will be blocked for instance."
510         print red("***")+" This is intended behaviour, to ensure the manifest is recommited for a package."
511         print red("***")
512         err("Unable to identify level we're commiting from for %s" % '/'.join(reposplit))
513
514 startdir = normalize_path(mydir)
515 repodir = startdir
516 for x in range(0, repolevel - 1):
517         repodir = os.path.dirname(repodir)
518
519 def caterror(mycat):
520         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.")
521
522 # setup a uselist from portage
523 uselist=[]
524 try:
525         uselist=portage.grabfile(portdir+"/profiles/use.desc")
526         for l in range(0,len(uselist)):
527                 uselist[l]=uselist[l].split()[0]
528         for var in repoman_settings["USE_EXPAND"].split():
529                 vardescs = portage.grabfile(portdir+"/profiles/desc/"+var.lower()+".desc")
530                 for l in range(0, len(vardescs)):
531                         uselist.append(var.lower() + "_" + vardescs[l].split()[0])
532 except (IOError, OSError, ParseError), e:
533         logging.exception("Couldn't read USE flags from use.desc")
534         sys.exit(1)
535
536 # retrieve a list of current licenses in portage
537 liclist = set(portage.listdir(os.path.join(portdir, "licenses")))
538 if not liclist:
539         logging.fatal("Couldn't find licenses?")
540         sys.exit(1)
541 if portdir_overlay != portdir:
542         liclist.update(portage.listdir(os.path.join(portdir_overlay, "licenses")))
543
544 # retrieve list of offical keywords
545 kwlist = set(portage.grabfile(os.path.join(portdir, "profiles", "arch.list")))
546 if not kwlist:
547         logging.fatal("Couldn't read KEYWORDS from arch.list")
548         sys.exit(1)
549
550 if portdir_overlay != portdir:
551         kwlist.update(portage.grabfile(
552                 os.path.join(portdir_overlay, "profiles", "arch.list")))
553
554 scanlist=[]
555 if repolevel==2:
556         #we are inside a category directory
557         catdir=reposplit[-1]
558         if catdir not in repoman_settings.categories:
559                 caterror(catdir)
560         mydirlist=os.listdir(startdir)
561         for x in mydirlist:
562                 if x == "CVS" or x.startswith("."):
563                         continue
564                 if os.path.isdir(startdir+"/"+x):
565                         scanlist.append(catdir+"/"+x)
566         repo_subdir = catdir + os.sep
567 elif repolevel==1:
568         for x in repoman_settings.categories:
569                 if not os.path.isdir(startdir+"/"+x):
570                         continue
571                 for y in os.listdir(startdir+"/"+x):
572                         if y == "CVS" or y.startswith("."):
573                                 continue
574                         if os.path.isdir(startdir+"/"+x+"/"+y):
575                                 scanlist.append(x+"/"+y)
576         repo_subdir = ""
577 elif repolevel==3:
578         catdir = reposplit[-2]
579         if catdir not in repoman_settings.categories:
580                 caterror(catdir)
581         scanlist.append(catdir+"/"+reposplit[-1])
582         repo_subdir = scanlist[-1] + os.sep
583 repo_subdir_len = len(repo_subdir)
584 scanlist.sort()
585
586 logging.debug("Found the following packages to scan:\n%s" % '\n'.join(scanlist))
587
588 profiles={}
589 valid_profile_types = frozenset(["dev", "exp", "stable"])
590 descfile=portdir+"/profiles/profiles.desc"
591 if os.path.exists(descfile):
592         for i, x in enumerate(open(descfile, 'rb')):
593                 if x[0]=="#":
594                         continue
595                 arch=x.split()
596                 if len(arch) == 0:
597                         continue
598                 if len(arch)!=3:
599                         err("wrong format: \"" + bad(x.strip()) + "\" in " + \
600                                 descfile + " line %d" % (i+1, ))
601                 elif arch[0] not in kwlist:
602                         err("invalid arch: \"" + bad(arch[0]) + "\" in " + \
603                                 descfile + " line %d" % (i+1, ))
604                 elif arch[2] not in valid_profile_types:
605                         err("invalid profile type: \"" + bad(arch[2]) + "\" in " + \
606                                 descfile + " line %d" % (i+1, ))
607                 if not os.path.isdir(portdir+"/profiles/"+arch[1]):
608                         print "Invalid "+arch[2]+" profile ("+arch[1]+") for arch "+arch[0]
609                         continue
610                 if arch[0] in profiles:
611                         profiles[arch[0]]+= [[arch[1], arch[2]]]
612                 else:
613                         profiles[arch[0]] = [[arch[1], arch[2]]]
614
615         for x in repoman_settings.archlist():
616                 if x[0] == "~":
617                         continue
618                 if x not in profiles:
619                         print red("\""+x+"\" doesn't have a valid profile listed in profiles.desc.")
620                         print red("You need to either \"cvs update\" your profiles dir or follow this")
621                         print red("up with the "+x+" team.")
622                         print
623 else:
624         print red("profiles.desc does not exist: "+descfile)
625         print red("You need to do \"cvs update\" in profiles dir.")
626         print
627         sys.exit(1)
628
629 def dev_keywords(profiles):
630         """
631         Create a set of KEYWORDS values that exist in 'dev'
632         profiles. These are used
633         to trigger a message notifying the user when they might
634         want to add the --include-dev option.
635         """
636         type_arch_map = {}
637         for arch, arch_profiles in profiles.iteritems():
638                 for profile_path, profile_type in arch_profiles:
639                         arch_set = type_arch_map.get(profile_type)
640                         if arch_set is None:
641                                 arch_set = set()
642                                 type_arch_map[profile_type] = arch_set
643                         arch_set.add(arch)
644
645         dev_keywords = type_arch_map.get('dev', set())
646         dev_keywords.update(['~' + arch for arch in dev_keywords])
647         return frozenset(dev_keywords)
648
649 dev_keywords = dev_keywords(profiles)
650
651 stats={}
652 fails={}
653
654 # provided by the desktop-file-utils package
655 desktop_file_validate = find_binary("desktop-file-validate")
656 desktop_pattern = re.compile(r'.*\.desktop$')
657
658 for x in qacats:
659         stats[x]=0
660         fails[x]=[]
661 xmllint_capable = False
662 metadata_dtd = os.path.join(repoman_settings["DISTDIR"], 'metadata.dtd')
663 if options.mode == "manifest":
664         pass
665 elif not find_binary('xmllint'):
666         print red("!!! xmllint not found. Can't check metadata.xml.\n")
667         if options.xml_parse or repolevel==3:
668                 print red("!!!")+" sorry, xmllint is needed.  failing\n"
669                 sys.exit(1)
670 else:
671         #hardcoded paths/urls suck. :-/
672         must_fetch=1
673         backup_exists=0
674         try:
675                 # if it's been over a week since fetching (or the system clock is fscked), grab an updated copy of metadata.dtd 
676                 # clock is fscked or it's been a week. time to grab a new one.
677                 ct = os.stat(metadata_dtd)[ST_CTIME]
678                 if abs(time.time() - ct) > (60*60*24*7):
679                         # don't trap the exception, we're watching for errno 2 (file not found), anything else is a bug.
680                         backup_exists=1
681                 else:
682                         must_fetch=0
683
684         except (OSError,IOError), e:
685                 if e.errno != 2:
686                         print red("!!!")+" caught exception '%s' for %s/metadata.dtd, bailing" % (str(e), portage.CACHE_PATH)
687                         sys.exit(1)
688
689         if must_fetch:
690                 print 
691                 print green("***")+" the local copy of metadata.dtd needs to be refetched, doing that now"
692                 print
693                 val = 0
694                 try:
695                         try:
696                                 os.unlink(metadata_dtd)
697                         except OSError, e:
698                                 if e.errno != errno.ENOENT:
699                                         raise
700                                 del e
701                         val=portage.fetch(['http://www.gentoo.org/dtd/metadata.dtd'],repoman_settings,fetchonly=0, \
702                                 try_mirrors=0)
703
704                 except SystemExit, e:
705                         raise  # Need to propogate this
706                 except Exception,e:
707                         print
708                         print red("!!!")+" attempting to fetch 'http://www.gentoo.org/dtd/metadata.dtd', caught"
709                         print red("!!!")+" exception '%s' though." % str(e)
710                         val=0
711                 if not val:
712                         print red("!!!")+" fetching new metadata.dtd failed, aborting"
713                         sys.exit(1)
714         #this can be problematic if xmllint changes their output
715         xmllint_capable=True
716
717 if options.mode == 'commit' and vcs:
718         utilities.detect_vcs_conflicts(options, vcs)
719
720 if options.mode == "manifest":
721         pass
722 elif options.pretend:
723         print green("\nRepoMan does a once-over of the neighborhood...")
724 else:
725         print green("\nRepoMan scours the neighborhood...")
726
727 new_ebuilds = set()
728 modified_changelogs = set()
729 mychanged = []
730 mynew = []
731 myremoved = []
732
733 if vcs == "cvs":
734         mycvstree = cvstree.getentries("./", recursive=1)
735         mychanged = cvstree.findchanged(mycvstree, recursive=1, basedir="./")
736         mynew = cvstree.findnew(mycvstree, recursive=1, basedir="./")
737 if vcs == "svn":
738         svnstatus = os.popen("svn status").readlines()
739         mychanged = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem and elem[:1] in "MR" ]
740         mynew     = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem.startswith("A") ]
741 elif vcs == "git":
742         mychanged = os.popen("git ls-files -m --with-tree=HEAD").readlines()
743         mychanged = [ "./" +  elem[:-1] for elem in mychanged ]
744         mynew = os.popen("git diff --cached --name-only --diff-filter=A").readlines()
745         strip_levels = repolevel - 1
746         if strip_levels:
747                 mynew = [elem[repo_subdir_len:] for elem in mynew \
748                         if elem[:repo_subdir_len] == repo_subdir]
749         mynew = ["./" + elem[:-1] for elem in mynew]
750 if vcs:
751         new_ebuilds.update(x for x in mynew if x.endswith(".ebuild"))
752         modified_changelogs.update(x for x in chain(mychanged, mynew) \
753                 if os.path.basename(x) == "ChangeLog")
754
755 have_pmasked = False
756 have_dev_keywords = False
757 dofail = 0
758 arch_caches={}
759 arch_xmatch_caches = {}
760 shared_xmatch_caches = {"cp-list":{}}
761
762 # Disable the "ebuild.notadded" check when not in commit mode and
763 # running `svn status` in every package dir will be too expensive.
764
765 check_ebuild_notadded = not \
766         (vcs == "svn" and repolevel < 3 and options.mode != "commit")
767
768 # Build a regex from thirdpartymirrors for the SRC_URI.mirror check.
769 thirdpartymirrors = portage.flatten(repoman_settings.thirdpartymirrors().values())
770
771 for x in scanlist:
772         #ebuilds and digests added to cvs respectively.
773         logging.info("checking package %s" % x)
774         eadded=[]
775         catdir,pkgdir=x.split("/")
776         checkdir=repodir+"/"+x
777         checkdir_relative = ""
778         if repolevel < 3:
779                 checkdir_relative = os.path.join(pkgdir, checkdir_relative)
780         if repolevel < 2:
781                 checkdir_relative = os.path.join(catdir, checkdir_relative)
782         checkdir_relative = os.path.join(".", checkdir_relative)
783
784         if options.mode == "manifest" or \
785           options.mode in ('commit', 'fix') and not options.pretend:
786                 repoman_settings["O"] = checkdir
787                 if not portage.digestgen([], repoman_settings, myportdb=portdb):
788                         print "Unable to generate manifest."
789                         dofail = 1
790                 if options.mode == "manifest":
791                         continue
792                 elif dofail:
793                         sys.exit(1)
794
795         checkdirlist=os.listdir(checkdir)
796         ebuildlist=[]
797         pkgs = {}
798         for y in checkdirlist:
799                 if y in no_exec and \
800                         stat.S_IMODE(os.stat(os.path.join(checkdir, y)).st_mode) & 0111:
801                                 stats["file.executable"] += 1
802                                 fails["file.executable"].append(os.path.join(checkdir, y))
803                 if y.endswith(".ebuild"):
804                         pf = y[:-7]
805                         ebuildlist.append(pf)
806                         cpv = "%s/%s" % (catdir, pf)
807                         try:
808                                 myaux = dict(izip(allvars, portdb.aux_get(cpv, allvars)))
809                         except KeyError:
810                                 stats["ebuild.syntax"] += 1
811                                 fails["ebuild.syntax"].append(os.path.join(x, y))
812                                 continue
813                         except IOError:
814                                 stats["ebuild.output"] += 1
815                                 fails["ebuild.output"].append(os.path.join(x, y))
816                                 continue
817                         if not portage.eapi_is_supported(myaux["EAPI"]):
818                                 stats["EAPI.unsupported"] += 1
819                                 fails["EAPI.unsupported"].append(os.path.join(x, y))
820                                 continue
821                         pkgs[pf] = Package(cpv=cpv, metadata=myaux,
822                                 root_config=root_config)
823
824         # Sort ebuilds in ascending order for the KEYWORDS.dropped check.
825         pkgsplits = {}
826         for i in xrange(len(ebuildlist)):
827                 ebuild_split = portage.pkgsplit(ebuildlist[i])
828                 pkgsplits[ebuild_split] = ebuildlist[i]
829                 ebuildlist[i] = ebuild_split
830         ebuildlist.sort(portage.pkgcmp)
831         for i in xrange(len(ebuildlist)):
832                 ebuildlist[i] = pkgsplits[ebuildlist[i]]
833         del pkgsplits
834
835         slot_keywords = {}
836
837         if len(pkgs) != len(ebuildlist):
838                 # If we can't access all the metadata then it's totally unsafe to
839                 # commit since there's no way to generate a correct Manifest.
840                 # Do not try to do any more QA checks on this package since missing
841                 # metadata leads to false positives for several checks, and false
842                 # positives confuse users.
843                 can_force = False
844                 continue
845
846         for y in checkdirlist:
847                 for c in y.strip(os.path.sep):
848                         if c not in allowed_filename_chars_set:
849                                 stats["file.name"] += 1
850                                 fails["file.name"].append("%s/%s: char '%s'" % (checkdir, y, c))
851                                 break
852
853                 if not (y in ("ChangeLog", "metadata.xml") or y.endswith(".ebuild")):
854                         continue
855                 try:
856                         line = 1
857                         for l in codecs.open(checkdir+"/"+y, "r", "utf8"):
858                                 line +=1
859                 except UnicodeDecodeError, ue:
860                         stats["file.UTF8"] += 1
861                         s = ue.object[:ue.start]
862                         l2 = s.count("\n")
863                         line += l2
864                         if l2 != 0:
865                                 s = s[s.rfind("\n") + 1:]
866                         fails["file.UTF8"].append("%s/%s: line %i, just after: '%s'" % (checkdir, y, line, s))
867
868         if vcs == "git" and check_ebuild_notadded:
869                 myf = os.popen("git ls-files --others %s" % \
870                         (portage._shell_quote(checkdir_relative),))
871                 for l in myf:
872                         if l[:-1][-7:] == ".ebuild":
873                                 stats["ebuild.notadded"] += 1
874                                 fails["ebuild.notadded"].append(
875                                         os.path.join(x, os.path.basename(l[:-1])))
876                 myf.close()
877
878         if vcs in ("cvs", "svn") and check_ebuild_notadded:
879                 try:
880                         if vcs == "cvs":
881                                 myf=open(checkdir+"/CVS/Entries","r")
882                         if vcs == "svn":
883                                 myf = os.popen("svn status --depth=files --verbose " + checkdir)
884                         myl = myf.readlines()
885                         myf.close()
886                         for l in myl:
887                                 if vcs == "cvs":
888                                         if l[0]!="/":
889                                                 continue
890                                         splitl=l[1:].split("/")
891                                         if not len(splitl):
892                                                 continue
893                                         if splitl[0][-7:]==".ebuild":
894                                                 eadded.append(splitl[0][:-7])
895                                 if vcs == "svn":
896                                         if l[:1] == "?":
897                                                 continue
898                                         l = l.split()[-1]
899                                         if l[-7:] == ".ebuild":
900                                                 eadded.append(os.path.basename(l[:-7]))
901                         if vcs == "svn":
902                                 myf = os.popen("svn status " + checkdir)
903                                 myl=myf.readlines()
904                                 myf.close()
905                                 for l in myl:
906                                         if l[0] == "A":
907                                                 l = l.rstrip().split(' ')[-1]
908                                                 if l[-7:] == ".ebuild":
909                                                         eadded.append(os.path.basename(l[:-7]))
910                 except IOError:
911                         if options.mode == 'commit' and vcs == "cvs":
912                                 stats["CVS/Entries.IO_error"] += 1
913                                 fails["CVS/Entries.IO_error"].append(checkdir+"/CVS/Entries")
914                         if options.mode == 'commit' and vcs == "svn":
915                                 stats["svn.IO_error"] += 1
916                                 fails["svn.IO_error"].append(checkdir+"svn info")
917                         continue
918
919         mf = Manifest(checkdir, repoman_settings["DISTDIR"])
920         mydigests=mf.getTypeDigests("DIST")
921
922         fetchlist_dict = portage.FetchlistDict(checkdir, repoman_settings, portdb)
923         myfiles_all = []
924         src_uri_error = False
925         for mykey in fetchlist_dict:
926                 try:
927                         myfiles_all.extend(fetchlist_dict[mykey])
928                 except portage.exception.InvalidDependString, e:
929                         src_uri_error = True
930                         try:
931                                 portdb.aux_get(mykey, ["SRC_URI"])
932                         except KeyError:
933                                 # This will be reported as an "ebuild.syntax" error.
934                                 pass
935                         else:
936                                 stats["SRC_URI.syntax"] = stats["SRC_URI.syntax"] + 1
937                                 fails["SRC_URI.syntax"].append(
938                                         "%s.ebuild SRC_URI: %s" % (mykey, e))
939         del fetchlist_dict
940         if not src_uri_error:
941                 # This test can produce false positives if SRC_URI could not
942                 # be parsed for one or more ebuilds. There's no point in
943                 # producing a false error here since the root cause will
944                 # produce a valid error elsewhere, such as "SRC_URI.syntax"
945                 # or "ebuild.sytax".
946                 myfiles_all = set(myfiles_all)
947                 for entry in mydigests:
948                         if entry not in myfiles_all:
949                                 stats["digestentry.unused"] += 1
950                                 fails["digestentry.unused"].append(checkdir+"::"+entry)
951         del myfiles_all
952
953         if os.path.exists(checkdir+"/files"):
954                 filesdirlist=os.listdir(checkdir+"/files")
955
956                 # recurse through files directory
957                 # use filesdirlist as a stack, appending directories as needed so people can't hide > 20k files in a subdirectory.
958                 while filesdirlist:
959                         y = filesdirlist.pop(0)
960                         relative_path = os.path.join(x, "files", y)
961                         full_path = os.path.join(repodir, relative_path)
962                         try:
963                                 mystat = os.stat(full_path)
964                         except OSError, oe:
965                                 if oe.errno == 2:
966                                         # don't worry about it.  it likely was removed via fix above.
967                                         continue
968                                 else:
969                                         raise oe
970                         if S_ISDIR(mystat.st_mode):
971                                 # !!! VCS "portability" alert!  Need some function isVcsDir() or alike !!!
972                                 if y == "CVS" or y == ".svn":
973                                         continue
974                                 for z in os.listdir(checkdir+"/files/"+y):
975                                         if z == "CVS" or z == ".svn":
976                                                 continue
977                                         filesdirlist.append(y+"/"+z)
978                         # current policy is no files over 20k, this is the check.
979                         elif mystat.st_size > 20480:
980                                 stats["file.size"] += 1
981                                 fails["file.size"].append("("+ str(mystat.st_size/1024) + "K) "+x+"/files/"+y)
982
983                         for c in os.path.basename(y.rstrip(os.path.sep)):
984                                 if c not in allowed_filename_chars_set:
985                                         stats["file.name"] += 1
986                                         fails["file.name"].append("%s/files/%s: char '%s'" % (checkdir, y, c))
987                                         break
988
989                         if desktop_file_validate and desktop_pattern.match(y):
990                                 status, cmd_output = commands.getstatusoutput(
991                                         "'%s' '%s'" % (desktop_file_validate, full_path))
992                                 if os.WIFEXITED(status) and os.WEXITSTATUS(status) != os.EX_OK:
993                                         # Note: in the future we may want to grab the
994                                         # warnings in addition to the errors. We're
995                                         # just doing errors now since we don't want
996                                         # to generate too much noise at first.
997                                         error_re = re.compile(r'.*\s*error:\s*(.*)')
998                                         for line in cmd_output.splitlines():
999                                                 error_match = error_re.match(line)
1000                                                 if error_match is None:
1001                                                         continue
1002                                                 stats["desktop.invalid"] += 1
1003                                                 fails["desktop.invalid"].append(
1004                                                         relative_path + ': %s' % error_match.group(1))
1005
1006         del mydigests
1007
1008         if "ChangeLog" not in checkdirlist:
1009                 stats["changelog.missing"]+=1
1010                 fails["changelog.missing"].append(x+"/ChangeLog")
1011         
1012         #metadata.xml file check
1013         muselist = []
1014
1015         if "metadata.xml" not in checkdirlist:
1016                 stats["metadata.missing"]+=1
1017                 fails["metadata.missing"].append(x+"/metadata.xml")
1018         #metadata.xml parse check
1019         else:
1020                 metadata_bad = False
1021
1022                 # load USE flags from metadata.xml
1023                 try:
1024                         f = open(os.path.join(checkdir, "metadata.xml"))
1025                         utilities.parse_metadata_use(f, muselist)
1026                         f.close()
1027                 except (EnvironmentError, ParseError), e:
1028                         metadata_bad = True
1029                         stats["metadata.bad"] += 1
1030                         fails["metadata.bad"].append("%s/metadata.xml: %s" % (x, e))
1031                         del e
1032
1033                 #Only carry out if in package directory or check forced
1034                 if xmllint_capable and not metadata_bad:
1035                         # xmlint can produce garbage output even on success, so only dump
1036                         # the ouput when it fails.
1037                         st, out = commands.getstatusoutput(
1038                                 "xmllint --nonet --noout --dtdvalid '%s' '%s'" % \
1039                                  (metadata_dtd, os.path.join(checkdir, "metadata.xml")))
1040                         if st != os.EX_OK:
1041                                 print red("!!!") + " metadata.xml is invalid:"
1042                                 for z in out.splitlines():
1043                                         print red("!!! ")+z
1044                                 stats["metadata.bad"]+=1
1045                                 fails["metadata.bad"].append(x+"/metadata.xml")
1046
1047                 del metadata_bad
1048
1049         changelog_path = os.path.join(checkdir_relative, "ChangeLog")
1050         changelog_modified = changelog_path in modified_changelogs
1051
1052         allmasked = True
1053
1054         for y in ebuildlist:
1055                 relative_path = os.path.join(x, y + ".ebuild")
1056                 full_path = os.path.join(repodir, relative_path)
1057                 ebuild_path = y + ".ebuild"
1058                 if repolevel < 3:
1059                         ebuild_path = os.path.join(pkgdir, ebuild_path)
1060                 if repolevel < 2:
1061                         ebuild_path = os.path.join(catdir, ebuild_path)
1062                 ebuild_path = os.path.join(".", ebuild_path)
1063                 if not changelog_modified and ebuild_path in new_ebuilds:
1064                         stats['changelog.ebuildadded'] += 1
1065                         fails['changelog.ebuildadded'].append(relative_path)
1066
1067                 if stat.S_IMODE(os.stat(full_path).st_mode) & 0111:
1068                         stats["file.executable"] += 1
1069                         fails["file.executable"].append(x+"/"+y+".ebuild")
1070                 if vcs in ("cvs", "svn") and check_ebuild_notadded and y not in eadded:
1071                         #ebuild not added to vcs
1072                         stats["ebuild.notadded"]=stats["ebuild.notadded"]+1
1073                         fails["ebuild.notadded"].append(x+"/"+y+".ebuild")
1074                 myesplit=portage.pkgsplit(y)
1075                 if myesplit is None or myesplit[0] != x.split("/")[-1]:
1076                         stats["ebuild.invalidname"]=stats["ebuild.invalidname"]+1
1077                         fails["ebuild.invalidname"].append(x+"/"+y+".ebuild")
1078                         continue
1079                 elif myesplit[0]!=pkgdir:
1080                         print pkgdir,myesplit[0]
1081                         stats["ebuild.namenomatch"]=stats["ebuild.namenomatch"]+1
1082                         fails["ebuild.namenomatch"].append(x+"/"+y+".ebuild")
1083                         continue
1084
1085                 pkg = pkgs[y]
1086                 myaux = pkg.metadata
1087                 eapi = myaux["EAPI"]
1088                 inherited = pkg.inherited
1089                 live_ebuild = live_eclasses.intersection(inherited)
1090
1091                 if not src_uri_error:
1092                         # Check that URIs don't reference a server from thirdpartymirrors.
1093                         for uri in portage.flatten(portage.dep.use_reduce(
1094                                 portage.dep.paren_reduce(myaux["SRC_URI"]), matchall=True)):
1095                                 contains_mirror = False
1096                                 for mirror in thirdpartymirrors:
1097                                         if uri.startswith(mirror):
1098                                                 contains_mirror = True
1099                                                 break
1100                                 if not contains_mirror:
1101                                         continue
1102
1103                                 stats["SRC_URI.mirror"] += 1
1104                                 fails["SRC_URI.mirror"].append(
1105                                         "%s: '%s' found in thirdpartymirrors" % \
1106                                         (relative_path, mirror))
1107
1108                 # Test for negative logic and bad words in the RESTRICT var.
1109                 #for x in myaux[allvars.index("RESTRICT")].split():
1110                 #       if x.startswith("no"):
1111                 #               print "Bad RESTRICT value: %s" % x
1112                 try:
1113                         myaux["PROVIDE"] = portage.dep.use_reduce(
1114                                 portage.dep.paren_reduce(myaux["PROVIDE"]), matchall=1)
1115                 except portage.exception.InvalidDependString, e:
1116                         stats["PROVIDE.syntax"] = stats["PROVIDE.syntax"] + 1
1117                         fails["PROVIDE.syntax"].append(mykey+".ebuild PROVIDE: "+str(e))
1118                         del e
1119                         continue
1120                 myaux["PROVIDE"] = " ".join(portage.flatten(myaux["PROVIDE"]))
1121                 for myprovide in myaux["PROVIDE"].split():
1122                         prov_cp = portage.dep_getkey(myprovide)
1123                         if prov_cp != myprovide:
1124                                 stats["virtual.versioned"]+=1
1125                                 fails["virtual.versioned"].append(x+"/"+y+".ebuild: "+myprovide)
1126                         prov_pkg = portage.dep_getkey(
1127                                 portage.best(portdb.xmatch("match-all", prov_cp)))
1128                         if prov_cp == prov_pkg:
1129                                 stats["virtual.exists"]+=1
1130                                 fails["virtual.exists"].append(x+"/"+y+".ebuild: "+prov_cp)
1131
1132                 for pos, missing_var in enumerate(missingvars):
1133                         if not myaux.get(missing_var):
1134                                 if catdir == "virtual" and \
1135                                         missing_var in ("HOMEPAGE", "LICENSE"):
1136                                         continue
1137                                 if live_ebuild and missing_var == "KEYWORDS":
1138                                         continue
1139                                 myqakey=missingvars[pos]+".missing"
1140                                 stats[myqakey]=stats[myqakey]+1
1141                                 fails[myqakey].append(x+"/"+y+".ebuild")
1142
1143                 # 14 is the length of DESCRIPTION=""
1144                 if len(myaux['DESCRIPTION']) > max_desc_len:
1145                         stats['DESCRIPTION.toolong'] += 1
1146                         fails['DESCRIPTION.toolong'].append(
1147                                 "%s: DESCRIPTION is %d characters (max %d)" % \
1148                                 (relative_path, len(myaux['DESCRIPTION']), max_desc_len))
1149
1150                 keywords = myaux["KEYWORDS"].split()
1151                 stable_keywords = []
1152                 for keyword in keywords:
1153                         if not keyword.startswith("~") and \
1154                                 not keyword.startswith("-"):
1155                                 stable_keywords.append(keyword)
1156                 if stable_keywords:
1157                         if ebuild_path in new_ebuilds:
1158                                 stable_keywords.sort()
1159                                 stats["KEYWORDS.stable"] += 1
1160                                 fails["KEYWORDS.stable"].append(
1161                                         x + "/" + y + ".ebuild added with stable keywords: %s" % \
1162                                                 " ".join(stable_keywords))
1163
1164                 ebuild_archs = set(kw.lstrip("~") for kw in keywords \
1165                         if not kw.startswith("-"))
1166
1167                 previous_keywords = slot_keywords.get(myaux["SLOT"])
1168                 if previous_keywords is None:
1169                         slot_keywords[myaux["SLOT"]] = set()
1170                 elif not live_ebuild:
1171                         dropped_keywords = previous_keywords.difference(ebuild_archs)
1172                         if dropped_keywords:
1173                                 stats["KEYWORDS.dropped"] += 1
1174                                 fails["KEYWORDS.dropped"].append(
1175                                         relative_path + ": %s" % \
1176                                         " ".join(sorted(dropped_keywords)))
1177
1178                 slot_keywords[myaux["SLOT"]].update(ebuild_archs)
1179
1180                 # KEYWORDS="-*" is a stupid replacement for package.mask and screws general KEYWORDS semantics
1181                 if "-*" in keywords:
1182                         haskeyword = False
1183                         for kw in keywords:
1184                                 if kw[0] == "~":
1185                                         kw = kw[1:]
1186                                 if kw in kwlist:
1187                                         haskeyword = True
1188                         if not haskeyword:
1189                                 stats["KEYWORDS.stupid"] += 1
1190                                 fails["KEYWORDS.stupid"].append(x+"/"+y+".ebuild")
1191
1192                 """
1193                 Ebuilds that inherit a "Live" eclass (darcs,subversion,git,cvs,etc..) should
1194                 not be allowed to be marked stable
1195                 """
1196                 if live_ebuild:
1197                         bad_stable_keywords = []
1198                         for keyword in keywords:
1199                                 if not keyword.startswith("~") and \
1200                                         not keyword.startswith("-"):
1201                                         bad_stable_keywords.append(keyword)
1202                                 del keyword
1203                         if bad_stable_keywords:
1204                                 stats["LIVEVCS.stable"] += 1
1205                                 fails["LIVEVCS.stable"].append(
1206                                         x + "/" + y + ".ebuild with stable keywords:%s " % \
1207                                                 bad_stable_keywords)
1208                         del bad_stable_keywords
1209
1210                 if options.ignore_arches:
1211                         arches = [[repoman_settings["ARCH"], repoman_settings["ARCH"],
1212                                 repoman_settings["ACCEPT_KEYWORDS"].split()]]
1213                 else:
1214                         arches=[]
1215                         for keyword in myaux["KEYWORDS"].split():
1216                                 if (keyword[0]=="-"):
1217                                         continue
1218                                 elif (keyword[0]=="~"):
1219                                         arches.append([keyword, keyword[1:], [keyword[1:], keyword]])
1220                                 else:
1221                                         arches.append([keyword, keyword, [keyword]])
1222                                         allmasked = False
1223
1224                 baddepsyntax = False
1225                 badlicsyntax = False
1226                 badprovsyntax = False
1227                 catpkg = catdir+"/"+y
1228                 myiuse = set(repoman_settings.archlist())
1229                 for myflag in myaux["IUSE"].split():
1230                         if myflag.startswith("+"):
1231                                 myflag = myflag[1:]
1232                         myiuse.add(myflag)
1233
1234                 inherited_java_eclass = "java-pkg-2" in inherited or \
1235                         "java-pkg-opt-2" in inherited
1236                 operator_tokens = set(["||", "(", ")"])
1237                 type_list, badsyntax = [], []
1238                 for mytype in ("DEPEND", "RDEPEND", "PDEPEND",
1239                         "LICENSE", "PROPERTIES", "PROVIDE"):
1240                         mydepstr = myaux[mytype]
1241                         
1242                         if mydepstr.find(" ?") != -1:
1243                                 badsyntax.append("'?' preceded by space")
1244
1245                         try:
1246                                 # Missing closing parenthesis will result in a ValueError
1247                                 mydeplist = portage.dep.paren_reduce(mydepstr)
1248                                 # Missing opening parenthesis will result in a final "" element
1249                                 if "" in mydeplist or "(" in mydeplist:
1250                                         raise ValueError
1251                         except ValueError:
1252                                 badsyntax.append("parenthesis mismatch")
1253                                 mydeplist = []
1254                         except portage.exception.InvalidDependString, e:
1255                                 badsyntax.append(str(e))
1256                                 del e
1257                                 mydeplist = []
1258
1259                         try:
1260                                 portage.dep.use_reduce(mydeplist, matchall=1)
1261                         except portage.exception.InvalidDependString, e:
1262                                 badsyntax.append(str(e))
1263
1264                         for token in operator_tokens:
1265                                 if mydepstr.startswith(token+" "):
1266                                         myteststr = mydepstr[len(token):]
1267                                 else:
1268                                         myteststr = mydepstr
1269                                 if myteststr.endswith(" "+token):
1270                                         myteststr = myteststr[:-len(token)]
1271                                 while myteststr.find(" "+token+" ") != -1:
1272                                         myteststr = " ".join(myteststr.split(" "+token+" ", 1))
1273                                 if myteststr.find(token) != -1:
1274                                         badsyntax.append("'%s' not separated by space" % (token))
1275
1276                         if mytype in ("DEPEND", "RDEPEND", "PDEPEND"):
1277                                 for token in mydepstr.split():
1278                                         if token in operator_tokens or \
1279                                                 token.endswith("?"):
1280                                                 continue
1281                                         try:
1282                                                 atom = portage.dep.Atom(token)
1283                                         except portage.exception.InvalidAtom:
1284                                                 badsyntax.append("'%s' not a valid atom" % token)
1285                                         else:
1286                                                 is_blocker = atom.blocker
1287
1288                                                 if mytype == "DEPEND" and \
1289                                                         not is_blocker and \
1290                                                         not inherited_java_eclass and \
1291                                                         portage.dep_getkey(atom) == "virtual/jdk":
1292                                                         stats['java.eclassesnotused'] += 1
1293                                                         fails['java.eclassesnotused'].append(relative_path)
1294                                                 elif mytype == "RDEPEND":
1295                                                         if not is_blocker and \
1296                                                                 portage.dep_getkey(atom) in suspect_rdepend:
1297                                                                 stats['RDEPEND.suspect'] += 1
1298                                                                 fails['RDEPEND.suspect'].append(
1299                                                                         relative_path + ": '%s'" % atom)
1300                                                 if eapi == "0":
1301                                                         if portage.dep.dep_getslot(atom):
1302                                                                 stats['EAPI.incompatible'] += 1
1303                                                                 fails['EAPI.incompatible'].append(
1304                                                                         (relative_path + ": %s slot dependency" + \
1305                                                                         " not supported with EAPI='%s':" + \
1306                                                                         " '%s'") % (mytype, eapi, atom))
1307                                                 if atom.use and eapi in ("0", "1"):
1308                                                         stats['EAPI.incompatible'] += 1
1309                                                         fails['EAPI.incompatible'].append(
1310                                                                 (relative_path + ": %s use dependency" + \
1311                                                                 " not supported with EAPI='%s':" + \
1312                                                                 " '%s'") % (mytype, eapi, atom))
1313                                                 if atom.blocker and atom.blocker.overlap.forbid \
1314                                                         and eapi in ("0", "1"):
1315                                                         stats['EAPI.incompatible'] += 1
1316                                                         fails['EAPI.incompatible'].append(
1317                                                                 (relative_path + ": %s new blocker syntax" + \
1318                                                                 " not supported with EAPI='%s':" + \
1319                                                                 " '%s'") % (mytype, eapi, atom))
1320
1321                         type_list.extend([mytype] * (len(badsyntax) - len(type_list)))
1322
1323                 for m,b in zip(type_list, badsyntax):
1324                         stats[m+".syntax"] += 1
1325                         fails[m+".syntax"].append(catpkg+".ebuild "+m+": "+b)
1326
1327                 badlicsyntax = len(filter(lambda x:x=="LICENSE", type_list))
1328                 badprovsyntax = len(filter(lambda x:x=="PROVIDE", type_list))
1329                 baddepsyntax = len(type_list) != badlicsyntax + badprovsyntax 
1330                 badlicsyntax = badlicsyntax > 0
1331                 badprovsyntax = badprovsyntax > 0
1332
1333                 # uselist checks - global
1334                 myuse = []
1335                 default_use = []
1336                 for myflag in myaux["IUSE"].split():
1337                         flag_name = myflag.lstrip("+-")
1338                         if myflag != flag_name:
1339                                 default_use.append(myflag)
1340                         if flag_name not in uselist:
1341                                 myuse.append(flag_name)
1342
1343                 # uselist checks - metadata
1344                 for mypos in range(len(myuse)-1,-1,-1):
1345                         if myuse[mypos] and (myuse[mypos] in muselist):
1346                                 del myuse[mypos]
1347
1348                 if default_use and eapi == "0":
1349                         for myflag in default_use:
1350                                 stats['EAPI.incompatible'] += 1
1351                                 fails['EAPI.incompatible'].append(
1352                                         (relative_path + ": IUSE defaults" + \
1353                                         " not supported with EAPI='%s':" + \
1354                                         " '%s'") % (eapi, myflag))
1355
1356                 for mypos in range(len(myuse)):
1357                         stats["IUSE.invalid"]=stats["IUSE.invalid"]+1
1358                         fails["IUSE.invalid"].append(x+"/"+y+".ebuild: %s" % myuse[mypos])      
1359
1360                 # license checks
1361                 if not badlicsyntax:
1362                         myuse = myaux["LICENSE"]
1363                         # Parse the LICENSE variable, remove USE conditions and
1364                         # flatten it.
1365                         myuse=portage.dep.use_reduce(portage.dep.paren_reduce(myuse), matchall=1)
1366                         myuse=portage.flatten(myuse)
1367                         # Check each entry to ensure that it exists in PORTDIR's
1368                         # license directory.
1369                         for mypos in range(0,len(myuse)):
1370                                 # Need to check for "||" manually as no portage
1371                                 # function will remove it without removing values.
1372                                 if myuse[mypos] not in liclist and myuse[mypos] != "||":
1373                                         stats["LICENSE.invalid"]=stats["LICENSE.invalid"]+1
1374                                         fails["LICENSE.invalid"].append(x+"/"+y+".ebuild: %s" % myuse[mypos])
1375
1376                 #keyword checks
1377                 myuse = myaux["KEYWORDS"].split()
1378                 for mykey in myuse:
1379                         myskey=mykey[:]
1380                         if myskey[0]=="-":
1381                                 myskey=myskey[1:]
1382                         if myskey[0]=="~":
1383                                 myskey=myskey[1:]
1384                         if mykey!="-*":
1385                                 if myskey not in kwlist:
1386                                         stats["KEYWORDS.invalid"] += 1
1387                                         fails["KEYWORDS.invalid"].append(x+"/"+y+".ebuild: %s" % mykey)
1388                                 elif myskey not in profiles:
1389                                         stats["KEYWORDS.invalid"] += 1
1390                                         fails["KEYWORDS.invalid"].append(x+"/"+y+".ebuild: %s (profile invalid)" % mykey)
1391
1392                 #restrict checks
1393                 myrestrict = None
1394                 try:
1395                         myrestrict = portage.dep.use_reduce(
1396                                 portage.dep.paren_reduce(myaux["RESTRICT"]), matchall=1)
1397                 except portage.exception.InvalidDependString, e:
1398                         stats["RESTRICT.syntax"] = stats["RESTRICT.syntax"] + 1
1399                         fails["RESTRICT.syntax"].append(
1400                                 "%s: RESTRICT: %s" % (relative_path, e))
1401                         del e
1402                 if myrestrict:
1403                         myrestrict = set(portage.flatten(myrestrict))
1404                         mybadrestrict = myrestrict.difference(valid_restrict)
1405                         if mybadrestrict:
1406                                 stats["RESTRICT.invalid"] += len(mybadrestrict)
1407                                 for mybad in mybadrestrict:
1408                                         fails["RESTRICT.invalid"].append(x+"/"+y+".ebuild: %s" % mybad)
1409                 # Syntax Checks
1410                 relative_path = os.path.join(x, y + ".ebuild")
1411                 full_path = os.path.join(repodir, relative_path)
1412                 f = open(full_path, 'rb')
1413                 try:
1414                         for check_name, e in run_checks(f, pkg):
1415                                 stats[check_name] += 1
1416                                 fails[check_name].append(relative_path + ': %s' % e)
1417                 finally:
1418                         f.close()
1419                         del f
1420
1421                 if options.force:
1422                         # The dep_check() calls are the most expensive QA test. If --force
1423                         # is enabled, there's no point in wasting time on these since the
1424                         # user is intent on forcing the commit anyway.
1425                         continue
1426
1427                 for keyword,arch,groups in arches:
1428
1429                         if arch not in profiles:
1430                                 # A missing profile will create an error further down
1431                                 # during the KEYWORDS verification.
1432                                 continue
1433                                 
1434                         for prof in profiles[arch]:
1435
1436                                 if prof[1] not in ("stable", "dev") or \
1437                                         prof[1] == "dev" and not options.include_dev:
1438                                         continue
1439
1440                                 profdir = portdir+"/profiles/"+prof[0]
1441         
1442                                 if prof[0] in arch_caches:
1443                                         dep_settings = arch_caches[prof[0]]
1444                                 else:
1445                                         dep_settings = portage.config(
1446                                                 config_profile_path=profdir,
1447                                                 config_incrementals=portage.const.INCREMENTALS,
1448                                                 local_config=False)
1449                                         if options.without_mask:
1450                                                 dep_settings.pmaskdict.clear()
1451                                         arch_caches[prof[0]] = dep_settings
1452                                         while True:
1453                                                 try:
1454                                                         # Protect ACCEPT_KEYWORDS from config.regenerate()
1455                                                         # (just in case)
1456                                                         dep_settings.incrementals.remove("ACCEPT_KEYWORDS")
1457                                                 except ValueError:
1458                                                         break
1459
1460                                 xmatch_cache_key = (prof[0], tuple(groups))
1461                                 xcache = arch_xmatch_caches.get(xmatch_cache_key)
1462                                 if xcache is None:
1463                                         portdb.melt()
1464                                         portdb.freeze()
1465                                         xcache = portdb.xcache
1466                                         xcache.update(shared_xmatch_caches)
1467                                         arch_xmatch_caches[xmatch_cache_key] = xcache
1468
1469                                 trees["/"]["porttree"].settings = dep_settings
1470                                 portdb.mysettings = dep_settings
1471                                 portdb.xcache = xcache
1472                                 # for package.use.mask support inside dep_check
1473                                 dep_settings.setcpv(pkg)
1474                                 dep_settings["ACCEPT_KEYWORDS"] = " ".join(groups)
1475                                 # just in case, prevent config.reset() from nuking these.
1476                                 dep_settings.backup_changes("ACCEPT_KEYWORDS")
1477
1478                                 for myprovide in myaux["PROVIDE"].split():
1479                                         prov_cp = portage.dep_getkey(myprovide)
1480                                         if prov_cp not in dep_settings.getvirtuals():
1481                                                 stats["virtual.unavailable"]+=1
1482                                                 fails["virtual.unavailable"].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+prov_cp)
1483
1484                                 if not baddepsyntax:
1485                                         ismasked = os.path.join(catdir, y) not in \
1486                                                 portdb.xmatch("list-visible", x)
1487                                         if ismasked:
1488                                                 if not have_pmasked:
1489                                                         have_pmasked = bool(dep_settings._getMaskAtom(
1490                                                                 pkg.cpv, pkg.metadata))
1491                                                 if options.ignore_masked:
1492                                                         continue
1493                                                 #we are testing deps for a masked package; give it some lee-way
1494                                                 suffix="masked"
1495                                                 matchmode = "minimum-all"
1496                                         else:
1497                                                 suffix=""
1498                                                 matchmode = "minimum-visible"
1499
1500                                         if not have_dev_keywords:
1501                                                 have_dev_keywords = \
1502                                                         bool(dev_keywords.intersection(keywords))
1503
1504                                         if prof[1] == "dev":
1505                                                 suffix=suffix+"indev"
1506
1507                                         for mytype,mypos in [["DEPEND",len(missingvars)],["RDEPEND",len(missingvars)+1],["PDEPEND",len(missingvars)+2]]:
1508                                                 
1509                                                 mykey=mytype+".bad"+suffix
1510                                                 myvalue = myaux[mytype]
1511                                                 if not myvalue:
1512                                                         continue
1513                                                 try:
1514                                                         mydep = portage.dep_check(myvalue, portdb,
1515                                                                 dep_settings, use="all", mode=matchmode,
1516                                                                 trees=trees)
1517                                                 except KeyError, e:
1518                                                         stats[mykey]=stats[mykey]+1
1519                                                         fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(e[0]))
1520                                                         continue
1521         
1522                                                 if mydep[0]==1:
1523                                                         if mydep[1]!=[]:
1524                                                                 #we have some unsolvable deps
1525                                                                 #remove ! deps, which always show up as unsatisfiable
1526                                                                 d=0
1527                                                                 while d<len(mydep[1]):
1528                                                                         if mydep[1][d][0]=="!":
1529                                                                                 del mydep[1][d]
1530                                                                         else:
1531                                                                                 d += 1
1532                                                                 #if we emptied out our list, continue:
1533                                                                 if not mydep[1]:
1534                                                                         continue
1535                                                                 stats[mykey]=stats[mykey]+1
1536                                                                 fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
1537                                                 else:
1538                                                         stats[mykey]=stats[mykey]+1
1539                                                         fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
1540
1541         # Check for 'all unstable' or 'all masked' -- ACCEPT_KEYWORDS is stripped
1542         # XXX -- Needs to be implemented in dep code. Can't determine ~arch nicely.
1543         #if not portage.portdb.xmatch("bestmatch-visible",x):
1544         #    stats["ebuild.nostable"]+=1
1545         #    fails["ebuild.nostable"].append(x)
1546         if allmasked and repolevel == 3:
1547                 stats["ebuild.allmasked"]+=1
1548                 fails["ebuild.allmasked"].append(x)
1549
1550 if options.mode == "manifest":
1551         sys.exit(dofail)
1552
1553 #dofail will be set to 1 if we have failed in at least one non-warning category
1554 dofail=0
1555 #dowarn will be set to 1 if we tripped any warnings
1556 dowarn=0
1557 #dofull will be set if we should print a "repoman full" informational message
1558 dofull = options.mode != 'full'
1559
1560 for x in qacats:
1561         if not stats[x]:
1562                 continue
1563         dowarn = 1
1564         if x not in qawarnings:
1565                 dofail = 1
1566
1567 if dofail or \
1568         (dowarn and not (options.quiet or options.mode == "scan")):
1569         dofull = 0
1570
1571 # Save QA output so that it can be conveniently displayed
1572 # in $EDITOR while the user creates a commit message.
1573 # Otherwise, the user would not be able to see this output
1574 # once the editor has taken over the screen.
1575 qa_output = StringIO.StringIO()
1576 style_file = ConsoleStyleFile(sys.stdout)
1577 if options.mode == 'commit' and \
1578         (not commitmessage or not commitmessage.strip()):
1579         style_file.write_listener = qa_output
1580 console_writer = StyleWriter(file=style_file, maxcol=9999)
1581 console_writer.style_listener = style_file.new_styles
1582
1583 f = formatter.AbstractFormatter(console_writer)
1584
1585 utilities.format_qa_output(f, stats, fails, dofull, dofail, options, qawarnings)
1586
1587 style_file.flush()
1588 del console_writer, f, style_file
1589 qa_output = qa_output.getvalue()
1590 qa_output = qa_output.splitlines(True)
1591
1592 def grouplist(mylist,seperator="/"):
1593         """(list,seperator="/") -- Takes a list of elements; groups them into
1594         same initial element categories. Returns a dict of {base:[sublist]}
1595         From: ["blah/foo","spork/spatula","blah/weee/splat"]
1596         To:   {"blah":["foo","weee/splat"], "spork":["spatula"]}"""
1597         mygroups={}
1598         for x in mylist:
1599                 xs=x.split(seperator)
1600                 if xs[0]==".":
1601                         xs=xs[1:]
1602                 if xs[0] not in mygroups:
1603                         mygroups[xs[0]]=[seperator.join(xs[1:])]
1604                 else:
1605                         mygroups[xs[0]]+=[seperator.join(xs[1:])]
1606         return mygroups
1607
1608 if have_pmasked and not (options.without_mask or options.ignore_masked):
1609         print bold("Note: use --without-mask to check " + \
1610                 "KEYWORDS on dependencies of masked packages")
1611
1612 if have_dev_keywords and not options.include_dev:
1613         print bold("Note: use --include-dev (-d) to check " + \
1614                 "dependencies for 'dev' profiles")
1615
1616 if options.mode != 'commit':
1617         if dofull:
1618                 print bold("Note: type \"repoman full\" for a complete listing.")
1619         if dowarn and not dofail:
1620                 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.\""
1621         elif not dofail:
1622                 print green("RepoMan sez:"),"\"If everyone were like you, I'd be out of business!\""
1623         elif dofail:
1624                 print turquoise("Please fix these important QA issues first.")
1625                 print green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n"
1626                 sys.exit(1)
1627 else:
1628         if dofail and can_force and options.force and not options.pretend:
1629                 print green("RepoMan sez:") + \
1630                         " \"You want to commit even with these QA issues?\n" + \
1631                         "              I'll take it this time, but I'm not happy.\"\n"
1632         elif dofail:
1633                 if options.force and not can_force:
1634                         print bad("The --force option has been disabled due to extraordinary issues.")
1635                 print turquoise("Please fix these important QA issues first.")
1636                 print green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n"
1637                 sys.exit(1)
1638
1639         if options.pretend:
1640                 print green("RepoMan sez:"), "\"So, you want to play it safe. Good call.\"\n"
1641
1642         myunadded = []
1643         if vcs == "cvs":
1644                 try:
1645                         myvcstree=portage.cvstree.getentries("./",recursive=1)
1646                         myunadded=portage.cvstree.findunadded(myvcstree,recursive=1,basedir="./")
1647                 except SystemExit, e:
1648                         raise  # TODO propogate this
1649                 except:
1650                         err("Error retrieving CVS tree; exiting.")
1651         if vcs == "svn":
1652                 try:
1653                         svnstatus=os.popen("svn status --no-ignore").readlines()
1654                         myunadded = [ "./"+elem.rstrip().split()[1] for elem in svnstatus if elem.startswith("?") or elem.startswith("I") ]
1655                 except SystemExit, e:
1656                         raise  # TODO propogate this
1657                 except:
1658                         err("Error retrieving SVN info; exiting.")
1659         if vcs == "git":
1660                 # get list of files not under version control or missing
1661                 myf = os.popen("git ls-files --others")
1662                 myunadded = [ "./" + elem[:-1] for elem in myf ]
1663                 myf.close()
1664
1665         myautoadd=[]
1666         if myunadded:
1667                 for x in range(len(myunadded)-1,-1,-1):
1668                         xs=myunadded[x].split("/")
1669                         if xs[-1]=="files":
1670                                 print "!!! files dir is not added! Please correct this."
1671                                 sys.exit(-1)
1672                         elif xs[-1]=="Manifest":
1673                                 # It's a manifest... auto add
1674                                 myautoadd+=[myunadded[x]]
1675                                 del myunadded[x]
1676
1677         if myautoadd:
1678                 print ">>> Auto-Adding missing Manifest(s)..."
1679                 if options.pretend:
1680                         if vcs == "cvs":
1681                                 print "(cvs add "+" ".join(myautoadd)+")"
1682                         if vcs == "svn":
1683                                 print "(svn add "+" ".join(myautoadd)+")"
1684                         elif vcs == "git":
1685                                 print "(git add "+" ".join(myautoadd)+")"
1686                         retval=0
1687                 else:
1688                         if vcs == "cvs":
1689                                 retval=os.system("cvs add "+" ".join(myautoadd))
1690                         if vcs == "svn":
1691                                 retval=os.system("svn add "+" ".join(myautoadd))
1692                         elif vcs == "git":
1693                                 retval=os.system("git add "+" ".join(myautoadd))
1694                 if retval:
1695                         writemsg_level("!!! Exiting on %s (shell) error code: %s\n" % \
1696                                 (vcs, retval), level=logging.ERROR, noiselevel=-1)
1697                         sys.exit(retval)
1698
1699         if myunadded:
1700                 print red("!!! The following files are in your local tree but are not added to the master")
1701                 print red("!!! tree. Please remove them from the local tree or add them to the master tree.")
1702                 for x in myunadded:
1703                         print "   ",x
1704                 print
1705                 print
1706                 sys.exit(1)
1707
1708         if vcs == "cvs":
1709                 mycvstree = cvstree.getentries("./", recursive=1)
1710                 mychanged = cvstree.findchanged(mycvstree, recursive=1, basedir="./")
1711                 mynew = cvstree.findnew(mycvstree, recursive=1, basedir="./")
1712                 myremoved=portage.cvstree.findremoved(mycvstree,recursive=1,basedir="./")
1713                 bin_blob_pattern = re.compile("^-kb$")
1714                 no_expansion = set(portage.cvstree.findoption(mycvstree, bin_blob_pattern,
1715                         recursive=1, basedir="./"))
1716
1717
1718         if vcs == "svn":
1719                 svnstatus = os.popen("svn status").readlines()
1720                 mychanged = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem and elem[:1] in "MR" ]
1721                 mynew     = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem.startswith("A") ]
1722                 myremoved = [ "./" + elem.split()[-1:][0] for elem in svnstatus if elem.startswith("D") ]
1723                 # in contrast to CVS, SVN expands nothing by default.
1724                 # bin_blobs historically
1725                 # were just there to see what files need to be checked for
1726                 # keyword expansion, which is exactly what we do here, so
1727                 # slightly change the semantic meaning of "bin_blob"...  In the
1728                 # future we could store which keyword is expanded.
1729                 props = os.popen("svn propget -R svn:keywords").readlines()
1730
1731                 # For files with multiple props set, props are delimited by newlines,
1732                 # so exclude lines that don't contain " - " since each of those lines
1733                 # only a contain props for a file listed on a previous line.
1734                 expansion = set("./" + prop.split(" - ")[0] \
1735                         for prop in props if " - " in prop)
1736
1737         elif vcs == "git":
1738                 mychanged = os.popen("git ls-files -m --with-tree=HEAD").readlines()
1739                 mychanged = [ "./" +  elem[:-1] for elem in mychanged ]
1740                 mynew = os.popen("git diff --cached --name-only --diff-filter=A").readlines()
1741                 strip_levels = repolevel - 1
1742                 if strip_levels:
1743                         mynew = [elem[repo_subdir_len:] for elem in mynew \
1744                                 if elem[:repo_subdir_len] == repo_subdir]
1745                 mynew = ["./" + elem[:-1] for elem in mynew]
1746                 myremoved = os.popen("git diff --cached --name-only --diff-filter=D").readlines()
1747                 if strip_levels:
1748                         myremoved = [elem[repo_subdir_len:] for elem in myremoved \
1749                                 if elem[:repo_subdir_len] == repo_subdir]
1750                 myremoved = ["./" + elem[:-1] for elem in myremoved]
1751
1752         if vcs:
1753                 if not (mychanged or mynew or myremoved):
1754                         print green("RepoMan sez:"), "\"Doing nothing is not always good for QA.\""
1755                         print
1756                         print "(Didn't find any changed files...)"
1757                         print
1758                         sys.exit(0)
1759
1760         # Manifests need to be regenerated after all other commits, so don't commit
1761         # them now even if they have changed.
1762         mymanifests = set()
1763         myupdates = set()
1764         for f in mychanged + mynew:
1765                 if "Manifest" == os.path.basename(f):
1766                         mymanifests.add(f)
1767                 else:
1768                         myupdates.add(f)
1769         if vcs == 'git':
1770                 myupdates.difference_update(myremoved)
1771         myupdates = list(myupdates)
1772         mymanifests = list(mymanifests)
1773         myheaders = []
1774         mydirty = []
1775         headerstring = "'\$(Header|Id)"
1776         headerstring += ".*\$'"
1777         for myfile in myupdates:
1778
1779                 # for CVS, no_expansion contains files that are excluded from expansion
1780                 if vcs == "cvs":
1781                         if myfile in no_expansion:
1782                                 continue
1783
1784                 # for SVN, expansion contains files that are included in expansion
1785                 elif vcs == "svn":
1786                         if myfile not in expansion:
1787                                 continue
1788
1789                 myout = commands.getstatusoutput("egrep -q "+headerstring+" "+myfile)
1790                 if myout[0] == 0:
1791                         myheaders.append(myfile)
1792
1793         print "* %s files being committed..." % green(str(len(myupdates))),
1794         if vcs == 'git':
1795                 # With git, there's never any keyword expansion, so there's
1796                 # no need to regenerate manifests and all files will be
1797                 # committed in one big commit at the end.
1798                 print
1799         else:
1800                 print "%s have headers that will change." % green(str(len(myheaders)))
1801                 print "* Files with headers will cause the " + \
1802                         "manifests to be made and recommited."
1803         logging.info("myupdates:", str(myupdates))
1804         logging.info("myheaders:", str(myheaders))
1805
1806         commitmessage = options.commitmsg
1807         if options.commitmsgfile:
1808                 try:
1809                         f = open(options.commitmsgfile)
1810                         commitmessage = f.read()
1811                         f.close()
1812                         del f
1813                 except (IOError, OSError), e:
1814                         if e.errno == errno.ENOENT:
1815                                 portage.writemsg("!!! File Not Found: --commitmsgfile='%s'\n" % options.commitmsgfile)
1816                         else:
1817                                 raise
1818                 # We've read the content so the file is no longer needed.
1819                 commitmessagefile = None
1820         if not commitmessage or not commitmessage.strip():
1821                 try:
1822                         editor = os.environ.get("EDITOR")
1823                         if editor and utilities.editor_is_executable(editor):
1824                                 commitmessage = utilities.get_commit_message_with_editor(
1825                                         editor, message=qa_output)
1826                         else:
1827                                 commitmessage = utilities.get_commit_message_with_stdin()
1828                 except KeyboardInterrupt:
1829                         exithandler()
1830                 if not commitmessage or not commitmessage.strip():
1831                         print "* no commit message?  aborting commit."
1832                         sys.exit(1)
1833         commitmessage = commitmessage.rstrip()
1834         portage_version = getattr(portage, "VERSION", None)
1835         if portage_version is None:
1836                 sys.stderr.write("Failed to insert portage version in message!\n")
1837                 sys.stderr.flush()
1838                 portage_version = "Unknown"
1839         unameout = platform.system() + " " + platform.release() + " "
1840         if platform.system() in ["Darwin", "SunOS"]:
1841                 unameout += platform.processor()
1842         else:
1843                 unameout += platform.machine()
1844         commitmessage += "\n(Portage version: %s/%s/%s" % \
1845                 (portage_version, vcs, unameout)
1846         if options.force:
1847                 commitmessage += ", RepoMan options: --force"
1848         commitmessage += ")"
1849
1850         if vcs != 'git' and (myupdates or myremoved):
1851                 myfiles = myupdates + myremoved
1852                 if not myheaders and "sign" not in repoman_settings.features:
1853                         myfiles += mymanifests
1854                 fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
1855                 mymsg = os.fdopen(fd, "w")
1856                 mymsg.write(commitmessage)
1857                 mymsg.close()
1858
1859                 print
1860                 print green("Using commit message:")
1861                 print green("------------------------------------------------------------------------------")
1862                 print commitmessage
1863                 print green("------------------------------------------------------------------------------")
1864                 print
1865
1866                 # Having a leading ./ prefix on file paths can trigger a bug in
1867                 # the cvs server when committing files to multiple directories,
1868                 # so strip the prefix.
1869                 myfiles = [f.lstrip("./") for f in myfiles]
1870
1871                 commit_cmd = [vcs]
1872                 commit_cmd.extend(vcs_global_opts)
1873                 commit_cmd.append("commit")
1874                 commit_cmd.extend(vcs_local_opts)
1875                 commit_cmd.extend(["-F", commitmessagefile])
1876                 commit_cmd.extend(myfiles)
1877
1878                 try:
1879                         if options.pretend:
1880                                 print "(%s)" % (" ".join(commit_cmd),)
1881                         else:
1882                                 retval = spawn(commit_cmd, env=os.environ)
1883                                 if retval != os.EX_OK:
1884                                         writemsg_level(("!!! Exiting on %s (shell) " + \
1885                                                 "error code: %s\n") % (vcs, retval),
1886                                                 level=logging.ERROR, noiselevel=-1)
1887                                         sys.exit(retval)
1888                 finally:
1889                         try:
1890                                 os.unlink(commitmessagefile)
1891                         except OSError:
1892                                 pass
1893
1894         # Setup the GPG commands
1895         def gpgsign(filename):
1896                 if "PORTAGE_GPG_KEY" not in repoman_settings:
1897                         raise portage.exception.MissingParameter("PORTAGE_GPG_KEY is unset!")
1898                 if "PORTAGE_GPG_DIR" not in repoman_settings:
1899                         if "HOME" in os.environ:
1900                                 repoman_settings["PORTAGE_GPG_DIR"] = os.path.join(os.environ["HOME"], ".gnupg")
1901                                 logging.info("Automatically setting PORTAGE_GPG_DIR to %s" % repoman_settings["PORTAGE_GPG_DIR"])
1902                         else:
1903                                 raise portage.exception.MissingParameter("PORTAGE_GPG_DIR is unset!")
1904                 gpg_dir = repoman_settings["PORTAGE_GPG_DIR"]
1905                 if gpg_dir.startswith("~") and "HOME" in os.environ:
1906                         repoman_settings["PORTAGE_GPG_DIR"] = os.path.join(
1907                                 os.environ["HOME"], gpg_dir[1:].lstrip(os.path.sep))
1908                 if not os.access(repoman_settings["PORTAGE_GPG_DIR"], os.X_OK):
1909                         raise portage.exception.InvalidLocation(
1910                                 "Unable to access directory: PORTAGE_GPG_DIR='%s'" % \
1911                                 repoman_settings["PORTAGE_GPG_DIR"])
1912                 gpgcmd = "gpg --sign --clearsign --yes "
1913                 gpgcmd+= "--default-key "+repoman_settings["PORTAGE_GPG_KEY"]
1914                 if "PORTAGE_GPG_DIR" in repoman_settings:
1915                         gpgcmd += " --homedir "+repoman_settings["PORTAGE_GPG_DIR"]
1916                 if options.pretend:
1917                         print "("+gpgcmd+" "+filename+")"
1918                 else:
1919                         rValue = os.system(gpgcmd+" "+filename)
1920                         if rValue == os.EX_OK:
1921                                 os.rename(filename+".asc", filename)
1922                         else:
1923                                 raise portage.exception.PortageException("!!! gpg exited with '" + str(rValue) + "' status")
1924
1925         # When files are removed and re-added, the cvs server will put /Attic/
1926         # inside the $Header path. This code detects the problem and corrects it
1927         # so that the Manifest will generate correctly. See bug #169500.
1928         from portage.util import write_atomic
1929         cvs_header = re.compile(r'^#\s*\$Header.*\$$')
1930         for x in myheaders:
1931                 f = open(x)
1932                 mylines = f.readlines()
1933                 f.close()
1934                 modified = False
1935                 for i, line in enumerate(mylines):
1936                         if cvs_header.match(line) and "/Attic/" in line:
1937                                 mylines[i] = line.replace("/Attic/", "/")
1938                                 modified = True
1939                 if modified:
1940                         write_atomic(x, "".join(mylines))
1941
1942         manifest_commit_required = True
1943         if vcs != 'git' and (myupdates or myremoved):
1944                 myfiles = myupdates + myremoved
1945                 for x in range(len(myfiles)-1, -1, -1):
1946                         if myfiles[x].count("/") < 4-repolevel:
1947                                 del myfiles[x]
1948                 mydone=[]
1949                 if repolevel==3:   # In a package dir
1950                         repoman_settings["O"] = startdir
1951                         portage.digestgen([], repoman_settings, manifestonly=1,
1952                                 myportdb=portdb)
1953                 elif repolevel==2: # In a category dir
1954                         for x in myfiles:
1955                                 xs=x.split("/")
1956                                 if len(xs) < 4-repolevel:
1957                                         continue
1958                                 if xs[0]==".":
1959                                         xs=xs[1:]
1960                                 if xs[0] in mydone:
1961                                         continue
1962                                 mydone.append(xs[0])
1963                                 repoman_settings["O"] = os.path.join(startdir, xs[0])
1964                                 if not os.path.isdir(repoman_settings["O"]):
1965                                         continue
1966                                 portage.digestgen([], repoman_settings, manifestonly=1,
1967                                         myportdb=portdb)
1968                 elif repolevel==1: # repo-cvsroot
1969                         print green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n"
1970                         for x in myfiles:
1971                                 xs=x.split("/")
1972                                 if len(xs) < 4-repolevel:
1973                                         continue
1974                                 if xs[0]==".":
1975                                         xs=xs[1:]
1976                                 if "/".join(xs[:2]) in mydone:
1977                                         continue
1978                                 mydone.append("/".join(xs[:2]))
1979                                 repoman_settings["O"] = os.path.join(startdir, xs[0], xs[1])
1980                                 if not os.path.isdir(repoman_settings["O"]):
1981                                         continue
1982                                 portage.digestgen([], repoman_settings, manifestonly=1,
1983                                         myportdb=portdb)
1984                 else:
1985                         print red("I'm confused... I don't know where I am!")
1986                         sys.exit(1)
1987
1988                 # Force an unsigned commit when more than one Manifest needs to be signed.
1989                 if repolevel < 3 and "sign" in repoman_settings.features:
1990
1991                         fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
1992                         mymsg = os.fdopen(fd, "w")
1993                         mymsg.write(commitmessage)
1994                         mymsg.write("\n (Unsigned Manifest commit)")
1995                         mymsg.close()
1996
1997                         commit_cmd = [vcs]
1998                         commit_cmd.extend(vcs_global_opts)
1999                         commit_cmd.append("commit")
2000                         commit_cmd.extend(vcs_local_opts)
2001                         commit_cmd.extend(["-F", commitmessagefile])
2002                         commit_cmd.extend(f.lstrip("./") for f in mymanifests)
2003
2004                         try:
2005                                 if options.pretend:
2006                                         print "(%s)" % (" ".join(commit_cmd),)
2007                                 else:
2008                                         retval = spawn(commit_cmd, env=os.environ)
2009                                         if retval:
2010                                                 writemsg_level(("!!! Exiting on %s (shell) " + \
2011                                                         "error code: %s\n") % (vcs, retval),
2012                                                         level=logging.ERROR, noiselevel=-1)
2013                                                 sys.exit(retval)
2014                         finally:
2015                                 try:
2016                                         os.unlink(commitmessagefile)
2017                                 except OSError:
2018                                         pass
2019                         manifest_commit_required = False
2020
2021         signed = False
2022         if "sign" in repoman_settings.features:
2023                 signed = True
2024                 myfiles = myupdates + myremoved + mymanifests
2025                 try:
2026                         if repolevel==3:   # In a package dir
2027                                 repoman_settings["O"] = "."
2028                                 gpgsign(os.path.join(repoman_settings["O"], "Manifest"))
2029                         elif repolevel==2: # In a category dir
2030                                 mydone=[]
2031                                 for x in myfiles:
2032                                         xs=x.split("/")
2033                                         if len(xs) < 4-repolevel:
2034                                                 continue
2035                                         if xs[0]==".":
2036                                                 xs=xs[1:]
2037                                         if xs[0] in mydone:
2038                                                 continue
2039                                         mydone.append(xs[0])
2040                                         repoman_settings["O"] = os.path.join(".", xs[0])
2041                                         if not os.path.isdir(repoman_settings["O"]):
2042                                                 continue
2043                                         gpgsign(os.path.join(repoman_settings["O"], "Manifest"))
2044                         elif repolevel==1: # repo-cvsroot
2045                                 print green("RepoMan sez:"), "\"You're rather crazy... doing the entire repository.\"\n"
2046                                 mydone=[]
2047                                 for x in myfiles:
2048                                         xs=x.split("/")
2049                                         if len(xs) < 4-repolevel:
2050                                                 continue
2051                                         if xs[0]==".":
2052                                                 xs=xs[1:]
2053                                         if "/".join(xs[:2]) in mydone:
2054                                                 continue
2055                                         mydone.append("/".join(xs[:2]))
2056                                         repoman_settings["O"] = os.path.join(".", xs[0], xs[1])
2057                                         if not os.path.isdir(repoman_settings["O"]):
2058                                                 continue
2059                                         gpgsign(os.path.join(repoman_settings["O"], "Manifest"))
2060                 except portage.exception.PortageException, e:
2061                         portage.writemsg("!!! %s\n" % str(e))
2062                         portage.writemsg("!!! Disabled FEATURES='sign'\n")
2063                         signed = False
2064
2065         if vcs == 'git':
2066                 # It's not safe to use the git commit -a option since there might
2067                 # be some modified files elsewhere in the working tree that the
2068                 # user doesn't want to commit. Therefore, call git update-index
2069                 # in order to ensure that the index is updated with the latest
2070                 # versions of all new and modified files in the relevant portion
2071                 # of the working tree.
2072                 myfiles = mymanifests + myupdates
2073                 myfiles.sort()
2074                 update_index_cmd = ["git", "update-index"]
2075                 update_index_cmd.extend(f.lstrip("./") for f in myfiles)
2076                 if options.pretend:
2077                         print "(%s)" % (" ".join(update_index_cmd),)
2078                 else:
2079                         retval = spawn(update_index_cmd, env=os.environ)
2080                         if retval != os.EX_OK:
2081                                 writemsg_level(("!!! Exiting on %s (shell) " + \
2082                                         "error code: %s\n") % (vcs, retval),
2083                                         level=logging.ERROR, noiselevel=-1)
2084                                 sys.exit(retval)
2085
2086         if vcs == 'git' or manifest_commit_required or signed:
2087
2088                 myfiles = mymanifests[:]
2089                 if vcs == 'git':
2090                         myfiles += myupdates
2091                         myfiles += myremoved
2092                 myfiles.sort()
2093
2094                 fd, commitmessagefile = tempfile.mkstemp(".repoman.msg")
2095                 mymsg = os.fdopen(fd, "w")
2096                 mymsg.write(commitmessage)
2097                 if signed:
2098                         mymsg.write("\n (Signed Manifest commit)")
2099                 else:
2100                         mymsg.write("\n (Unsigned Manifest commit)")
2101                 mymsg.close()
2102
2103                 commit_cmd = [vcs]
2104                 commit_cmd.extend(vcs_global_opts)
2105                 commit_cmd.append("commit")
2106                 commit_cmd.extend(vcs_local_opts)
2107                 commit_cmd.extend(["-F", commitmessagefile])
2108                 commit_cmd.extend(f.lstrip("./") for f in myfiles)
2109
2110                 try:
2111                         if options.pretend:
2112                                 print "(%s)" % (" ".join(commit_cmd),)
2113                         else:
2114                                 retval = spawn(commit_cmd, env=os.environ)
2115                                 if retval != os.EX_OK:
2116                                         writemsg_level(("!!! Exiting on %s (shell) " + \
2117                                                 "error code: %s\n") % (vcs, retval),
2118                                                 level=logging.ERROR, noiselevel=-1)
2119                                         sys.exit(retval)
2120                 finally:
2121                         try:
2122                                 os.unlink(commitmessagefile)
2123                         except OSError:
2124                                 pass
2125
2126         print
2127         if vcs:
2128                 print "Commit complete."
2129         else:
2130                 print "repoman was too scared by not seeing any familiar version control file that he forgot to commit anything"
2131         print green("RepoMan sez:"), "\"If everyone were like you, I'd be out of business!\"\n"
2132 sys.exit(0)
2133