for managing tags on a module per module basis - first draft - use with care
authorThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Mon, 7 Jan 2008 16:51:19 +0000 (16:51 +0000)
committerThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Mon, 7 Jan 2008 16:51:19 +0000 (16:51 +0000)
module-diff.py [new symlink]
module-tag.py [new file with mode: 0755]

diff --git a/module-diff.py b/module-diff.py
new file mode 120000 (symlink)
index 0000000..d008749
--- /dev/null
@@ -0,0 +1 @@
+module-tag.py
\ No newline at end of file
diff --git a/module-tag.py b/module-tag.py
new file mode 100755 (executable)
index 0000000..7bb25af
--- /dev/null
@@ -0,0 +1,419 @@
+#!/usr/bin/env python
+
+subversion_id = "$Id: TestMain.py 7635 2008-01-04 09:46:06Z thierry $"
+
+import sys, os, os.path
+import re
+import time
+import glob
+from optparse import OptionParser
+
+####################
+# where to store user's config
+globals_storage="GLOBALS"
+svnpath_example="svn+ssh://thierry@svn.planet-lab.org/svn/"
+# what to parse in a spec file
+name_varname="name"
+major_varname="version"
+minor_varname="subversion"
+varnames = [name_varname,major_varname,minor_varname]
+varmatcher=re.compile("%define\s+(\S+)\s+(.*)")
+# 
+svn_magic_line="--This line, and those below, will be ignored--"
+####################
+
+def prompt (question,default=True):
+    if default:
+        question += " [y]/n ? "
+    else:
+        question += " y/[n] ? "
+    answer=raw_input(question)
+    if not answer:
+        return default
+    elif answer[0] in [ 'y','Y']:
+        return True
+    elif answer[0] in [ 'n','N']:
+        return False
+    else:
+        return prompt(question,default)
+
+class Command:
+    def __init__ (self,command,options):
+        self.command=command
+        self.options=options
+        self.tmp="/tmp/command-%d"%os.getpid()
+
+    def run (self):
+        if self.options.verbose:
+            print '+',self.command
+            sys.stdout.flush()
+        return os.system(self.command)
+
+    def run_silent (self):
+        if self.options.verbose:
+            print '+',self.command,' .. ',
+            sys.stdout.flush()
+        retcod=os.system(self.command + " &> " + self.tmp)
+        if retcod != 0:
+            print "FAILED ! -- output quoted below "
+            os.system("cat " + self.tmp)
+            print "FAILED ! -- end of quoted output"
+        elif self.options.verbose:
+            print "OK"
+        os.unlink(self.tmp)
+        return retcod
+
+    def run_fatal(self):
+        if self.run_silent() !=0:
+            raise Exception,"Command %s failed"%self.command
+
+    # returns stdout, like bash's $(mycommand)
+    def output_of (self):
+        tmp="/tmp/status-%d"%os.getpid()
+        if self.options.vverbose:
+            print '+',self.command,' .. ',
+            sys.stdout.flush()
+        os.system(self.command + " &> " + tmp)
+        result=file(tmp).read()
+        os.unlink(tmp)
+        if self.options.vverbose:
+            print '+',self.command,'Done',
+        return result
+
+class svnpath:
+    def __init__(self,path,options):
+        self.path=path
+        self.options=options
+
+    def url_exists (self):
+        if self.options.verbose:
+            print 'Checking url',self.path
+        return os.system("svn list %s &> /dev/null"%self.path) == 0
+
+    def dir_needs_revert (self):
+        command="svn status %s"%self.path
+        return len(Command(command,self.options).output_of()) != 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()) != 0
+
+class Module:
+    def __init__ (self,name,options):
+        self.name=name
+        self.options=options
+        self.moddir="%s/%s/%s"%(os.getenv("HOME"),options.modules,name)
+        self.trunkdir="%s/trunk"%(self.moddir)
+
+    globals={}
+    globalKeys=[ ('svnpath',"Enter your toplevel svnpath (e.g. %s)"%svnpath_example),
+                 ('username',"Enter your firstname and lastname for changelogs"),
+                 ("email","Enter your email address for changelogs") ]
+
+    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):
+        if not self.options.verbose:
+            question=message
+        else:
+            question="Want to run " + command
+        if prompt(question,True):
+            self.run(command)            
+
+    @staticmethod
+    def init_homedir (options):
+        topdir="%s/%s"%(os.getenv("HOME"),options.modules)
+        if options.verbose:
+            print 'Checking for',topdir
+        storage="%s/%s"%(topdir,globals_storage)
+        if not os.path.isdir (topdir):
+            # prompt for login or whatever svnpath
+            print "Cannot find",topdir,"let's create it"
+            for (key,message) in Module.globalKeys:
+                Module.globals[key]=raw_input(message+" : ").strip()
+            Command("svn co -N %s %s"%(Module.globals['svnpath'],topdir),options).run_fatal()
+            # store globals
+            f=file(storage,"w")
+            for (key,message) in Module.globalKeys:
+                f.write("%s=%s\n"%(key,Module.globals[key]))
+            f.close()
+            if options.vverbose:
+                print 'Stored',storage
+                Command("cat %s"%storage,options).run()
+        else:
+            # read globals
+            f=open(storage)
+            for line in f.readlines():
+                (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
+                Module.globals[key]=value                
+            f.close()
+            if options.vverbose:
+                print 'Using globals'
+                for (key,message) in Module.globalKeys:
+                    print key,'=',Module.globals[key]
+
+    def init_moddir (self):
+        if self.options.verbose:
+            print 'Checking for',self.moddir
+        if not os.path.isdir (self.moddir):
+            self.run_fatal("svn up -N %s"%self.moddir)
+        if not os.path.isdir (self.moddir):
+            print 'Cannot find %s - check module name'%self.moddir
+            sys.exit(1)
+
+    def init_trunkdir (self):
+        if self.options.verbose:
+            print 'Checking for',self.trunkdir
+        if not os.path.isdir (self.trunkdir):
+            self.run_fatal("svn up %s"%self.trunkdir)
+
+    def revert_trunkdir (self):
+        if self.options.verbose:
+            print 'Checking whether',self.trunkdir,'needs being reverted'
+        if svnpath(self.trunkdir,self.options).dir_needs_revert():
+            self.run_fatal("svn revert -R %s"%self.trunkdir)
+
+    def update_trunkdir (self):
+        if self.options.verbose:
+            print 'Updating',self.trunkdir
+        self.run_fatal("svn update %s"%self.trunkdir)
+
+    def guess_specname (self):
+        attempt="%s/%s.spec"%(self.trunkdir,self.name)
+        if os.path.isfile (attempt):
+            return attempt
+        else:
+            from glob import glob
+            try:
+                return glob("%s/*.spec"%self.trunkdir)[0]
+            except:
+                print 'Cannot guess specfile for module %s'%self.name
+                sys.exit(1)
+
+    def spec_dict (self):
+        specfile=self.guess_specname()
+        if self.options.verbose:
+            print 'Parsing',specfile,
+        result={}
+        f=open(specfile)
+        for line in f.readlines():
+            if varmatcher.match(line):
+                (var,value)=varmatcher.match(line).groups()
+                if var in varnames:
+                    result[var]=value
+        f.close()
+        if self.options.verbose:
+            print 'found',len(result),'keys'
+        return result
+
+    def patch_spec_var (self, patch_dict):
+        specfile=self.guess_specname()
+        newspecfile=specfile+".new"
+        if self.options.verbose:
+            print 'Patching',specfile,'for',patch_dict.keys()
+        spec=open (specfile)
+        new=open(newspecfile,"w")
+
+        for line in spec.readlines():
+            if varmatcher.match(line):
+                (var,value)=varmatcher.match(line).groups()
+                if var in patch_dict.keys():
+                    new.write('%%define %s %s\n'%(var,patch_dict[var]))
+                    continue
+            new.write(line)
+        spec.close()
+        new.close()
+        os.rename(newspecfile,specfile)
+
+    def unignored_lines (self, logfile):
+        result=[]
+        for logline in file(logfile).readlines():
+            if logline.strip() == svn_magic_line:
+                break
+            result += logline
+        return result
+
+    def insert_changelog (self, logfile, oldtag, newtag):
+        specfile=self.guess_specname()
+        newspecfile=specfile+".new"
+        if self.options.verbose:
+            print 'Inserting changelog from %s into %s'%(logfile,specfile)
+        spec=open (specfile)
+        new=open(newspecfile,"w")
+        for line in spec.readlines():
+            new.write(line)
+            if re.compile('%changelog').match(line):
+                dateformat="* %a %b %d %Y"
+                datepart=time.strftime(dateformat)
+                logpart="%s <%s> - %s %s"%(Module.globals['username'],
+                                             Module.globals['email'],
+                                             oldtag,newtag)
+                new.write(datepart+" "+logpart+"\n")
+                for logline in self.unignored_lines(logfile):
+                    new.write(logline)
+                new.write("\n")
+        spec.close()
+        new.close()
+        os.rename(newspecfile,specfile)
+            
+    def show_dict (self, spec_dict):
+        if self.options.verbose:
+            for (k,v) in spec_dict.iteritems():
+                print k,'=',v
+
+    def trunk_url (self):
+        return "%s/%s/trunk"%(Module.globals['svnpath'],self.name)
+    def tag_name (self, spec_dict):
+        return "%s-%s.%s"%(spec_dict['name'],spec_dict['version'],spec_dict['subversion'])
+    def tag_url (self, spec_dict):
+        return "%s/%s/tags/%s"%(Module.globals['svnpath'],self.name,self.tag_name(spec_dict))
+
+    def diff (self):
+        self.init_moddir()
+        self.init_trunkdir()
+        self.revert_trunkdir()
+        self.update_trunkdir()
+        spec_dict = self.spec_dict()
+        self.show_dict(spec_dict)
+
+        trunk_url=self.trunk_url()
+        tag_url=self.tag_url(spec_dict)
+        for url in [ trunk_url, tag_url ] :
+            if not svnpath(url,self.options).url_exists():
+                print 'Could not find svn URL %s'%url
+                sys.exit(1)
+
+        self.run("svn diff %s %s"%(tag_url,trunk_url))
+
+    def patch_tags_files (self, tagsfile, oldname, newname):
+        newtagsfile=tagsfile+".new"
+        if self.options.verbose:
+            print 'Replacing %s into %s in %s'%(oldname,newname,tagsfile)
+        tags=open (tagsfile)
+        new=open(newtagsfile,"w")
+        matcher=re.compile("^(.*)%s(.*)"%oldname)
+        for line in tags.readlines():
+            if not matcher.match(line):
+                new.write(line)
+            else:
+                (begin,end)=matcher.match(line).groups()
+                new.write(begin+newname+end+"\n")
+        tags.close()
+        new.close()
+        os.rename(newtagsfile,tagsfile)
+
+    def create_tag (self):
+        self.init_moddir()
+        self.init_trunkdir()
+        self.revert_trunkdir()
+        self.update_trunkdir()
+        spec_dict = self.spec_dict()
+        self.show_dict(spec_dict)
+        
+        # parse specfile, check that the old tag exists and the new one does not
+        trunk_url=self.trunk_url()
+        old_tag_name = self.tag_name(spec_dict)
+        old_tag_url=self.tag_url(spec_dict)
+        # increment subversion
+        new_subversion = str ( int (spec_dict['subversion']) + 1)
+        spec_dict['subversion'] = new_subversion
+        new_tag_name = self.tag_name(spec_dict)
+        new_tag_url=self.tag_url(spec_dict)
+        for url in [ trunk_url, old_tag_url ] :
+            if not svnpath(url,self.options).url_exists():
+                print 'Could not find svn URL %s'%url
+                sys.exit(1)
+        if svnpath(new_tag_url,self.options).url_exists():
+            print 'New tag\'s svn URL %s already exists ! '%url
+            sys.exit(1)
+
+        # side effect in trunk's specfile
+        self.patch_spec_var({"subversion":new_subversion})
+
+        # prepare changelog file 
+        # 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("""
+%s
+module %s
+old tag %s
+new tag %s
+"""%(svn_magic_line,self.name,old_tag_url,new_tag_url))
+
+        if not self.options.verbose or prompt('Want to run diff',True):
+            self.run("(echo 'DIFF========='; svn diff %s %s) >> %s"%(old_tag_url,trunk_url,changelog))
+        # edit it        
+        self.run("%s %s"%(self.options.editor,changelog))
+        # insert changelog in spec
+        if self.options.changelog:
+            self.insert_changelog (changelog,old_tag_name,new_tag_name)
+
+        ## update build
+        build = Module(self.options.build,self.options)
+        build.init_moddir()
+        build.init_trunkdir()
+        build.revert_trunkdir()
+        build.update_trunkdir()
+        
+        for tagsfile in glob.glob(build.trunkdir+"/*-tags.mk"):
+            print 'tagsfile : ',tagsfile
+            self.patch_tags_files(tagsfile,old_tag_name,new_tag_name)
+
+        paths=""
+        paths += self.trunkdir + " "
+        paths += build.trunkdir + " "
+        self.run_prompt("Check","svn diff " + paths)
+        self.run_prompt("Commit","svn commit --file %s %s"%(changelog,paths))
+        self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog,trunk_url,new_tag_url))
+
+        if self.options.vverbose:
+            print 'Preserving',changelog
+        else:
+            os.unlink(changelog)
+            
+usage="""Usage: %prog options module1 [ .. modulen ]
+Purpose:
+  manage subversion tags and specfile
+Behaviour:
+  show the diffs between the trunk and the latests tag
+  or, if -t or --tag is provided, the specfile is updated and the module is tagged"""
+
+def main():
+    parser=OptionParser(usage=usage,version=subversion_id)
+    diff_mode =  (sys.argv[0].find("diff") >= 0)
+    parser.add_option("-d","--diff", action="store_true", dest="diff", default=diff_mode, 
+                      help="Show diff between trunk and latest tag")
+    parser.add_option("-e","--editor", action="store", dest="editor", default="emacs",
+                      help="Specify editor")
+    parser.add_option("-c","--changelog", action="store_false", dest="changelog", default=True,
+                      help="Does not update changelog when tagging")
+    parser.add_option("-m","--modules", action="store", dest="modules", default="modules",
+                      help="Name for topdir - defaults to modules")
+    parser.add_option("-b","--build", action="store", dest="build", default="build",
+                      help="Set module name for build")
+    parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
+                      help="Run in verbose mode")
+    parser.add_option("-V","--veryverbose", action="store_true", dest="vverbose", default=False, 
+                      help="Very verbose mode")
+    (options, args) = parser.parse_args()
+    if options.vverbose: options.verbose=True
+
+    if len(args) == 0:
+        parser.print_help()
+        sys.exit(1)
+    else:
+        Module.init_homedir(options)
+        for modname in args:
+            module=Module(modname,options)
+            if options.diff:
+                module.diff()
+            else:
+                module.create_tag()
+
+# basically, we exit if anything goes wrong
+if __name__ == "__main__" :
+    main()