add module-diff command
[build.git] / module-tools.py
index e833cd9..0aa635e 100755 (executable)
@@ -1,7 +1,5 @@
 #!/usr/bin/python -u
 
-subversion_id = "$Id$"
-
 import sys, os
 import re
 import time
@@ -163,8 +161,8 @@ class SvnRepository:
         out = Command("svn info %s" % self.path, self.options).output_of()
         for line in out.split('\n'):
             if line.startswith("Repository Root:"):
-                repo_root = line.split()[2].strip()
-                return "%s/svn/%s" (repo_root, self.name)
+                root = line.split()[2].strip()
+                return "%s/%s" % (root, self.name())
 
     @classmethod
     def checkout(cls, remote, local, options, recursive=False):
@@ -212,18 +210,26 @@ class SvnRepository:
         self_url = self.url()
         Command("svn copy -F %s %s %s" % (logfile, self_url, tag_url), self.options).run_fatal()
 
-    def diff(self):
-        return Command("svn diff %s" % self.path, self.options).output_of(True)
+    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):
-        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 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
@@ -298,8 +304,8 @@ class GitRepository:
         self.__run_command_in_repo("git tag %s -F %s" % (tagname, logfile))
         self.commit(logfile)
 
-    def diff(self):
-        c = Command("git diff", self.options)
+    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):
@@ -309,11 +315,16 @@ class GitRepository:
     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):
-        self.__run_command_in_repo("git --no-pager reset --hard")
-        self.__run_command_in_repo("git --no-pager clean -f")
+    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():
@@ -374,17 +385,22 @@ class Module:
     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",""),
                  ]
 
+    @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.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
+                cls.prompt_config_option(key, message, default)
 
 
     # for parsing module spec name:branch
@@ -445,7 +461,7 @@ class Module:
 
     @classmethod
     def git_remote_dir (cls, name):
-        return "%s:/git/%s.git" % (cls.config['gitserver'], name)
+        return "%s@%s:/git/%s.git" % (cls.config['gituser'], cls.config['gitserver'], name)
 
     @classmethod
     def svn_remote_dir (cls, name):
@@ -481,16 +497,15 @@ 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""" % options.workdir
             sys.exit(1)
-        if not os.path.isdir (options.workdir):
-            print "Cannot find",options.workdir,"let's create it"
-            cls.prompt_config()
-            print "Checking ...",
+
+        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]))
@@ -498,7 +513,8 @@ that for other purposes than tagging""" % options.workdir
             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():
@@ -506,13 +522,38 @@ that for other purposes than tagging""" % options.workdir
                 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'])
-            build = Repository(build_dir, options)
-            if not build.is_clean():
-                print "build module needs a revert"
-                build.revert()
-                print "OK"
-            build.update()
+            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'
@@ -791,9 +832,13 @@ that for other purposes than tagging""" % options.workdir
                 found_tagname = old_svn_tag_name
 
         if (found and need_it) or (not found and not need_it):
-            print "OK "
+            if self.options.verbose:
+                print "OK",
+                if found: print "- found"
+                else: print "- not found"
         else:
-            print "KO"
+            if self.options.verbose:
+                print "KO"
             if found:
                 raise Exception, "tag (%s) is already there" % tagname
             else:
@@ -802,6 +847,7 @@ that for other purposes than tagging""" % options.workdir
         return found_tagname
 
 
+##############################
     def do_tag (self):
         self.init_module_dir()
         self.revert_module_dir()
@@ -897,9 +943,9 @@ 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:
@@ -934,7 +980,147 @@ n: move to next file"""%locals()
         else:
             os.unlink(changelog)
             os.unlink(changelog_svn)
-            
+
+
+##############################
+    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()
+
+
+##############################
+    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)
+
+        # side effects
+        tag_name = self.tag_name(spec_dict)
+        old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
+
+        # sanity check
+        tag_name = self.check_tag(tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
+
+        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:
+            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
+
+##############################
+    # 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)
+
+    @staticmethod
+    def html_quote (text):
+        return text.replace('&','&#38;').replace('<','&lt;').replace('>','&gt;')
+
+    # 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)
+
+    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:
+            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:
@@ -1010,7 +1196,7 @@ Branches:
             usage = Main.release_usage
             usage += Main.common_usage
 
-        parser=OptionParser(usage=usage,version=subversion_id)
+        parser=OptionParser(usage=usage)
         
         if mode == "tag" or mode == 'branch':
             parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
@@ -1023,6 +1209,14 @@ Branches:
         if mode == "tag" or mode == "sync" :
             parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
                               help="specify editor")
+
+        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")
             
         default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
         parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
@@ -1062,7 +1256,13 @@ Branches:
             sys.exit(1)
         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()
@@ -1071,9 +1271,28 @@ Branches:
             try:
                 method(module)
             except Exception,e:
-                import traceback
-                traceback.print_exc()
-                print 'Skipping module %s: '%modname,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:
+                    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__" :