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