update build at startup
[build.git] / module-tools.py
1 #!/usr/bin/python -u
2
3 subversion_id = "$Id$"
4
5 import sys, os
6 import re
7 import time
8 from glob import glob
9 from optparse import OptionParser
10
11 # HARDCODED NAME CHANGES
12 #
13 # Moving to git we decided to rename some of the repositories. Here is
14 # a map of name changes applied in git repositories.
15 RENAMED_SVN_MODULES = {
16     "PLEWWW": "plewww"
17     }
18
19 def svn_to_git_name(module):
20     if RENAMED_SVN_MODULES.has_key(module):
21         return RENAMED_SVN_MODULES[module]
22     return module
23
24 def git_to_svn_name(module):
25     for key in RENAMED_SVN_MODULES:
26         if module == RENAMED_SVN_MODULES[key]:
27             return key
28     return module
29     
30
31 # e.g. other_choices = [ ('d','iff') , ('g','uess') ] - lowercase 
32 def prompt (question,default=True,other_choices=[],allow_outside=False):
33     if not isinstance (other_choices,list):
34         other_choices = [ other_choices ]
35     chars = [ c for (c,rest) in other_choices ]
36
37     choices = []
38     if 'y' not in chars:
39         if default is True: choices.append('[y]')
40         else : choices.append('y')
41     if 'n' not in chars:
42         if default is False: choices.append('[n]')
43         else : choices.append('n')
44
45     for (char,choice) in other_choices:
46         if default == char:
47             choices.append("["+char+"]"+choice)
48         else:
49             choices.append("<"+char+">"+choice)
50     try:
51         answer=raw_input(question + " " + "/".join(choices) + " ? ")
52         if not answer:
53             return default
54         answer=answer[0].lower()
55         if answer == 'y':
56             if 'y' in chars: return 'y'
57             else: return True
58         elif answer == 'n':
59             if 'n' in chars: return 'n'
60             else: return False
61         elif other_choices:
62             for (char,choice) in other_choices:
63                 if answer == char:
64                     return char
65             if allow_outside:
66                 return answer
67         return prompt(question,default,other_choices)
68     except:
69         raise
70
71 def default_editor():
72     try:
73         editor = os.environ['EDITOR']
74     except:
75         editor = "emacs"
76     return editor
77
78 ### fold long lines
79 fold_length=132
80
81 def print_fold (line):
82     while len(line) >= fold_length:
83         print line[:fold_length],'\\'
84         line=line[fold_length:]
85     print line
86
87 class Command:
88     def __init__ (self,command,options):
89         self.command=command
90         self.options=options
91         self.tmp="/tmp/command-%d"%os.getpid()
92
93     def run (self):
94         if self.options.dry_run:
95             print 'dry_run',self.command
96             return 0
97         if self.options.verbose and self.options.mode not in Main.silent_modes:
98             print '+',self.command
99             sys.stdout.flush()
100         return os.system(self.command)
101
102     def run_silent (self):
103         if self.options.dry_run:
104             print 'dry_run',self.command
105             return 0
106         if self.options.verbose:
107             print '+',self.command,' .. ',
108             sys.stdout.flush()
109         retcod=os.system(self.command + " &> " + self.tmp)
110         if retcod != 0:
111             print "FAILED ! -- out+err below (command was %s)"%self.command
112             os.system("cat " + self.tmp)
113             print "FAILED ! -- end of quoted output"
114         elif self.options.verbose:
115             print "OK"
116         os.unlink(self.tmp)
117         return retcod
118
119     def run_fatal(self):
120         if self.run_silent() !=0:
121             raise Exception,"Command %s failed"%self.command
122
123     # returns stdout, like bash's $(mycommand)
124     def output_of (self,with_stderr=False):
125         if self.options.dry_run:
126             print 'dry_run',self.command
127             return 'dry_run output'
128         tmp="/tmp/status-%d"%os.getpid()
129         if self.options.debug:
130             print '+',self.command,' .. ',
131             sys.stdout.flush()
132         command=self.command
133         if with_stderr:
134             command += " &> "
135         else:
136             command += " > "
137         command += tmp
138         os.system(command)
139         result=file(tmp).read()
140         os.unlink(tmp)
141         if self.options.debug:
142             print 'Done',
143         return result
144
145
146 class SvnRepository:
147     type = "svn"
148
149     def __init__(self, path, options):
150         self.path = path
151         self.options = options
152
153     def name(self):
154         return os.path.basename(self.path)
155
156     def url(self):
157         out = Command("svn info %s" % self.path, self.options).output_of()
158         for line in out.split('\n'):
159             if line.startswith("URL:"):
160                 return line.split()[1].strip()
161
162     def repo_root(self):
163         out = Command("svn info %s" % self.path, self.options).output_of()
164         for line in out.split('\n'):
165             if line.startswith("Repository Root:"):
166                 repo_root = line.split()[2].strip()
167                 return "%s/svn/%s" (repo_root, self.name)
168
169     @classmethod
170     def checkout(cls, remote, local, options, recursive=False):
171         if recursive:
172             svncommand = "svn co %s %s" % (remote, local)
173         else:
174             svncommand = "svn co -N %s %s" % (remote, local)
175         Command("rm -rf %s" % local, options).run_silent()
176         Command(svncommand, options).run_fatal()
177
178         return SvnRepository(local, options)
179
180     @classmethod
181     def remote_exists(cls, remote):
182         return os.system("svn list %s &> /dev/null" % remote) == 0
183
184     def tag_exists(self, tagname):
185         url = "%s/tags/%s" % (self.repo_root(), tagname)
186         return SvnRepository.remote_exists(url)
187
188     def update(self, subdir="", recursive=True):
189         path = os.path.join(self.path, subdir)
190         if recursive:
191             svncommand = "svn up %s" % path
192         else:
193             svncommand = "svn up -N %s" % path
194         Command(svncommand, self.options).run_fatal()
195
196     def commit(self, logfile):
197         # add all new files to the repository
198         Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs svn add" %
199                 self.path, self.options).run_silent()
200         Command("svn commit -F %s %s" % (logfile, self.path), self.options).run_fatal()
201
202     def to_branch(self, branch):
203         remote = "%s/branches/%s" % (self.repo_root(), branch)
204         SvnRepository.checkout(remote, self.path, self.options, recursive=True)
205
206     def to_tag(self, tag):
207         remote = "%s/tags/%s" % (self.repo_root(), branch)
208         SvnRepository.checkout(remote, self.path, self.options, recursive=True)
209
210     def tag(self, tagname, logfile):
211         tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
212         self_url = self.url()
213         Command("svn copy -F %s %s %s" % (logfile, self_url, tag_url), self.options).run_fatal()
214
215     def diff(self):
216         return Command("svn diff %s" % self.path, self.options).output_of(True)
217
218     def diff_with_tag(self, tagname):
219         tag_url = "%s/tags/%s" % (self.repo_root(), tagname)
220         return Command("svn diff %s %s" % (tag_url, self.url()),
221                        self.options).output_of(True)
222
223     def revert(self):
224         Command("svn revert %s -R" % self.path, self.options).run_fatal()
225         Command("svn status %s | grep '^\?' | sed -e 's/? *//' | sed -e 's/ /\\ /g' | xargs rm -rf " %
226                 self.path, self.options).run_silent()
227
228     def is_clean(self):
229         command="svn status %s" % self.path
230         return len(Command(command,self.options).output_of(True)) == 0
231
232     def is_valid(self):
233         return os.path.exists(os.path.join(self.path, ".svn"))
234     
235
236 class GitRepository:
237     type = "git"
238
239     def __init__(self, path, options):
240         self.path = path
241         self.options = options
242
243     def name(self):
244         return os.path.basename(self.path)
245
246     def url(self):
247         self.repo_root()
248
249     def repo_root(self):
250         c = Command("git remote show origin", self.options)
251         out = self.__run_in_repo(c.output_of)
252         for line in out.split('\n'):
253             if line.strip().startswith("Fetch URL:"):
254                 repo = line.split()[2]
255
256     @classmethod
257     def checkout(cls, remote, local, options, depth=1):
258         Command("rm -rf %s" % local, options).run_silent()
259         Command("git clone --depth %d %s %s" % (depth, remote, local), options).run_fatal()
260         return GitRepository(local, options)
261
262     @classmethod
263     def remote_exists(cls, remote):
264         return os.system("git --no-pager ls-remote %s &> /dev/null" % remote) == 0
265
266     def tag_exists(self, tagname):
267         command = 'git tag -l | grep "^%s$"' % tagname
268         c = Command(command, self.options)
269         out = self.__run_in_repo(c.output_of, with_stderr=True)
270         return len(out) > 0
271
272     def __run_in_repo(self, fun, *args, **kwargs):
273         cwd = os.getcwd()
274         os.chdir(self.path)
275         ret = fun(*args, **kwargs)
276         os.chdir(cwd)
277         return ret
278
279     def __run_command_in_repo(self, command):
280         c = Command(command, self.options)
281         return self.__run_in_repo(c.run_fatal)
282
283     def update(self, subdir=None, recursive=None):
284         return self.__run_command_in_repo("git pull")
285
286     def to_branch(self, branch, remote=True):
287         if remote:
288             branch = "origin/%s" % branch
289         return self.__run_command_in_repo("git checkout %s" % branch)
290
291     def to_tag(self, tag):
292         return self.__run_command_in_repo("git checkout %s" % tag)
293
294     def tag(self, tagname, logfile):
295         self.__run_command_in_repo("git tag %s -F %s" % (tagname, logfile))
296         self.commit(logfile)
297
298     def diff(self):
299         c = Command("git diff", self.options)
300         return self.__run_in_repo(c.output_of, with_stderr=True)
301
302     def diff_with_tag(self, tagname):
303         c = Command("git diff %s" % tagname, self.options)
304         return self.__run_in_repo(c.output_of, with_stderr=True)
305
306     def commit(self, logfile):
307         self.__run_command_in_repo("git add -A")
308         self.__run_command_in_repo("git commit -F  %s" % logfile)
309         self.__run_command_in_repo("git push --tags")
310
311     def revert(self):
312         self.__run_command_in_repo("git --no-pager reset --hard")
313         self.__run_command_in_repo("git --no-pager clean -f")
314
315     def is_clean(self):
316         def check_commit():
317             command="git status"
318             s="nothing to commit (working directory clean)"
319             return Command(command, self.options).output_of(True).find(s) >= 0
320         return self.__run_in_repo(check_commit)
321
322     def is_valid(self):
323         return os.path.exists(os.path.join(self.path, ".git"))
324     
325
326 class Repository:
327     """ Generic repository """
328     supported_repo_types = [SvnRepository, GitRepository]
329
330     def __init__(self, path, options):
331         self.path = path
332         self.options = options
333         for repo in self.supported_repo_types:
334             self.repo = repo(self.path, self.options)
335             if self.repo.is_valid():
336                 break
337
338     @classmethod
339     def has_moved_to_git(cls, module, svnpath):
340         module = git_to_svn_name(module)
341         return SvnRepository.remote_exists("%s/%s/aaaa-has-moved-to-git" % (svnpath, module))
342
343     @classmethod
344     def remote_exists(cls, remote):
345         for repo in Repository.supported_repo_types:
346             if repo.remote_exists(remote):
347                 return True
348         return False
349
350     def __getattr__(self, attr):
351         return getattr(self.repo, attr)
352
353
354
355 # support for tagged module is minimal, and is for the Build class only
356 class Module:
357
358     svn_magic_line="--This line, and those below, will be ignored--"
359     setting_tag_format = "Setting tag %s"
360     
361     redirectors=[ # ('module_name_varname','name'),
362                   ('module_version_varname','version'),
363                   ('module_taglevel_varname','taglevel'), ]
364
365     # where to store user's config
366     config_storage="CONFIG"
367     # 
368     config={}
369
370     import commands
371     configKeys=[ ('svnpath',"Enter your toplevel svnpath",
372                   "svn+ssh://%s@svn.planet-lab.org/svn/"%commands.getoutput("id -un")),
373                  ('gitserver', "Enter your git server's hostname", "git.onelab.eu"),
374                  ("build", "Enter the name of your build module","build"),
375                  ('username',"Enter your firstname and lastname for changelogs",""),
376                  ("email","Enter your email address for changelogs",""),
377                  ]
378
379     @classmethod
380     def prompt_config (cls):
381         for (key,message,default) in cls.configKeys:
382             cls.config[key]=""
383             while not cls.config[key]:
384                 cls.config[key]=raw_input("%s [%s] : "%(message,default)).strip() or default
385
386
387     # for parsing module spec name:branch
388     matcher_branch_spec=re.compile("\A(?P<name>[\w\.-]+):(?P<branch>[\w\.-]+)\Z")
389     # special form for tagged module - for Build
390     matcher_tag_spec=re.compile("\A(?P<name>[\w-]+)@(?P<tagname>[\w\.-]+)\Z")
391     # parsing specfiles
392     matcher_rpm_define=re.compile("%(define|global)\s+(\S+)\s+(\S*)\s*")
393
394     def __init__ (self,module_spec,options):
395         # parse module spec
396         attempt=Module.matcher_branch_spec.match(module_spec)
397         if attempt:
398             self.name=attempt.group('name')
399             self.branch=attempt.group('branch')
400         else:
401             attempt=Module.matcher_tag_spec.match(module_spec)
402             if attempt:
403                 self.name=attempt.group('name')
404                 self.tagname=attempt.group('tagname')
405             else:
406                 self.name=module_spec
407
408         # when available prefer to use git module name internally
409         self.name = svn_to_git_name(self.name)
410
411         self.options=options
412         self.module_dir="%s/%s"%(options.workdir,self.name)
413         self.repository = None
414         self.build = None
415
416     def run (self,command):
417         return Command(command,self.options).run()
418     def run_fatal (self,command):
419         return Command(command,self.options).run_fatal()
420     def run_prompt (self,message,fun, *args):
421         fun_msg = "%s(%s)" % (fun.func_name, ",".join(args))
422         if not self.options.verbose:
423             while True:
424                 choice=prompt(message,True,('s','how'))
425                 if choice is True:
426                     fun(*args)
427                     return
428                 elif choice is False:
429                     print 'About to run function:', fun_msg
430         else:
431             question=message+" - want to run function: " + fun_msg
432             if prompt(question,True):
433                 fun(*args)
434
435     def friendly_name (self):
436         if hasattr(self,'branch'):
437             return "%s:%s"%(self.name,self.branch)
438         elif hasattr(self,'tagname'):
439             return "%s@%s"%(self.name,self.tagname)
440         else:
441             return self.name
442
443     @classmethod
444     def git_remote_dir (cls, name):
445         return "%s:/git/%s.git" % (cls.config['gitserver'], name)
446
447     @classmethod
448     def svn_remote_dir (cls, name):
449         name = git_to_svn_name(name)
450         svn = cls.config['svnpath']
451         if svn.endswith('/'):
452             return "%s%s" % (svn, name)
453         return "%s/%s" % (svn, name)
454
455     def svn_selected_remote(self):
456         svn_name = git_to_svn_name(self.name)
457         remote = self.svn_remote_dir(svn_name)
458         if hasattr(self,'branch'):
459             remote = "%s/branches/%s" % (remote, self.branch)
460         elif hasattr(self,'tagname'):
461             remote = "%s/tags/%s" % (remote, self.tagname)
462         else:
463             remote = "%s/trunk" % remote
464         return remote
465
466     ####################
467     @classmethod
468     def init_homedir (cls, options):
469         if options.verbose and options.mode not in Main.silent_modes:
470             print 'Checking for', options.workdir
471         storage="%s/%s"%(options.workdir, cls.config_storage)
472         # sanity check. Either the topdir exists AND we have a config/storage
473         # or topdir does not exist and we create it
474         # to avoid people use their own daily svn repo
475         if os.path.isdir(options.workdir) and not os.path.isfile(storage):
476             print """The directory %s exists and has no CONFIG file
477 If this is your regular working directory, please provide another one as the
478 module-* commands need a fresh working dir. Make sure that you do not use 
479 that for other purposes than tagging""" % options.workdir
480             sys.exit(1)
481         if not os.path.isdir (options.workdir):
482             print "Cannot find",options.workdir,"let's create it"
483             cls.prompt_config()
484             print "Checking ...",
485             remote = cls.git_remote_dir(cls.config['build'])
486             local = os.path.join(options.workdir, cls.config['build'])
487             GitRepository.checkout(remote, local, options, depth=1)
488             print "OK"
489             
490             # store config
491             f=file(storage,"w")
492             for (key,message,default) in Module.configKeys:
493                 f.write("%s=%s\n"%(key,Module.config[key]))
494             f.close()
495             if options.debug:
496                 print 'Stored',storage
497                 Command("cat %s"%storage,options).run()
498         else:
499             # read config
500             f=open(storage)
501             for line in f.readlines():
502                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
503                 Module.config[key]=value                
504             f.close()
505
506             build_dir = os.path.join(options.workdir, cls.config['build'])
507             build = Repository(build_dir, options)
508             if not build.is_clean():
509                 print "build module needs a revert"
510                 build.revert()
511                 print "OK"
512             build.update()
513
514         if options.verbose and options.mode not in Main.silent_modes:
515             print '******** Using config'
516             for (key,message,default) in Module.configKeys:
517                 print '\t',key,'=',Module.config[key]
518
519     def init_module_dir (self):
520         if self.options.verbose:
521             print 'Checking for',self.module_dir
522
523         if not os.path.isdir (self.module_dir):
524             if Repository.has_moved_to_git(self.name, Module.config['svnpath']):
525                 self.repository = GitRepository.checkout(self.git_remote_dir(self.name),
526                                                          self.module_dir,
527                                                          self.options)
528             else:
529                 remote = self.svn_selected_remote()
530                 self.repository = SvnRepository.checkout(remote,
531                                                          self.module_dir,
532                                                          self.options, recursive=False)
533
534         self.repository = Repository(self.module_dir, self.options)
535         if self.repository.type == "svn":
536             # check if module has moved to git    
537             if Repository.has_moved_to_git(self.name, Module.config['svnpath']):
538                 Command("rm -rf %s" % self.module_dir, self.options).run_silent()
539                 self.init_module_dir()
540             # check if we have the required branch/tag
541             if self.repository.url() != self.svn_selected_remote():
542                 Command("rm -rf %s" % self.module_dir, self.options).run_silent()
543                 self.init_module_dir()
544
545         elif self.repository.type == "git":
546             if hasattr(self,'branch'):
547                 self.repository.to_branch(self.branch)
548             elif hasattr(self,'tagname'):
549                 self.repository.to_tag(self.tagname)
550
551         else:
552             raise Exception, 'Cannot find %s - check module name'%self.module_dir
553
554
555     def revert_module_dir (self):
556         if self.options.fast_checks:
557             if self.options.verbose: print 'Skipping revert of %s' % self.module_dir
558             return
559         if self.options.verbose:
560             print 'Checking whether', self.module_dir, 'needs being reverted'
561         
562         if not self.repository.is_clean():
563             self.repository.revert()
564
565     def update_module_dir (self):
566         if self.options.fast_checks:
567             if self.options.verbose: print 'Skipping update of %s' % self.module_dir
568             return
569         if self.options.verbose:
570             print 'Updating', self.module_dir
571         self.repository.update()
572
573     def main_specname (self):
574         attempt="%s/%s.spec"%(self.module_dir,self.name)
575         if os.path.isfile (attempt):
576             return attempt
577         pattern1="%s/*.spec"%self.module_dir
578         level1=glob(pattern1)
579         if level1:
580             return level1[0]
581         pattern2="%s/*/*.spec"%self.module_dir
582         level2=glob(pattern2)
583
584         if level2:
585             return level2[0]
586         raise Exception, 'Cannot guess specfile for module %s -- patterns were %s or %s'%(self.name,pattern1,pattern2)
587
588     def all_specnames (self):
589         level1=glob("%s/*.spec" % self.module_dir)
590         if level1: return level1
591         level2=glob("%s/*/*.spec" % self.module_dir)
592         return level2
593
594     def parse_spec (self, specfile, varnames):
595         if self.options.verbose:
596             print 'Parsing',specfile,
597             for var in varnames:
598                 print "[%s]"%var,
599             print ""
600         result={}
601         f=open(specfile)
602         for line in f.readlines():
603             attempt=Module.matcher_rpm_define.match(line)
604             if attempt:
605                 (define,var,value)=attempt.groups()
606                 if var in varnames:
607                     result[var]=value
608         f.close()
609         if self.options.debug:
610             print 'found',len(result),'keys'
611             for (k,v) in result.iteritems():
612                 print k,'=',v
613         return result
614                 
615     # stores in self.module_name_varname the rpm variable to be used for the module's name
616     # and the list of these names in self.varnames
617     def spec_dict (self):
618         specfile=self.main_specname()
619         redirector_keys = [ varname for (varname,default) in Module.redirectors]
620         redirect_dict = self.parse_spec(specfile,redirector_keys)
621         if self.options.debug:
622             print '1st pass parsing done, redirect_dict=',redirect_dict
623         varnames=[]
624         for (varname,default) in Module.redirectors:
625             if redirect_dict.has_key(varname):
626                 setattr(self,varname,redirect_dict[varname])
627                 varnames += [redirect_dict[varname]]
628             else:
629                 setattr(self,varname,default)
630                 varnames += [ default ] 
631         self.varnames = varnames
632         result = self.parse_spec (specfile,self.varnames)
633         if self.options.debug:
634             print '2st pass parsing done, varnames=',varnames,'result=',result
635         return result
636
637     def patch_spec_var (self, patch_dict,define_missing=False):
638         for specfile in self.all_specnames():
639             # record the keys that were changed
640             changed = dict ( [ (x,False) for x in patch_dict.keys() ] )
641             newspecfile=specfile+".new"
642             if self.options.verbose:
643                 print 'Patching',specfile,'for',patch_dict.keys()
644             spec=open (specfile)
645             new=open(newspecfile,"w")
646
647             for line in spec.readlines():
648                 attempt=Module.matcher_rpm_define.match(line)
649                 if attempt:
650                     (define,var,value)=attempt.groups()
651                     if var in patch_dict.keys():
652                         if self.options.debug:
653                             print 'rewriting %s as %s'%(var,patch_dict[var])
654                         new.write('%%%s %s %s\n'%(define,var,patch_dict[var]))
655                         changed[var]=True
656                         continue
657                 new.write(line)
658             if define_missing:
659                 for (key,was_changed) in changed.iteritems():
660                     if not was_changed:
661                         if self.options.debug:
662                             print 'rewriting missing %s as %s'%(key,patch_dict[key])
663                         new.write('\n%%define %s %s\n'%(key,patch_dict[key]))
664             spec.close()
665             new.close()
666             os.rename(newspecfile,specfile)
667
668     # returns all lines until the magic line
669     def unignored_lines (self, logfile):
670         result=[]
671         white_line_matcher = re.compile("\A\s*\Z")
672         for logline in file(logfile).readlines():
673             if logline.strip() == Module.svn_magic_line:
674                 break
675             elif white_line_matcher.match(logline):
676                 continue
677             else:
678                 result.append(logline.strip()+'\n')
679         return result
680
681     # creates a copy of the input with only the unignored lines
682     def stripped_magic_line_filename (self, filein, fileout ,new_tag_name):
683        f=file(fileout,'w')
684        f.write(self.setting_tag_format%new_tag_name + '\n')
685        for line in self.unignored_lines(filein):
686            f.write(line)
687        f.close()
688
689     def insert_changelog (self, logfile, oldtag, newtag):
690         for specfile in self.all_specnames():
691             newspecfile=specfile+".new"
692             if self.options.verbose:
693                 print 'Inserting changelog from %s into %s'%(logfile,specfile)
694             spec=open (specfile)
695             new=open(newspecfile,"w")
696             for line in spec.readlines():
697                 new.write(line)
698                 if re.compile('%changelog').match(line):
699                     dateformat="* %a %b %d %Y"
700                     datepart=time.strftime(dateformat)
701                     logpart="%s <%s> - %s"%(Module.config['username'],
702                                                  Module.config['email'],
703                                                  newtag)
704                     new.write(datepart+" "+logpart+"\n")
705                     for logline in self.unignored_lines(logfile):
706                         new.write("- " + logline)
707                     new.write("\n")
708             spec.close()
709             new.close()
710             os.rename(newspecfile,specfile)
711             
712     def show_dict (self, spec_dict):
713         if self.options.verbose:
714             for (k,v) in spec_dict.iteritems():
715                 print k,'=',v
716
717     def last_tag (self, spec_dict):
718         try:
719             return "%s-%s" % (spec_dict[self.module_version_varname],
720                               spec_dict[self.module_taglevel_varname])
721         except KeyError,err:
722             raise Exception,'Something is wrong with module %s, cannot determine %s - exiting'%(self.name,err)
723
724     def tag_name (self, spec_dict, old_svn_name=False):
725         base_tag_name = self.name
726         if old_svn_name:
727             base_tag_name = git_to_svn_name(self.name)
728         return "%s-%s" % (base_tag_name, self.last_tag(spec_dict))
729     
730
731 ##############################
732     # using fine_grain means replacing only those instances that currently refer to this tag
733     # otherwise, <module>-SVNPATH is replaced unconditionnally
734     def patch_tags_file (self, tagsfile, oldname, newname,fine_grain=True):
735         newtagsfile=tagsfile+".new"
736         tags=open (tagsfile)
737         new=open(newtagsfile,"w")
738
739         matches=0
740         # fine-grain : replace those lines that refer to oldname
741         if fine_grain:
742             if self.options.verbose:
743                 print 'Replacing %s into %s\n\tin %s .. '%(oldname,newname,tagsfile),
744             matcher=re.compile("^(.*)%s(.*)"%oldname)
745             for line in tags.readlines():
746                 if not matcher.match(line):
747                     new.write(line)
748                 else:
749                     (begin,end)=matcher.match(line).groups()
750                     new.write(begin+newname+end+"\n")
751                     matches += 1
752         # brute-force : change uncommented lines that define <module>-SVNPATH
753         else:
754             if self.options.verbose:
755                 print 'Searching for -SVNPATH lines referring to /%s/\n\tin %s .. '%(self.name,tagsfile),
756             pattern="\A\s*(?P<make_name>[^\s]+)-SVNPATH\s*(=|:=)\s*(?P<url_main>[^\s]+)/%s/[^\s]+"\
757                                           %(self.name)
758             matcher_module=re.compile(pattern)
759             for line in tags.readlines():
760                 attempt=matcher_module.match(line)
761                 if attempt:
762                     svnpath="%s-SVNPATH"%(attempt.group('make_name'))
763                     if self.options.verbose:
764                         print ' '+svnpath, 
765                     replacement = "%-32s:= %s/%s/tags/%s\n"%(svnpath,attempt.group('url_main'),self.name,newname)
766                     new.write(replacement)
767                     matches += 1
768                 else:
769                     new.write(line)
770         tags.close()
771         new.close()
772         os.rename(newtagsfile,tagsfile)
773         if self.options.verbose: print "%d changes"%matches
774         return matches
775
776     def check_tag(self, tagname, need_it=False, old_svn_tag_name=None):
777         if self.options.verbose:
778             print "Checking %s repository tag: %s - " % (self.repository.type, tagname),
779
780         found_tagname = tagname
781         found = self.repository.tag_exists(tagname)
782         if not found and old_svn_tag_name:
783             if self.options.verbose:
784                 print "KO"
785                 print "Checking %s repository tag: %s - " % (self.repository.type, old_svn_tag_name),
786             found = self.repository.tag_exists(old_svn_tag_name)
787             if found:
788                 found_tagname = old_svn_tag_name
789
790         if (found and need_it) or (not found and not need_it):
791             print "OK "
792         else:
793             print "KO"
794             if found:
795                 raise Exception, "tag (%s) is already there" % tagname
796             else:
797                 raise Exception, "can not find required tag (%s)" % tagname
798
799         return found_tagname
800
801
802     def do_tag (self):
803         self.init_module_dir()
804         self.revert_module_dir()
805         self.update_module_dir()
806         # parse specfile
807         spec_dict = self.spec_dict()
808         self.show_dict(spec_dict)
809         
810         # side effects
811         old_tag_name = self.tag_name(spec_dict)
812         old_svn_tag_name = self.tag_name(spec_dict, old_svn_name=True)
813
814         if (self.options.new_version):
815             # new version set on command line
816             spec_dict[self.module_version_varname] = self.options.new_version
817             spec_dict[self.module_taglevel_varname] = 0
818         else:
819             # increment taglevel
820             new_taglevel = str ( int (spec_dict[self.module_taglevel_varname]) + 1)
821             spec_dict[self.module_taglevel_varname] = new_taglevel
822
823         new_tag_name = self.tag_name(spec_dict)
824
825         # sanity check
826         old_tag_name = self.check_tag(old_tag_name, need_it=True, old_svn_tag_name=old_svn_tag_name)
827         new_tag_name = self.check_tag(new_tag_name, need_it=False)
828
829         # checking for diffs
830         diff_output = self.repository.diff_with_tag(old_tag_name)
831         if len(diff_output) == 0:
832             if not prompt ("No pending difference in module %s, want to tag anyway"%self.name,False):
833                 return
834
835         # side effect in trunk's specfile
836         self.patch_spec_var(spec_dict)
837
838         # prepare changelog file 
839         # we use the standard subversion magic string (see svn_magic_line)
840         # so we can provide useful information, such as version numbers and diff
841         # in the same file
842         changelog="/tmp/%s-%d.edit"%(self.name,os.getpid())
843         changelog_svn="/tmp/%s-%d.svn"%(self.name,os.getpid())
844         setting_tag_line=Module.setting_tag_format%new_tag_name
845         file(changelog,"w").write("""
846 %s
847 %s
848 Please write a changelog for this new tag in the section above
849 """%(Module.svn_magic_line,setting_tag_line))
850
851         if not self.options.verbose or prompt('Want to see diffs while writing changelog',True):
852             file(changelog,"a").write('DIFF=========\n' + diff_output)
853         
854         if self.options.debug:
855             prompt('Proceed ?')
856
857         # edit it        
858         self.run("%s %s"%(self.options.editor,changelog))
859         # strip magic line in second file - looks like svn has changed its magic line with 1.6
860         # so we do the job ourselves
861         self.stripped_magic_line_filename(changelog,changelog_svn,new_tag_name)
862         # insert changelog in spec
863         if self.options.changelog:
864             self.insert_changelog (changelog,old_tag_name,new_tag_name)
865
866         ## update build
867         build_path = os.path.join(self.options.workdir,
868                                   Module.config['build'])
869         build = Repository(build_path, self.options)
870         if self.options.build_branch:
871             build.to_branch(self.options.build_branch)
872         if not build.is_clean():
873             build.revert()
874
875         tagsfiles=glob(build.path+"/*-tags*.mk")
876         tagsdict=dict( [ (x,'todo') for x in tagsfiles ] )
877         default_answer = 'y'
878         tagsfiles.sort()
879         while True:
880             for tagsfile in tagsfiles:
881                 status=tagsdict[tagsfile]
882                 basename=os.path.basename(tagsfile)
883                 print ".................... Dealing with %s"%basename
884                 while tagsdict[tagsfile] == 'todo' :
885                     choice = prompt ("insert %s in %s    "%(new_tag_name,basename),default_answer,
886                                      [ ('y','es'), ('n', 'ext'), ('f','orce'), 
887                                        ('d','iff'), ('r','evert'), ('c', 'at'), ('h','elp') ] ,
888                                      allow_outside=True)
889                     if choice == 'y':
890                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=True)
891                     elif choice == 'n':
892                         print 'Done with %s'%os.path.basename(tagsfile)
893                         tagsdict[tagsfile]='done'
894                     elif choice == 'f':
895                         self.patch_tags_file(tagsfile,old_tag_name,new_tag_name,fine_grain=False)
896                     elif choice == 'd':
897                         self.run("svn diff %s"%tagsfile)
898                     elif choice == 'r':
899                         self.run("svn revert %s"%tagsfile)
900                     elif choice == 'c':
901                         self.run("cat %s"%tagsfile)
902                     else:
903                         name=self.name
904                         print """y: change %(name)s-SVNPATH only if it currently refers to %(old_tag_name)s
905 f: unconditionnally change any line that assigns %(name)s-SVNPATH to using %(new_tag_name)s
906 d: show current diff for this tag file
907 r: revert that tag file
908 c: cat the current tag file
909 n: move to next file"""%locals()
910
911             if prompt("Want to review changes on tags files",False):
912                 tagsdict = dict ( [ (x, 'todo') for x in tagsfiles ] )
913                 default_answer='d'
914             else:
915                 break
916
917         def diff_all_changes():
918             print build.diff()
919             print self.repository.diff()
920
921         def commit_all_changes(log):
922             self.repository.commit(log)
923             build.commit(log)
924
925         self.run_prompt("Review module and build", diff_all_changes)
926         self.run_prompt("Commit module and build", commit_all_changes, changelog_svn)
927         self.run_prompt("Create tag", self.repository.tag, new_tag_name, changelog_svn)
928
929         if self.options.debug:
930             print 'Preserving',changelog,'and stripped',changelog_svn
931         else:
932             os.unlink(changelog)
933             os.unlink(changelog_svn)
934             
935
936 ##############################
937 class Main:
938
939     module_usage="""Usage: %prog [options] module_desc [ .. module_desc ]
940
941 module-tools : a set of tools to manage subversion tags and specfile
942   requires the specfile to either
943   * define *version* and *taglevel*
944   OR alternatively 
945   * define redirection variables module_version_varname / module_taglevel_varname
946 Trunk:
947   by default, the trunk of modules is taken into account
948   in this case, just mention the module name as <module_desc>
949 Branches:
950   if you wish to work on a branch rather than on the trunk, 
951   you can use something like e.g. Mom:2.1 as <module_desc>
952 """
953     release_usage="""Usage: %prog [options] tag1 .. tagn
954   Extract release notes from the changes in specfiles between several build tags, latest first
955   Examples:
956       release-changelog 4.2-rc25 4.2-rc24 4.2-rc23 4.2-rc22
957   You can refer to a (build) branch by prepending a colon, like in
958       release-changelog :4.2 4.2-rc25
959   You can refer to the build trunk by just mentioning 'trunk', e.g.
960       release-changelog -t coblitz-tags.mk coblitz-2.01-rc6 trunk
961 """
962     common_usage="""More help:
963   see http://svn.planet-lab.org/wiki/ModuleTools"""
964
965     modes={ 
966         'list' : "displays a list of available tags or branches",
967         'version' : "check latest specfile and print out details",
968         'diff' : "show difference between module (trunk or branch) and latest tag",
969         'tag'  : """increment taglevel in specfile, insert changelog in specfile,
970                 create new tag and and monitor its adoption in build/*-tags*.mk""",
971         'branch' : """create a branch for this module, from the latest tag on the trunk, 
972                   and change trunk's version number to reflect the new branch name;
973                   you can specify the new branch name by using module:branch""",
974         'sync' : """create a tag from the module
975                 this is a last resort option, mostly for repairs""",
976         'changelog' : """extract changelog between build tags
977                 expected arguments are a list of tags""",
978         }
979
980     silent_modes = ['list']
981     release_modes = ['changelog']
982
983     @staticmethod
984     def optparse_list (option, opt, value, parser):
985         try:
986             setattr(parser.values,option.dest,getattr(parser.values,option.dest)+value.split())
987         except:
988             setattr(parser.values,option.dest,value.split())
989
990     def run(self):
991
992         mode=None
993         for function in Main.modes.keys():
994             if sys.argv[0].find(function) >= 0:
995                 mode = function
996                 break
997         if not mode:
998             print "Unsupported command",sys.argv[0]
999             print "Supported commands:" + " ".join(Main.modes.keys())
1000             sys.exit(1)
1001
1002         if mode not in Main.release_modes:
1003             usage = Main.module_usage
1004             usage += Main.common_usage
1005             usage += "\nmodule-%s : %s"%(mode,Main.modes[mode])
1006         else:
1007             usage = Main.release_usage
1008             usage += Main.common_usage
1009
1010         parser=OptionParser(usage=usage,version=subversion_id)
1011         
1012         if mode == "tag" or mode == 'branch':
1013             parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
1014                               help="set new version and reset taglevel to 0")
1015         if mode == "tag" :
1016             parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
1017                               help="do not update changelog section in specfile when tagging")
1018             parser.add_option("-b","--build-branch", action="store", dest="build_branch", default=None,
1019                               help="specify a build branch; used for locating the *tags*.mk files where adoption is to take place")
1020         if mode == "tag" or mode == "sync" :
1021             parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
1022                               help="specify editor")
1023             
1024         default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
1025         parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
1026                           help="dry run - shell commands are only displayed")
1027         parser.add_option("-t","--distrotags",action="callback",callback=Main.optparse_list, dest="distrotags",
1028                           default=[], nargs=1,type="string",
1029                           help="""specify distro-tags files, e.g. onelab-tags-4.2.mk
1030 -- can be set multiple times, or use quotes""")
1031
1032         parser.add_option("-w","--workdir", action="store", dest="workdir", 
1033                           default="%s/%s"%(os.getenv("HOME"),"modules"),
1034                           help="""name for dedicated working dir - defaults to ~/modules
1035 ** THIS MUST NOT ** be your usual working directory""")
1036         parser.add_option("-F","--fast-checks",action="store_true",dest="fast_checks",default=False,
1037                           help="skip safety checks, such as svn updates -- use with care")
1038
1039         # default verbosity depending on function - temp
1040         verbose_modes= ['tag', 'sync', 'branch']
1041         
1042         if mode not in verbose_modes:
1043             parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
1044                               help="run in verbose mode")
1045         else:
1046             parser.add_option("-q","--quiet", action="store_false", dest="verbose", default=True,
1047                               help="run in quiet (non-verbose) mode")
1048         (options, args) = parser.parse_args()
1049         options.mode=mode
1050         if not hasattr(options,'dry_run'):
1051             options.dry_run=False
1052         if not hasattr(options,'www'):
1053             options.www=False
1054         options.debug=False
1055
1056         ########## module-*
1057         if len(args) == 0:
1058             parser.print_help()
1059             sys.exit(1)
1060         Module.init_homedir(options)
1061
1062         modules=[ Module(modname,options) for modname in args ]
1063         for module in modules:
1064             if len(args)>1 and mode not in Main.silent_modes:
1065                 print '========================================',module.friendly_name()
1066             # call the method called do_<mode>
1067             method=Module.__dict__["do_%s"%mode]
1068             try:
1069                 method(module)
1070             except Exception,e:
1071                 import traceback
1072                 traceback.print_exc()
1073                 print 'Skipping module %s: '%modname,e
1074
1075 ####################
1076 if __name__ == "__main__" :
1077     try:
1078         Main().run()
1079     except KeyboardInterrupt:
1080         print '\nBye'