#!/usr/bin/python2.2
# Copyright 1999-2003 Gentoo Technologies, Inc.
# Distributed under the terms of the GNU General Public License v2
# Author: Daniel Robbins <drobbins@gentoo.org>
# $Header: /home/cvsroot/gentoo-src/portage/bin/repoman,v 1.14 2003/03/11 11:33:07 carpaski Exp $

# Next to do: dep syntax checking in ebuilds, mask files
# Then, check to make sure deps are satisfiable (to avoid "can't find match for" problems)
# that last one is tricky because multiple profiles need to be checked.

import os,sys,string,signal,readline,portage
from output import *
exename=os.path.basename(sys.argv[0])	
version="1.2"	

def err(txt):
	print exename+": "+txt
	sys.exit(1)

def exithandler(signum=None,frame=None):
	sys.stderr.write("\n"+exename+": Interrupted; exiting...\n")
	sys.exit(1)
	os.kill(0,signal.SIGKILL)
signal.signal(signal.SIGINT,exithandler)

REPOROOTS=["gentoo-x86"]
modes=["scan","fix","full","help","commit"]
shortmodes={"ci":"commit"}
options=["--pretend","--help"]
modeshelp={"scan":"Scan current directory tree for QA issues (default)",
"fix":"Fix those issues that can be fixed (stray digests, missing digests)",
"full":"Scan current directory tree for QA issues (full listing)",
"help":"Show this screen",
"commit":"Scan current directory tree for QA issues; if OK, commit via cvs" }
optionshelp={"--pretend":"Don't actually perform commit or fix steps; just show what would be done.",
"--help":"Show this screen"
}
qacats=["digest.stray","digest.missing","ebuild.invalidname","ebuild.namenomatch","changelog.missing","ebuild.notadded","digest.notadded","ebuild.disjointed","digest.disjointed", "DEPEND.bad","RDEPEND.bad","DEPEND.badmasked","RDEPEND.badmasked","ebuild.syntax","ebuild.output","ebuild.nesteddie"]
qawarnings=["changelog.missing","ebuild.notadded","digest.notadded","DEPEND.badmasked","RDEPEND.badmasked"]
missingvars=["KEYWORDS","LICENSE","DESCRIPTION","SLOT"]
allvars=missingvars[:]
allvars.extend(["DEPEND","RDEPEND"])
for x in missingvars:
	qacats.append(x+".missing")
	qawarnings.append(x+".missing")
qahelp={
	"digest.stray":"Digest files that do not have a corresponding ebuild",
	"digest.missing":"Digest files that are missing (ebuild exists, digest doesn't)",
	"ebuild.invalidname":"Ebuild files with a non-parseable name",
	"ebuild.namenomatch":"Ebuild files that do not have the same name as their parent directory",
	"changelog.missing":"Missing ChangeLog files",
	"ebuild.disjointed":"Ebuilds not added to cvs when the matching digest has been added",
	"digest.disjointed":"Digests not added to cvs when the matching ebuild has been added",
	"digest.notadded":"Digests that exist but have not been added to cvs",
	"ebuild.notadded":"Ebuilds that exist but have not been added to cvs",
	"KEYWORDS.missing":"Ebuilds that have a missing KEYWORDS variable",
	"LICENSE.missing":"Ebuilds that have a missing LICENSE variable",
	"DESCRIPTION.missing":"Ebuilds that have a missing DESCRIPTION variable",
	"SLOT.missing":"Ebuilds that have a missing SLOT variable",
	"DEPEND.bad":"User-visible ebuilds with bad DEPEND settings (matched against *visible* ebuilds)",
	"RDEPEND.bad":"User-visible ebuilds with bad RDEPEND settings (matched against *visible* ebuilds)",
	"DEPEND.badmasked":"Masked ebuilds with bad DEPEND settings (matched against *all* ebuilds)",
	"RDEPEND.badmasked":"Masked ebuilds with RDEPEND settings (matched against *all* ebuilds)",
	"ebuild.syntax":"Error generating cache entry for ebuild; typically caused by ebuild syntax error",
	"ebuild.output":"A simple sourcing of the ebuild produces output; this breaks ebuild policy.",
	"ebuild.nesteddie":"Placing 'die' inside ( ) prints an error, but doesn't stop the ebuild."
}

def err(txt):
	print exename+": "+txt
	sys.exit(1)

def help():
	print
	print green(exename+" "+version)
	print " \"Quality is job zero.\""
	print " Copyright 1999-2003 Gentoo Technologies, Inc."
	print " Distributed under the terms of the GNU General Public License v2"
	print
	print bold(" Usage:"),turquoise(exename),"[",green("option"),"] [",green("mode"),"]"
	print bold(" Modes:"),turquoise("scan (default)"),
	for x in modes[1:]:
		print "|",turquoise(x),
	print "\n"
	print " "+green(string.ljust("Option",20)+" Description")
	for x in options:
		print " "+string.ljust(x,20),optionshelp[x]
	print
	print " "+green(string.ljust("Mode",20)+" Description")
	for x in modes:
		print " "+string.ljust(x,20),modeshelp[x]
	print
	print " "+green(string.ljust("QA keyword",20)+" Description")
	for x in qacats:
		print " "+string.ljust(x,20),qahelp[x]
	print
	sys.exit(1)


mymode=None
myoptions=[]
if len(sys.argv)>1:
	for x in sys.argv[1:]:
		if x in shortmodes.keys():
			x=shortmodes[x]
		if x in modes:
			if mymode==None:
				mymode=x
			else:
				err("Please specify either \""+mymode+"\" or \""+x+"\", but not both.")
		elif x in options:
			if not x in myoptions:
				myoptions.append(x)
		else:
			err("\""+x+"\" is not a valid mode or option.")
if mymode==None:
	mymode="scan"
if mymode=="help" or ("--help" in myoptions):
	help()

if not os.path.isdir("CVS"):
	err("We do not appear to be inside a local repository. Exiting.")
try:
	myrepofile=open("CVS/Repository")
	myreporoot=myrepofile.readline()[:-1]
	myrepofile.close()
except:
	err("Error grabbing repository information; exiting.")

try: # Determine if we're in PORTDIR... If not tell portage to accomodate.
	mydir=os.getcwd()
	if portage.settings["PORTDIR"][-1]!="/":
		portage.settings["PORTDIR"]=portage.settings["PORTDIR"]+"/"
	if portage.settings["PORTDIR"]!=mydir[:len(portage.settings["PORTDIR"])]:
		# We're not in the PORTDIR
		print
		print darkred("We're not in PORTDIR..."),
		while mydir!="/":
			if os.path.exists(mydir+"/profiles/package.mask"):
				# mydir == a PORTDIR root... We make PORTDIR = mydir.
				print darkgreen("setting to:")+" "+bold(mydir)
				os.environ["PORTDIR"]=mydir
				reload(portage)
				break;
			else:
				mydir=os.path.normpath(mydir+"/..")
		if mydir=="/":
			print darkred("unable to determine a PORTDIR.")
except Exception, e:
	print "!!! Error when determining valid(ity of) PORTDIR:"
	print "!!!",e
	pass

reporoot=None
for x in REPOROOTS:
	if myreporoot[0:len(x)]==x:
		reporoot=myreporoot
if not reporoot:
	err("Couldn't recognize repository type.  Supported repositories:\n"+repr(REPOROOTS))
reposplit=string.split(myreporoot,"/")
repolevel=len(reposplit)
startdir=os.getcwd()
for x in range(0,repolevel-1):
	os.chdir("..")
repodir=os.getcwd()
os.chdir(startdir)
def caterror(mycat):
	err(mycat+" is not an official category.  Skipping QA checks in this directory.\nPlease ensure that you add "+catdir+" to "+repodir+"/profiles/categories\nif it is a new category.")
print
if "--pretend" in myoptions:
	print green("RepoMan pretends to scour the neighborhood...")
else:
	print green("RepoMan scours the neighborhood...")
scanlist=[]
if repolevel==2:
	#we are inside a category directory
	catdir=reposplit[-1]
	if catdir not in portage.categories:
		caterror(catdir)
	mydirlist=os.listdir(startdir)
	for x in mydirlist:
		if x=="CVS":
			continue
		if os.path.isdir(startdir+"/"+x):
			scanlist.append(catdir+"/"+x)
elif repolevel==1:
	for x in portage.categories:
		if not os.path.isdir(startdir+"/"+x):
			continue
		for y in os.listdir(startdir+"/"+x):
			if y=="CVS":
				continue
			if os.path.isdir(startdir+"/"+x+"/"+y):
				scanlist.append(x+"/"+y)
elif repolevel==3:
	catdir = reposplit[-2]
	if catdir not in portage.categories:
		caterror(catdir)
	scanlist.append(catdir+"/"+reposplit[-1])

stats={}
fails={}
#objsadded records all object being added to cvs
objsadded=[]
for x in qacats:
	stats[x]=0
	fails[x]=[]
for x in scanlist:
	#ebuilds and digests added to cvs respectively.
	eadded=[]
	dadded=[]
	catdir,pkgdir=x.split("/")
	checkdir=repodir+"/"+x
	checkdirlist=os.listdir(checkdir)
	ebuildlist=[]
	for y in checkdirlist:
		if y[-7:]==".ebuild":
			ebuildlist.append(y[:-7])
	digestlist=[]
	try:
		myf=open(checkdir+"/CVS/Entries","r")
		myl=myf.readlines()
		for l in myl:
			if l[0]!="/":
				continue
			splitl=l[1:].split("/")
			if not len(splitl):
				continue
			objsadded.append(splitl[0])
			if splitl[0][-7:]==".ebuild":
				eadded.append(splitl[0][:-7])
	except IOError:
		continue
	try:
		myf=open(checkdir+"/files/CVS/Entries","r")
		myl=myf.readlines()
		for l in myl:
			if l[0]!="/":
				continue
			splitl=l[1:].split("/")
			if not len(splitl):
				continue
			objsadded.append(splitl[0])
			if splitl[0][:7]=="digest-":
				dadded.append(splitl[0][7:])
	except IOError:
		continue
	if os.path.exists(checkdir+"/files"):
		filesdirlist=os.listdir(checkdir+"/files")
		for y in filesdirlist:
			if y[:7]=="digest-":
				if y[7:] not in dadded:
					#digest not added to cvs
					stats["digest.notadded"]=stats["digest.notadded"]+1
					fails["digest.notadded"].append(x+"/files/"+y)
					if y[7:] in eadded:
						stats["digest.disjointed"]=stats["digest.disjointed"]+1
						fails["digest.disjointed"].append(x+"/files/"+y)
				if y[7:] not in ebuildlist:
					#stray digest
					if mymode=="fix":
						if "--pretend" in myoptions:
							print "(cd "+repodir+"/"+x+"/files; cvs rm -f "+y+")"
						else:
							os.system("(cd "+repodir+"/"+x+"/files; cvs rm -f "+y+")")					
					else:
						stats["digest.stray"]=stats["digest.stray"]+1
						fails["digest.stray"].append(x+"/files/"+y)
					
	if not "ChangeLog" in checkdirlist:
		stats["changelog.missing"]=stats["changelog.missing"]+1
		fails["changelog.missing"].append(x+"/ChangeLog")
	for y in ebuildlist:
		if y not in eadded:
			#ebuild not added to cvs
			stats["ebuild.notadded"]=stats["ebuild.notadded"]+1
			fails["ebuild.notadded"].append(x+"/"+y+".ebuild")
			if y in dadded:
				stats["ebuild.disjointed"]=stats["ebuild.disjointed"]+1
				fails["ebuild.disjointed"].append(x+"/"+y+".ebuild")
		if not os.path.exists(checkdir+"/files/digest-"+y):
			if mymode=="fix":
				if "--pretend" in myoptions:
					print "/usr/sbin/ebuild "+repodir+"/"+x+"/"+y+".ebuild digest"
				else:
					os.system("/usr/sbin/ebuild "+repodir+"/"+x+"/"+y+".ebuild digest")
			else:
				stats["digest.missing"]=stats["digest.missing"]+1
				fails["digest.missing"].append(x+"/files/digest-"+y)
		myesplit=portage.pkgsplit(y)
		if myesplit==None:
			stats["ebuild.invalidname"]=stats["ebuild.invalidname"]+1
			fails["ebuild.invalidname"].append(x+"/"+y+".ebuild")
		elif myesplit[0]!=pkgdir:
			print pkgdir,myesplit[0]
			stats["ebuild.namenomatch"]=stats["ebuild.namenomatch"]+1
			fails["ebuild.namenomatch"].append(x+"/"+y+".ebuild")
		try:
			myaux=portage.db["/"]["porttree"].dbapi.aux_get(catdir+"/"+y,allvars,strict=1)
		except KeyError:
			stats["ebuild.syntax"]=stats["ebuild.syntax"]+1
			fails["ebuild.syntax"].append(x+"/"+y+".ebuild")
			continue
		except IOError:
			stats["ebuild.output"]=stats["ebuild.output"]+1
			fails["ebuild.output"].append(x+"/"+y+".ebuild")
			continue
		for pos in range(0,len(missingvars)):
			if myaux[pos]=="":
				myqakey=missingvars[pos]+".missing"
				stats[myqakey]=stats[myqakey]+1
				fails[myqakey].append(x+"/"+y+".ebuild")
		if not catdir+"/"+y in portage.db["/"]["porttree"].dbapi.xmatch("list-visible",x):
			#we are testing deps for a masked package; give it some lee-way
			suffix="masked"
			matchmode="match-all"
		else:
			suffix=""
			matchmode="match-visible"
		for mytype,mypos in [["DEPEND",len(missingvars)],["RDEPEND",len(missingvars)+1]]:
			mykey=mytype+".bad"+suffix
			mydep=portage.dep_check(myaux[mypos],portage.db["/"]["porttree"].dbapi,use="all",mode=matchmode)
			if mydep[0]==1:
				if mydep[1]!=[]:
					#we have some unsolvable deps
					#remove ! deps, which always show up as unsatisfiable
					d=0
					while d<len(mydep[1]):
						if mydep[1][d][0]=="!":
							del mydep[1][d]
						else:
							d += 1
					#if we emptied out our list, continue:
					if not mydep[1]:
						continue
					stats[mykey]=stats[mykey]+1
					fails[mykey].append(x+"/"+y+".ebuild: "+repr(mydep[1]))
			else:
					stats[mykey]=stats[mykey]+1
					fails[mykey].append(x+"/"+y+".ebuild: "+repr(mydep[1]))
		if not os.system("egrep '\([^)]*\<die\>' "+checkdir+"/"+y+".ebuild >/dev/null 2>&1"):
			stats["ebuild.nesteddie"]=stats["ebuild.nesteddie"]+1
			fails["ebuild.nesteddie"].append(x+"/"+y+".ebuild")

print
#dofail will be set to 1 if we have failed in at least one non-warning category
dofail=0
#dowarn will be set to 1 if we tripped any warnings
dowarn=0
#dofull will be set if we should print a "repoman full" informational message
dofull=0
for x in qacats:
	if stats[x]:
		dowarn=1
		if x not in qawarnings:
			dofail=1
	else:
		if mymode!="full":
			continue
	print "  "+string.ljust(x,20),
	if stats[x]==0:
		print green(`stats[x]`)
		continue
	elif x in qawarnings:
		print yellow(`stats[x]`)
	else:
		print red(`stats[x]`)
	if mymode!="full":
		if stats[x]<12:
			for y in fails[x]:
				print "   "+y
		else:
			dofull=1
	else:
		for y in fails[x]:
			print "   "+y
print
if mymode!="commit":
	if dofull:
		print bold("Note: type \"repoman full\" for a complete listing.")
		print
	if dowarn and not dofail:
		print green("RepoMan sez:"),"\"You're only giving me a partial QA payment?\nI'll take it this time, but I'm not happy.\""
	elif not dofail:
		print green("RepoMan sez:"),"\"If everyone were like you, I'd be out of business!\""
	print
else:
	if dofail:
		print turquoise("Please fix these important QA issues first.")
		print green("RepoMan sez:"),"\"Make your QA payment on time and you'll never see the likes of me.\"\n"
		sys.exit(1)
	if "--pretend" in myoptions:
		print
		retval=os.system("/usr/bin/cvs -qn update")
		print
		print "Dry-run cvs update complete."
		print green("RepoMan sez:"), "\"So, you want to play it safe. Good call.\"\n"
	else:
		print "Please enter a CVS commit message at the prompt:"
		try:
			mycomment=raw_input(green("> "))
		except KeyboardInterrupt: 
			exithandler()
		print
		#backslash any single quotes
		mymsg=open("/tmp/.repoman.msg","w")
		mymsg.write(mycomment)
		mymsg.close()
		retval=os.system("/usr/bin/cvs -q commit -F /tmp/.repoman.msg")
		os.unlink("/tmp/.repoman.msg")
		print
		print "Cvs commit complete."
		print green("RepoMan sez:"), "\"If everyone were like you, I'd be out of business!\"\n"
	sys.exit(retval)
sys.exit(0)
