#!/usr/bin/python import sys, os, output, commands, types, re, time, shutil from os import * from os.path import * from commands import * from popen2 import * from select import * from re import match from optparse import OptionParser err_closed = re.compile(".*Connection closed unexpectedly.*") err_timeout = re.compile(".*Connection timed out.*") err_malformed = re.compile(".*Malformed.*") err_validate = re.compile("$Error validating server certificate for.*") err_locked = re.compile(".*Working copy.*locked.*") err_nonexistent = re.compile("(.*Not a versioned resource.*)|(.*Not a valid URL.*)|(.*File not found.*)|(.*is not a working copy.*)") sleep_time=30 def einfo(message): sys.stdout.write(output.green(" * ") + message + "\n") def ewarn(message): sys.stderr.write(output.yellow(" * ") + message + "\n") def eerror(message): sys.stderr.write(output.red(" * ") + message + "\n") def svninfo(message): sys.stdout.write(output.teal(" SVN: ") + message + "\n") def svnwarn(message): sys.stderr.write(output.yellow(" SVN: ") + message + "\n") def svnerr(message): sys.stderr.write(output.red(" SVN: ") + message + "\n") ## Class item_info encapsulates information from the svn info command regarding a module class item_info: def __init__(self, handler, item): self.handler = handler self.item = item.strip("/ ") self.wc = self.handler.base + "/" + self.item self.tags = {} self.update(False) if not self.handler.offline: self.update(True) def info(self, item, local=False): tags = {} def on_output(line, tags=tags): line=line.strip() if err_nonexistent.match(line): tags["Node Kind"] = "nonexistent" elif line != "": (tag, value) = line.split(":", 1) tags[tag] = value.strip() def on_error(line, tags=tags): if err_nonexistent.match(line): tags["Node Kind"] = "nonexistent" if local and not exists(item): tags["Node Kind"] = "nonexistent" else: command="svn info " + item self.handler.perform(command, on_output, on_error) return tags def modifiedP(self): return (not self.existsP(False)) or (self.existsP(True) and self.revision(False) != self.revision(True)) # Determine type of module (file, directory or missing) def type(self, remote=None): if remote == None: remote = not self.handler.offline return self.tags[remote]["Node Kind"] # Determine if module is a file def fileP(self, remote=None): if remote == None: remote = not self.handler.offline return self.type(remote) == "file" # Determine if module is a directory def directoryP(self, remote=None): if remote == None: remote = not self.handler.offline return self.type(remote) == "directory" # Determine if module exists def existsP(self, remote=None): if remote == None: remote = not self.handler.offline return not self.type(remote) == "nonexistent" def revision(self, remote=None): if remote == None: remote = not self.handler.offline for tag in "Last Changed Rev", "Revision": if tag in self.tags[remote]: return self.tags[remote][tag] else: eerror("QA notice: revision could not be determined") def mode(self, oldmode): modepath = self.wc + "/.svn.eclass.mode" if isfile(modepath): modefile = file(modepath, "r") mode = {"deep":True, "shallow":False}[modefile.readline().strip()] modefile.close() return mode elif self.directoryP(False): return True else: return oldmode def setmode(self, mode): modepath = self.wc + "/.svn.eclass.mode" modefile = file(modepath, "w") modefile.write({True:"deep", False:"shallow"}[mode]) modefile.close() def remove(self): ewarn("Deleting " + self.wc) shutil.rmtree(self.wc) self.update(False) def update(self, remote=False): if not remote: self.tags[False] = self.info(self.wc, True) else: if self.existsP(False): self.repository = self.tags[False]["URL"].strip("/ ") else: self.repository = self.handler.repository + "/" + self.item self.tags[True] = self.info(self.repository) def log(self, revision): einfo("Differences between revision " + revision + " and latest revision " + self.revision()) def on_output(line): if line!="": svninfo(line) command="svn log --revision " + str(int(revision)+1) + ":" + self.revision() + " " + self.repository self.handler.perform(command, on_output) # Construct complete list of improper ancestors of item class subversion_handler: def __init__(self, repository, path, offline): self.repository = repository self.path = path self.offline = offline self.infos = {} self.base = repository[repository.rfind("/")+1:] chdir(path) def handle_error(self, error, command, on_output, on_error): if err_closed.match(error) or err_timeout.match(error) or err_malformed.match(error): ewarn("Subversion has reported the following network error:") svnwarn(error.split(":")[1].strip()) einfo("Retrying in " + str(sleep_time) + " seconds") time.sleep(sleep_time) self.perform(command, on_output, on_error) elif err_locked.match(error): ewarn("Subversion reports a lock, another ebuild might be active.") ewarn("If no other ebuilds are active, interrupt, and execute svn cleanup on the reported directory:") svnwarn(error.split(":")[1].strip()) einfo("Retrying in " + str(sleep_time) + " seconds") time.sleep(sleep_time) self.perform(command, on_output, on_error) else: on_error(error) return True def perform(self, command, on_output=svninfo, on_error=svnerr): process=Popen3(command, True) (stdin, stdout, stderr) = (process.tochild, process.fromchild, process.childerr) error = False while not error: (readables, writables, errors) = select([stdout, stderr], [], [stdin, stdout, stderr]) for stream in readables: for line in stream: if stream == stdout: on_output(line.strip()) elif stream == stderr: error = self.handle_error(line.strip(), command, on_output, on_error) if error: break if stderr.readline() == "" and stdout.readline() == "": break else: stdin.close and stdout.close and stderr.close def checkout(self, module="", recurse=False, ignore_externals=False): url=(self.repository + module).rstrip("/ ") einfo("Performing initial " + {True:"recursive "}.get(recurse, "") + "checkout of " + url) command="svn checkout --ignore-externals " + {False:"-N "}.get(recurse, "") + url self.perform(command) def update(self, module="", recurse=False, ignore_externals=False): if not self.offline: url=(self.base + "/" + module).rstrip("/ ") einfo("Updating working copy of " + url + {True:" recursively"}.get(recurse, "")) command="svn update --ignore-externals " + {False:"-N "}.get(recurse, "") + url self.perform(command) def info(self, item=""): if item in self.infos: return self.infos[item] else: return item_info(self, item) def ancestors(item, recurse, descendents=[]): if item == "": return descendents[0] else: return ancestors(dirname(item), False, [[[[item, recurse]] + descendents]]) # Create tree of ancestries def ancestree(ancestries, ancestors): for ancestry in ancestries: if ancestry[0][0] == ancestors[0][0][0]: if ancestors[0][0][1] or ancestry[0][1]: ancestry[0][1] = True if len(ancestry) >= 1: del ancestry[1:] elif len(ancestry) == 1: ancestry.append(ancestors[0][1]) elif len(ancestors[0]) > 1: ancestree(ancestry[1], ancestors[0][1]) break else: ancestries += ancestors def print_ancestries(ancestries, indent=0): for ancestry in ancestries: if type(ancestry[0][0]) == types.StringType: tabspace = "" for i in range(indent): tabspace += " " print tabspace + {True:output.red, False:output.blue}[ancestry[0][1]](ancestry[0][0]) if len(ancestry) > 1: print_ancestries(ancestry[1], indent+1) else: print_ancestries(ancestry[1:], indent+1) def update_modules(ancestries, svnrevs): for ancestry in ancestries: module=ancestry[0][0] info = subversion.info(module) if type(module == types.StringType): recurse=ancestry[0][1] if info.directoryP(): if info.mode(recurse): recurse = True elif recurse: info.remove() #if info.modifiedP(): # Unfortunately there is no way to determine if files have been erased # So update always subversion.update(module, recurse) info.update() info.setmode(recurse) if svnrevs: svnrevs.write(module + ":" + info.revision(False) + "\n") if len(ancestry) > 1: update_modules(ancestry[1], svnrevs) else: update_modules(ancestry[1:], svnrevs) if __name__ == "__main__": parser = OptionParser() parser.add_option("--work-base", action="store", type="string", dest="work_base", help="local storage for working copies") parser.add_option("--repository", action="store", type="string", dest="repository", help="repository to check out from") parser.add_option("--revdb-in", action="store", type="string", dest="revdb_in", help="path to file to read revision numbers from") parser.add_option("--revdb-out", action="store", type="string", dest="revdb_out", help="path to file to record revision numbers in") parser.add_option("--deep", action="append", type="string", dest="deep", default=[], help="item to be checked out recursively") parser.add_option("--shallow", action="append", type="string", dest="shallow", default=[], help="item to be checked out non-recursively") parser.add_option("--check", action="append", type="string", dest="check", default=[], help="item to be checked for revision changes") parser.add_option("--logonly", action="store_true", dest="logonly", help="") parser.add_option("--checkrevs", action="store_true", dest="checkrevs", help="") parser.add_option("--offline", action="store_true", dest="offline", help="specify if operation is to be in offline mode") (values, args) = parser.parse_args() work_base = values.work_base repository = values.repository revdb_in = values.revdb_in and file(values.revdb_in, "r") revdb_out = values.revdb_out and file(values.revdb_out, "w") deep = [ deep.strip("/ ") for deep in values.deep ] shallow = [ shallow.strip("/ ") for shallow in values.shallow ] check = [ check.strip("/ ") for check in values.check ] checkrevs = values.checkrevs logonly = values.logonly revisions = {} base_module=repository[repository.rfind("/")+1:] working_copy=work_base + "/" + base_module if not isdir(work_base): einfo("Creating local repository for working copies in " + work_base) mkdir(work_base) subversion = subversion_handler(repository, work_base, values.offline) # Extract revisions from input revision database of item:revision pairs if revdb_in: for line in revdb_in: (item, revision) = line.split(":") revisions[item] = revision.strip() revdb_in.close() if checkrevs and len(check) > 0: for item in check: einfo({True:"(offline) "}.get(values.offline, "") + "Comparing installed vs. latest revision of " + item) info = subversion.info(item) # If repository claims item as directory, compare its revision to revision provided if info.directoryP(): if item not in revisions: break elif info.revision() != revisions[item]: if logonly: info.log(revisions[item]) else: break # If repository claims item as file, compare its topdir's revision to revision provided for topdir elif info.fileP(): topdir = dirname(item) einfo({True:"(offline) "}.get(values.offline, "") + "Comparing installed vs. latest revision of " + topdir) topdir_info = subversion.info(topdir) if topdir not in revisions: break elif topdir_info.revision() != revisions[topdir]: if logonly: topdir_info.log(revisions[topdir]) else: break # If repository claims item does not exist, see if it is in provided revision elif item in revisions: if not logonly: break else: if logonly: sys.exit(17) else: einfo("Revisions have NOT changed") sys.exit(16) einfo("Revisions have changed") if logonly: sys.exit(17) if not values.offline: einfo("Synchronizing with repository at " + repository + " in " + work_base) if isdir(working_copy + "/.svn"): if subversion.info().modifiedP(): subversion.update() else: subversion.checkout() ancestries=[] for item in deep: ancestree(ancestries, ancestors(item, True)) for item in shallow: ancestree(ancestries, ancestors(item, False)) #print_ancestries(ancestries) update_modules(ances