(re)start on git support
[build.git] / module-tools.py
1 #!/usr/bin/python -u
2
3 subversion_id = "$Id$"
4
5 import sys, os, os.path
6 import re
7 import time
8 from glob import glob
9 from optparse import OptionParser
10
11 # e.g. other_choices = [ ('d','iff') , ('g','uess') ] - lowercase 
12 def prompt (question,default=True,other_choices=[],allow_outside=False):
13     if not isinstance (other_choices,list):
14         other_choices = [ other_choices ]
15     chars = [ c for (c,rest) in other_choices ]
16
17     choices = []
18     if 'y' not in chars:
19         if default is True: choices.append('[y]')
20         else : choices.append('y')
21     if 'n' not in chars:
22         if default is False: choices.append('[n]')
23         else : choices.append('n')
24
25     for (char,choice) in other_choices:
26         if default == char:
27             choices.append("["+char+"]"+choice)
28         else:
29             choices.append("<"+char+">"+choice)
30     try:
31         answer=raw_input(question + " " + "/".join(choices) + " ? ")
32         if not answer:
33             return default
34         answer=answer[0].lower()
35         if answer == 'y':
36             if 'y' in chars: return 'y'
37             else: return True
38         elif answer == 'n':
39             if 'n' in chars: return 'n'
40             else: return False
41         elif other_choices:
42             for (char,choice) in other_choices:
43                 if answer == char:
44                     return char
45             if allow_outside:
46                 return answer
47         return prompt(question,default,other_choices)
48     except:
49         raise
50
51 def default_editor():
52     try:
53         editor = os.environ['EDITOR']
54     except:
55         editor = "emacs"
56     return editor
57
58 ### fold long lines
59 fold_length=132
60
61 def print_fold (line):
62     while len(line) >= fold_length:
63         print line[:fold_length],'\\'
64         line=line[fold_length:]
65     print line
66
67 class Command:
68     def __init__ (self,command,options):
69         self.command=command
70         self.options=options
71         self.tmp="/tmp/command-%d"%os.getpid()
72
73     def run (self):
74         if self.options.dry_run:
75             print 'dry_run',self.command
76             return 0
77         if self.options.verbose and self.options.mode not in Main.silent_modes:
78             print '+',self.command
79             sys.stdout.flush()
80         return os.system(self.command)
81
82     def run_silent (self):
83         if self.options.dry_run:
84             print 'dry_run',self.command
85             return 0
86         if self.options.verbose:
87             print '+',self.command,' .. ',
88             sys.stdout.flush()
89         retcod=os.system(self.command + " &> " + self.tmp)
90         if retcod != 0:
91             print "FAILED ! -- out+err below (command was %s)"%self.command
92             os.system("cat " + self.tmp)
93             print "FAILED ! -- end of quoted output"
94         elif self.options.verbose:
95             print "OK"
96         os.unlink(self.tmp)
97         return retcod
98
99     def run_fatal(self):
100         if self.run_silent() !=0:
101             raise Exception,"Command %s failed"%self.command
102
103     # returns stdout, like bash's $(mycommand)
104     def output_of (self,with_stderr=False):
105         if self.options.dry_run:
106             print 'dry_run',self.command
107             return 'dry_run output'
108         tmp="/tmp/status-%d"%os.getpid()
109         if self.options.debug:
110             print '+',self.command,' .. ',
111             sys.stdout.flush()
112         command=self.command
113         if with_stderr:
114             command += " &> "
115         else:
116             command += " > "
117         command += tmp
118         os.system(command)
119         result=file(tmp).read()
120         os.unlink(tmp)
121         if self.options.debug:
122             print 'Done',
123         return result
124
125
126
127 class SvnRepository:
128     def __init__(self, path, options):
129         self.path = path
130         self.options = options
131
132     @classmethod
133     def checkout(remote, path):
134         Command("svn co %s %s" % (remote, path), self.options).run_fatal()
135
136     @classmethod
137     def remote_exists(remote):
138         return os.system("svn list %s &> /dev/null" % remote) == 0
139
140     def update(self):
141         Command("svn up %s" % self.path, self.options).run_fatal()
142
143     def commit(self, logfile):
144         Command("svn commmit -F %s %s" % (logfile, self.path), self.options).run_fatal()
145
146     def revert(self):
147         Command("svn revert %s -R" % self.path, self.options).run_fatal()
148
149     def is_clean(self):
150         command="svn status %s" % self.path
151         return len(Command(command,self.options).output_of(True)) > 0
152
153     def is_valid(self):
154         return os.path.exists(os.path.join(self.path, ".svn"))
155     
156
157
158 class GitRepository:
159     def __init__(self, path, options):
160         self.path = path
161         self.options = options
162
163     @classmethod
164     def checkout(remote, path, depth=1):
165         Command("git clone --depth %d %s %s" % (depth, remote, path), self.options).run_fatal()
166
167     @classmethod
168     def remote_exists(remote):
169         return os.system("git --no-pager ls-remote %s &> /dev/null" % remote) == 0
170
171     def __run_in_repo(self, fun, *args):
172         cwd = os.getcwd()
173         os.chdir(self.path)
174         ret = fun(*args)
175         os.chdir(cwd)
176         return ret
177
178     def __run_command_in_repo(self, command):
179         c = Command(command, self.options)
180         return self.__run_in_repo(c.run_fatal)
181
182     def update(self):
183         return self.__run_command_in_repo("git pull")
184
185     def commit(self, logfile):
186         self.__run_command_in_repo("git commit -F %s" % logfile)
187         self.__run_command_in_repo("git push")
188
189     def revert(self):
190         return self.__run_command_in_repo("git --no-pager reset --hard")
191
192     def is_clean(self):
193         def check_commit():
194             command="git status"
195             s="nothing to commit (working directory clean)"
196             return Command(command, self.options).output_of(True).find(s) >= 0
197         return self.__run_in_repo(check_commit)
198
199     def is_valid(self):
200         return os.path.exists(os.path.join(self.path, ".git"))
201     
202
203 class Repository:
204     """ Generic repository """
205     supported_repo_types = [SvnRepository, GitRepository]
206
207     def __init__(self, path, options):
208         self.path = path
209         self.options = options
210         for repo in self.supported_repo_types:
211             self.repo = repo(self.path, self.options)
212             if self.repo.is_valid():
213                 break
214
215     def __getattr__(self, attr):
216         return getattr(self.repo, attr)
217
218
219 class Svnpath:
220     def __init__(self,path,options):
221         self.path=path
222         self.options=options
223
224     def url_exists (self):
225         return os.system("svn list %s &> /dev/null"%self.path) == 0
226
227     def dir_needs_revert (self):
228         command="svn status %s"%self.path
229         return len(Command(command,self.options).output_of(True)) != 0
230     # turns out it's the same implem.
231     def file_needs_commit (self):
232         command="svn status %s"%self.path
233         return len(Command(command,self.options).output_of(True)) != 0
234
235 # support for tagged module is minimal, and is for the Build class only
236 class Module:
237
238     svn_magic_line="--This line, and those below, will be ignored--"
239     setting_tag_format = "Setting tag %s"
240     
241     redirectors=[ # ('module_name_varname','name'),
242                   ('module_version_varname','version'),
243                   ('module_taglevel_varname','taglevel'), ]
244
245     # where to store user's config
246     config_storage="CONFIG"
247     # 
248     config={}
249
250     import commands
251     configKeys=[ ('svnpath',"Enter your toplevel svnpath",
252                   "svn+ssh://%s@svn.planet-lab.org/svn/"%commands.getoutput("id -un")),
253                  ("build", "Enter the name of your build module","build"),
254                  ('username',"Enter your firstname and lastname for changelogs",""),
255                  ("email","Enter your email address for changelogs",""),
256                  ]
257
258     @staticmethod
259     def prompt_config ():
260         for (key,message,default) in Module.configKeys:
261             Module.config[key]=""
262             while not Module.config[key]:
263                 Module.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
264
265
266     # for parsing module spec name:branch
267     matcher_branch_spec=re.compile("\A(?P<name>[\w\.-]+):(?P<branch>[\w\.-]+)\Z")
268     # special form for tagged module - for Build
269     matcher_tag_spec=re.compile("\A(?P<name>[\w-]+)@(?P<tagname>[\w\.-]+)\Z")
270     # parsing specfiles
271     matcher_rpm_define=re.compile("%(define|global)\s+(\S+)\s+(\S*)\s*")
272
273     def __init__ (self,module_spec,options):
274         # parse module spec
275         attempt=Module.matcher_branch_spec.match(module_spec)
276         if attempt:
277             self.name=attempt.group('name')
278             self.branch=attempt.group('branch')
279         else:
280             attempt=Module.matcher_tag_spec.match(module_spec)
281             if attempt:
282                 self.name=attempt.group('name')
283                 self.tagname=attempt.group('tagname')
284             else:
285                 self.name=module_spec
286
287         self.options=options
288         self.module_dir="%s/%s"%(options.workdir,self.name)
289
290     def friendly_name (self):
291         if hasattr(self,'branch'):
292             return "%s:%s"%(self.name,self.branch)
293         elif hasattr(self,'tagname'):
294             return "%s@%s"%(self.name,self.tagname)
295         else:
296             return self.name
297
298     def edge_dir (self):
299         if hasattr(self,'branch'):
300             return "%s/branches/%s"%(self.module_dir,self.branch)
301         elif hasattr(self,'tagname'):
302             return "%s/tags/%s"%(self.module_dir,self.tagname)
303         else:
304             return "%s/trunk"%(self.module_dir)
305
306     def tags_dir (self):
307         return "%s/tags"%(self.module_dir)
308
309     def run (self,command):
310         return Command(command,self.options).run()
311     def run_fatal (self,command):
312         return Command(command,self.options).run_fatal()
313     def run_prompt (self,message,command):
314         if not self.options.verbose:
315             while True:
316                 choice=prompt(message,True,('s','how'))
317                 if choice is True:
318                     self.run(command)
319                     return
320                 elif choice is False:
321                     return
322                 else:
323                     print 'About to run:',command
324         else:
325             question=message+" - want to run " + command
326             if prompt(question,True):
327                 self.run(command)            
328
329     ####################
330     # store and restitute html fragments
331     @staticmethod 
332     def html_href (url,text): return '<a href="%s">%s</a>'%(url,text)
333     @staticmethod 
334     def html_anchor (url,text): return '<a name="%s">%s</a>'%(url,text)
335     # there must be some smarter means to do that - dirty for now
336     @staticmethod
337     def html_quote (text):
338         return text.replace('&','&#38;').replace('<','&lt;').replace('>','&gt;')
339
340     # only the fake error module has multiple titles
341     def html_store_title (self, title):
342         if not hasattr(self,'titles'): self.titles=[]
343         self.titles.append(title)
344     def html_store_raw (self, html):
345         if not hasattr(self,'body'): self.body=''
346         self.body += html
347     def html_store_pre (self, text):
348         if not hasattr(self,'body'): self.body=''
349         self.body += '<pre>' + self.html_quote(text) + '</pre>'
350
351     def html_print (self, txt):
352         if not self.options.www:
353             print txt
354         else:
355             if not hasattr(self,'in_list') or not self.in_list:
356                 self.html_store_raw('<ul>')
357                 self.in_list=True
358             self.html_store_raw('<li>'+txt+'</li>')
359     def html_print_end (self):
360         if self.options.www:
361             self.html_store_raw ('</ul>')
362
363     @staticmethod
364     def html_dump_header(title):
365         nowdate=time.strftime("%Y-%m-%d")
366         nowtime=time.strftime("%H:%M (%Z)")
367         print """
368 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
369 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
370 <head>
371 <title> %s </title>
372 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
373 <style type="text/css">
374 body { font-family:georgia, serif; }
375 h1 {font-size: large; }
376 p.title {font-size: x-large; }
377 span.error {text-weight:bold; color: red; }
378 </style>
379 </head>
380 <body>
381 <p class='title'> %s - status on %s at %s</p>
382 <ul>
383 """%(title,title,nowdate,nowtime)
384
385     @staticmethod
386     def html_dump_middle():
387         print "</ul>"
388
389     @staticmethod
390     def html_dump_footer():
391         print "</body></html"
392
393     def html_dump_toc(self):
394         if hasattr(self,'titles'):
395             for title in self.titles:
396                 print '<li>',self.html_href ('#'+self.friendly_name(),title),'</li>'
397
398     def html_dump_body(self):
399         if hasattr(self,'titles'):
400             for title in self.titles:
401                 print '<hr /><h1>',self.html_anchor(self.friendly_name(),title),'</h1>'
402         if hasattr(self,'body'):
403             print self.body
404             print '<p class="top">',self.html_href('#','Back to top'),'</p>'
405
406     ####################
407     @staticmethod
408     def init_homedir (options):
409         topdir=options.workdir
410         if options.verbose and options.mode not in Main.silent_modes:
411             print 'Checking for',topdir
412         storage="%s/%s"%(topdir,Module.config_storage)
413         # sanity check. Either the topdir exists AND we have a config/storage
414         # or topdir does not exist and we create it
415         # to avoid people use their own daily svn repo
416         if os.path.isdir(topdir) and not os.path.isfile(storage):
417             print """The directory %s exists and has no CONFIG file
418 If this is your regular working directory, please provide another one as the
419 module-* commands need a fresh working dir. Make sure that you do not use 
420 that for other purposes than tagging"""%topdir
421             sys.exit(1)
422         if not os.path.isdir (topdir):
423             print "Cannot find",topdir,"let's create it"
424             Module.prompt_config()
425             print "Checking ...",
426             Command("svn co -N %s %s"%(Module.config['svnpath'],topdir),options).run_fatal()
427             Command("svn co %s/%s %s/%s"%(Module.config['svnpath'],
428                                              Module.config['build'],
429                                              topdir,
430                                              Module.config['build']),options).run_fatal()
431             print "OK"
432             
433             # store config
434             f=file(storage,"w")
435             for (key,message,default) in Module.configKeys:
436                 f.write("%s=%s\n"%(key,Module.config[key]))
437             f.close()
438             if options.debug:
439                 print 'Stored',storage
440                 Command("cat %s"%storage,options).run()
441         else:
442             # read config
443             f=open(storage)
444             for line in f.readlines():
445                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
446                 Module.config[key]=value                
447             f.close()
448         if options.verbose and options.mode not in Main.silent_modes:
449             print '******** Using config'
450             for (key,message,default) in Module.configKeys:
451                 print '\t',key,'=',Module.config[key]
452
453     def init_module_dir (self):
454         if self.options.verbose:
455             print 'Checking for',self.module_dir
456         if not os.path.isdir (self.module_dir):
457             self.run_fatal("svn update -N %s"%self.module_dir)
458         if not os.path.isdir (self.module_dir):
459             raise Exception, 'Cannot find %s - check module name'%self.module_dir
460
461     def init_subdir (self,fullpath, deep=False):
462         if self.options.verbose:
463             print 'Checking for',fullpath
464         opt=""
465         if not deep: opt="-N"
466         if not os.path.isdir (fullpath):
467             self.run_fatal("svn update %s %s"%(opt,fullpath))
468
469     def revert_subdir (self,fullpath):
470         if self.options.fast_checks:
471             if self.options.verbose: print 'Skipping revert of %s'%fullpath
472             return
473         if self.options.verbose:
474             print 'Checking whether',fullpath,'needs being reverted'
475         if Svnpath(fullpath,self.options).dir_needs_revert():
476             self.run_fatal("svn revert -R %s"%fullpath)
477
478     def update_subdir (self,fullpath):
479         if self.options.fast_checks:
480             if self.options.verbose: print 'Skipping update of %s'%fullpath
481             return
482         if self.options.verbose:
483             print 'Updating',fullpath
484         self.run_fatal("svn update %s"%fullpath)
485
486     def init_edge_dir (self):
487         # if branch, edge_dir is two steps down
488         if hasattr(self,'branch'):
489             self.init_subdir("%s/branches"%self.module_dir,deep=False)
490         elif hasattr(self,'tagname'):
491             self.init_subdir("%s/tags"%self.module_dir,deep=False)
492         self.init_subdir(self.edge_dir(),deep=True)
493
494     def revert_edge_dir (self):
495         self.revert_subdir(self.edge_dir())
496
497     def update_edge_dir (self):
498         self.update_subdir(self.edge_dir())
499
500     def main_specname (self):
501         attempt="%s/%s.spec"%(self.edge_dir(),self.name)
502         if os.path.isfile (attempt):
503             return attempt
504         pattern1="%s/*.spec"%self.edge_dir()
505         level1=glob(pattern1)
506         if level1:
507             return level1[0]
508         pattern2="%s/*/*.spec"%self.edge_dir()
509         level2=glob(pattern2)
510         if level2:
511             return level2[0]
512         raise Exception, 'Cannot guess specfile for module %s -- patterns were %s or %s'%(self.name,pattern1,pattern2)
513
514     def all_specnames (self):
515         level1=glob("%s/*.spec"%self.edge_dir())
516         if level1: return level1
517         level2=glob("%s/*/*.spec"%self.edge_dir())
518         return level2
519
520     def parse_spec (self, specfile, varnames):
521         if self.options.verbose:
522             print 'Parsing',specfile,
523             for var in varnames:
524                 print "[%s]"%var,
525             print ""
526         result={}
527         f=open(specfile)
528         for line in f.readlines():
529             attempt=Module.matcher_rpm_define.match(line)
530             if attempt:
531                 (define,var,value)=attempt.groups()
532                 if var in varnames:
533                     result[var]=value
534         f.close()
535         if self.options.debug:
536             print 'found',len(result),'keys'
537             for (k,v) in result.iteritems():
538                 print k,'=',v
539         return result
540                 
541     # stores in self.module_name_varname the rpm variable to be used for the module's name
542     # and the list of these names in self.varnames
543     def spec_dict (self):
544         specfile=self.main_specname()
545         redirector_keys = [ varname for (varname,default) in Module.redirectors]
546         redirect_dict = self.parse_spec(specfile,redirector_keys)
547         if self.options.debug:
548             print '1st pass parsing done, redirect_dict=',redirect_dict
549         varnames=[]
550         for (varname,default) in Module.redirectors:
551             if redirect_dict.has_key(varname):
552                 setattr(self,varname,redirect_dict[varname])
553                 varnames += [redirect_dict[varname]]
554             else:
555                 setattr(self,varname,default)
556                 varnames += [ default ] 
557         self.varnames = varnames
558         result = self.parse_spec (specfile,self.varnames)
559         if self.options.debug:
560             print '2st pass parsing done, varnames=',varnames,'result=',result
561         return result
562
563     def patch_spec_var (self, patch_dict,define_missing=False):
564         for specfile in self.all_specnames():
565             # record the keys that were changed
566             changed = dict ( [ (x,False) for x in patch_dict.keys() ] )
567             newspecfile=specfile+".new"
568             if self.options.verbose:
569                 print 'Patching',specfile,'for',patch_dict.keys()
570             spec=open (specfile)
571             new=open(newspecfile,"w")
572
573             for line in spec.readlines():
574                 attempt=Module.matcher_rpm_define.match(line)
575                 if attempt:
576                     (define,var,value)=attempt.groups()
577                     if var in patch_dict.keys():
578                         if self.options.debug:
579                             print 'rewriting %s as %s'%(var,patch_dict[var])
580                         new.write('%%%s %s %s\n'%(define,var,patch_dict[var]))
581                         changed[var]=True
582                         continue
583                 new.write(line)
584             if define_missing:
585                 for (key,was_changed) in changed.iteritems():
586                     if not was_changed:
587                         if self.options.debug:
588                             print 'rewriting missing %s as %s'%(key,patch_dict[key])
589                         new.write('\n%%define %s %s\n'%(key,patch_dict[key]))
590             spec.close()
591             new.close()
592             os.rename(newspecfile,specfile)
593
594     # returns all lines until the magic line
595     def unignored_lines (self, logfile):
596         result=[]
597         white_line_matcher = re.compile("\A\s*\Z")
598         for logline in file(logfile).readlines():
599             if logline.strip() == Module.svn_magic_line:
600                 break
601             elif white_line_matcher.match(logline):
602                 continue
603             else:
604                 result.append(logline.strip()+'\n')
605         return result
606
607     # creates a copy of the input with only the unignored lines
608     def stripped_magic_line_filename (self, filein, fileout ,new_tag_name):
609        f=file(fileout,'w')
610        f.write(self.setting_tag_format%new_tag_name + '\n')
611        for line in self.unignored_lines(filein):
612            f.write(line)
613        f.close()
614
615     def insert_changelog (self, logfile, oldtag, newtag):
616         for specfile in self.all_specnames():
617             newspecfile=specfile+".new"
618             if self.options.verbose:
619                 print 'Inserting changelog from %s into %s'%(logfile,specfile)
620             spec=open (specfile)
621             new=open(newspecfile,"w")
622             for line in spec.readlines():
623                 new.write(line)
624                 if re.compile('%changelog').match(line):
625                     dateformat="* %a %b %d %Y"
626                     datepart=time.strftime(dateformat)
627                     logpart="%s <%s> - %s"%(Module.config['username'],
628                                                  Module.config['email'],
629                                                  newtag)
630                     new.write(datepart+" "+logpart+"\n")
631                     for logline in self.unignored_lines(logfile):
632                         new.write("- " + logline)
633                     new.write("\n")
634             spec.close()
635             new.close()
636             os.rename(newspecfile,specfile)
637             
638     def show_dict (self, spec_dict):
639         if self.options.verbose:
640             for (k,v) in spec_dict.iteritems():
641                 print k,'=',v
642
643     def mod_url (self):
644         return "%s/%s"%(Module.config['svnpath'],self.name)
645
646     def edge_url (self):
647         if hasattr(self,'branch'):
648             return "%s/branches/%s"%(self.mod_url(),self.branch)
649         elif hasattr(self,'tagname'):
650             return "%s/tags/%s"%(self.mod_url(),self.tagname)
651         else:
652             return "%s/trunk"%(self.mod_url())
653
654     def last_tag (self, spec_dict):
655         return "%s-%s"%(spec_dict[self.module_version_varname],spec_dict[self.module_taglevel_varname])
656
657     def tag_name (self, spec_dict):
658         try:
659             return "%s-%s"%(self.name,
660                             self.last_tag(spec_dict))
661         except KeyError,err:
662             raise Exception, 'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
663
664     def tag_url (self, spec_dict):
665         return "%s/tags/%s"%(self.mod_url(),self.tag_name(spec_dict))
666
667     def check_svnpath_exists (self, url, message):
668         if self.options.fast_checks:
669             return
670         if self.options.verbose:
671             print 'Checking url (%s) %s'%(url,message),
672         ok=Svnpath(url,self.options).url_exists()
673         if ok:
674             if self.options.verbose: print 'exists - OK'
675         else:
676             if self.options.verbose: print 'KO'
677             raise Exception, 'Could not find %s URL %s'%(message,url)
678
679     def check_svnpath_not_exists (self, url, message):
680         if self.options.fast_checks:
681             return
682         if self.options.verbose:
683             print 'Checking url (%s) %s'%(url,message),
684         ok=not Svnpath(url,self.options).url_exists()
685         if ok:
686             if self.options.verbose: print 'does not exist - OK'
687         else:
688             if self.options.verbose: print 'KO'
689             raise Exception, '%s URL %s already exists - exiting'%(message,url)
690
691     # locate specfile, parse it, check it and show values
692
693 ##############################
694     def do_version (self):
695         self.init_module_dir()
696         self.init_edge_dir()
697         self.revert_edge_dir()
698         self.update_edge_dir()
699         spec_dict = self.spec_dict()
700         if self.options.www:
701             self.html_store_title('Version for module %s (%s)' % (self.friendly_name(),self.last_tag(spec_dict)))
702         for varname in self.varnames:
703             if not spec_dict.has_key(varname):
704                 self.html_print ('Could not find %%define for %s'%varname)
705                 return
706             else:
707                 self.html_print ("%-16s %s"%(varname,spec_dict[varname]))
708         if self.options.show_urls:
709             self.html_print ("%-16s %s"%('edge url',self.edge_url()))
710             self.html_print ("%-16s %s"%('latest tag url',self.tag_url(spec_dict)))
711         if self.options.verbose:
712             self.html_print ("%-16s %s"%('main specfile:',self.main_specname()))
713             self.html_print ("%-16s %s"%('specfiles:',self.all_specnames()))
714         self.html_print_end()
715
716 ##############################
717     def do_list (self):
718 #        print 'verbose',self.options.verbose
719 #        print 'list_tags',self.options.list_tags
720 #        print 'list_branches',self.options.list_branches
721 #        print 'all_modules',self.options.all_modules
722         
723         (verbose,branches,pattern,exact) = (self.options.verbose,self.options.list_branches,
724                                             self.options.list_pattern,self.options.list_exact)
725
726         extra_command=""
727         extra_message=""
728         if hasattr(self,'branch'):
729             pattern=self.branch
730         if pattern or exact:
731             if exact:
732                 if verbose: grep="%s/$"%exact
733                 else: grep="^%s$"%exact
734             else:
735                 grep=pattern
736             extra_command=" | grep %s"%grep
737             extra_message=" matching %s"%grep
738
739         if not branches:
740             message="==================== tags for %s"%self.friendly_name()
741             command="svn list "
742             if verbose: command+="--verbose "
743             command += "%s/tags"%self.mod_url()
744             command += extra_command
745             message += extra_message
746             if verbose: print message
747             self.run(command)
748
749         else:
750             message="==================== branches for %s"%self.friendly_name()
751             command="svn list "
752             if verbose: command+="--verbose "
753             command += "%s/branches"%self.mod_url()
754             command += extra_command
755             message += extra_message
756             if verbose: print message
757             self.run(command)
758
759 ##############################
760     sync_warning="""*** WARNING
761 The module-sync function has the following limitations
762 * it does not handle changelogs
763 * it does not scan the -tags*.mk files to adopt the new tags"""
764
765     def do_sync(self):
766         if self.options.verbose:
767             print Module.sync_warning
768             if not prompt('Want to proceed anyway'):
769                 return
770
771         self.init_module_dir()
772         self.init_edge_dir()
773         self.revert_edge_dir()
774         self.update_edge_dir()
775         spec_dict = self.spec_dict()
776
777         edge_url=self.edge_url()
778         tag_name=self.tag_name(spec_dict)
779         tag_url=self.tag_url(spec_dict)
780         # check the tag does not exist yet
781         self.check_svnpath_not_exists(tag_url,"new tag")
782
783         if self.options.message:
784             svnopt='--message "%s"'%self.options.message
785         else:
786             svnopt='--editor-cmd=%s'%self.options.editor
787         self.run_prompt("Create initial tag",
788                         "svn copy %s %s %s"%(svnopt,edge_url,tag_url))
789
790 ##############################
791     def do_diff (self,compute_only=False):
792         self.init_module_dir()
793         self.init_edge_dir()
794         self.revert_edge_dir()
795         self.update_edge_dir()
796         spec_dict = self.spec_dict()
797         self.show_dict(spec_dict)
798
799         edge_url=self.edge_url()
800         tag_url=self.tag_url(spec_dict)
801         self.check_svnpath_exists(edge_url,"edge track")
802         self.check_svnpath_exists(tag_url,"latest tag")
803         command="svn diff %s %s"%(tag_url,edge_url)
804         if compute_only:
805             if self.options.verbose:
806                 print 'Getting diff with %s'%command
807         diff_output = Command(command,self.options).output_of()
808         # if used as a utility
809         if compute_only:
810             return (spec_dict,edge_url,tag_url,diff_output)
811         # otherwise print the result
812         if self.options.list:
813             if diff_output:
814                 print self.name
815         else:
816             thename=self.friendly_name()
817             do_print=False
818             if self.options.www and diff_output:
819                 self.html_store_title("Diffs in module %s (%s) : %d chars"%(\
820                         thename,self.last_tag(spec_dict),len(diff_output)))
821                 link=self.html_href(tag_url,tag_url)
822                 self.html_store_raw ('<p> &lt; (left) %s </p>'%link)
823                 link=self.html_href(edge_url,edge_url)
824                 self.html_store_raw ('<p> &gt; (right) %s </p>'%link)
825                 self.html_store_pre (diff_output)
826             elif not self.options.www:
827                 print 'x'*30,'module',thename
828                 print 'x'*20,'<',tag_url
829                 print 'x'*20,'>',edge_url
830                 print diff_output
831
832 ##############################
833     # using fine_grain means replacing only those instances that currently refer to this tag
834     # otherwise, <module>-SVNPATH is replaced unconditionnally
835     def patch_tags_file (self, tagsfile, oldname, newname,fine_grain=True):
836         newtagsfile=tagsfile+".new"
837         tags=open (tagsfile)
838         new=open(newtagsfile,"w")
839
840         matches=0
841         # fine-grain : replace those lines that refer to oldname
842         if fine_grain:
843             if self.options.verbose:
844                 print 'Replacing %s into %s\n\tin %s .. '%(oldname,newname,tagsfile),
845             matcher=re.compile("^(.*)%s(.*)"%oldname)
846             for line in tags.readlines():
847                 if not matcher.match(line):
848                     new.write(line)
849                 else:
850                     (begin,end)=matcher.match(line).groups()
851                     new.write(begin+newname+end+"\n")
852                     matches += 1
853         # brute-force : change uncommented lines that define <module>-SVNPATH
854         else:
855             if self.options.verbose:
856                 print 'Searching for -SVNPATH lines referring to /%s/\n\tin %s .. '%(self.name,tagsfile),
857             pattern="\A\s*(?P<make_name>[^\s]+)-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
858                                           %(self.name)
859             matcher_module=re.compile(pattern)
860             for line in tags.readlines():
861                 attempt=matcher_module.match(line)
862                 if attempt:
863                     svnpath="%s-SVNPATH"%(attempt.group('make_name'))
864                     if self.options.verbose:
865                         print ' '+svnpath, 
866                     replacement = "%-32s:= %s/%s/tags/%s\n"%(svnpath,attempt.group('url_main'),self.name,newname)
867                     new.write(replacement)
868                     matches += 1
869                 else:
870                     new.write(line)
871         tags.close()
872         new.close()
873         os.rename(newtagsfile,tagsfile)
874         if self.options.verbose: print "%d changes"%matches
875         return matches
876
877     def do_tag (self):
878         self.init_module_dir()
879         self.init_edge_dir()
880         self.revert_edge_dir()
881         self.update_edge_dir()
882         # parse specfile
883         spec_dict = self.spec_dict()
884         self.show_dict(spec_dict)
885         
886         # side effects
887         edge_url=self.edge_url()
888         old_tag_name = self.tag_name(spec_dict)
889         old_tag_url=self.tag_url(spec_dict)
890         if (self.options.new_version):
891             # new version set on command line
892             spec_dict[self.module_version_varname] = self.options.new_version
893             spec_dict[self.module_taglevel_varname] = 0
894         else:
895             # increment taglevel
896             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
897             spec_dict[self.module_taglevel_varname] = new_taglevel
898
899         # sanity check
900         new_tag_name = self.tag_name(spec_dict)
901         new_tag_url=self.tag_url(spec_dict)
902         self.check_svnpath_exists (edge_url,"edge track")
903         self.check_svnpath_exists (old_tag_url,"previous tag")
904         self.check_svnpath_not_exists (new_tag_url,"new tag")
905
906         # checking for diffs
907         diff_output=Command("svn diff %s %s"%(old_tag_url,edge_url),
908                             self.options).output_of()
909         if len(diff_output) == 0:
910             if not prompt ("No pending difference in module %s, want to tag anyway"%self.name,False):
911                 return
912
913         # side effect in trunk's specfile
914         self.patch_spec_var(spec_dict)
915
916         # prepare changelog file 
917         # we use the standard subversion magic string (see svn_magic_line)
918         # so we can provide useful information, such as version numbers and diff
919         # in the same file
920         changelog="/tmp/%s-%d.edit"%(self.name,os.getpid())
921         changelog_svn="/tmp/%s-%d.svn"%(self.name,os.getpid())
922         setting_tag_line=Module.setting_tag_format%new_tag_name
923         file(changelog,"w").write("""
924 %s
925 %s
926 Please write a changelog for this new tag in the section above
927 """%(Module.svn_magic_line,setting_tag_line))
928
929         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
930             file(changelog,"a").write('DIFF=========\n' + diff_output)
931         
932         if self.options.debug:
933             prompt('Proceed ?')
934
935         # edit it        
936         self.run("%s %s"%(self.options.editor,changelog))
937         # strip magic line in second file - looks like svn has changed its magic line with 1.6
938         # so we do the job ourselves
939         self.stripped_magic_line_filename(changelog,changelog_svn,new_tag_name)
940         # insert changelog in spec
941         if self.options.changelog:
942             self.insert_changelog (changelog,old_tag_name,new_tag_name)
943
944         ## update build
945         try:
946             buildname=Module.config['build']
947         except:
948             buildname="build"
949         if self.options.build_branch:
950             buildname+=":"+self.options.build_branch
951         build = Module(buildname,self.options)
952         build.init_module_dir()
953         build.init_edge_dir()
954         build.revert_edge_dir()
955         build.update_edge_dir()
956         
957         tagsfiles=glob(build.edge_dir()+"/*-tags*.mk")
958         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
959         default_answer = 'y'
960         tagsfiles.sort()
961         while True:
962             for tagsfile in tagsfiles:
963                 status=tagsdict[tagsfile]
964                 basename=os.path.basename(tagsfile)
965                 print ".................... Dealing with %s"%basename
966                 while tagsdict[tagsfile] == 'todo' :
967                     choice = prompt ("insert %s in %s    "%(new_tag_name,basename),default_answer,
968                                      [ ('y','es'), ('n', 'ext'), ('f','orce'), 
969                                        ('d','iff'), ('r','evert'), ('c', 'at'), ('h','elp') ] ,
970                                      allow_outside=True)
971                     if choice == 'y':
972                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
973                     elif choice == 'n':
974                         print 'Done with %s'%os.path.basename(tagsfile)
975                         tagsdict[tagsfile]='done'
976                     elif choice == 'f':
977                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
978                     elif choice == 'd':
979                         self.run("svn diff %s"%tagsfile)
980                     elif choice == 'r':
981                         self.run("svn revert %s"%tagsfile)
982                     elif choice == 'c':
983                         self.run("cat %s"%tagsfile)
984                     else:
985                         name=self.name
986                         print """y: change %(name)s-SVNPATH only if it currently refers to %(old_tag_name)s
987 f: unconditionnally change any line that assigns %(name)s-SVNPATH to using %(new_tag_name)s
988 d: show current diff for this tag file
989 r: revert that tag file
990 c: cat the current tag file
991 n: move to next file"""%locals()
992
993             if prompt("Want to review changes on tags files",False):
994                 tagsdict = dict ( [ (x, 'todo') for x in tagsfiles ] )
995                 default_answer='d'
996             else:
997                 break
998
999         paths=""
1000         paths += self.edge_dir() + " "
1001         paths += build.edge_dir() + " "
1002         self.run_prompt("Review module and build","svn diff " + paths)
1003         self.run_prompt("Commit module and build","svn commit --file %s %s"%(changelog_svn,paths))
1004         self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog_svn,edge_url,new_tag_url))
1005
1006         if self.options.debug:
1007             print 'Preserving',changelog,'and stripped',changelog_svn
1008         else:
1009             os.unlink(changelog)
1010             os.unlink(changelog_svn)
1011             
1012 ##############################
1013     def do_branch (self):
1014
1015         # save self.branch if any, as a hint for the new branch 
1016         # do this before anything else and restore .branch to None, 
1017         # as this is part of the class's logic
1018         new_trunk_name=None
1019         if hasattr(self,'branch'):
1020             new_trunk_name=self.branch
1021             del self.branch
1022         elif self.options.new_version:
1023             new_trunk_name = self.options.new_version
1024
1025         # compute diff - a way to initialize the whole stuff
1026         # do_diff already does edge_dir initialization
1027         # and it checks that edge_url and tag_url exist as well
1028         (spec_dict,edge_url,tag_url,diff_listing) = self.do_diff(compute_only=True)
1029
1030         # the version name in the trunk becomes the new branch name
1031         branch_name = spec_dict[self.module_version_varname]
1032
1033         # figure new branch name (the one for the trunk) if not provided on the command line
1034         if not new_trunk_name:
1035             # heuristic is to assume 'version' is a dot-separated name
1036             # we isolate the rightmost part and try incrementing it by 1
1037             version=spec_dict[self.module_version_varname]
1038             try:
1039                 m=re.compile("\A(?P<leftpart>.+)\.(?P<rightmost>[^\.]+)\Z")
1040                 (leftpart,rightmost)=m.match(version).groups()
1041                 incremented = int(rightmost)+1
1042                 new_trunk_name="%s.%d"%(leftpart,incremented)
1043             except:
1044                 raise Exception, 'Cannot figure next branch name from %s - exiting'%version
1045
1046         # record starting point tagname
1047         latest_tag_name = self.tag_name(spec_dict)
1048
1049         print "**********"
1050         print "Using starting point %s (%s)"%(tag_url,latest_tag_name)
1051         print "Creating branch %s  &  moving trunk to %s"%(branch_name,new_trunk_name)
1052         print "**********"
1053
1054         # print warning if pending diffs
1055         if diff_listing:
1056             print """*** WARNING : Module %s has pending diffs on its trunk
1057 It is safe to proceed, but please note that branch %s
1058 will be based on latest tag %s and *not* on the current trunk"""%(self.name,branch_name,latest_tag_name)
1059             while True:
1060                 answer = prompt ('Are you sure you want to proceed with branching',True,('d','iff'))
1061                 if answer is True:
1062                     break
1063                 elif answer is False:
1064                     raise Exception,"User quit"
1065                 elif answer == 'd':
1066                     print '<<<< %s'%tag_url
1067                     print '>>>> %s'%edge_url
1068                     print diff_listing
1069
1070         branch_url = "%s/%s/branches/%s"%(Module.config['svnpath'],self.name,branch_name)
1071         self.check_svnpath_not_exists (branch_url,"new branch")
1072         
1073         # patching trunk
1074         spec_dict[self.module_version_varname]=new_trunk_name
1075         spec_dict[self.module_taglevel_varname]='0'
1076         # remember this in the trunk for easy location of the current branch
1077         spec_dict['module_current_branch']=branch_name
1078         self.patch_spec_var(spec_dict,True)
1079         
1080         # create commit log file
1081         tmp="/tmp/branching-%d"%os.getpid()
1082         f=open(tmp,"w")
1083         f.write("Branch %s for module %s created (as new trunk) from tag %s\n"%(branch_name,self.name,latest_tag_name))
1084         f.close()
1085
1086         # review the renumbering changes in trunk
1087         command="svn diff %s"%self.edge_dir()
1088         self.run_prompt("Review (renumbering) changes in trunk",command)
1089         # create branch
1090         command="svn copy --file %s %s %s"%(tmp,tag_url,branch_url)
1091         self.run_prompt("Create branch",command)
1092         # commit trunk
1093         command="svn commit --file %s %s"%(tmp,self.edge_dir())
1094         self.run_prompt("Commit trunk",command)
1095         # create initial tag for the new trunk
1096         new_tag_url=self.tag_url(spec_dict)
1097         command="svn copy --file %s %s %s"%(tmp,tag_url,new_tag_url)
1098         self.run_prompt("Create initial tag in trunk",command)
1099         os.unlink(tmp)
1100         # print message about BRANCH
1101         print """You might now wish to review your tags files
1102 Please make sure you mention as appropriate 
1103 %s-BRANCH := %s""" %(self.name,branch_name)
1104
1105 ##############################
1106 class Package:
1107
1108     def __init__(self, package, module, svnpath, spec,options):
1109         self.package=package
1110         self.module=module
1111         self.svnrev = None
1112         self.svnpath=svnpath    
1113         if svnpath.rfind('@') > 0:
1114             self.svnpath, self.svnrev = svnpath.split('@')
1115         self.spec=spec
1116         self.specpath="%s/%s"%(self.svnpath,self.spec)
1117         if self.svnrev:
1118             self.specpath += "@%s" % self.svnrev
1119         self.basename=os.path.basename(svnpath)
1120         self.options=options
1121
1122     # returns a http URL to the trac path where full diff can be viewed (between self and pkg)
1123     # typically http://svn.planet-lab.org/changeset?old_path=Monitor%2Ftags%2FMonitor-1.0-7&new_path=Monitor%2Ftags%2FMonitor-1.0-13
1124     # xxx quick & dirty: rough url parsing 
1125     def trac_full_diff (self, pkg):
1126         matcher=re.compile("\A(?P<method>.*)://(?P<hostname>[^/]+)/(svn/)?(?P<path>.*)\Z")
1127         self_match=matcher.match(self.svnpath)
1128         pkg_match=matcher.match(pkg.svnpath)
1129         if self_match and pkg_match:
1130             (method,hostname,svn,path)=self_match.groups()
1131             self_path=path.replace("/","%2F")
1132             pkg_path=pkg_match.group('path').replace("/","%2F")
1133             return "%s://%s/changeset?old_path=%s&new_path=%s"%(method,hostname,self_path,pkg_path)
1134         else:
1135             return None
1136     
1137     def inline_full_diff (self, pkg):
1138         print '{{{'
1139         command='svn diff %s %s'%(self.svnpath,pkg.svnpath)
1140         Command(command,self.options).run()
1141         print '}}}'
1142
1143     def details (self):
1144         return "[%s %s] [%s (spec)]"%(self.svnpath,self.basename,self.specpath)
1145
1146 class Build (Module):
1147     
1148     # we cannot get build's svnpath as for other packages as we'd get something in svn+ssh
1149     # xxx quick & dirty
1150     def __init__ (self, buildtag, options):
1151         self.buildtag=buildtag
1152         if buildtag == "trunk":
1153             module_name="build"
1154             self.display="trunk"
1155             self.svnpath="http://svn.planet-lab.org/svn/build/trunk"
1156         # if the buildtag start with a : (to use a branch rather than a tag)
1157         elif buildtag.find(':') == 0 : 
1158             module_name="build%(buildtag)s"%locals()
1159             self.display=buildtag[1:]
1160             self.svnpath="http://svn.planet-lab.org/svn/build/branches/%s"%self.display
1161         else : 
1162             module_name="build@%(buildtag)s"%locals()
1163             self.display=buildtag
1164             self.svnpath="http://svn.planet-lab.org/svn/build/tags/%s"%self.buildtag
1165         Module.__init__(self,module_name,options)
1166
1167     @staticmethod
1168     def get_distro_from_distrotag (distrotag):
1169         # mhh: remove -tag* from distrotags to get distro
1170         n=distrotag.find('-tag')
1171         if n>0:
1172             return distrotag[:n]
1173         else:
1174             return None
1175
1176     def get_packages (self,distrotag):
1177         result={}
1178         distro=Build.get_distro_from_distrotag(distrotag)
1179         if not distro:
1180             return result
1181         make_options="--no-print-directory -C %s stage1=true PLDISTRO=%s PLDISTROTAGS=%s 2> /dev/null"%(self.edge_dir(),distro,distrotag)
1182         command="make %s packages"%make_options
1183         make_packages=Command(command,self.options).output_of()
1184         if self.options.verbose:
1185             print 'obtaining packages information with command:'
1186             print command
1187         pkg_line=re.compile("\Apackage=(?P<package>[^\s]+)\s+ref_module=(?P<module>[^\s]+)\s.*\Z")
1188         for line in make_packages.split("\n"):
1189             if not line:
1190                 continue
1191             attempt=pkg_line.match(line)
1192             if line and not attempt:
1193                 print "====="
1194                 print "WARNING: line not understood from make packages"
1195                 print "in dir %s"%self.edge_dir
1196                 print "with options",make_options
1197                 print 'line=',line
1198                 print "====="
1199             else:
1200                 (package,module) = (attempt.group('package'),attempt.group('module')) 
1201                 command="make %s +%s-SVNPATH"%(make_options,module)
1202                 svnpath=Command(command,self.options).output_of().strip()
1203                 command="make %s +%s-SPEC"%(make_options,package)
1204                 spec=Command(command,self.options).output_of().strip()
1205                 result[module]=Package(package,module,svnpath,spec,self.options)
1206         return result
1207
1208     def get_distrotags (self):
1209         return [os.path.basename(p) for p in glob("%s/*tags*mk"%self.edge_dir())]
1210
1211 class DiffCache:
1212
1213     def __init__ (self):
1214         self._cache={}
1215
1216     def key(self, frompath,topath):
1217         return frompath+'-to-'+topath
1218
1219     def fetch (self, frompath, topath):
1220         key=self.key(frompath,topath)
1221         if not self._cache.has_key(key):
1222             return None
1223         return self._cache[key]
1224
1225     def store (self, frompath, topath, diff):
1226         key=self.key(frompath,topath)
1227         self._cache[key]=diff
1228
1229 class Release:
1230
1231     # header in diff output
1232     discard_matcher=re.compile("\A(\+\+\+|---).*")
1233
1234     @staticmethod
1235     def do_changelog (buildtag_new,buildtag_old,options):
1236         print "----"
1237         print "----"
1238         print "----"
1239         (build_new,build_old) = (Build (buildtag_new,options), Build (buildtag_old,options))
1240         print "= build tag %s to %s = #build-%s"%(build_old.display,build_new.display,build_new.display)
1241         for b in (build_new,build_old):
1242             b.init_module_dir()
1243             b.init_edge_dir()
1244             b.update_edge_dir()
1245         # find out the tags files that are common, unless option was specified
1246         if options.distrotags:
1247             distrotags=options.distrotags
1248         else:
1249             distrotags_new=build_new.get_distrotags()
1250             distrotags_old=build_old.get_distrotags()
1251             distrotags = list(set(distrotags_new).intersection(set(distrotags_old)))
1252             distrotags.sort()
1253         if options.verbose: print "Found distrotags",distrotags
1254         first_distrotag=True
1255         diffcache = DiffCache()
1256         for distrotag in distrotags:
1257             distro=Build.get_distro_from_distrotag(distrotag)
1258             if not distro:
1259                 continue
1260             if first_distrotag:
1261                 first_distrotag=False
1262             else:
1263                 print '----'
1264             print '== distro %s (%s to %s) == #distro-%s-%s'%(distrotag,build_old.display,build_new.display,distro,build_new.display)
1265             print ' * from %s/%s'%(build_old.svnpath,distrotag)
1266             print ' * to %s/%s'%(build_new.svnpath,distrotag)
1267
1268             # parse make packages
1269             packages_new=build_new.get_packages(distrotag)
1270             pnames_new=set(packages_new.keys())
1271             packages_old=build_old.get_packages(distrotag)
1272             pnames_old=set(packages_old.keys())
1273
1274             # get names of created, deprecated, and preserved modules
1275             pnames_created = list(pnames_new-pnames_old)
1276             pnames_deprecated = list(pnames_old-pnames_new)
1277             pnames = list(pnames_new.intersection(pnames_old))
1278
1279             pnames_created.sort()
1280             pnames_deprecated.sort()
1281             pnames.sort()
1282
1283             if options.verbose: 
1284                 print '--------------------'
1285                 print 'got packages for ',build_new.display
1286                 print pnames_new
1287                 print '--------------------'
1288                 print 'got packages for ',build_old.display
1289                 print pnames_old
1290                 print '--------------------'
1291                 print "Found new modules",pnames_created
1292                 print '--------------------'
1293                 print "Found deprecated modules",pnames_deprecated
1294                 print '--------------------'
1295                 print "Found preserved modules",pnames
1296                 print '--------------------'
1297
1298             # display created and deprecated 
1299             for name in pnames_created:
1300                 print '=== %s : new package %s -- appeared in %s === #package-%s-%s-%s'%(
1301                     distrotag,name,build_new.display,name,distro,build_new.display)
1302                 pobj=packages_new[name]
1303                 print ' * %s'%pobj.details()
1304             for name in pnames_deprecated:
1305                 print '=== %s : package %s -- deprecated, last occurrence in %s === #package-%s-%s-%s'%(
1306                     distrotag,name,build_old.display,name,distro,build_new.display)
1307                 pobj=packages_old[name]
1308                 if not pobj.svnpath:
1309                     print ' * codebase stored in CVS, specfile is %s'%pobj.spec
1310                 else:
1311                     print ' * %s'%pobj.details()
1312
1313             # display other packages
1314             for name in pnames:
1315                 (pobj_new,pobj_old)=(packages_new[name],packages_old[name])
1316                 if options.verbose: print "Dealing with package",name
1317                 if pobj_old.specpath == pobj_new.specpath:
1318                     continue
1319                 specdiff = diffcache.fetch(pobj_old.specpath,pobj_new.specpath)
1320                 if specdiff is None:
1321                     command="svn diff %s %s"%(pobj_old.specpath,pobj_new.specpath)
1322                     specdiff=Command(command,options).output_of()
1323                     diffcache.store(pobj_old.specpath,pobj_new.specpath,specdiff)
1324                 else:
1325                     if options.verbose: print 'got diff from cache'
1326                 if not specdiff:
1327                     continue
1328                 print '=== %s - %s to %s : package %s === #package-%s-%s-%s'%(
1329                     distrotag,build_old.display,build_new.display,name,name,distro,build_new.display)
1330                 print ' * from %s to %s'%(pobj_old.details(),pobj_new.details())
1331                 if options.inline_diff:
1332                     pobj_old.inline_full_diff(pobj_new)
1333                 else:
1334                     trac_diff_url=pobj_old.trac_full_diff(pobj_new)
1335                     if trac_diff_url:
1336                         print ' * [%s View full diff]'%trac_diff_url
1337                     else:
1338                         print ' * No full diff available'
1339                     print '{{{'
1340                     for line in specdiff.split('\n'):
1341                         if not line:
1342                             continue
1343                         if Release.discard_matcher.match(line):
1344                             continue
1345                         if line[0] in ['@']:
1346                             print '----------'
1347                         elif line[0] in ['+','-']:
1348                             print_fold(line)
1349                     print '}}}'
1350
1351 ##############################
1352 class Main:
1353
1354     module_usage="""Usage: %prog [options] module_desc [ .. module_desc ]
1355 Revision: $Revision$
1356
1357 module-tools : a set of tools to manage subversion tags and specfile
1358   requires the specfile to either
1359   * define *version* and *taglevel*
1360   OR alternatively 
1361   * define redirection variables module_version_varname / module_taglevel_varname
1362 Trunk:
1363   by default, the trunk of modules is taken into account
1364   in this case, just mention the module name as <module_desc>
1365 Branches:
1366   if you wish to work on a branch rather than on the trunk, 
1367   you can use something like e.g. Mom:2.1 as <module_desc>
1368 """
1369     release_usage="""Usage: %prog [options] tag1 .. tagn
1370   Extract release notes from the changes in specfiles between several build tags, latest first
1371   Examples:
1372       release-changelog 4.2-rc25 4.2-rc24 4.2-rc23 4.2-rc22
1373   You can refer to a (build) branch by prepending a colon, like in
1374       release-changelog :4.2 4.2-rc25
1375   You can refer to the build trunk by just mentioning 'trunk', e.g.
1376       release-changelog -t coblitz-tags.mk coblitz-2.01-rc6 trunk
1377 """
1378     common_usage="""More help:
1379   see http://svn.planet-lab.org/wiki/ModuleTools"""
1380
1381     modes={ 
1382         'list' : "displays a list of available tags or branches",
1383         'version' : "check latest specfile and print out details",
1384         'diff' : "show difference between module (trunk or branch) and latest tag",
1385         'tag'  : """increment taglevel in specfile, insert changelog in specfile,
1386                 create new tag and and monitor its adoption in build/*-tags*.mk""",
1387         'branch' : """create a branch for this module, from the latest tag on the trunk, 
1388                   and change trunk's version number to reflect the new branch name;
1389                   you can specify the new branch name by using module:branch""",
1390         'sync' : """create a tag from the module
1391                 this is a last resort option, mostly for repairs""",
1392         'changelog' : """extract changelog between build tags
1393                 expected arguments are a list of tags""",
1394         }
1395
1396     silent_modes = ['list']
1397     release_modes = ['changelog']
1398
1399     @staticmethod
1400     def optparse_list (option, opt, value, parser):
1401         try:
1402             setattr(parser.values,option.dest,getattr(parser.values,option.dest)+value.split())
1403         except:
1404             setattr(parser.values,option.dest,value.split())
1405
1406     def run(self):
1407
1408         mode=None
1409         for function in Main.modes.keys():
1410             if sys.argv[0].find(function) >= 0:
1411                 mode = function
1412                 break
1413         if not mode:
1414             print "Unsupported command",sys.argv[0]
1415             print "Supported commands:" + " ".join(Main.modes.keys())
1416             sys.exit(1)
1417
1418         if mode not in Main.release_modes:
1419             usage = Main.module_usage
1420             usage += Main.common_usage
1421             usage += "\nmodule-%s : %s"%(mode,Main.modes[mode])
1422         else:
1423             usage = Main.release_usage
1424             usage += Main.common_usage
1425
1426         parser=OptionParser(usage=usage,version=subversion_id)
1427         
1428         if mode == 'list':
1429             parser.add_option("-b","--branches",action="store_true",dest="list_branches",default=False,
1430                               help="list branches")
1431             parser.add_option("-t","--tags",action="store_false",dest="list_branches",
1432                               help="list tags")
1433             parser.add_option("-m","--match",action="store",dest="list_pattern",default=None,
1434                                help="grep pattern for filtering output")
1435             parser.add_option("-x","--exact-match",action="store",dest="list_exact",default=None,
1436                                help="exact grep pattern for filtering output")
1437         if mode == "tag" or mode == 'branch':
1438             parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
1439                               help="set new version and reset taglevel to 0")
1440         if mode == "tag" :
1441             parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
1442                               help="do not update changelog section in specfile when tagging")
1443             parser.add_option("-b","--build-branch", action="store", dest="build_branch", default=None,
1444                               help="specify a build branch; used for locating the *tags*.mk files where adoption is to take place")
1445         if mode == "tag" or mode == "sync" :
1446             parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
1447                               help="specify editor")
1448         if mode == "sync" :
1449             parser.add_option("-m","--message", action="store", dest="message", default=None,
1450                               help="specify log message")
1451         if mode in ["diff","version"] :
1452             parser.add_option("-W","--www", action="store", dest="www", default=False,
1453                               help="export diff in html format, e.g. -W trunk")
1454         if mode == "diff" :
1455             parser.add_option("-l","--list", action="store_true", dest="list", default=False,
1456                               help="just list modules that exhibit differences")
1457
1458         if mode  == 'version':
1459             parser.add_option("-u","--url", action="store_true", dest="show_urls", default=False,
1460                               help="display URLs")
1461             
1462         default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
1463         if mode not in Main.release_modes:
1464             parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
1465                               help="run on all modules as found in %s"%default_modules_list)
1466             parser.add_option("-f","--file",action="store",dest="modules_list",default=None,
1467                               help="run on all modules found in specified file")
1468         else:
1469             parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
1470                               help="dry run - shell commands are only displayed")
1471             parser.add_option("-i","--inline-diff",action="store_true",dest="inline_diff",default=False,
1472                               help="calls svn diff on whole module, not just only the spec file")
1473             parser.add_option("-t","--distrotags",action="callback",callback=Main.optparse_list, dest="distrotags",
1474                               default=[], nargs=1,type="string",
1475                               help="""specify distro-tags files, e.g. onelab-tags-4.2.mk
1476 -- can be set multiple times, or use quotes""")
1477
1478         parser.add_option("-w","--workdir", action="store", dest="workdir", 
1479                           default="%s/%s"%(os.getenv("HOME"),"modules"),
1480                           help="""name for dedicated working dir - defaults to ~/modules
1481 ** THIS MUST NOT ** be your usual working directory""")
1482         parser.add_option("-F","--fast-checks",action="store_true",dest="fast_checks",default=False,
1483                           help="skip safety checks, such as svn updates -- use with care")
1484
1485         # default verbosity depending on function - temp
1486         verbose_modes= ['tag', 'sync', 'branch']
1487         
1488         if mode not in verbose_modes:
1489             parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
1490                               help="run in verbose mode")
1491         else:
1492             parser.add_option("-q","--quiet", action="store_false", dest="verbose", default=True,
1493                               help="run in quiet (non-verbose) mode")
1494 #        parser.add_option("-d","--debug", action="store_true", dest="debug", default=False, 
1495 #                          help="debug mode - mostly more verbose")
1496         (options, args) = parser.parse_args()
1497         options.mode=mode
1498         if not hasattr(options,'dry_run'):
1499             options.dry_run=False
1500         if not hasattr(options,'www'):
1501             options.www=False
1502         options.debug=False
1503
1504         ########## release-*
1505         if mode in Main.release_modes :
1506             ########## changelog
1507             if len(args) <= 1:
1508                 parser.print_help()
1509                 sys.exit(1)
1510             Module.init_homedir(options)
1511             for n in range(len(args)-1):
1512                 [t_new,t_old]=args[n:n+2]
1513                 Release.do_changelog (t_new,t_old,options)
1514         else:
1515             ########## module-*
1516             if len(args) == 0:
1517                 if options.all_modules:
1518                     options.modules_list=default_modules_list
1519                 if options.modules_list:
1520                     args=Command("grep -v '#' %s"%options.modules_list,options).output_of().split()
1521                 else:
1522                     parser.print_help()
1523                     sys.exit(1)
1524             Module.init_homedir(options)
1525
1526             # 2 passes for www output
1527             modules=[ Module(modname,options) for modname in args ]
1528             # hack: create a dummy Module to store errors/warnings
1529             error_module = Module('__errors__',options)
1530
1531             # pass 1 : do it, except if options.www
1532             for module in modules:
1533                 if len(args)>1 and mode not in Main.silent_modes and not options.www:
1534                     print '========================================',module.friendly_name()
1535                 # call the method called do_<mode>
1536                 method=Module.__dict__["do_%s"%mode]
1537                 try:
1538                     method(module)
1539                 except Exception,e:
1540                     if options.www:
1541                         title='<span class="error"> Skipping module %s - failure: %s </span>'%\
1542                             (module.friendly_name(), str(e))
1543                         error_module.html_store_title(title)
1544                     else:
1545                         print 'Skipping module %s: '%modname,e
1546
1547             # in which case we do the actual printing in the second pass
1548             if options.www:
1549                 if mode == "diff":
1550                     modetitle="Changes to tag in %s"%options.www
1551                 elif mode == "version":
1552                     modetitle="Latest tags in %s"%options.www
1553                 modules.append(error_module)
1554                 error_module.html_dump_header(modetitle)
1555                 for module in modules:
1556                     module.html_dump_toc()
1557                 Module.html_dump_middle()
1558                 for module in modules:
1559                     module.html_dump_body()
1560                 Module.html_dump_footer()
1561
1562 ####################
1563 if __name__ == "__main__" :
1564     try:
1565         Main().run()
1566     except KeyboardInterrupt:
1567         print '\nBye'