fixes module-branch
[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,define_missing=False):
335         for specfile in self.all_specnames():
336             # record the keys that were changed
337             changed = dict ( [ (x,False) for x in patch_dict.keys() ] )
338             newspecfile=specfile+".new"
339             if self.options.verbose:
340                 print 'Patching',specfile,'for',patch_dict.keys()
341             spec=open (specfile)
342             new=open(newspecfile,"w")
343
344             for line in spec.readlines():
345                 attempt=Module.matcher_rpm_define.match(line)
346                 if attempt:
347                     (define,var,value)=attempt.groups()
348                     if var in patch_dict.keys():
349                         if self.options.debug:
350                             print 'rewriting %s as %s'%(var,patch_dict[var])
351                         new.write('%%%s %s %s\n'%(define,var,patch_dict[var]))
352                         changed[var]=True
353                         continue
354                 new.write(line)
355             if define_missing:
356                 for (key,was_changed) in changed.iteritems():
357                     if not was_changed:
358                         if self.options.debug:
359                             print 'rewriting missed %s as %s'%(key,patch_dict[key])
360                         new.write('%%define %s %s\n'%(key,patch_dict[key]))
361             spec.close()
362             new.close()
363             os.rename(newspecfile,specfile)
364
365     def unignored_lines (self, logfile):
366         result=[]
367         exclude="Tagging module %s"%self.name
368         for logline in file(logfile).readlines():
369             if logline.strip() == Module.svn_magic_line:
370                 break
371             if logline.find(exclude) < 0:
372                 result += [ logline ]
373         return result
374
375     def insert_changelog (self, logfile, oldtag, newtag):
376         for specfile in self.all_specnames():
377             newspecfile=specfile+".new"
378             if self.options.verbose:
379                 print 'Inserting changelog from %s into %s'%(logfile,specfile)
380             spec=open (specfile)
381             new=open(newspecfile,"w")
382             for line in spec.readlines():
383                 new.write(line)
384                 if re.compile('%changelog').match(line):
385                     dateformat="* %a %b %d %Y"
386                     datepart=time.strftime(dateformat)
387                     logpart="%s <%s> - %s %s"%(Module.config['username'],
388                                                  Module.config['email'],
389                                                  oldtag,newtag)
390                     new.write(datepart+" "+logpart+"\n")
391                     for logline in self.unignored_lines(logfile):
392                         new.write("- " + logline)
393                     new.write("\n")
394             spec.close()
395             new.close()
396             os.rename(newspecfile,specfile)
397             
398     def show_dict (self, spec_dict):
399         if self.options.verbose:
400             for (k,v) in spec_dict.iteritems():
401                 print k,'=',v
402
403     def mod_url (self):
404         return "%s/%s"%(Module.config['svnpath'],self.name)
405
406     def edge_url (self):
407         if not self.branch:
408             return "%s/trunk"%(self.mod_url())
409         else:
410             return "%s/branches/%s"%(self.mod_url(),self.branch)
411
412     def tag_name (self, spec_dict):
413         try:
414             return "%s-%s-%s"%(#spec_dict[self.module_name_varname],
415                 self.name,
416                 spec_dict[self.module_version_varname],
417                 spec_dict[self.module_taglevel_varname])
418         except KeyError,err:
419             print 'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
420             sys.exit(1)
421
422     def tag_url (self, spec_dict):
423         return "%s/tags/%s"%(self.mod_url(),self.tag_name(spec_dict))
424
425     def check_svnpath_exists (self, url, message):
426         if self.options.fast_checks:
427             return
428         if self.options.verbose:
429             print 'Checking url (%s) %s'%(url,message),
430         ok=Svnpath(url,self.options).url_exists()
431         if ok:
432             if self.options.verbose: print 'exists - OK'
433         else:
434             if self.options.verbose: print 'KO'
435             print 'Could not find %s URL %s'%(message,url)
436             sys.exit(1)
437     def check_svnpath_not_exists (self, url, message):
438         if self.options.fast_checks:
439             return
440         if self.options.verbose:
441             print 'Checking url (%s) %s'%(url,message),
442         ok=not Svnpath(url,self.options).url_exists()
443         if ok:
444             if self.options.verbose: print 'does not exist - OK'
445         else:
446             if self.options.verbose: print 'KO'
447             print '%s URL %s already exists - exiting'%(message,url)
448             sys.exit(1)
449
450     # locate specfile, parse it, check it and show values
451 ##############################
452     def do_version (self):
453         self.init_moddir()
454         self.init_edge_dir()
455         self.revert_edge_dir()
456         self.update_edge_dir()
457         spec_dict = self.spec_dict()
458         for varname in self.varnames:
459             if not spec_dict.has_key(varname):
460                 print 'Could not find %%define for %s'%varname
461                 return
462             else:
463                 print varname+":",spec_dict[varname]
464         print 'edge url',self.edge_url()
465         print 'latest tag url',self.tag_url(spec_dict)
466         if self.options.verbose:
467             print 'main specfile:',self.main_specname()
468             print 'specfiles:',self.all_specnames()
469
470     init_warning="""*** WARNING
471 The module-init function has the following limitations
472 * it does not handle changelogs
473 * it does not scan the -tags*.mk files to adopt the new tags"""
474 ##############################
475     def do_sync(self):
476         if self.options.verbose:
477             print Module.init_warning
478             if not prompt('Want to proceed anyway'):
479                 return
480
481         self.init_moddir()
482         self.init_edge_dir()
483         self.revert_edge_dir()
484         self.update_edge_dir()
485         spec_dict = self.spec_dict()
486
487         edge_url=self.edge_url()
488         tag_name=self.tag_name(spec_dict)
489         tag_url=self.tag_url(spec_dict)
490         # check the tag does not exist yet
491         self.check_svnpath_not_exists(tag_url,"new tag")
492
493         if self.options.message:
494             svnopt='--message "%s"'%self.options.message
495         else:
496             svnopt='--editor-cmd=%s'%self.options.editor
497         self.run_prompt("Create initial tag",
498                         "svn copy %s %s %s"%(svnopt,edge_url,tag_url))
499
500 ##############################
501     def do_diff (self,compute_only=False):
502         self.init_moddir()
503         self.init_edge_dir()
504         self.revert_edge_dir()
505         self.update_edge_dir()
506         spec_dict = self.spec_dict()
507         self.show_dict(spec_dict)
508
509         edge_url=self.edge_url()
510         tag_url=self.tag_url(spec_dict)
511         self.check_svnpath_exists(edge_url,"edge track")
512         self.check_svnpath_exists(tag_url,"latest tag")
513         command="svn diff %s %s"%(tag_url,edge_url)
514         if compute_only:
515             print 'Getting diff with %s'%command
516         diff_output = Command(command,self.options).output_of()
517         # if used as a utility
518         if compute_only:
519             return (spec_dict,edge_url,tag_url,diff_output)
520         # otherwise print the result
521         if self.options.list:
522             if diff_output:
523                 print self.name
524         else:
525             if not self.options.only or diff_output:
526                 print 'x'*30,'module',self.friendly_name()
527                 print 'x'*20,'<',tag_url
528                 print 'x'*20,'>',edge_url
529                 print diff_output
530
531 ##############################
532     # using fine_grain means replacing only those instances that currently refer to this tag
533     # otherwise, <module>-SVNPATH is replaced unconditionnally
534     def patch_tags_file (self, tagsfile, oldname, newname,fine_grain=True):
535         newtagsfile=tagsfile+".new"
536         tags=open (tagsfile)
537         new=open(newtagsfile,"w")
538
539         matches=0
540         # fine-grain : replace those lines that refer to oldname
541         if fine_grain:
542             if self.options.verbose:
543                 print 'Replacing %s into %s\n\tin %s .. '%(oldname,newname,tagsfile),
544             matcher=re.compile("^(.*)%s(.*)"%oldname)
545             for line in tags.readlines():
546                 if not matcher.match(line):
547                     new.write(line)
548                 else:
549                     (begin,end)=matcher.match(line).groups()
550                     new.write(begin+newname+end+"\n")
551                     matches += 1
552         # brute-force : change uncommented lines that define <module>-SVNPATH
553         else:
554             if self.options.verbose:
555                 print 'Setting %s-SVNPATH for using %s\n\tin %s .. '%(self.name,newname,tagsfile),
556             pattern="\A\s*%s-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
557                                           %(self.name,self.name)
558             matcher_module=re.compile(pattern)
559             for line in tags.readlines():
560                 attempt=matcher_module.match(line)
561                 if attempt:
562                     svnpath="%s-SVNPATH"%self.name
563                     replacement = "%-32s:= %s/%s/tags/%s\n"%(svnpath,attempt.group('url_main'),self.name,newname)
564                     new.write(replacement)
565                     matches += 1
566                 else:
567                     new.write(line)
568         tags.close()
569         new.close()
570         os.rename(newtagsfile,tagsfile)
571         if self.options.verbose: print "%d changes"%matches
572         return matches
573
574     def do_tag (self):
575         self.init_moddir()
576         self.init_edge_dir()
577         self.revert_edge_dir()
578         self.update_edge_dir()
579         # parse specfile
580         spec_dict = self.spec_dict()
581         self.show_dict(spec_dict)
582         
583         # side effects
584         edge_url=self.edge_url()
585         old_tag_name = self.tag_name(spec_dict)
586         old_tag_url=self.tag_url(spec_dict)
587         if (self.options.new_version):
588             # new version set on command line
589             spec_dict[self.module_version_varname] = self.options.new_version
590             spec_dict[self.module_taglevel_varname] = 0
591         else:
592             # increment taglevel
593             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
594             spec_dict[self.module_taglevel_varname] = new_taglevel
595
596         # sanity check
597         new_tag_name = self.tag_name(spec_dict)
598         new_tag_url=self.tag_url(spec_dict)
599         self.check_svnpath_exists (edge_url,"edge track")
600         self.check_svnpath_exists (old_tag_url,"previous tag")
601         self.check_svnpath_not_exists (new_tag_url,"new tag")
602
603         # checking for diffs
604         diff_output=Command("svn diff %s %s"%(old_tag_url,edge_url),
605                             self.options).output_of()
606         if len(diff_output) == 0:
607             if not prompt ("No difference in trunk for module %s, want to tag anyway"%self.name,False):
608                 return
609
610         # side effect in trunk's specfile
611         self.patch_spec_var(spec_dict)
612
613         # prepare changelog file 
614         # we use the standard subversion magic string (see svn_magic_line)
615         # so we can provide useful information, such as version numbers and diff
616         # in the same file
617         changelog="/tmp/%s-%d.txt"%(self.name,os.getpid())
618         file(changelog,"w").write("""Tagging module %s - %s
619
620 %s
621 Please write a changelog for this new tag in the section above
622 """%(self.name,new_tag_name,Module.svn_magic_line))
623
624         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
625             file(changelog,"a").write('DIFF=========\n' + diff_output)
626         
627         if self.options.debug:
628             prompt('Proceed ?')
629
630         # edit it        
631         self.run("%s %s"%(self.options.editor,changelog))
632         # insert changelog in spec
633         if self.options.changelog:
634             self.insert_changelog (changelog,old_tag_name,new_tag_name)
635
636         ## update build
637         try:
638             buildname=Module.config['build']
639         except:
640             buildname="build"
641         build = Module(buildname,self.options)
642         build.init_moddir()
643         build.init_edge_dir()
644         build.revert_edge_dir()
645         build.update_edge_dir()
646         
647         tagsfiles=glob(build.edge_dir()+"/*-tags*.mk")
648         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
649         while True:
650             for (tagsfile,status) in tagsdict.iteritems():
651                 while tagsdict[tagsfile] == 'todo' :
652                     choice = prompt ("Want to adopt %s in %s    "%(new_tag_name,os.path.basename(tagsfile)),'a',
653                                      [ ('n', 'ext'), ('a','uto'), ('d','iff'), ('r','evert'), ('h','elp') ] )
654                     if choice is True:
655                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
656                     elif choice is 'n':
657                         print 'Done with %s'%os.path.basename(tagsfile)
658                         tagsdict[tagsfile]='done'
659                     elif choice == 'a':
660                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
661                     elif choice == 'd':
662                         self.run("svn diff %s"%tagsfile)
663                     elif choice == 'r':
664                         self.run("svn revert %s"%tagsfile)
665                     elif choice == 'h':
666                         name=self.name
667                         print """y: unconditionnally changes any line setting %(name)s-SVNPATH to using %(new_tag_name)s
668 a: changes the definition of %(name)s only if it currently refers to %(old_tag_name)s
669 d: shows current diff for this tag file
670 r: reverts that tag file
671 n: move to next file"""%locals()
672                     else:
673                         print 'unexpected'
674             if prompt("Want to review changes on tags files",False):
675                 tagsdict = dict ( [ (x, 'todo') for tagsfile in tagsfiles ] )
676             else:
677                 break
678
679         paths=""
680         paths += self.edge_dir() + " "
681         paths += build.edge_dir() + " "
682         self.run_prompt("Check","svn diff " + paths)
683         self.run_prompt("Commit","svn commit --file %s %s"%(changelog,paths))
684         self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog,edge_url,new_tag_url))
685
686         if self.options.debug:
687             print 'Preserving',changelog
688         else:
689             os.unlink(changelog)
690             
691 ##############################
692     def do_branch (self):
693
694         # save self.branch if any, as a hint for the new branch 
695         # do this before anything else and restore .branch to None, 
696         # as this is part of the class's logic
697         new_branch_name=None
698         if self.branch:
699             new_branch_name=self.branch
700             self.branch=None
701
702         # check module-diff is empty
703         # used in do_diff, and should be options for diff only
704         (spec_dict,edge_url,tag_url,diff_listing) = self.do_diff(compute_only=True)
705         if diff_listing:
706             print '*** WARNING : Module %s has pending diffs on its trunk'%self.name
707             while True:
708                 answer = prompt ('Are you sure you want to proceed with branching',False,('d','iff'))
709                 if answer is True:
710                     break
711                 elif answer is False:
712                     sys.exit(1)
713                 elif answer == 'd':
714                     print '<<<< %s'%tag_url
715                     print '>>>> %s'%edge_url
716                     print diff_listing
717
718         # do_diff already does edge_dir initialization
719         # and it checks that edge_url and tag_url exist as well
720
721         # figure new branch name
722         old_branch_name = spec_dict[self.module_version_varname]
723         if not new_branch_name:
724             # heuristic is to assume 'version' is a dot-separated name
725             # we isolate the rightmost part and try incrementing it by 1
726             version=spec_dict[self.module_version_varname]
727             try:
728                 m=re.compile("\A(?P<leftpart>.+)\.(?P<rightmost>[^\.]+)\Z")
729                 (leftpart,rightmost)=m.match(version).groups()
730                 incremented = int(rightmost)+1
731                 new_branch_name="%s.%d"%(leftpart,incremented)
732             except:
733                 print 'Cannot figure next branch name from %s - exiting'%version
734                 sys.exit(1)
735
736         print "**********"
737         print "Using starting point %s"%tag_url
738         print "Using branch name %s"%old_branch_name
739         print "Moving trunk to %s"%new_branch_name
740         print "**********"
741
742         branch_url = "%s/%s/branches/%s"%(Module.config['svnpath'],self.name,old_branch_name)
743         self.check_svnpath_not_exists (branch_url,"new branch")
744         
745         # record starting point tagname
746         old_tag_name = self.tag_name(spec_dict)
747
748         # patching trunk
749         spec_dict[self.module_version_varname]=new_branch_name
750         spec_dict[self.module_taglevel_varname]='0'
751         # remember this in the trunk for easy location of the current branch
752         spec_dict['module_current_branch']=old_branch_name
753         self.patch_spec_var(spec_dict,True)
754         
755         # create commit log file
756         tmp="/tmp/branching-%d"%os.getpid()
757         f=open(tmp,"w")
758         f.write("Branch %s for module %s created from tag %s\n"%(new_branch_name,self.name,old_tag_name))
759         f.close()
760
761         # we're done, let's commit the stuff
762         command="svn diff %s"%self.edge_dir()
763         self.run_prompt("Changes in trunk",command)
764         command="svn copy --file %s %s %s"%(tmp,self.edge_url(),branch_url)
765         self.run_prompt("Create branch",command)
766         command="svn commit --file %s %s"%(tmp,self.edge_dir())
767         self.run_prompt("Commit trunk",command)
768         new_tag_url=self.tag_url(spec_dict)
769         command="svn copy --file %s %s %s"%(tmp,self.edge_url(),new_tag_url)
770         self.run_prompt("Create initial tag in trunk",command)
771         os.unlink(tmp)
772
773 ##############################
774 usage="""Usage: %prog options module_desc [ .. module_desc ]
775 Purpose:
776   manage subversion tags and specfile
777   requires the specfile to define *version* and *taglevel*
778   OR alternatively 
779   redirection variables module_version_varname / module_taglevel_varname
780 Trunk:
781   by default, the trunk of modules is taken into account
782   in this case, just mention the module name as <module_desc>
783 Branches:
784   if you wish to work on a branch rather than on the trunk, 
785   you can use something like e.g. Mom:2.1 as <module_desc>
786 More help:
787   see http://svn.planet-lab.org/wiki/ModuleTools
788 """
789
790 functions={ 
791     'version' : "only check specfile and print out details",
792     'diff' : "show difference between trunk and latest tag",
793     'tag'  : """increment taglevel in specfile, insert changelog in specfile,
794                 create new tag and and monitor its adoption in build/*-tags*.mk""",
795     'branch' : """create a branch for this module, from the latest tag on the trunk, 
796                   and change trunk's version number to reflect the new branch name;
797                   you can specify the new branch name by using module:branch""",
798     'sync' : """create a tag from the trunk
799                 this is a last resort option, mostly for repairs""",
800 }
801
802 def main():
803
804     mode=None
805     for function in functions.keys():
806         if sys.argv[0].find(function) >= 0:
807             mode = function
808             break
809     if not mode:
810         print "Unsupported command",sys.argv[0]
811         sys.exit(1)
812
813     global usage
814     usage += "\nmodule-%s : %s"%(mode,functions[mode])
815     all_modules=os.path.dirname(sys.argv[0])+"/modules.list"
816
817     parser=OptionParser(usage=usage,version=subversion_id)
818     parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
819                       help="run on all modules as found in %s"%all_modules)
820     parser.add_option("-f","--fast-checks",action="store_true",dest="fast_checks",default=False,
821                       help="skip safety checks, such as svn updates -- use with care")
822     if mode == "tag" or mode == 'branch':
823         parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
824                           help="set new version and reset taglevel to 0")
825     if mode == "tag" :
826         parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
827                           help="do not update changelog section in specfile when tagging")
828     if mode == "tag" or mode == "sync" :
829         parser.add_option("-e","--editor", action="store", dest="editor", default="emacs",
830                           help="specify editor")
831     if mode == "sync" :
832         parser.add_option("-m","--message", action="store", dest="message", default=None,
833                           help="specify log message")
834     if mode == "diff" :
835         parser.add_option("-o","--only", action="store_true", dest="only", default=False,
836                           help="report diff only for modules that exhibit differences")
837     if mode == "diff" :
838         parser.add_option("-l","--list", action="store_true", dest="list", default=False,
839                           help="just list modules that exhibit differences")
840     parser.add_option("-w","--workdir", action="store", dest="workdir", 
841                       default="%s/%s"%(os.getenv("HOME"),"modules"),
842                       help="""name for dedicated working dir - defaults to ~/modules
843 ** THIS MUST NOT ** be your usual working directory""")
844     parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=True, 
845                       help="run in verbose mode")
846     parser.add_option("-q","--quiet", action="store_false", dest="verbose", 
847                       help="run in quiet (non-verbose) mode")
848     parser.add_option("-d","--debug", action="store_true", dest="debug", default=False, 
849                       help="debug mode - mostly more verbose")
850     (options, args) = parser.parse_args()
851
852     if len(args) == 0:
853         if options.all_modules:
854             args=Command("grep -v '#' %s"%all_modules,options).output_of().split()
855         else:
856             parser.print_help()
857             sys.exit(1)
858     Module.init_homedir(options)
859     for modname in args:
860         module=Module(modname,options)
861         print '========================================',module.friendly_name()
862         # call the method called do_<mode>
863         method=Module.__dict__["do_%s"%mode]
864         method(module)
865
866 # basically, we exit if anything goes wrong
867 if __name__ == "__main__" :
868     try:
869         main()
870     except KeyboardInterrupt:
871         print '\nBye'
872