Setting tag util-vserver-pl-0.4-11
[build.git] / module-tools.py
index 45cddb9..6b41c35 100755 (executable)
@@ -1,13 +1,31 @@
 #!/usr/bin/python -u
 
-subversion_id = "$Id$"
-
-import sys, os, os.path
+import sys, os
 import re
 import time
 from glob import glob
 from optparse import OptionParser
 
+# HARDCODED NAME CHANGES
+#
+# Moving to git we decided to rename some of the repositories. Here is
+# a map of name changes applied in git repositories.
+RENAMED_SVN_MODULES = {
+    "PLEWWW": "plewww"
+    }
+
+def svn_to_git_name(module):
+    if RENAMED_SVN_MODULES.has_key(module):
+        return RENAMED_SVN_MODULES[module]
+    return module
+
+def git_to_svn_name(module):
+    for key in RENAMED_SVN_MODULES:
+        if module == RENAMED_SVN_MODULES[key]:
+            return key
+    return module
+    
+
 # e.g. other_choices = [ ('d','iff') , ('g','uess') ] - lowercase 
 def prompt (question,default=True,other_choices=[],allow_outside=False):
     if not isinstance (other_choices,list):
@@ -55,6 +73,15 @@ def default_editor():
         editor = "emacs"
     return editor
 
+### fold long lines
+fold_length=132
+
+def print_fold (line):
+    while len(line) >= fold_length:
+        print line[:fold_length],'\\'
+        line=line[fold_length:]
+    print line
+
 class Command:
     def __init__ (self,command,options):
         self.command=command
@@ -113,26 +140,237 @@ class Command:
             print 'Done',
         return result
 
-class Svnpath:
-    def __init__(self,path,options):
-        self.path=path
-        self.options=options
 
-    def url_exists (self):
-        return os.system("svn list %s &> /dev/null"%self.path) == 0
+class SvnRepository:
+    type = "svn"
+
+    def __init__(self, path, options):
+        self.path = path
+        self.options = options
+
+    def name(self):
+        return os.path.basename(self.path)
+
+    def url(self):
+        out = Command("svn info %s" % self.path, self.options).output_of()
+        for line in out.split('\n'):
+            if line.startswith("URL:"):
+                return line.split()[1].strip()
+
+    def repo_root(self):
+        out = Command("svn info %s" % self.path, self.options).output_of()
+        for line in out.split('\n'):
+            if line.startswith("Repository Root:"):
+                root = line.split()[2].strip()
+                return "%s/%s" % (root, self.name())
+
+    @classmethod
+    def checkout(cls, remote, local, options, recursive=False):
+        if recursive:
+            svncommand = "svn co %s %s" % (remote, local)
+        else:
+            svncommand = "svn co -N %s %s" % (remote, local)
+        Command("rm -rf %s" % local, options).run_silent()
+        Command(svncommand, options).run_fatal()
+
+        return SvnRepository(local, options)
+
+    @classmethod
+    def remote_exists(cls, remote):
+        return os.system("svn list %s &> /dev/null" % remote) == 0
+
+    def tag_exists(self, tagname):
+        url = "%s/tags/%s" % (self.repo_root(), tagname)
+        return SvnRepository.remote_exists(url)
+
+    def update(self, subdir="", recursive=True):
+        path = os.path.join(self.path, subdir)
+        if recursive:
+            svncommand = "svn up %s" % path
+        else:
+            svncommand = "svn up -N %s" % path
+        Command(svncommand, self.options).run_fatal()
+
+    def commit(self, logfile):
+        # add all new files to the repository
+        Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs svn add" %
+                self.path, self.options).run_silent()
+        Command("svn commit -F %s %s" % (logfile, self.path), self.options).run_fatal()
+
+    def to_branch(self, branch):
+        remote = "%s/branches/%s" % (self.repo_root(), branch)
+        SvnRepository.checkout(remote, self.path, self.options, recursive=True)
+
+    def to_tag(self, tag):
+        remote = "%s/tags/%s" % (self.repo_root(), branch)
+        SvnRepository.checkout(remote, self.path, self.options, recursive=True)
+
+    def tag(self, tagname, logfile):
+        tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
+        self_url = self.url()
+        Command("svn copy -F %s %s %s" % (logfile, self_url, tag_url), self.options).run_fatal()
+
+    def diff(self, f=""):
+        if f:
+            f = os.path.join(self.path, f)
+        else:
+            f = self.path
+        return Command("svn diff %s" % f, self.options).output_of(True)
+
+    def diff_with_tag(self, tagname):
+        tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
+        return Command("svn diff %s %s" % (tag_url, self.url()),
+                       self.options).output_of(True)
+
+    def revert(self, f=""):
+        if f:
+            Command("svn revert %s" % os.path.join(self.path, f), self.options).run_fatal()
+        else:
+            # revert all
+            Command("svn revert %s -R" % self.path, self.options).run_fatal()
+            Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs rm -rf " %
+                    self.path, self.options).run_silent()
+
+    def is_clean(self):
+        command="svn status %s" % self.path
+        return len(Command(command,self.options).output_of(True)) == 0
+
+    def is_valid(self):
+        return os.path.exists(os.path.join(self.path, ".svn"))
+    
+
+class GitRepository:
+    type = "git"
+
+    def __init__(self, path, options):
+        self.path = path
+        self.options = options
+
+    def name(self):
+        return os.path.basename(self.path)
+
+    def url(self):
+        self.repo_root()
+
+    def repo_root(self):
+        c = Command("git remote show origin", self.options)
+        out = self.__run_in_repo(c.output_of)
+        for line in out.split('\n'):
+            if line.strip().startswith("Fetch URL:"):
+                repo = line.split()[2]
+
+    @classmethod
+    def checkout(cls, remote, local, options, depth=1):
+        Command("rm -rf %s" % local, options).run_silent()
+        Command("git clone --depth %d %s %s" % (depth, remote, local), options).run_fatal()
+        return GitRepository(local, options)
+
+    @classmethod
+    def remote_exists(cls, remote):
+        return os.system("git --no-pager ls-remote %s &> /dev/null" % remote) == 0
+
+    def tag_exists(self, tagname):
+        command = 'git tag -l | grep "^%s$"' % tagname
+        c = Command(command, self.options)
+        out = self.__run_in_repo(c.output_of, with_stderr=True)
+        return len(out) > 0
+
+    def __run_in_repo(self, fun, *args, **kwargs):
+        cwd = os.getcwd()
+        os.chdir(self.path)
+        ret = fun(*args, **kwargs)
+        os.chdir(cwd)
+        return ret
+
+    def __run_command_in_repo(self, command, ignore_errors=False):
+        c = Command(command, self.options)
+        if ignore_errors:
+            return self.__run_in_repo(c.output_of)
+        else:
+            return self.__run_in_repo(c.run_fatal)
+
+    def update(self, subdir=None, recursive=None):
+        return self.__run_command_in_repo("git pull")
+
+    def to_branch(self, branch, remote=True):
+        if remote:
+            branch = "origin/%s" % branch
+        return self.__run_command_in_repo("git checkout %s" % branch)
+
+    def to_tag(self, tag):
+        return self.__run_command_in_repo("git checkout %s" % tag)
+
+    def tag(self, tagname, logfile):
+        self.__run_command_in_repo("git tag %s -F %s" % (tagname, logfile))
+        self.commit(logfile)
+
+    def diff(self, f=""):
+        c = Command("git diff %s" % f, self.options)
+        return self.__run_in_repo(c.output_of, with_stderr=True)
+
+    def diff_with_tag(self, tagname):
+        c = Command("git diff %s" % tagname, self.options)
+        return self.__run_in_repo(c.output_of, with_stderr=True)
+
+    def commit(self, logfile):
+        self.__run_command_in_repo("git add -A", ignore_errors=True)
+        self.__run_command_in_repo("git commit -F  %s" % logfile, ignore_errors=True)
+        self.__run_command_in_repo("git push")
+        self.__run_command_in_repo("git push --tags")
+
+    def revert(self, f=""):
+        if f:
+            self.__run_command_in_repo("git checkout %s" % f)
+        else:
+            # revert all
+            self.__run_command_in_repo("git --no-pager reset --hard")
+            self.__run_command_in_repo("git --no-pager clean -f")
+
+    def is_clean(self):
+        def check_commit():
+            command="git status"
+            s="nothing to commit (working directory clean)"
+            return Command(command, self.options).output_of(True).find(s) >= 0
+        return self.__run_in_repo(check_commit)
+
+    def is_valid(self):
+        return os.path.exists(os.path.join(self.path, ".git"))
+    
+
+class Repository:
+    """ Generic repository """
+    supported_repo_types = [SvnRepository, GitRepository]
+
+    def __init__(self, path, options):
+        self.path = path
+        self.options = options
+        for repo in self.supported_repo_types:
+            self.repo = repo(self.path, self.options)
+            if self.repo.is_valid():
+                break
+
+    @classmethod
+    def has_moved_to_git(cls, module, svnpath):
+        module = git_to_svn_name(module)
+        return SvnRepository.remote_exists("%s/%s/aaaa-has-moved-to-git" % (svnpath, module))
+
+    @classmethod
+    def remote_exists(cls, remote):
+        for repo in Repository.supported_repo_types:
+            if repo.remote_exists(remote):
+                return True
+        return False
+
+    def __getattr__(self, attr):
+        return getattr(self.repo, attr)
+
 
-    def dir_needs_revert (self):
-        command="svn status %s"%self.path
-        return len(Command(command,self.options).output_of(True)) != 0
-    # turns out it's the same implem.
-    def file_needs_commit (self):
-        command="svn status %s"%self.path
-        return len(Command(command,self.options).output_of(True)) != 0
 
 # support for tagged module is minimal, and is for the Build class only
 class Module:
 
     svn_magic_line="--This line, and those below, will be ignored--"
+    setting_tag_format = "Setting tag %s"
     
     redirectors=[ # ('module_name_varname','name'),
                   ('module_version_varname','version'),
@@ -146,21 +384,27 @@ class Module:
     import commands
     configKeys=[ ('svnpath',"Enter your toplevel svnpath",
                   "svn+ssh://%s@svn.planet-lab.org/svn/"%commands.getoutput("id -un")),
+                 ('gitserver', "Enter your git server's hostname", "git.onelab.eu"),
+                 ('gituser', "Enter your user name (login name) on git server", os.getlogin()),
                  ("build", "Enter the name of your build module","build"),
                  ('username',"Enter your firstname and lastname for changelogs",""),
                  ("email","Enter your email address for changelogs",""),
                  ]
 
-    @staticmethod
-    def prompt_config ():
-        for (key,message,default) in Module.configKeys:
-            Module.config[key]=""
-            while not Module.config[key]:
-                Module.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
+    @classmethod
+    def prompt_config_option(cls, key, message, default):
+        cls.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
+
+    @classmethod
+    def prompt_config (cls):
+        for (key,message,default) in cls.configKeys:
+            cls.config[key]=""
+            while not cls.config[key]:
+                cls.prompt_config_option(key, message, default)
 
 
     # for parsing module spec name:branch
-    matcher_branch_spec=re.compile("\A(?P<name>[\w-]+):(?P<branch>[\w\.-]+)\Z")
+    matcher_branch_spec=re.compile("\A(?P<name>[\w\.-]+):(?P<branch>[\w\.-]+)\Z")
     # special form for tagged module - for Build
     matcher_tag_spec=re.compile("\A(?P<name>[\w-]+)@(?P<tagname>[\w\.-]+)\Z")
     # parsing specfiles
@@ -180,75 +424,88 @@ class Module:
             else:
                 self.name=module_spec
 
+        # when available prefer to use git module name internally
+        self.name = svn_to_git_name(self.name)
+
         self.options=options
         self.module_dir="%s/%s"%(options.workdir,self.name)
-
-    def friendly_name (self):
-        if hasattr(self,'branch'):
-            return "%s:%s"%(self.name,self.branch)
-        elif hasattr(self,'tagname'):
-            return "%s@%s"%(self.name,self.tagname)
-        else:
-            return self.name
-
-    def edge_dir (self):
-        if hasattr(self,'branch'):
-            return "%s/branches/%s"%(self.module_dir,self.branch)
-        elif hasattr(self,'tagname'):
-            return "%s/tags/%s"%(self.module_dir,self.tagname)
-        else:
-            return "%s/trunk"%(self.module_dir)
-
-    def tags_dir (self):
-        return "%s/tags"%(self.module_dir)
+        self.repository = None
+        self.build = None
 
     def run (self,command):
         return Command(command,self.options).run()
     def run_fatal (self,command):
         return Command(command,self.options).run_fatal()
-    def run_prompt (self,message,command):
+    def run_prompt (self,message,fun, *args):
+        fun_msg = "%s(%s)" % (fun.func_name, ",".join(args))
         if not self.options.verbose:
             while True:
                 choice=prompt(message,True,('s','how'))
                 if choice is True:
-                    self.run(command)
+                    fun(*args)
                     return
                 elif choice is False:
-                    return
-                else:
-                    print 'About to run:',command
+                    print 'About to run function:', fun_msg
         else:
-            question=message+" - want to run " + command
+            question=message+" - want to run function: " + fun_msg
             if prompt(question,True):
-                self.run(command)            
+                fun(*args)
 
-    @staticmethod
-    def init_homedir (options):
-        topdir=options.workdir
+    def friendly_name (self):
+        if hasattr(self,'branch'):
+            return "%s:%s"%(self.name,self.branch)
+        elif hasattr(self,'tagname'):
+            return "%s@%s"%(self.name,self.tagname)
+        else:
+            return self.name
+
+    @classmethod
+    def git_remote_dir (cls, name):
+        return "%s@%s:/git/%s.git" % (cls.config['gituser'], cls.config['gitserver'], name)
+
+    @classmethod
+    def svn_remote_dir (cls, name):
+        name = git_to_svn_name(name)
+        svn = cls.config['svnpath']
+        if svn.endswith('/'):
+            return "%s%s" % (svn, name)
+        return "%s/%s" % (svn, name)
+
+    def svn_selected_remote(self):
+        svn_name = git_to_svn_name(self.name)
+        remote = self.svn_remote_dir(svn_name)
+        if hasattr(self,'branch'):
+            remote = "%s/branches/%s" % (remote, self.branch)
+        elif hasattr(self,'tagname'):
+            remote = "%s/tags/%s" % (remote, self.tagname)
+        else:
+            remote = "%s/trunk" % remote
+        return remote
+
+    ####################
+    @classmethod
+    def init_homedir (cls, options):
         if options.verbose and options.mode not in Main.silent_modes:
-            print 'Checking for',topdir
-        storage="%s/%s"%(topdir,Module.config_storage)
+            print 'Checking for', options.workdir
+        storage="%s/%s"%(options.workdir, cls.config_storage)
         # sanity check. Either the topdir exists AND we have a config/storage
         # or topdir does not exist and we create it
         # to avoid people use their own daily svn repo
-        if os.path.isdir(topdir) and not os.path.isfile(storage):
+        if os.path.isdir(options.workdir) and not os.path.isfile(storage):
             print """The directory %s exists and has no CONFIG file
 If this is your regular working directory, please provide another one as the
 module-* commands need a fresh working dir. Make sure that you do not use 
-that for other purposes than tagging"""%topdir
+that for other purposes than tagging""" % options.workdir
             sys.exit(1)
-        if not os.path.isdir (topdir):
-            print "Cannot find",topdir,"let's create it"
-            Module.prompt_config()
-            print "Checking ...",
-            Command("svn co -N %s %s"%(Module.config['svnpath'],topdir),options).run_fatal()
-            Command("svn co -N %s/%s %s/%s"%(Module.config['svnpath'],
-                                             Module.config['build'],
-                                             topdir,
-                                             Module.config['build']),options).run_fatal()
+
+        def checkout_build():
+            print "Checking out build module..."
+            remote = cls.git_remote_dir(cls.config['build'])
+            local = os.path.join(options.workdir, cls.config['build'])
+            GitRepository.checkout(remote, local, options, depth=1)
             print "OK"
-            
-            # store config
+
+        def store_config():
             f=file(storage,"w")
             for (key,message,default) in Module.configKeys:
                 f.write("%s=%s\n"%(key,Module.config[key]))
@@ -256,13 +513,48 @@ that for other purposes than tagging"""%topdir
             if options.debug:
                 print 'Stored',storage
                 Command("cat %s"%storage,options).run()
-        else:
+
+        def read_config():
             # read config
             f=open(storage)
             for line in f.readlines():
                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
                 Module.config[key]=value                
             f.close()
+
+        if not os.path.isdir (options.workdir):
+            print "Cannot find",options.workdir,"let's create it"
+            Command("mkdir -p %s" % options.workdir, options).run_silent()
+            cls.prompt_config()
+            checkout_build()
+            store_config()
+        else:
+            read_config()
+            # check missing config options
+            old_layout = False
+            for (key,message,default) in cls.configKeys:
+                if not Module.config.has_key(key):
+                    print "Configuration changed for module-tools"
+                    cls.prompt_config_option(key, message, default)
+                    old_layout = True
+                    
+            if old_layout:
+                Command("rm -rf %s" % options.workdir, options).run_silent()
+                Command("mkdir -p %s" % options.workdir, options).run_silent()
+                checkout_build()
+                store_config()
+
+            build_dir = os.path.join(options.workdir, cls.config['build'])
+            if not os.path.isdir(build_dir):
+                checkout_build()
+            else:
+                build = Repository(build_dir, options)
+                if not build.is_clean():
+                    print "build module needs a revert"
+                    build.revert()
+                    print "OK"
+                build.update()
+
         if options.verbose and options.mode not in Main.silent_modes:
             print '******** Using config'
             for (key,message,default) in Module.configKeys:
@@ -271,60 +563,77 @@ that for other purposes than tagging"""%topdir
     def init_module_dir (self):
         if self.options.verbose:
             print 'Checking for',self.module_dir
+
         if not os.path.isdir (self.module_dir):
-            self.run_fatal("svn update -N %s"%self.module_dir)
-        if not os.path.isdir (self.module_dir):
+            if Repository.has_moved_to_git(self.name, Module.config['svnpath']):
+                self.repository = GitRepository.checkout(self.git_remote_dir(self.name),
+                                                         self.module_dir,
+                                                         self.options)
+            else:
+                remote = self.svn_selected_remote()
+                self.repository = SvnRepository.checkout(remote,
+                                                         self.module_dir,
+                                                         self.options, recursive=False)
+
+        self.repository = Repository(self.module_dir, self.options)
+        if self.repository.type == "svn":
+            # check if module has moved to git    
+            if Repository.has_moved_to_git(self.name, Module.config['svnpath']):
+                Command("rm -rf %s" % self.module_dir, self.options).run_silent()
+                self.init_module_dir()
+            # check if we have the required branch/tag
+            if self.repository.url() != self.svn_selected_remote():
+                Command("rm -rf %s" % self.module_dir, self.options).run_silent()
+                self.init_module_dir()
+
+        elif self.repository.type == "git":
+            if hasattr(self,'branch'):
+                self.repository.to_branch(self.branch)
+            elif hasattr(self,'tagname'):
+                self.repository.to_tag(self.tagname)
+
+        else:
             raise Exception, 'Cannot find %s - check module name'%self.module_dir
 
-    def init_subdir (self,fullpath):
-        if self.options.verbose:
-            print 'Checking for',fullpath
-        if not os.path.isdir (fullpath):
-            self.run_fatal("svn update -N %s"%fullpath)
 
-    def revert_subdir (self,fullpath):
+    def revert_module_dir (self):
         if self.options.fast_checks:
-            if self.options.verbose: print 'Skipping revert of %s'%fullpath
+            if self.options.verbose: print 'Skipping revert of %s' % self.module_dir
             return
         if self.options.verbose:
-            print 'Checking whether',fullpath,'needs being reverted'
-        if Svnpath(fullpath,self.options).dir_needs_revert():
-            self.run_fatal("svn revert -R %s"%fullpath)
+            print 'Checking whether', self.module_dir, 'needs being reverted'
+        
+        if not self.repository.is_clean():
+            self.repository.revert()
 
-    def update_subdir (self,fullpath):
+    def update_module_dir (self):
         if self.options.fast_checks:
-            if self.options.verbose: print 'Skipping update of %s'%fullpath
+            if self.options.verbose: print 'Skipping update of %s' % self.module_dir
             return
         if self.options.verbose:
-            print 'Updating',fullpath
-        self.run_fatal("svn update -N %s"%fullpath)
-
-    def init_edge_dir (self):
-        # if branch, edge_dir is two steps down
-        if hasattr(self,'branch'):
-            self.init_subdir("%s/branches"%self.module_dir)
-        elif hasattr(self,'tagname'):
-            self.init_subdir("%s/tags"%self.module_dir)
-        self.init_subdir(self.edge_dir())
-
-    def revert_edge_dir (self):
-        self.revert_subdir(self.edge_dir())
-
-    def update_edge_dir (self):
-        self.update_subdir(self.edge_dir())
+            print 'Updating', self.module_dir
+        self.repository.update()
 
     def main_specname (self):
-        attempt="%s/%s.spec"%(self.edge_dir(),self.name)
+        attempt="%s/%s.spec"%(self.module_dir,self.name)
         if os.path.isfile (attempt):
             return attempt
-        else:
-            try:
-                return glob("%s/*.spec"%self.edge_dir())[0]
-            except:
-                raise Exception, 'Cannot guess specfile for module %s'%self.name
+        pattern1="%s/*.spec"%self.module_dir
+        level1=glob(pattern1)
+        if level1:
+            return level1[0]
+        pattern2="%s/*/*.spec"%self.module_dir
+        level2=glob(pattern2)
+
+        if level2:
+            return level2[0]
+        raise Exception, 'Cannot guess specfile for module %s -- patterns were %s or %s'%(self.name,pattern1,pattern2)
 
     def all_specnames (self):
-        return glob("%s/*.spec"%self.edge_dir())
+        level1=glob("%s/*.spec" % self.module_dir)
+        if level1: return level1
+        level2=glob("%s/*/*.spec" % self.module_dir)
+        return level2
 
     def parse_spec (self, specfile, varnames):
         if self.options.verbose:
@@ -400,21 +709,27 @@ that for other purposes than tagging"""%topdir
             new.close()
             os.rename(newspecfile,specfile)
 
+    # returns all lines until the magic line
     def unignored_lines (self, logfile):
         result=[]
-        exclude="Tagging module %s"%self.name
         white_line_matcher = re.compile("\A\s*\Z")
         for logline in file(logfile).readlines():
             if logline.strip() == Module.svn_magic_line:
                 break
-            if logline.find(exclude) >= 0:
-                continue
             elif white_line_matcher.match(logline):
                 continue
             else:
                 result.append(logline.strip()+'\n')
         return result
 
+    # creates a copy of the input with only the unignored lines
+    def stripped_magic_line_filename (self, filein, fileout ,new_tag_name):
+       f=file(fileout,'w')
+       f.write(self.setting_tag_format%new_tag_name + '\n')
+       for line in self.unignored_lines(filein):
+           f.write(line)
+       f.close()
+
     def insert_changelog (self, logfile, oldtag, newtag):
         for specfile in self.all_specnames():
             newspecfile=specfile+".new"
@@ -443,180 +758,19 @@ that for other purposes than tagging"""%topdir
             for (k,v) in spec_dict.iteritems():
                 print k,'=',v
 
-    def mod_url (self):
-        return "%s/%s"%(Module.config['svnpath'],self.name)
-
-    def edge_url (self):
-        if hasattr(self,'branch'):
-            return "%s/branches/%s"%(self.mod_url(),self.branch)
-        elif hasattr(self,'tagname'):
-            return "%s/tags/%s"%(self.mod_url(),self.tagname)
-        else:
-            return "%s/trunk"%(self.mod_url())
-
-    def tag_name (self, spec_dict):
+    def last_tag (self, spec_dict):
         try:
-            return "%s-%s-%s"%(#spec_dict[self.module_name_varname],
-                self.name,
-                spec_dict[self.module_version_varname],
-                spec_dict[self.module_taglevel_varname])
+            return "%s-%s" % (spec_dict[self.module_version_varname],
+                              spec_dict[self.module_taglevel_varname])
         except KeyError,err:
-            raise Exception, 'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
-
-    def tag_url (self, spec_dict):
-        return "%s/tags/%s"%(self.mod_url(),self.tag_name(spec_dict))
-
-    def check_svnpath_exists (self, url, message):
-        if self.options.fast_checks:
-            return
-        if self.options.verbose:
-            print 'Checking url (%s) %s'%(url,message),
-        ok=Svnpath(url,self.options).url_exists()
-        if ok:
-            if self.options.verbose: print 'exists - OK'
-        else:
-            if self.options.verbose: print 'KO'
-            raise Exception, 'Could not find %s URL %s'%(message,url)
-
-    def check_svnpath_not_exists (self, url, message):
-        if self.options.fast_checks:
-            return
-        if self.options.verbose:
-            print 'Checking url (%s) %s'%(url,message),
-        ok=not Svnpath(url,self.options).url_exists()
-        if ok:
-            if self.options.verbose: print 'does not exist - OK'
-        else:
-            if self.options.verbose: print 'KO'
-            raise Exception, '%s URL %s already exists - exiting'%(message,url)
-
-    # locate specfile, parse it, check it and show values
-
-##############################
-    def do_version (self):
-        self.init_module_dir()
-        self.init_edge_dir()
-        self.revert_edge_dir()
-        self.update_edge_dir()
-        spec_dict = self.spec_dict()
-        for varname in self.varnames:
-            if not spec_dict.has_key(varname):
-                print 'Could not find %%define for %s'%varname
-                return
-            else:
-                print "%-16s %s"%(varname,spec_dict[varname])
-        if self.options.show_urls:
-            print "%-16s %s"%('edge url',self.edge_url())
-            print "%-16s %s"%('latest tag url',self.tag_url(spec_dict))
-        if self.options.verbose:
-            print "%-16s %s"%('main specfile:',self.main_specname())
-            print "%-16s %s"%('specfiles:',self.all_specnames())
-
-##############################
-    def do_list (self):
-#        print 'verbose',self.options.verbose
-#        print 'list_tags',self.options.list_tags
-#        print 'list_branches',self.options.list_branches
-#        print 'all_modules',self.options.all_modules
-        
-        (verbose,branches,pattern,exact) = (self.options.verbose,self.options.list_branches,
-                                            self.options.list_pattern,self.options.list_exact)
-
-        extra_command=""
-        extra_message=""
-        if hasattr(self,'branch'):
-            pattern=self.branch
-        if pattern or exact:
-            if exact:
-                if verbose: grep="%s/$"%exact
-                else: grep="^%s$"%exact
-            else:
-                grep=pattern
-            extra_command=" | grep %s"%grep
-            extra_message=" matching %s"%grep
-
-        if not branches:
-            message="==================== tags for %s"%self.friendly_name()
-            command="svn list "
-            if verbose: command+="--verbose "
-            command += "%s/tags"%self.mod_url()
-            command += extra_command
-            message += extra_message
-            if verbose: print message
-            self.run(command)
-
-        else:
-            message="==================== branches for %s"%self.friendly_name()
-            command="svn list "
-            if verbose: command+="--verbose "
-            command += "%s/branches"%self.mod_url()
-            command += extra_command
-            message += extra_message
-            if verbose: print message
-            self.run(command)
-
-##############################
-    sync_warning="""*** WARNING
-The module-sync function has the following limitations
-* it does not handle changelogs
-* it does not scan the -tags*.mk files to adopt the new tags"""
-
-    def do_sync(self):
-        if self.options.verbose:
-            print Module.sync_warning
-            if not prompt('Want to proceed anyway'):
-                return
-
-        self.init_module_dir()
-        self.init_edge_dir()
-        self.revert_edge_dir()
-        self.update_edge_dir()
-        spec_dict = self.spec_dict()
-
-        edge_url=self.edge_url()
-        tag_name=self.tag_name(spec_dict)
-        tag_url=self.tag_url(spec_dict)
-        # check the tag does not exist yet
-        self.check_svnpath_not_exists(tag_url,"new tag")
-
-        if self.options.message:
-            svnopt='--message "%s"'%self.options.message
-        else:
-            svnopt='--editor-cmd=%s'%self.options.editor
-        self.run_prompt("Create initial tag",
-                        "svn copy %s %s %s"%(svnopt,edge_url,tag_url))
-
-##############################
-    def do_diff (self,compute_only=False):
-        self.init_module_dir()
-        self.init_edge_dir()
-        self.revert_edge_dir()
-        self.update_edge_dir()
-        spec_dict = self.spec_dict()
-        self.show_dict(spec_dict)
+            raise Exception,'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
 
-        edge_url=self.edge_url()
-        tag_url=self.tag_url(spec_dict)
-        self.check_svnpath_exists(edge_url,"edge track")
-        self.check_svnpath_exists(tag_url,"latest tag")
-        command="svn diff %s %s"%(tag_url,edge_url)
-        if compute_only:
-            if self.options.verbose:
-                print 'Getting diff with %s'%command
-        diff_output = Command(command,self.options).output_of()
-        # if used as a utility
-        if compute_only:
-            return (spec_dict,edge_url,tag_url,diff_output)
-        # otherwise print the result
-        if self.options.list:
-            if diff_output:
-                print self.name
-        else:
-            if not self.options.only or diff_output:
-                print 'x'*30,'module',self.friendly_name()
-                print 'x'*20,'<',tag_url
-                print 'x'*20,'>',edge_url
-                print diff_output
+    def tag_name (self, spec_dict, old_svn_name=False):
+        base_tag_name = self.name
+        if old_svn_name:
+            base_tag_name = git_to_svn_name(self.name)
+        return "%s-%s" % (base_tag_name, self.last_tag(spec_dict))
+    
 
 ##############################
     # using fine_grain means replacing only those instances that currently refer to this tag
@@ -642,14 +796,16 @@ The module-sync function has the following limitations
         # brute-force : change uncommented lines that define <module>-SVNPATH
         else:
             if self.options.verbose:
-                print 'Setting %s-SVNPATH for using %s\n\tin %s .. '%(self.name,newname,tagsfile),
-            pattern="\A\s*%s-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
-                                          %(self.name,self.name)
+                print 'Searching for -SVNPATH lines referring to /%s/\n\tin %s .. '%(self.name,tagsfile),
+            pattern="\A\s*(?P<make_name>[^\s]+)-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
+                                          %(self.name)
             matcher_module=re.compile(pattern)
             for line in tags.readlines():
                 attempt=matcher_module.match(line)
                 if attempt:
-                    svnpath="%s-SVNPATH"%self.name
+                    svnpath="%s-SVNPATH"%(attempt.group('make_name'))
+                    if self.options.verbose:
+                        print ' '+svnpath, 
                     replacement = "%-32s:= %s/%s/tags/%s\n"%(svnpath,attempt.group('url_main'),self.name,newname)
                     new.write(replacement)
                     matches += 1
@@ -661,19 +817,49 @@ The module-sync function has the following limitations
         if self.options.verbose: print "%d changes"%matches
         return matches
 
+    def check_tag(self, tagname, need_it=False, old_svn_tag_name=None):
+        if self.options.verbose:
+            print "Checking %s repository tag: %s - " % (self.repository.type, tagname),
+
+        found_tagname = tagname
+        found = self.repository.tag_exists(tagname)
+        if not found and old_svn_tag_name:
+            if self.options.verbose:
+                print "KO"
+                print "Checking %s repository tag: %s - " % (self.repository.type, old_svn_tag_name),
+            found = self.repository.tag_exists(old_svn_tag_name)
+            if found:
+                found_tagname = old_svn_tag_name
+
+        if (found and need_it) or (not found and not need_it):
+            if self.options.verbose:
+                print "OK",
+                if found: print "- found"
+                else: print "- not found"
+        else:
+            if self.options.verbose:
+                print "KO"
+            if found:
+                raise Exception, "tag (%s) is already there" % tagname
+            else:
+                raise Exception, "can not find required tag (%s)" % tagname
+
+        return found_tagname
+
+
+##############################
     def do_tag (self):
         self.init_module_dir()
-        self.init_edge_dir()
-        self.revert_edge_dir()
-        self.update_edge_dir()
+        self.revert_module_dir()
+        self.update_module_dir()
         # parse specfile
         spec_dict = self.spec_dict()
         self.show_dict(spec_dict)
         
         # side effects
-        edge_url=self.edge_url()
         old_tag_name = self.tag_name(spec_dict)
-        old_tag_url=self.tag_url(spec_dict)
+        old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
+
         if (self.options.new_version):
             # new version set on command line
             spec_dict[self.module_version_varname] = self.options.new_version
@@ -683,16 +869,14 @@ The module-sync function has the following limitations
             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
             spec_dict[self.module_taglevel_varname] = new_taglevel
 
-        # sanity check
         new_tag_name = self.tag_name(spec_dict)
-        new_tag_url=self.tag_url(spec_dict)
-        self.check_svnpath_exists (edge_url,"edge track")
-        self.check_svnpath_exists (old_tag_url,"previous tag")
-        self.check_svnpath_not_exists (new_tag_url,"new tag")
+
+        # sanity check
+        old_tag_name = self.check_tag(old_tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
+        new_tag_name = self.check_tag(new_tag_name, need_it=False)
 
         # checking for diffs
-        diff_output=Command("svn diff %s %s"%(old_tag_url,edge_url),
-                            self.options).output_of()
+        diff_output = self.repository.diff_with_tag(old_tag_name)
         if len(diff_output) == 0:
             if not prompt ("No pending difference in module %s, want to tag anyway"%self.name,False):
                 return
@@ -704,12 +888,14 @@ The module-sync function has the following limitations
         # we use the standard subversion magic string (see svn_magic_line)
         # so we can provide useful information, such as version numbers and diff
         # in the same file
-        changelog="/tmp/%s-%d.txt"%(self.name,os.getpid())
-        file(changelog,"w").write("""Tagging module %s - %s
-
+        changelog="/tmp/%s-%d.edit"%(self.name,os.getpid())
+        changelog_svn="/tmp/%s-%d.svn"%(self.name,os.getpid())
+        setting_tag_line=Module.setting_tag_format%new_tag_name
+        file(changelog,"w").write("""
+%s
 %s
 Please write a changelog for this new tag in the section above
-"""%(self.name,new_tag_name,Module.svn_magic_line))
+"""%(Module.svn_magic_line,setting_tag_line))
 
         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
             file(changelog,"a").write('DIFF=========\n' + diff_output)
@@ -719,34 +905,35 @@ Please write a changelog for this new tag in the section above
 
         # edit it        
         self.run("%s %s"%(self.options.editor,changelog))
+        # strip magic line in second file - looks like svn has changed its magic line with 1.6
+        # so we do the job ourselves
+        self.stripped_magic_line_filename(changelog,changelog_svn,new_tag_name)
         # insert changelog in spec
         if self.options.changelog:
             self.insert_changelog (changelog,old_tag_name,new_tag_name)
 
         ## update build
-        try:
-            buildname=Module.config['build']
-        except:
-            buildname="build"
+        build_path = os.path.join(self.options.workdir,
+                                  Module.config['build'])
+        build = Repository(build_path, self.options)
         if self.options.build_branch:
-            buildname+=":"+self.options.build_branch
-        build = Module(buildname,self.options)
-        build.init_module_dir()
-        build.init_edge_dir()
-        build.revert_edge_dir()
-        build.update_edge_dir()
-        
-        tagsfiles=glob(build.edge_dir()+"/*-tags*.mk")
+            build.to_branch(self.options.build_branch)
+        if not build.is_clean():
+            build.revert()
+
+        tagsfiles=glob(build.path+"/*-tags*.mk")
         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
         default_answer = 'y'
+        tagsfiles.sort()
         while True:
-            for (tagsfile,status) in tagsdict.iteritems():
+            for tagsfile in tagsfiles:
+                status=tagsdict[tagsfile]
                 basename=os.path.basename(tagsfile)
                 print ".................... Dealing with %s"%basename
                 while tagsdict[tagsfile] == 'todo' :
                     choice = prompt ("insert %s in %s    "%(new_tag_name,basename),default_answer,
                                      [ ('y','es'), ('n', 'ext'), ('f','orce'), 
-                                       ('d','iff'), ('r','evert'), ('h','elp') ] ,
+                                       ('d','iff'), ('r','evert'), ('c', 'at'), ('h','elp') ] ,
                                      allow_outside=True)
                     if choice == 'y':
                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
@@ -756,261 +943,190 @@ Please write a changelog for this new tag in the section above
                     elif choice == 'f':
                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
                     elif choice == 'd':
-                        self.run("svn diff %s"%tagsfile)
+                        print build.diff(f=tagsfile)
                     elif choice == 'r':
-                        self.run("svn revert %s"%tagsfile)
+                        build.revert(f=tagsfile)
+                    elif choice == 'c':
+                        self.run("cat %s"%tagsfile)
                     else:
                         name=self.name
                         print """y: change %(name)s-SVNPATH only if it currently refers to %(old_tag_name)s
-f: unconditionnally change any line setting %(name)s-SVNPATH to using %(new_tag_name)s
+f: unconditionnally change any line that assigns %(name)s-SVNPATH to using %(new_tag_name)s
 d: show current diff for this tag file
 r: revert that tag file
+c: cat the current tag file
 n: move to next file"""%locals()
 
             if prompt("Want to review changes on tags files",False):
-                tagsdict = dict ( [ (x, 'todo') for tagsfile in tagsfiles ] )
+                tagsdict = dict ( [ (x, 'todo') for x in tagsfiles ] )
                 default_answer='d'
             else:
                 break
 
-        paths=""
-        paths += self.edge_dir() + " "
-        paths += build.edge_dir() + " "
-        self.run_prompt("Review module and build","svn diff " + paths)
-        self.run_prompt("Commit module and build","svn commit --file %s %s"%(changelog,paths))
-        self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog,edge_url,new_tag_url))
+        def diff_all_changes():
+            print build.diff()
+            print self.repository.diff()
+
+        def commit_all_changes(log):
+            self.repository.commit(log)
+            build.commit(log)
+
+        self.run_prompt("Review module and build", diff_all_changes)
+        self.run_prompt("Commit module and build", commit_all_changes, changelog_svn)
+        self.run_prompt("Create tag", self.repository.tag, new_tag_name, changelog_svn)
 
         if self.options.debug:
-            print 'Preserving',changelog
+            print 'Preserving',changelog,'and stripped',changelog_svn
         else:
             os.unlink(changelog)
-            
+            os.unlink(changelog_svn)
+
+
 ##############################
-    def do_branch (self):
-
-        # save self.branch if any, as a hint for the new branch 
-        # do this before anything else and restore .branch to None, 
-        # as this is part of the class's logic
-        new_trunk_name=None
-        if self.branch:
-            new_trunk_name=self.branch
-            self.branch=None
-
-        # compute diff - a way to initialize the whole stuff
-        # do_diff already does edge_dir initialization
-        # and it checks that edge_url and tag_url exist as well
-        (spec_dict,edge_url,tag_url,diff_listing) = self.do_diff(compute_only=True)
-
-        # the version name in the trunk becomes the new branch name
-        branch_name = spec_dict[self.module_version_varname]
-
-        # figure new branch name (the one for the trunk) if not provided on the command line
-        if not new_trunk_name:
-            # heuristic is to assume 'version' is a dot-separated name
-            # we isolate the rightmost part and try incrementing it by 1
-            version=spec_dict[self.module_version_varname]
-            try:
-                m=re.compile("\A(?P<leftpart>.+)\.(?P<rightmost>[^\.]+)\Z")
-                (leftpart,rightmost)=m.match(version).groups()
-                incremented = int(rightmost)+1
-                new_trunk_name="%s.%d"%(leftpart,incremented)
-            except:
-                raise Exception, 'Cannot figure next branch name from %s - exiting'%version
-
-        # record starting point tagname
-        latest_tag_name = self.tag_name(spec_dict)
-
-        print "**********"
-        print "Using starting point %s (%s)"%(tag_url,latest_tag_name)
-        print "Creating branch %s  &  moving trunk to %s"%(branch_name,new_trunk_name)
-        print "**********"
-
-        # print warning if pending diffs
-        if diff_listing:
-            print """*** WARNING : Module %s has pending diffs on its trunk
-It is safe to proceed, but please note that branch %s
-will be based on latest tag %s and *not* on the current trunk"""%(self.name,branch_name,latest_tag_name)
-            while True:
-                answer = prompt ('Are you sure you want to proceed with branching',True,('d','iff'))
-                if answer is True:
-                    break
-                elif answer is False:
-                    raise Exception,"User quit"
-                elif answer == 'd':
-                    print '<<<< %s'%tag_url
-                    print '>>>> %s'%edge_url
-                    print diff_listing
-
-        branch_url = "%s/%s/branches/%s"%(Module.config['svnpath'],self.name,branch_name)
-        self.check_svnpath_not_exists (branch_url,"new branch")
-        
-        # patching trunk
-        spec_dict[self.module_version_varname]=new_trunk_name
-        spec_dict[self.module_taglevel_varname]='0'
-        # remember this in the trunk for easy location of the current branch
-        spec_dict['module_current_branch']=branch_name
-        self.patch_spec_var(spec_dict,True)
-        
-        # create commit log file
-        tmp="/tmp/branching-%d"%os.getpid()
-        f=open(tmp,"w")
-        f.write("Branch %s for module %s created (as new trunk) from tag %s\n"%(new_trunk_name,self.name,latest_tag_name))
-        f.close()
+    def do_version (self):
+        self.init_module_dir()
+        self.revert_module_dir()
+        self.update_module_dir()
+        spec_dict = self.spec_dict()
+        if self.options.www:
+            self.html_store_title('Version for module %s (%s)' % (self.friendly_name(),
+                                                                  self.last_tag(spec_dict)))
+        for varname in self.varnames:
+            if not spec_dict.has_key(varname):
+                self.html_print ('Could not find %%define for %s'%varname)
+                return
+            else:
+                self.html_print ("%-16s %s"%(varname,spec_dict[varname]))
+        if self.options.verbose:
+            self.html_print ("%-16s %s"%('main specfile:',self.main_specname()))
+            self.html_print ("%-16s %s"%('specfiles:',self.all_specnames()))
+        self.html_print_end()
 
-        # we're done, let's commit the stuff
-        command="svn diff %s"%self.edge_dir()
-        self.run_prompt("Review changes in trunk",command)
-        command="svn copy --file %s %s %s"%(tmp,self.edge_url(),branch_url)
-        self.run_prompt("Create branch",command)
-        command="svn commit --file %s %s"%(tmp,self.edge_dir())
-        self.run_prompt("Commit trunk",command)
-        new_tag_url=self.tag_url(spec_dict)
-        command="svn copy --file %s %s %s"%(tmp,self.edge_url(),new_tag_url)
-        self.run_prompt("Create initial tag in trunk",command)
-        os.unlink(tmp)
 
 ##############################
-class PackageSpec:
+    def do_diff (self):
+        self.init_module_dir()
+        self.revert_module_dir()
+        self.update_module_dir()
+        spec_dict = self.spec_dict()
+        self.show_dict(spec_dict)
 
-    def __init__(self, package, module, svnpath, spec):
-        self.package=package
-        self.module=module
-        self.svnpath=svnpath
-        self.spec=spec
-        self.specpath="%s/%s"%(svnpath,spec)
+        # side effects
+        tag_name = self.tag_name(spec_dict)
+        old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
 
-    def show(self):
-        print 'package=',self.package,'module=',self.module,'svnpath=',self.svnpath,'spec=',self.spec
+        # sanity check
+        tag_name = self.check_tag(tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
 
-class Build (Module):
-    
-    def __init__ (self, buildtag,options):
-        self.buildtag=buildtag
-        Module.__init__(self,"build@%s"%buildtag,options)
-
-    # we cannot get build's svnpath as for other packages as we'd get something in svn+ssh
-    # xxx quick & dirty
-    def get_svnpath (self):
-        self.svnpath="http://svn.planet-lab.org/svn/build/tags/%s"%self.buildtag
-
-    def get_packages (self,distrotag):
-        # mhh: remove -tag* from distrotags to get distro
-        n=distrotag.find('-tag')
-        if n>0:
-            distro=distrotag[:n]
+        if self.options.verbose:
+            print 'Getting diff'
+        diff_output = self.repository.diff_with_tag(tag_name)
+
+        if self.options.list:
+            if diff_output:
+                print self.name
         else:
-            distro='planetlab'
-        result={}
-        make_options="-C %s stage1=true DISTRO=%s PLDISTROTAGS=%s 2> /dev/null"%(self.edge_dir(),distro,distrotag)
-        command="make %s packages"%make_options
-        make_packages=Command(command,self.options).output_of()
-        pkg_line=re.compile("\Apackage=(?P<package>[^\s]+)\s+ref_module=(?P<module>[^\s]+)\s.*\Z")
-        for line in make_packages.split("\n"):
-            if not line:
-                continue
-            attempt=pkg_line.match(line)
-            if line and not attempt:
-                print "====="
-                print "WARNING: line not understood from make packages"
-                print "in dir %s"%self.edge_dir
-                print "with options",make_options
-                print 'line=',line
-                print "====="
-            else:
-                (package,module) = (attempt.group('package'),attempt.group('module')) 
-                command="make %s +%s-SVNPATH"%(make_options,module)
-                svnpath=Command(command,self.options).output_of().strip()
-                command="make %s +%s-SPEC"%(make_options,package)
-                spec=Command(command,self.options).output_of().strip()
-                result[package]=PackageSpec(package,module,svnpath,spec)
-        return result
+            thename=self.friendly_name()
+            do_print=False
+            if self.options.www and diff_output:
+                self.html_store_title("Diffs in module %s (%s) : %d chars"%(\
+                        thename,self.last_tag(spec_dict),len(diff_output)))
+
+                self.html_store_raw ('<p> &lt; (left) %s </p>' % tag_name)
+                self.html_store_raw ('<p> &gt; (right) %s </p>' % thename)
+                self.html_store_pre (diff_output)
+            elif not self.options.www:
+                print 'x'*30,'module',thename
+                print 'x'*20,'<',tag_name
+                print 'x'*20,'>',thename
+                print diff_output
 
-    def get_distrotags (self):
-        return [os.path.basename(p) for p in glob("%s/*tags*mk"%self.edge_dir())]
+##############################
+    # store and restitute html fragments
+    @staticmethod 
+    def html_href (url,text): return '<a href="%s">%s</a>'%(url,text)
 
+    @staticmethod 
+    def html_anchor (url,text): return '<a name="%s">%s</a>'%(url,text)
 
-class Release:
+    @staticmethod
+    def html_quote (text):
+        return text.replace('&','&#38;').replace('<','&lt;').replace('>','&gt;')
 
-    discard_matcher=re.compile("\A(\+\+\+|---).*")
+    # only the fake error module has multiple titles
+    def html_store_title (self, title):
+        if not hasattr(self,'titles'): self.titles=[]
+        self.titles.append(title)
 
-    # t1 is the most recent build tag, t2 should be older
-    @staticmethod
-    def do_changelog (t1,t2,options):
-        print "----"
-        print "= build tag %s to %s = #tag-%s-to-%s"%(t2,t1,t2,t1)
-        (b1,b2) = (Build (t1,options), Build (t2,options))
-        for b in (b1,b2):
-            b.init_module_dir()
-            b.init_edge_dir()
-            b.update_edge_dir()
-            b.get_svnpath()
-        # find out the tags files that are common, unless option was specified
-        if options.distrotags:
-            (d1s,d2s)=([options.distrotags],[options.distrotags])
+    def html_store_raw (self, html):
+        if not hasattr(self,'body'): self.body=''
+        self.body += html
+
+    def html_store_pre (self, text):
+        if not hasattr(self,'body'): self.body=''
+        self.body += '<pre>' + self.html_quote(text) + '</pre>'
+
+    def html_print (self, txt):
+        if not self.options.www:
+            print txt
         else:
-            d1s=b1.get_distrotags()
-            d2s=b2.get_distrotags()
-        print 'd1s',d1s,'d2s',d2s
-        distrotags = list(set(d1s).intersection(set(d2s)))
-        print 'common distrotags',distrotags
-        distrotags.sort()
-        print 'after sort',distrotags
-        first_distrotag=True
-        for distrotag in distrotags:
-            if first_distrotag:
-                first_distrotag=False
-            else:
-                print '----'
-            print '== distro %s (%s to %s) == #distro-%s-%s-to-%s'%(distrotag,t2,t1,distrotag,t2,t1)
-            print ' * from %s/%s'%(b2.svnpath,distrotag)
-            print ' * to %s/%s'%(b1.svnpath,distrotag)
-            p1s=b1.get_packages(distrotag)
-            p1names=set(p1s.keys())
-            p2s=b2.get_packages(distrotag)
-            p2names=set(p2s.keys())
-            in1not2 = list(p1names-p2names)
-            in2not1 = list(p2names-p1names)
-            inboth = list(p1names.intersection(p2names))
-            in1not2.sort()
-            in2not1.sort()
-            inboth.sort()
-            for name in in1not2:
-                print '=== %s : new package %s -- appeared in %s === #pkg-%s-%s'%(distrotag,name,t1,name,t1)
-                obj=p1s[name]
-                print ' * svn link: %s'%obj.svnpath
-                print ' * spec: %s'%obj.specpath
-            for name in in2not1:
-                print '=== %s : package %s -- deprecated, last occurrence in %s === #pkg-%s-%s'%(distrotag,name,t2,name,t1)
-                obj=p2s[name]
-                print ' * svn link: %s'%obj.svnpath
-                print ' * spec: %s'%obj.specpath
-            for name in inboth:
-                (o1,o2)=(p1s[name],p2s[name])
-                if o1.specpath == o2.specpath:
-                    continue
-                command="svn diff %s %s"%(o2.specpath,o1.specpath)
-                specdiff=Command(command,options).output_of()
-                if not specdiff:
-                    continue
-                print '=== %s - %s to %s : package %s === #pkg-%s-%s'%(distrotag,t2,t1,name,name,t1)
-                print ' * from %s '%o2.specpath
-                print ' * to %s '%o1.specpath
-                print '{{{'
-                for line in specdiff.split('\n'):
-                    if not line:
-                        continue
-                    if Release.discard_matcher.match(line):
-                        continue
-                    if line[0] in ['@']:
-                        print '----------'
-                    elif line[0] in ['+','-']:
-                        print line
-                print '}}}'
+            if not hasattr(self,'in_list') or not self.in_list:
+                self.html_store_raw('<ul>')
+                self.in_list=True
+            self.html_store_raw('<li>'+txt+'</li>')
+
+    def html_print_end (self):
+        if self.options.www:
+            self.html_store_raw ('</ul>')
+
+    @staticmethod
+    def html_dump_header(title):
+        nowdate=time.strftime("%Y-%m-%d")
+        nowtime=time.strftime("%H:%M (%Z)")
+        print """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+<title> %s </title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<style type="text/css">
+body { font-family:georgia, serif; }
+h1 {font-size: large; }
+p.title {font-size: x-large; }
+span.error {text-weight:bold; color: red; }
+</style>
+</head>
+<body>
+<p class='title'> %s - status on %s at %s</p>
+<ul>
+"""%(title,title,nowdate,nowtime)
+
+    @staticmethod
+    def html_dump_middle():
+        print "</ul>"
+
+    @staticmethod
+    def html_dump_footer():
+        print "</body></html"
+
+    def html_dump_toc(self):
+        if hasattr(self,'titles'):
+            for title in self.titles:
+                print '<li>',self.html_href ('#'+self.friendly_name(),title),'</li>'
+
+    def html_dump_body(self):
+        if hasattr(self,'titles'):
+            for title in self.titles:
+                print '<hr /><h1>',self.html_anchor(self.friendly_name(),title),'</h1>'
+        if hasattr(self,'body'):
+            print self.body
+            print '<p class="top">',self.html_href('#','Back to top'),'</p>'            
+
 
 ##############################
 class Main:
 
     module_usage="""Usage: %prog [options] module_desc [ .. module_desc ]
+
 module-tools : a set of tools to manage subversion tags and specfile
   requires the specfile to either
   * define *version* and *taglevel*
@@ -1025,6 +1141,12 @@ Branches:
 """
     release_usage="""Usage: %prog [options] tag1 .. tagn
   Extract release notes from the changes in specfiles between several build tags, latest first
+  Examples:
+      release-changelog 4.2-rc25 4.2-rc24 4.2-rc23 4.2-rc22
+  You can refer to a (build) branch by prepending a colon, like in
+      release-changelog :4.2 4.2-rc25
+  You can refer to the build trunk by just mentioning 'trunk', e.g.
+      release-changelog -t coblitz-tags.mk coblitz-2.01-rc6 trunk
 """
     common_usage="""More help:
   see http://svn.planet-lab.org/wiki/ModuleTools"""
@@ -1047,6 +1169,13 @@ Branches:
     silent_modes = ['list']
     release_modes = ['changelog']
 
+    @staticmethod
+    def optparse_list (option, opt, value, parser):
+        try:
+            setattr(parser.values,option.dest,getattr(parser.values,option.dest)+value.split())
+        except:
+            setattr(parser.values,option.dest,value.split())
+
     def run(self):
 
         mode=None
@@ -1056,7 +1185,7 @@ Branches:
                 break
         if not mode:
             print "Unsupported command",sys.argv[0]
-            print "Supported commands:" + Modes.modes.keys.join(" ")
+            print "Supported commands:" + " ".join(Main.modes.keys())
             sys.exit(1)
 
         if mode not in Main.release_modes:
@@ -1067,17 +1196,8 @@ Branches:
             usage = Main.release_usage
             usage += Main.common_usage
 
-        parser=OptionParser(usage=usage,version=subversion_id)
+        parser=OptionParser(usage=usage)
         
-        if mode == 'list':
-            parser.add_option("-b","--branches",action="store_true",dest="list_branches",default=False,
-                              help="list branches")
-            parser.add_option("-t","--tags",action="store_false",dest="list_branches",
-                              help="list tags")
-            parser.add_option("-m","--match",action="store",dest="list_pattern",default=None,
-                               help="grep pattern for filtering output")
-            parser.add_option("-x","--exact-match",action="store",dest="list_exact",default=None,
-                               help="exact grep pattern for filtering output")
         if mode == "tag" or mode == 'branch':
             parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
                               help="set new version and reset taglevel to 0")
@@ -1089,83 +1209,97 @@ Branches:
         if mode == "tag" or mode == "sync" :
             parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
                               help="specify editor")
-        if mode == "sync" :
-            parser.add_option("-m","--message", action="store", dest="message", default=None,
-                              help="specify log message")
-        if mode == "diff" :
-            parser.add_option("-o","--only", action="store_true", dest="only", default=False,
-                              help="report diff only for modules that exhibit differences")
+
+        if mode in ["diff","version"] :
+            parser.add_option("-W","--www", action="store", dest="www", default=False,
+                              help="export diff in html format, e.g. -W trunk")
+
         if mode == "diff" :
             parser.add_option("-l","--list", action="store_true", dest="list", default=False,
                               help="just list modules that exhibit differences")
-
-        if mode  == 'version':
-            parser.add_option("-u","--url", action="store_true", dest="show_urls", default=False,
-                              help="display URLs")
             
-        if mode not in Main.release_modes:
-            all_modules=os.path.dirname(sys.argv[0])+"/modules.list"
-            parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
-                              help="run on all modules as found in %s"%all_modules)
-        else:
-            parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
-                              help="dry run - shell commands are only displayed")
-            parser.add_option("-t","--distrotags",action="store",dest="distrotags",default=None,
-                              help="specify a distro-tags file, e.g. onelab-tags-4.2.mk")
+        default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
+        parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
+                          help="run on all modules as found in %s"%default_modules_list)
+        parser.add_option("-f","--file",action="store",dest="modules_list",default=None,
+                          help="run on all modules found in specified file")
+        parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
+                          help="dry run - shell commands are only displayed")
+        parser.add_option("-t","--distrotags",action="callback",callback=Main.optparse_list, dest="distrotags",
+                          default=[], nargs=1,type="string",
+                          help="""specify distro-tags files, e.g. onelab-tags-4.2.mk
+-- can be set multiple times, or use quotes""")
 
         parser.add_option("-w","--workdir", action="store", dest="workdir", 
                           default="%s/%s"%(os.getenv("HOME"),"modules"),
                           help="""name for dedicated working dir - defaults to ~/modules
 ** THIS MUST NOT ** be your usual working directory""")
-        parser.add_option("-f","--fast-checks",action="store_true",dest="fast_checks",default=False,
+        parser.add_option("-F","--fast-checks",action="store_true",dest="fast_checks",default=False,
                           help="skip safety checks, such as svn updates -- use with care")
 
         # default verbosity depending on function - temp
-        verbose_modes= ['tag','sync']
+        verbose_modes= ['tag', 'sync', 'branch']
         
         if mode not in verbose_modes:
             parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
                               help="run in verbose mode")
         else:
-            parser.add_parser("-q","--quiet", action="store_false", dest="verbose", default=True,
+            parser.add_option("-q","--quiet", action="store_false", dest="verbose", default=True,
                               help="run in quiet (non-verbose) mode")
-#        parser.add_option("-d","--debug", action="store_true", dest="debug", default=False, 
-#                          help="debug mode - mostly more verbose")
         (options, args) = parser.parse_args()
         options.mode=mode
         if not hasattr(options,'dry_run'):
             options.dry_run=False
+        if not hasattr(options,'www'):
+            options.www=False
         options.debug=False
 
-        ########## release-*
-        if mode in Main.release_modes :
-            ########## changelog
-            if len(args) <= 1:
+        ########## module-*
+        if len(args) == 0:
+            if options.all_modules:
+                options.modules_list=default_modules_list
+            if options.modules_list:
+                args=Command("grep -v '#' %s"%options.modules_list,options).output_of().split()
+            else:
                 parser.print_help()
                 sys.exit(1)
-            Module.init_homedir(options)
-            for n in range(len(args)-1):
-                [t_new,t_old]=args[n:n+2]
-                Release.do_changelog (t_new,t_old,options)
-        else:
-            ########## module-*
-            if len(args) == 0:
-                if options.all_modules:
-                    args=Command("grep -v '#' %s"%all_modules,options).output_of().split()
+        Module.init_homedir(options)
+        
+
+        modules=[ Module(modname,options) for modname in args ]
+        # hack: create a dummy Module to store errors/warnings
+        error_module = Module('__errors__',options)
+
+        for module in modules:
+            if len(args)>1 and mode not in Main.silent_modes:
+                print '========================================',module.friendly_name()
+            # call the method called do_<mode>
+            method=Module.__dict__["do_%s"%mode]
+            try:
+                method(module)
+            except Exception,e:
+                if options.www:
+                    title='<span class="error"> Skipping module %s - failure: %s </span>'%\
+                        (module.friendly_name(), str(e))
+                    error_module.html_store_title(title)
                 else:
-                    parser.print_help()
-                    sys.exit(1)
-            Module.init_homedir(options)
-            for modname in args:
-                module=Module(modname,options)
-                if len(args)>1 and mode not in Main.silent_modes:
-                    print '========================================',module.friendly_name()
-                # call the method called do_<mode>
-                method=Module.__dict__["do_%s"%mode]
-                try:
-                    method(module)
-                except Exception,e:
-                    print 'Skipping failed %s: '%modname,e
+                    import traceback
+                    traceback.print_exc()
+                    print 'Skipping module %s: '%modname,e
+
+        if options.www:
+            if mode == "diff":
+                modetitle="Changes to tag in %s"%options.www
+            elif mode == "version":
+                modetitle="Latest tags in %s"%options.www
+            modules.append(error_module)
+            error_module.html_dump_header(modetitle)
+            for module in modules:
+                module.html_dump_toc()
+            Module.html_dump_middle()
+            for module in modules:
+                module.html_dump_body()
+            Module.html_dump_footer()
 
 ####################
 if __name__ == "__main__" :