ecf635ad2c91fc72bb4efcb4f0b7ba7aaec95d73
[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=[]):
13     if not isinstance (other_choices,list):
14         other_choices = [ other_choices ]
15     chars = [ c for (c,rest) in other_choices ]
16
17     the_prompt = []
18     if 'y' not in chars:
19         if default is True: the_prompt.append('[y]')
20         else : the_prompt.append('y')
21     if 'n' not in chars:
22         if default is False: the_prompt.append('[n]')
23         else : the_prompt.append('n')
24
25     for (char,choice) in other_choices:
26         if default == char:
27             the_prompt.append("["+char+"]"+choice)
28         else:
29             the_prompt.append("<"+char+">"+choice)
30     try:
31         answer=raw_input(question + " " + "/".join(the_prompt) + " ? ")
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         return prompt(question,default,other_choices)
46     except:
47         raise
48
49 class Command:
50     def __init__ (self,command,options):
51         self.command=command
52         self.options=options
53         self.tmp="/tmp/command-%d"%os.getpid()
54
55     def run (self):
56         if self.options.verbose:
57             print '+',self.command
58             sys.stdout.flush()
59         return os.system(self.command)
60
61     def run_silent (self):
62         if self.options.verbose:
63             print '+',self.command,' .. ',
64             sys.stdout.flush()
65         retcod=os.system(self.command + " &> " + self.tmp)
66         if retcod != 0:
67             print "FAILED ! -- output quoted below "
68             os.system("cat " + self.tmp)
69             print "FAILED ! -- end of quoted output"
70         elif self.options.verbose:
71             print "OK"
72         os.unlink(self.tmp)
73         return retcod
74
75     def run_fatal(self):
76         if self.run_silent() !=0:
77             raise Exception,"Command %s failed"%self.command
78
79     # returns stdout, like bash's $(mycommand)
80     def output_of (self,with_stderr=False):
81         tmp="/tmp/status-%d"%os.getpid()
82         if self.options.debug:
83             print '+',self.command,' .. ',
84             sys.stdout.flush()
85         command=self.command
86         if with_stderr:
87             command += " &> "
88         else:
89             command += " > "
90         command += tmp
91         os.system(command)
92         result=file(tmp).read()
93         os.unlink(tmp)
94         if self.options.debug:
95             print 'Done',
96         return result
97
98 class Svnpath:
99     def __init__(self,path,options):
100         self.path=path
101         self.options=options
102
103     def url_exists (self):
104         return os.system("svn list %s &> /dev/null"%self.path) == 0
105
106     def dir_needs_revert (self):
107         command="svn status %s"%self.path
108         return len(Command(command,self.options).output_of(True)) != 0
109     # turns out it's the same implem.
110     def file_needs_commit (self):
111         command="svn status %s"%self.path
112         return len(Command(command,self.options).output_of(True)) != 0
113
114 class Module:
115
116     svn_magic_line="--This line, and those below, will be ignored--"
117     
118     redirectors=[ # ('module_name_varname','name'),
119                   ('module_version_varname','version'),
120                   ('module_taglevel_varname','taglevel'), ]
121
122     # where to store user's config
123     config_storage="CONFIG"
124     # 
125     config={}
126
127     import commands
128     configKeys=[ ('svnpath',"Enter your toplevel svnpath",
129                   "svn+ssh://%s@svn.planet-lab.org/svn/"%commands.getoutput("id -un")),
130                  ("build", "Enter the name of your build module","build"),
131                  ('username',"Enter your firstname and lastname for changelogs",""),
132                  ("email","Enter your email address for changelogs",""),
133                  ]
134
135     @staticmethod
136     def prompt_config ():
137         for (key,message,default) in Module.configKeys:
138             Module.config[key]=""
139             while not Module.config[key]:
140                 Module.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
141
142
143     # for parsing module spec name:branch
144     matcher_branch_spec=re.compile("\A(?P<name>[\w-]+):(?P<branch>[\w\.]+)\Z")
145     matcher_rpm_define=re.compile("%(define|global)\s+(\S+)\s+(\S*)\s*")
146
147     def __init__ (self,module_spec,options):
148         # parse module spec
149         attempt=Module.matcher_branch_spec.match(module_spec)
150         if attempt:
151             self.name=attempt.group('name')
152             self.branch=attempt.group('branch')
153         else:
154             self.name=module_spec
155             self.branch=None
156
157         self.options=options
158         self.moddir="%s/%s"%(options.workdir,self.name)
159
160     def friendly_name (self):
161         if not self.branch:
162             return self.name
163         else:
164             return "%s:%s"%(self.name,self.branch)
165
166     def edge_dir (self):
167         if not self.branch:
168             return "%s/trunk"%(self.moddir)
169         else:
170             return "%s/branches/%s"%(self.moddir,self.branch)
171
172     def tags_dir (self):
173         return "%s/tags"%(self.moddir)
174
175     def run (self,command):
176         return Command(command,self.options).run()
177     def run_fatal (self,command):
178         return Command(command,self.options).run_fatal()
179     def run_prompt (self,message,command):
180         if not self.options.verbose:
181             question=message
182         else:
183             question=message+" - want to run " + command
184         if prompt(question,True):
185             self.run(command)            
186
187     @staticmethod
188     def init_homedir (options):
189         topdir=options.workdir
190         if options.verbose:
191             print 'Checking for',topdir
192         storage="%s/%s"%(topdir,Module.config_storage)
193         # sanity check. Either the topdir exists AND we have a config/storage
194         # or topdir does not exist and we create it
195         # to avoid people use their own daily svn repo
196         if os.path.isdir(topdir) and not os.path.isfile(storage):
197             print """The directory %s exists and has no CONFIG file
198 If this is your regular working directory, please provide another one as the
199 module-* commands need a fresh working dir. Make sure that you do not use 
200 that for other purposes than tagging"""%topdir
201             sys.exit(1)
202         if not os.path.isdir (topdir):
203             print "Cannot find",topdir,"let's create it"
204             Module.prompt_config()
205             print "Checking ...",
206             Command("svn co -N %s %s"%(Module.config['svnpath'],topdir),options).run_fatal()
207             Command("svn co -N %s/%s %s/%s"%(Module.config['svnpath'],
208                                              Module.config['build'],
209                                              topdir,
210                                              Module.config['build']),options).run_fatal()
211             print "OK"
212             
213             # store config
214             f=file(storage,"w")
215             for (key,message,default) in Module.configKeys:
216                 f.write("%s=%s\n"%(key,Module.config[key]))
217             f.close()
218             if options.debug:
219                 print 'Stored',storage
220                 Command("cat %s"%storage,options).run()
221         else:
222             # read config
223             f=open(storage)
224             for line in f.readlines():
225                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
226                 Module.config[key]=value                
227             f.close()
228         if options.verbose:
229             print '******** Using config'
230             for (key,message,default) in Module.configKeys:
231                 print '\t',key,'=',Module.config[key]
232
233     def init_moddir (self):
234         if self.options.verbose:
235             print 'Checking for',self.moddir
236         if not os.path.isdir (self.moddir):
237             self.run_fatal("svn up -N %s"%self.moddir)
238         if not os.path.isdir (self.moddir):
239             print 'Cannot find %s - check module name'%self.moddir
240             sys.exit(1)
241
242     def init_subdir (self,fullpath):
243         if self.options.verbose:
244             print 'Checking for',fullpath
245         if not os.path.isdir (fullpath):
246             self.run_fatal("svn up -N %s"%fullpath)
247
248     def revert_subdir (self,fullpath):
249         if self.options.fast_checks:
250             if self.options.verbose: print 'Skipping revert of %s'%fullpath
251             return
252         if self.options.verbose:
253             print 'Checking whether',fullpath,'needs being reverted'
254         if Svnpath(fullpath,self.options).dir_needs_revert():
255             self.run_fatal("svn revert -R %s"%fullpath)
256
257     def update_subdir (self,fullpath):
258         if self.options.fast_checks:
259             if self.options.verbose: print 'Skipping update of %s'%fullpath
260             return
261         if self.options.verbose:
262             print 'Updating',fullpath
263         self.run_fatal("svn update -N %s"%fullpath)
264
265     def init_edge_dir (self):
266         # if branch, edge_dir is two steps down
267         if self.branch:
268             self.init_subdir("%s/branches"%self.moddir)
269         self.init_subdir(self.edge_dir())
270
271     def revert_edge_dir (self):
272         self.revert_subdir(self.edge_dir())
273
274     def update_edge_dir (self):
275         self.update_subdir(self.edge_dir())
276
277     def main_specname (self):
278         attempt="%s/%s.spec"%(self.edge_dir(),self.name)
279         if os.path.isfile (attempt):
280             return attempt
281         else:
282             try:
283                 return glob("%s/*.spec"%self.edge_dir())[0]
284             except:
285                 print 'Cannot guess specfile for module %s'%self.name
286                 sys.exit(1)
287
288     def all_specnames (self):
289         return glob("%s/*.spec"%self.edge_dir())
290
291     def parse_spec (self, specfile, varnames):
292         if self.options.verbose:
293             print 'Parsing',specfile,
294             for var in varnames:
295                 print "[%s]"%var,
296             print ""
297         result={}
298         f=open(specfile)
299         for line in f.readlines():
300             attempt=Module.matcher_rpm_define.match(line)
301             if attempt:
302                 (define,var,value)=attempt.groups()
303                 if var in varnames:
304                     result[var]=value
305         f.close()
306         if self.options.debug:
307             print 'found',len(result),'keys'
308             for (k,v) in result.iteritems():
309                 print k,'=',v
310         return result
311                 
312     # stores in self.module_name_varname the rpm variable to be used for the module's name
313     # and the list of these names in self.varnames
314     def spec_dict (self):
315         specfile=self.main_specname()
316         redirector_keys = [ varname for (varname,default) in Module.redirectors]
317         redirect_dict = self.parse_spec(specfile,redirector_keys)
318         if self.options.debug:
319             print '1st pass parsing done, redirect_dict=',redirect_dict
320         varnames=[]
321         for (varname,default) in Module.redirectors:
322             if redirect_dict.has_key(varname):
323                 setattr(self,varname,redirect_dict[varname])
324                 varnames += [redirect_dict[varname]]
325             else:
326                 setattr(self,varname,default)
327                 varnames += [ default ] 
328         self.varnames = varnames
329         result = self.parse_spec (specfile,self.varnames)
330         if self.options.debug:
331             print '2st pass parsing done, varnames=',varnames,'result=',result
332         return result
333
334     def patch_spec_var (self, patch_dict):
335         for specfile in self.all_specnames():
336             newspecfile=specfile+".new"
337             if self.options.verbose:
338                 print 'Patching',specfile,'for',patch_dict.keys()
339             spec=open (specfile)
340             new=open(newspecfile,"w")
341
342             for line in spec.readlines():
343                 attempt=Module.matcher_rpm_define.match(line)
344                 if attempt:
345                     (define,var,value)=attempt.groups()
346                     if var in patch_dict.keys():
347                         new.write('%%%s %s %s\n'%(define,var,patch_dict[var]))
348                         continue
349                 new.write(line)
350             spec.close()
351             new.close()
352             os.rename(newspecfile,specfile)
353
354     def unignored_lines (self, logfile):
355         result=[]
356         exclude="Tagging module %s"%self.name
357         for logline in file(logfile).readlines():
358             if logline.strip() == Module.svn_magic_line:
359                 break
360             if logline.find(exclude) < 0:
361                 result += [ logline ]
362         return result
363
364     def insert_changelog (self, logfile, oldtag, newtag):
365         for specfile in self.all_specnames():
366             newspecfile=specfile+".new"
367             if self.options.verbose:
368                 print 'Inserting changelog from %s into %s'%(logfile,specfile)
369             spec=open (specfile)
370             new=open(newspecfile,"w")
371             for line in spec.readlines():
372                 new.write(line)
373                 if re.compile('%changelog').match(line):
374                     dateformat="* %a %b %d %Y"
375                     datepart=time.strftime(dateformat)
376                     logpart="%s <%s> - %s %s"%(Module.config['username'],
377                                                  Module.config['email'],
378                                                  oldtag,newtag)
379                     new.write(datepart+" "+logpart+"\n")
380                     for logline in self.unignored_lines(logfile):
381                         new.write("- " + logline)
382                     new.write("\n")
383             spec.close()
384             new.close()
385             os.rename(newspecfile,specfile)
386             
387     def show_dict (self, spec_dict):
388         if self.options.verbose:
389             for (k,v) in spec_dict.iteritems():
390                 print k,'=',v
391
392     def mod_url (self):
393         return "%s/%s"%(Module.config['svnpath'],self.name)
394
395     def edge_url (self):
396         if not self.branch:
397             return "%s/trunk"%(self.mod_url())
398         else:
399             return "%s/branches/%s"%(self.mod_url(),self.branch)
400
401     def tag_name (self, spec_dict):
402         try:
403             return "%s-%s-%s"%(#spec_dict[self.module_name_varname],
404                 self.name,
405                 spec_dict[self.module_version_varname],
406                 spec_dict[self.module_taglevel_varname])
407         except KeyError,err:
408             print 'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
409             sys.exit(1)
410
411     def tag_url (self, spec_dict):
412         return "%s/tags/%s"%(self.mod_url(),self.tag_name(spec_dict))
413
414     def check_svnpath_exists (self, url, message):
415         if self.options.fast_checks:
416             return
417         if self.options.verbose:
418             print 'Checking url (%s) %s'%(url,message),
419         ok=Svnpath(url,self.options).url_exists()
420         if ok:
421             if self.options.verbose: print 'exists - OK'
422         else:
423             if self.options.verbose: print 'KO'
424             print 'Could not find %s URL %s'%(message,url)
425             sys.exit(1)
426     def check_svnpath_not_exists (self, url, message):
427         if self.options.fast_checks:
428             return
429         if self.options.verbose:
430             print 'Checking url (%s) %s'%(url,message),
431         ok=not Svnpath(url,self.options).url_exists()
432         if ok:
433             if self.options.verbose: print 'does not exist - OK'
434         else:
435             if self.options.verbose: print 'KO'
436             print '%s URL %s already exists - exiting'%(message,url)
437             sys.exit(1)
438
439     # locate specfile, parse it, check it and show values
440 ##############################
441     def do_version (self):
442         self.init_moddir()
443         self.init_edge_dir()
444         self.revert_edge_dir()
445         self.update_edge_dir()
446         spec_dict = self.spec_dict()
447         for varname in self.varnames:
448             if not spec_dict.has_key(varname):
449                 print 'Could not find %%define for %s'%varname
450                 return
451             else:
452                 print varname+":",spec_dict[varname]
453         print 'edge url',self.edge_url()
454         print 'latest tag url',self.tag_url(spec_dict)
455         if self.options.verbose:
456             print 'main specfile:',self.main_specname()
457             print 'specfiles:',self.all_specnames()
458
459     init_warning="""*** WARNING
460 The module-init function has the following limitations
461 * it does not handle changelogs
462 * it does not scan the -tags*.mk files to adopt the new tags"""
463 ##############################
464     def do_sync(self):
465         if self.options.verbose:
466             print Module.init_warning
467             if not prompt('Want to proceed anyway'):
468                 return
469
470         self.init_moddir()
471         self.init_edge_dir()
472         self.revert_edge_dir()
473         self.update_edge_dir()
474         spec_dict = self.spec_dict()
475
476         edge_url=self.edge_url()
477         tag_name=self.tag_name(spec_dict)
478         tag_url=self.tag_url(spec_dict)
479         # check the tag does not exist yet
480         self.check_svnpath_not_exists(tag_url,"new tag")
481
482         if self.options.message:
483             svnopt='--message "%s"'%self.options.message
484         else:
485             svnopt='--editor-cmd=%s'%self.options.editor
486         self.run_prompt("Create initial tag",
487                         "svn copy %s %s %s"%(svnopt,edge_url,tag_url))
488
489 ##############################
490     def do_diff (self,compute_only=False):
491         self.init_moddir()
492         self.init_edge_dir()
493         self.revert_edge_dir()
494         self.update_edge_dir()
495         spec_dict = self.spec_dict()
496         self.show_dict(spec_dict)
497
498         edge_url=self.edge_url()
499         tag_url=self.tag_url(spec_dict)
500         self.check_svnpath_exists(edge_url,"edge track")
501         self.check_svnpath_exists(tag_url,"latest tag")
502         command="svn diff %s %s"%(tag_url,edge_url)
503         if compute_only:
504             print 'Getting diff with %s'%command
505         diff_output = Command(command,self.options).output_of()
506         # if used as a utility
507         if compute_only:
508             return (spec_dict,edge_url,tag_url,diff_output)
509         # otherwise print the result
510         if self.options.list:
511             if diff_output:
512                 print self.name
513         else:
514             if not self.options.only or diff_output:
515                 print 'x'*30,'module',self.friendly_name()
516                 print 'x'*20,'<',tag_url
517                 print 'x'*20,'>',edge_url
518                 print diff_output
519
520 ##############################
521     # using fine_grain means replacing only those instances that currently refer to this tag
522     # otherwise, <module>-SVNPATH is replaced unconditionnally
523     def patch_tags_file (self, tagsfile, oldname, newname,fine_grain=True):
524         newtagsfile=tagsfile+".new"
525         tags=open (tagsfile)
526         new=open(newtagsfile,"w")
527
528         matches=0
529         # fine-grain : replace those lines that refer to oldname
530         if fine_grain:
531             if self.options.verbose:
532                 print 'Replacing %s into %s\n\tin %s .. '%(oldname,newname,tagsfile),
533             matcher=re.compile("^(.*)%s(.*)"%oldname)
534             for line in tags.readlines():
535                 if not matcher.match(line):
536                     new.write(line)
537                 else:
538                     (begin,end)=matcher.match(line).groups()
539                     new.write(begin+newname+end+"\n")
540                     matches += 1
541         # brute-force : change uncommented lines that define <module>-SVNPATH
542         else:
543             if self.options.verbose:
544                 print 'Setting %s-SVNPATH for using %s\n\tin %s .. '%(self.name,newname,tagsfile),
545             pattern="\A\s*%s-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
546                                           %(self.name,self.name)
547             matcher_module=re.compile(pattern)
548             for line in tags.readlines():
549                 attempt=matcher_module.match(line)
550                 if attempt:
551                     svnpath="%s-SVNPATH"%self.name
552                     replacement = "%-32s:= %s/%s/tags/%s\n"%(svnpath,attempt.group('url_main'),self.name,newname)
553                     new.write(replacement)
554                     matches += 1
555                 else:
556                     new.write(line)
557         tags.close()
558         new.close()
559         os.rename(newtagsfile,tagsfile)
560         if self.options.verbose: print "%d changes"%matches
561         return matches
562
563     def do_tag (self):
564         self.init_moddir()
565         self.init_edge_dir()
566         self.revert_edge_dir()
567         self.update_edge_dir()
568         # parse specfile
569         spec_dict = self.spec_dict()
570         self.show_dict(spec_dict)
571         
572         # side effects
573         edge_url=self.edge_url()
574         old_tag_name = self.tag_name(spec_dict)
575         old_tag_url=self.tag_url(spec_dict)
576         if (self.options.new_version):
577             # new version set on command line
578             spec_dict[self.module_version_varname] = self.options.new_version
579             spec_dict[self.module_taglevel_varname] = 0
580         else:
581             # increment taglevel
582             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
583             spec_dict[self.module_taglevel_varname] = new_taglevel
584
585         # sanity check
586         new_tag_name = self.tag_name(spec_dict)
587         new_tag_url=self.tag_url(spec_dict)
588         self.check_svnpath_exists (edge_url,"edge track")
589         self.check_svnpath_exists (old_tag_url,"previous tag")
590         self.check_svnpath_not_exists (new_tag_url,"new tag")
591
592         # checking for diffs
593         diff_output=Command("svn diff %s %s"%(old_tag_url,edge_url),
594                             self.options).output_of()
595         if len(diff_output) == 0:
596             if not prompt ("No difference in trunk for module %s, want to tag anyway"%self.name,False):
597                 return
598
599         # side effect in trunk's specfile
600         self.patch_spec_var(spec_dict)
601
602         # prepare changelog file 
603         # we use the standard subversion magic string (see svn_magic_line)
604         # so we can provide useful information, such as version numbers and diff
605         # in the same file
606         changelog="/tmp/%s-%d.txt"%(self.name,os.getpid())
607         file(changelog,"w").write("""Tagging module %s - %s
608
609 %s
610 Please write a changelog for this new tag in the section above
611 """%(self.name,new_tag_name,Module.svn_magic_line))
612
613         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
614             file(changelog,"a").write('DIFF=========\n' + diff_output)
615         
616         if self.options.debug:
617             prompt('Proceed ?')
618
619         # edit it        
620         self.run("%s %s"%(self.options.editor,changelog))
621         # insert changelog in spec
622         if self.options.changelog:
623             self.insert_changelog (changelog,old_tag_name,new_tag_name)
624
625         ## update build
626         try:
627             buildname=Module.config['build']
628         except:
629             buildname="build"
630         build = Module(buildname,self.options)
631         build.init_moddir()
632         build.init_edge_dir()
633         build.revert_edge_dir()
634         build.update_edge_dir()
635         
636         tagsfiles=glob(build.edge_dir()+"/*-tags*.mk")
637         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
638         while True:
639             for (tagsfile,status) in tagsdict.iteritems():
640                 while tagsdict[tagsfile] == 'todo' :
641                     choice = prompt ("Want to adopt %s in %s    "%(new_tag_name,os.path.basename(tagsfile)),'a',
642                                      [ ('n', 'ext'), ('a','uto'), ('d','iff'), ('r','evert'), ('h','elp') ] )
643                     if choice is True:
644                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
645                     elif choice is 'n':
646                         print 'Done with %s'%os.path.basename(tagsfile)
647                         tagsdict[tagsfile]='done'
648                     elif choice == 'a':
649                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
650                     elif choice == 'd':
651                         self.run("svn diff %s"%tagsfile)
652                     elif choice == 'r':
653                         self.run("svn revert %s"%tagsfile)
654                     elif choice == 'h':
655                         name=self.name
656                         print """y: unconditionnally changes any line setting %(name)s-SVNPATH to using %(new_tag_name)s
657 a: changes the definition of %(name)s only if it currently refers to %(old_tag_name)s
658 d: shows current diff for this tag file
659 r: reverts that tag file
660 n: move to next file"""%locals()
661                     else:
662                         print 'unexpected'
663             if prompt("Want to review changes on tags files",False):
664                 tagsdict = dict ( [ (x, 'todo') for tagsfile in tagsfiles ] )
665             else:
666                 break
667
668         paths=""
669         paths += self.edge_dir() + " "
670         paths += build.edge_dir() + " "
671         self.run_prompt("Check","svn diff " + paths)
672         self.run_prompt("Commit","svn commit --file %s %s"%(changelog,paths))
673         self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog,edge_url,new_tag_url))
674
675         if self.options.debug:
676             print 'Preserving',changelog
677         else:
678             os.unlink(changelog)
679             
680 ##############################
681     def do_branch (self):
682
683         # save self.branch if any, as a hint for the new branch 
684         # do this before anything else and restore .branch to None, 
685         # as this is part of the class's logic
686         new_branch_name=None
687         if self.branch:
688             new_branch_name=self.branch
689             self.branch=None
690
691         # check module-diff is empty
692         # used in do_diff, and should be options for diff only
693         (spec_dict,edge_url,tag_url,diff_listing) = self.do_diff(compute_only=True)
694         if diff_listing:
695             print '*** WARNING : Module %s has pending diffs on its trunk'%self.name
696             while True:
697                 answer = prompt ('Are you sure you want to proceed with branching',False,('d','iff'))
698                 if answer is True:
699                     break
700                 elif answer is False:
701                     sys.exit(1)
702                 elif answer == 'd':
703                     print '<<<< %s'%tag_url
704                     print '>>>> %s'%edge_url
705                     print diff_listing
706
707         # do_diff already does edge_dir initialization
708         # and it checks that edge_url and tag_url exist as well
709         print "Using starting point %s"%tag_url
710
711         # figure new branch name
712         if not new_branch_name:
713             # heuristic is to assume 'version' is a dot-separated name
714             # we isolate the rightmost part and try incrementing it by 1
715             print 'Trying to guess a new branch name for the trunk'
716             version=spec_dict[self.module_version_varname]
717             try:
718                 m=re.compile("\A(?P<leftpart>.+)\.(?P<rightmost>[^\.]+)\Z")
719                 (leftpart,rightmost)=m.match(version).groups()
720                 incremented = int(rightmost)+1
721                 new_branch_name="%s.%d"%(leftpart,incremented)
722             except:
723                 print 'Cannot figure next branch name from %s - exiting'%version
724                 sys.exit(1)
725
726         branch_url = "%s/%s/branches/%s"%(Module.config['svnpath'],self.name,new_branch_name)
727         self.check_svnpath_not_exists (branch_url,"new branch")
728         
729         # record starting point tagname
730         old_tag_name = self.tag_name(spec_dict)
731
732         # patching trunk
733         spec_dict[self.module_version_varname]=new_branch_name
734         spec_dict[self.module_taglevel_varname]='0'
735         
736         self.patch_spec_var(spec_dict)
737         
738         # create commit log file
739         tmp="/tmp/branching-%d"%os.getpid()
740         f=open(tmp,"w")
741         f.write("Branch %s for module %s created from tag %s\n"%(new_branch_name,self.name,old_tag_name))
742         f.close()
743
744         # we're done, let's commit the stuff
745         command="svn diff %s"%self.edge_dir()
746         self.run_prompt("Check trunk",command)
747         command="svn copy %s %s"%(self.edge_dir(),branch_url)
748         self.run_prompt("Create branch",command)
749         command="svn commit --file %s %s"%(tmp,self.edge_dir())
750         self.run_prompt("Commit trunk",command)
751         os.unlink(tmp)
752
753 ##############################
754 usage="""Usage: %prog options module_desc [ .. module_desc ]
755 Purpose:
756   manage subversion tags and specfile
757   requires the specfile to define *version* and *taglevel*
758   OR alternatively 
759   redirection variables module_version_varname / module_taglevel_varname
760 Trunk:
761   by default, the trunk of modules is taken into account
762   in this case, just mention the module name as <module_desc>
763 Branches:
764   if you wish to work on a branch rather than on the trunk, 
765   you can use something like e.g. Mom:2.1 as <module_desc>
766 More help:
767   see http://svn.planet-lab.org/wiki/ModuleTools
768 """
769
770 functions={ 
771     'version' : "only check specfile and print out details",
772     'diff' : "show difference between trunk and latest tag",
773     'tag'  : """increment taglevel in specfile, insert changelog in specfile,
774                 create new tag and and monitor its adoption in build/*-tags*.mk""",
775     'branch' : """create a branch for this module, from the latest tag on the trunk, 
776                   and change trunk's version number to reflect the new branch name;
777                   you can specify the new branch name by using module:branch""",
778     'sync' : """create a tag from the trunk
779                 this is a last resort option, mostly for repairs""",
780 }
781
782 def main():
783
784     mode=None
785     for function in functions.keys():
786         if sys.argv[0].find(function) >= 0:
787             mode = function
788             break
789     if not mode:
790         print "Unsupported command",sys.argv[0]
791         sys.exit(1)
792
793     global usage
794     usage += "\nmodule-%s : %s"%(mode,functions[mode])
795     all_modules=os.path.dirname(sys.argv[0])+"/modules.list"
796
797     parser=OptionParser(usage=usage,version=subversion_id)
798     parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
799                       help="run on all modules as found in %s"%all_modules)
800     parser.add_option("-f","--fast-checks",action="store_true",dest="fast_checks",default=False,
801                       help="skip safety checks, such as svn updates -- use with care")
802     if mode == "tag" or mode == 'branch':
803         parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
804                           help="set new version and reset taglevel to 0")
805     if mode == "tag" :
806         parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
807                           help="do not update changelog section in specfile when tagging")
808     if mode == "tag" or mode == "sync" :
809         parser.add_option("-e","--editor", action="store", dest="editor", default="emacs",
810                           help="specify editor")
811     if mode == "sync" :
812         parser.add_option("-m","--message", action="store", dest="message", default=None,
813                           help="specify log message")
814     if mode == "diff" :
815         parser.add_option("-o","--only", action="store_true", dest="only", default=False,
816                           help="report diff only for modules that exhibit differences")
817     if mode == "diff" :
818         parser.add_option("-l","--list", action="store_true", dest="list", default=False,
819                           help="just list modules that exhibit differences")
820     parser.add_option("-w","--workdir", action="store", dest="workdir", 
821                       default="%s/%s"%(os.getenv("HOME"),"modules"),
822                       help="""name for dedicated working dir - defaults to ~/modules
823 ** THIS MUST NOT ** be your usual working directory""")
824     parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=True, 
825                       help="run in verbose mode")
826     parser.add_option("-q","--quiet", action="store_false", dest="verbose", 
827                       help="run in quiet (non-verbose) mode")
828     parser.add_option("-d","--debug", action="store_true", dest="debug", default=False, 
829                       help="debug mode - mostly more verbose")
830     (options, args) = parser.parse_args()
831
832     if len(args) == 0:
833         if options.all_modules:
834             args=Command("grep -v '#' %s"%all_modules,options).output_of().split()
835         else:
836             parser.print_help()
837             sys.exit(1)
838     Module.init_homedir(options)
839     for modname in args:
840         module=Module(modname,options)
841         print '========================================',module.friendly_name()
842         # call the method called do_<mode>
843         method=Module.__dict__["do_%s"%mode]
844         method(module)
845
846 # basically, we exit if anything goes wrong
847 if __name__ == "__main__" :
848     try:
849         main()
850     except KeyboardInterrupt:
851         print '\nBye'
852