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