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