3 import sys, os, os.path
8 from optparse import OptionParser
10 # e.g. other_choices = [ ('d','iff') , ('g','uess') ] - lowercase
11 def prompt(question, default=True, other_choices=[], allow_outside=False):
12 if not isinstance(other_choices, list):
13 other_choices = [ other_choices ]
14 chars = [ c for (c,rest) in other_choices ]
28 for char, choice in other_choices:
30 choices.append("[{}]{}".format(char, choice))
32 choices.append("<{}>{}>".format(char, choice))
34 answer = input(question + " " + "/".join(choices) + " ? ")
37 answer = answer[0].lower()
49 for (char,choice) in other_choices:
54 return prompt(question, default, other_choices)
60 editor = os.environ['EDITOR']
69 while len(line) >= fold_length:
70 print(line[:fold_length],'\\')
71 line = line[fold_length:]
75 def __init__(self, command, options):
76 self.command = command
77 self.options = options
78 self.tmp = "/tmp/command-{}".format(os.getpid())
81 if self.options.dry_run:
82 print('dry_run', self.command)
84 if self.options.verbose and self.options.mode not in Main.silent_modes:
85 print('+', self.command)
87 return os.system(self.command)
90 if self.options.dry_run:
91 print('dry_run', self.command)
93 if self.options.verbose:
94 print('>', os.getcwd())
95 print('+', self.command, ' .. ', end=' ')
97 retcod = os.system("{} &> {}".format(self.command, self.tmp))
99 print("FAILED ! -- out+err below (command was {})".format(self.command))
100 os.system("cat {}".format(self.tmp))
101 print("FAILED ! -- end of quoted output")
102 elif self.options.verbose:
108 if self.run_silent() != 0:
109 raise Exception("Command {} failed".format(self.command))
111 # returns stdout, like bash's $(mycommand)
112 def output_of(self, with_stderr=False):
113 if self.options.dry_run:
114 print('dry_run', self.command)
115 return 'dry_run output'
116 tmp="/tmp/status-{}".format(os.getpid())
117 if self.options.debug:
118 print('+',self.command,' .. ', end=' ')
120 command = self.command
130 if self.options.debug:
131 print('Done', end=' ')
137 def __init__(self, path, options):
139 self.options = options
142 return os.path.basename(self.path)
145 return self.repo_root()
148 c = Command("git show | grep commit | awk '{{print $2;}}'", self.options)
149 out = self.__run_in_repo(c.output_of).strip()
150 return "http://git.onelab.eu/?p={}.git;a=commit;h={}".format(self.name(), out)
153 c = Command("git remote show origin", self.options)
154 out = self.__run_in_repo(c.output_of)
155 for line in out.split('\n'):
156 if line.strip().startswith("Fetch URL:"):
157 return line.split()[2]
160 def clone(cls, remote, local, options, depth=None):
161 Command("rm -rf {}".format(local), options).run_silent()
162 depth_option = "" if depth is None else " --depth {}".format(depth)
163 Command("git clone{} {} {}".format(depth_option, remote, local), options).run_fatal()
164 return GitRepository(local, options)
167 def remote_exists(cls, remote, options):
168 return Command("git --no-pager ls-remote {} &> /dev/null".format(remote), options).run()==0
170 def tag_exists(self, tagname):
171 command = 'git tag -l | grep "^{}$"'.format(tagname)
172 c = Command(command, self.options)
173 out = self.__run_in_repo(c.output_of, with_stderr=True)
176 def __run_in_repo(self, fun, *args, **kwargs):
179 ret = fun(*args, **kwargs)
183 def __run_command_in_repo(self, command, ignore_errors=False):
184 c = Command(command, self.options)
186 return self.__run_in_repo(c.output_of)
188 return self.__run_in_repo(c.run_fatal)
190 def __is_commit_id(self, id):
191 c = Command("git show {} | grep commit | awk '{{print $2;}}'".format(id), self.options)
192 ret = self.__run_in_repo(c.output_of, with_stderr=False)
193 if ret.strip() == id:
197 def update(self, subdir=None, recursive=None, branch="master"):
198 if branch == "master":
199 self.__run_command_in_repo("git checkout {}".format(branch))
201 self.to_branch(branch, remote=True)
202 self.__run_command_in_repo("git fetch origin --tags")
203 self.__run_command_in_repo("git fetch origin")
204 if not self.__is_commit_id(branch):
205 # we don't need to merge anything for commit ids.
206 self.__run_command_in_repo("git merge --ff origin/{}".format(branch))
208 def to_branch(self, branch, remote=True):
211 command = "git branch --track {} origin/{}".format(branch, branch)
212 c = Command(command, self.options)
213 self.__run_in_repo(c.output_of, with_stderr=True)
214 return self.__run_command_in_repo("git checkout {}".format(branch))
216 def to_tag(self, tag):
218 return self.__run_command_in_repo("git checkout {}".format(tag))
220 def tag(self, tagname, logfile):
221 self.__run_command_in_repo("git tag {} -F {}".format(tagname, logfile))
224 def diff(self, f=""):
225 c = Command("git diff {}".format(f), self.options)
226 return self.__run_in_repo(c.output_of, with_stderr=True)
228 def diff_with_tag(self, tagname):
229 c = Command("git diff {}".format(tagname), self.options)
230 return self.__run_in_repo(c.output_of, with_stderr=True)
232 def commit(self, logfile, branch="master"):
233 self.__run_command_in_repo("git add .", ignore_errors=True)
234 self.__run_command_in_repo("git add -u", ignore_errors=True)
235 self.__run_command_in_repo("git commit -F {}".format(logfile), ignore_errors=True)
236 if branch == "master" or self.__is_commit_id(branch):
237 self.__run_command_in_repo("git push")
239 self.__run_command_in_repo("git push origin {}:{}".format(branch, branch))
240 self.__run_command_in_repo("git push --tags")
242 def revert(self, f=""):
244 self.__run_command_in_repo("git checkout {}".format(f))
247 self.__run_command_in_repo("git --no-pager reset --hard")
248 self.__run_command_in_repo("git --no-pager clean -f")
252 command = "git status"
253 s = "nothing to commit, working directory clean"
254 return Command(command, self.options).output_of(True).find(s) >= 0
255 return self.__run_in_repo(check_commit)
258 return os.path.exists(os.path.join(self.path, ".git"))
264 From old times when we had svn and git
266 supported_repo_types = [ GitRepository ]
268 def __init__(self, path, options):
270 self.options = options
271 for repo_class in self.supported_repo_types:
272 self.repo = repo_class(self.path, self.options)
273 if self.repo.is_valid():
277 def remote_exists(cls, remote, options):
278 for repo_class in Repository.supported_repo_types:
279 if repo_class.remote_exists(remote, options):
283 def __getattr__(self, attr):
284 return getattr(self.repo, attr)
288 # support for tagged module is minimal, and is for the Build class only
291 edit_magic_line = "--This line, and those below, will be ignored--"
292 setting_tag_format = "Setting tag {}"
295 # ('module_name_varname', 'name'),
296 ('module_version_varname', 'version'),
297 ('module_taglevel_varname', 'taglevel'),
300 # where to store user's config
301 config_storage = "CONFIG"
307 ('gitserver', "Enter your git server's hostname", "git.onelab.eu"),
308 ('gituser', "Enter your user name (login name) on git server", subprocess.getoutput("id -un")),
309 ("build", "Enter the name of your build module", "build"),
310 ('username', "Enter your firstname and lastname for changelogs", ""),
311 ("email", "Enter your email address for changelogs", ""),
315 def prompt_config_option(cls, key, message, default):
316 cls.config[key]=input("{} [{}] : ".format(message,default)).strip() or default
319 def prompt_config(cls):
320 for (key,message,default) in cls.configKeys:
322 while not cls.config[key]:
323 cls.prompt_config_option(key, message, default)
325 # for parsing module spec name:branch
326 matcher_branch_spec = re.compile("\A(?P<name>[\w\.\-\/]+):(?P<branch>[\w\.\-]+)\Z")
327 # special form for tagged module - for Build
328 matcher_tag_spec = re.compile("\A(?P<name>[\w\.\-\/]+)@(?P<tagname>[\w\.\-]+)\Z")
331 matcher_rpm_define = re.compile("%(define|global)\s+(\S+)\s+(\S*)\s*")
334 def parse_module_spec(cls, module_spec):
335 name = branch_or_tagname = module_type = ""
337 attempt = Module.matcher_branch_spec.match(module_spec)
339 module_type = "branch"
340 name=attempt.group('name')
341 branch_or_tagname=attempt.group('branch')
343 attempt = Module.matcher_tag_spec.match(module_spec)
346 name=attempt.group('name')
347 branch_or_tagname=attempt.group('tagname')
350 return name, branch_or_tagname, module_type
353 def __init__(self, module_spec, options):
355 self.pathname, branch_or_tagname, module_type = self.parse_module_spec(module_spec)
356 self.name = os.path.basename(self.pathname)
358 if module_type == "branch":
359 self.branch = branch_or_tagname
360 elif module_type == "tag":
361 self.tagname = branch_or_tagname
364 self.module_dir="{}/{}".format(options.workdir,self.pathname)
365 self.repository = None
368 def run(self,command):
369 return Command(command,self.options).run()
370 def run_fatal(self,command):
371 return Command(command,self.options).run_fatal()
372 def run_prompt(self,message,fun, *args):
373 fun_msg = "{}({})".format(fun.__name__, ",".join(args))
374 if not self.options.verbose:
376 choice = prompt(message, True, ('s','how'))
381 print('About to run function:', fun_msg)
383 question = "{} - want to run function: {}".format(message, fun_msg)
384 if prompt(question, True):
387 def friendly_name(self):
388 if hasattr(self, 'branch'):
389 return "{}:{}".format(self.pathname, self.branch)
390 elif hasattr(self, 'tagname'):
391 return "{}@{}".format(self.pathname, self.tagname)
396 def git_remote_dir(cls, name):
397 return "{}@{}:/git/{}.git".format(cls.config['gituser'], cls.config['gitserver'], name)
401 def init_homedir(cls, options):
402 if options.verbose and options.mode not in Main.silent_modes:
403 print('Checking for', options.workdir)
404 storage="{}/{}".format(options.workdir, cls.config_storage)
405 # sanity check. Either the topdir exists AND we have a config/storage
406 # or topdir does not exist and we create it
407 # to avoid people use their own daily work repo
408 if os.path.isdir(options.workdir) and not os.path.isfile(storage):
409 print("""The directory {} exists and has no CONFIG file
410 If this is your regular working directory, please provide another one as the
411 module-* commands need a fresh working dir. Make sure that you do not use
412 that for other purposes than tagging""".format(options.workdir))
416 print("Checking out build module...")
417 remote = cls.git_remote_dir(cls.config['build'])
418 local = os.path.join(options.workdir, cls.config['build'])
419 GitRepository.clone(remote, local, options, depth=1)
423 with open(storage, 'w') as f:
424 for (key, message, default) in Module.configKeys:
425 f.write("{}={}\n".format(key, Module.config[key]))
427 print('Stored', storage)
428 Command("cat {}".format(storage),options).run()
432 with open(storage) as f:
433 for line in f.readlines():
434 key, value = re.compile("^(.+)=(.+)$").match(line).groups()
435 Module.config[key] = value
437 # owerride config variables using options.
438 if options.build_module:
439 Module.config['build'] = options.build_module
441 if not os.path.isdir(options.workdir):
442 print("Cannot find {}, let's create it".format(options.workdir))
443 Command("mkdir -p {}".format(options.workdir), options).run_silent()
449 # check missing config options
451 for key, message, default in cls.configKeys:
452 if key not in Module.config:
453 print("Configuration changed for module-tools")
454 cls.prompt_config_option(key, message, default)
458 Command("rm -rf {}".format(options.workdir), options).run_silent()
459 Command("mkdir -p {}".format(options.workdir), options).run_silent()
463 build_dir = os.path.join(options.workdir, cls.config['build'])
464 if not os.path.isdir(build_dir):
467 build = Repository(build_dir, options)
468 if not build.is_clean():
469 print("build module needs a revert")
474 if options.verbose and options.mode not in Main.silent_modes:
475 print('******** Using config')
476 for (key,message,default) in Module.configKeys:
477 print('\t{} = {}'.format(key,Module.config[key]))
479 def init_module_dir(self):
480 if self.options.verbose:
481 print('Checking for', self.module_dir)
483 if not os.path.isdir(self.module_dir):
484 self.repository = GitRepository.clone(self.git_remote_dir(self.pathname),
488 self.repository = Repository(self.module_dir, self.options)
490 if self.repository.type == "git":
491 if hasattr(self, 'branch'):
492 self.repository.to_branch(self.branch)
493 elif hasattr(self, 'tagname'):
494 self.repository.to_tag(self.tagname)
496 raise Exception('Cannot find {} - or not a git module'.format(self.module_dir))
499 def revert_module_dir(self):
500 if self.options.fast_checks:
501 if self.options.verbose: print('Skipping revert of {}'.format(self.module_dir))
503 if self.options.verbose:
504 print('Checking whether', self.module_dir, 'needs being reverted')
506 if not self.repository.is_clean():
507 self.repository.revert()
509 def update_module_dir(self):
510 if self.options.fast_checks:
511 if self.options.verbose: print('Skipping update of {}'.format(self.module_dir))
513 if self.options.verbose:
514 print('Updating', self.module_dir)
516 if hasattr(self, 'branch'):
517 self.repository.update(branch=self.branch)
518 elif hasattr(self, 'tagname'):
519 self.repository.update(branch=self.tagname)
521 self.repository.update()
523 def main_specname(self):
524 attempt = "{}/{}.spec".format(self.module_dir, self.name)
525 if os.path.isfile(attempt):
527 pattern1 = "{}/*.spec".format(self.module_dir)
528 level1 = glob(pattern1)
531 pattern2 = "{}/*/*.spec".format(self.module_dir)
532 level2 = glob(pattern2)
536 raise Exception('Cannot guess specfile for module {} -- patterns were {} or {}'\
537 .format(self.pathname,pattern1,pattern2))
539 def all_specnames(self):
540 level1 = glob("{}/*.spec".format(self.module_dir))
543 level2 = glob("{}/*/*.spec".format(self.module_dir))
546 def parse_spec(self, specfile, varnames):
547 if self.options.verbose:
548 print('Parsing',specfile, end=' ')
550 print("[{}]".format(var), end=' ')
553 with open(specfile, encoding='utf-8') as f:
554 for line in f.readlines():
555 attempt = Module.matcher_rpm_define.match(line)
557 define, var, value = attempt.groups()
560 if self.options.debug:
561 print('found {} keys'.format(len(result)))
562 for k, v in result.items():
563 print('{} = {}'.format(k, v))
566 # stores in self.module_name_varname the rpm variable to be used for the module's name
567 # and the list of these names in self.varnames
569 specfile = self.main_specname()
570 redirector_keys = [ varname for (varname, default) in Module.redirectors]
571 redirect_dict = self.parse_spec(specfile, redirector_keys)
572 if self.options.debug:
573 print('1st pass parsing done, redirect_dict=', redirect_dict)
575 for varname, default in Module.redirectors:
576 if varname in redirect_dict:
577 setattr(self, varname, redirect_dict[varname])
578 varnames += [redirect_dict[varname]]
580 setattr(self, varname, default)
581 varnames += [ default ]
582 self.varnames = varnames
583 result = self.parse_spec(specfile, self.varnames)
584 if self.options.debug:
585 print('2st pass parsing done, varnames={} result={}'.format(varnames, result))
588 def patch_spec_var(self, patch_dict,define_missing=False):
589 for specfile in self.all_specnames():
590 # record the keys that were changed
591 changed = {x : False for x in list(patch_dict.keys()) }
592 newspecfile = "{}.new".format(specfile)
593 if self.options.verbose:
594 print('Patching', specfile, 'for', list(patch_dict.keys()))
596 with open(specfile, encoding='utf-8') as spec:
597 with open(newspecfile, "w", encoding='utf-8') as new:
598 for line in spec.readlines():
599 attempt = Module.matcher_rpm_define.match(line)
601 define, var, value = attempt.groups()
602 if var in list(patch_dict.keys()):
603 if self.options.debug:
604 print('rewriting {} as {}'.format(var, patch_dict[var]))
605 new.write('%{} {} {}\n'.format(define, var, patch_dict[var]))
610 for key, was_changed in changed.items():
612 if self.options.debug:
613 print('rewriting missing {} as {}'.format(key, patch_dict[key]))
614 new.write('\n%define {} {}\n'.format(key, patch_dict[key]))
615 os.rename(newspecfile, specfile)
617 # returns all lines until the magic line
618 def unignored_lines(self, logfile):
620 white_line_matcher = re.compile("\A\s*\Z")
621 with open(logfile) as f:
622 for logline in f.readlines():
623 if logline.strip() == Module.edit_magic_line:
625 elif white_line_matcher.match(logline):
628 result.append(logline.strip()+'\n')
631 # creates a copy of the input with only the unignored lines
632 def strip_magic_line_filename(self, filein, fileout ,new_tag_name):
633 with open(fileout,'w') as f:
634 f.write(self.setting_tag_format.format(new_tag_name) + '\n')
635 for line in self.unignored_lines(filein):
638 def insert_changelog(self, logfile, newtag):
639 for specfile in self.all_specnames():
640 newspecfile = "{}.new".format(specfile)
641 if self.options.verbose:
642 print('Inserting changelog from {} into {}'.format(logfile, specfile))
644 with open(specfile) as spec:
645 with open(newspecfile,"w") as new:
646 for line in spec.readlines():
648 if re.compile('%changelog').match(line):
649 dateformat="* %a %b %d %Y"
650 datepart=time.strftime(dateformat)
651 logpart="{} <{}> - {}".format(Module.config['username'],
652 Module.config['email'],
654 new.write("{} {}\n".format(datepart,logpart))
655 for logline in self.unignored_lines(logfile):
656 new.write("- " + logline)
658 os.rename(newspecfile,specfile)
660 def show_dict(self, spec_dict):
661 if self.options.verbose:
662 for k, v in spec_dict.items():
663 print('{} = {}'.format(k, v))
665 def last_tag(self, spec_dict):
667 return "{}-{}".format(spec_dict[self.module_version_varname],
668 spec_dict[self.module_taglevel_varname])
669 except KeyError as err:
670 raise Exception('Something is wrong with module {}, cannot determine {} - exiting'\
671 .format(self.name, err))
673 def tag_name(self, spec_dict):
674 return "{}-{}".format(self.name, self.last_tag(spec_dict))
677 pattern_format="\A\s*{module}-(GITPATH)\s*(=|:=)\s*(?P<url_main>[^\s]+)/{module}[^\s]+"
679 def is_mentioned_in_tagsfile(self, tagsfile):
680 # so that {module} gets replaced from format
682 module_matcher = re.compile(Module.pattern_format.format(**locals()))
683 with open(tagsfile) as f:
684 for line in f.readlines():
685 if module_matcher.match(line):
689 ##############################
690 # using fine_grain means replacing only those instances that currently refer to this tag
691 # otherwise, <module>-GITPATH is replaced unconditionnally
692 def patch_tags_file(self, tagsfile, oldname, newname, fine_grain=True):
693 newtagsfile = "{}.new".format(tagsfile)
695 with open(tagsfile) as tags:
696 with open(newtagsfile,"w") as new:
698 # fine-grain : replace those lines that refer to oldname
700 if self.options.verbose:
701 print('Replacing {} into {}\n\tin {} .. '.format(oldname, newname, tagsfile), end=' ')
702 matcher = re.compile("^(.*){}(.*)".format(oldname))
703 for line in tags.readlines():
704 if not matcher.match(line):
707 begin, end = matcher.match(line).groups()
708 new.write(begin+newname+end+"\n")
710 # brute-force : change uncommented lines that define <module>-GITPATH
712 if self.options.verbose:
713 print('Searching for -GITPATH lines referring to /{}/\n\tin {} .. '\
714 .format(self.pathname, tagsfile), end=' ')
715 # so that {module} gets replaced from format
717 module_matcher = re.compile(Module.pattern_format.format(**locals()))
718 for line in tags.readlines():
719 attempt = module_matcher.match(line)
721 if line.find("-GITPATH") >= 0:
722 modulepath = "{}-GITPATH".format(self.name)
723 replacement = "{:<32}:= {}/{}.git@{}\n"\
724 .format(modulepath, attempt.group('url_main'), self.pathname, newname)
726 print("Could not locate {}-GITPATH (be aware that support for svn has been removed)"\
729 if self.options.verbose:
730 print(' ' + modulepath, end=' ')
731 new.write(replacement)
736 os.rename(newtagsfile,tagsfile)
737 if self.options.verbose:
738 print("{} changes".format(matches))
741 def check_tag(self, tagname, need_it=False):
742 if self.options.verbose:
743 print("Checking {} repository tag: {} - ".format(self.repository.type, tagname), end=' ')
745 found_tagname = tagname
746 found = self.repository.tag_exists(tagname)
748 if (found and need_it) or (not found and not need_it):
749 if self.options.verbose:
751 print("- found" if found else "- not found")
753 if self.options.verbose:
755 exception_format = "tag ({}) is already there" if found else "can not find required tag ({})"
756 raise Exception(exception_format.format(tagname))
761 ##############################
763 self.init_module_dir()
764 self.revert_module_dir()
765 self.update_module_dir()
767 spec_dict = self.spec_dict()
768 self.show_dict(spec_dict)
770 # compute previous tag - if not bypassed
771 if not self.options.bypass:
772 old_tag_name = self.tag_name(spec_dict)
774 old_tag_name = self.check_tag(old_tag_name, need_it=True)
776 if self.options.new_version:
777 # new version set on command line
778 spec_dict[self.module_version_varname] = self.options.new_version
779 spec_dict[self.module_taglevel_varname] = 0
782 new_taglevel = str( int(spec_dict[self.module_taglevel_varname]) + 1)
783 spec_dict[self.module_taglevel_varname] = new_taglevel
785 new_tag_name = self.tag_name(spec_dict)
787 new_tag_name = self.check_tag(new_tag_name, need_it=False)
790 if not self.options.bypass:
791 diff_output = self.repository.diff_with_tag(old_tag_name)
792 if len(diff_output) == 0:
793 if not prompt("No pending difference in module {}, want to tag anyway".format(self.pathname), False):
796 # side effect in head's specfile
797 self.patch_spec_var(spec_dict)
799 # prepare changelog file
800 # we use the standard subversion magic string (see edit_magic_line)
801 # so we can provide useful information, such as version numbers and diff
803 changelog_plain = "/tmp/{}-{}.edit".format(self.name, os.getpid())
804 changelog_strip = "/tmp/{}-{}.strip".format(self.name, os.getpid())
805 setting_tag_line = Module.setting_tag_format.format(new_tag_name)
806 with open(changelog_plain, 'w') as f:
810 Please write a changelog for this new tag in the section above
811 """.format(Module.edit_magic_line, setting_tag_line))
813 if self.options.bypass:
815 elif prompt('Want to see diffs while writing changelog', True):
816 with open(changelog_plain, "a") as f:
817 f.write('DIFF=========\n' + diff_output)
819 if self.options.debug:
823 self.run("{} {}".format(self.options.editor, changelog_plain))
824 # strip magic line in second file
825 self.strip_magic_line_filename(changelog_plain, changelog_strip, new_tag_name)
826 # insert changelog in spec
827 if self.options.changelog:
828 self.insert_changelog(changelog_plain, new_tag_name)
831 build_path = os.path.join(self.options.workdir, Module.config['build'])
832 build = Repository(build_path, self.options)
833 if self.options.build_branch:
834 build.to_branch(self.options.build_branch)
835 if not build.is_clean():
838 tagsfiles = glob(build.path+"/*-tags.mk")
839 tagsdict = { x : 'todo' for x in tagsfiles }
843 # do not bother if in bypass mode
844 if self.options.bypass:
846 for tagsfile in tagsfiles:
847 if not self.is_mentioned_in_tagsfile(tagsfile):
848 if self.options.verbose:
849 print("tagsfile {} does not mention {} - skipped".format(tagsfile, self.name))
851 status = tagsdict[tagsfile]
852 basename = os.path.basename(tagsfile)
853 print(".................... Dealing with {}".format(basename))
854 while tagsdict[tagsfile] == 'todo' :
855 choice = prompt("insert {} in {} ".format(new_tag_name, basename),
857 [ ('y','es'), ('n', 'ext'), ('f','orce'),
858 ('d','iff'), ('r','evert'), ('c', 'at'), ('h','elp') ] ,
861 self.patch_tags_file(tagsfile, old_tag_name, new_tag_name, fine_grain=True)
863 print('Done with {}'.format(os.path.basename(tagsfile)))
864 tagsdict[tagsfile] = 'done'
866 self.patch_tags_file(tagsfile, old_tag_name, new_tag_name, fine_grain=False)
868 print(build.diff(f=os.path.basename(tagsfile)))
870 build.revert(f=tagsfile)
872 self.run("cat {}".format(tagsfile))
876 """y: change {name}-GITPATH only if it currently refers to {old_tag_name}
877 f: unconditionnally change any line that assigns {name}-GITPATH to using {new_tag_name}
878 d: show current diff for this tag file
879 r: revert that tag file
880 c: cat the current tag file
881 n: move to next file""".format(**locals()))
883 if prompt("Want to review changes on tags files", False):
884 tagsdict = {x : 'todo' for x in tagsfiles }
889 def diff_all_changes():
891 print(self.repository.diff())
893 def commit_all_changes(log):
894 if hasattr(self, 'branch'):
895 self.repository.commit(log, branch=self.branch)
897 self.repository.commit(log)
900 self.run_prompt("Review module and build", diff_all_changes)
901 self.run_prompt("Commit module and build", commit_all_changes, changelog_strip)
902 self.run_prompt("Create tag", self.repository.tag, new_tag_name, changelog_strip)
904 if self.options.debug:
905 print('Preserving {} and stripped {}', changelog_plain, changelog_strip)
907 os.unlink(changelog_plain)
908 os.unlink(changelog_strip)
911 ##############################
912 def do_version(self):
913 self.init_module_dir()
914 self.revert_module_dir()
915 self.update_module_dir()
916 spec_dict = self.spec_dict()
918 self.html_store_title('Version for module {} ({})'\
919 .format(self.friendly_name(), self.last_tag(spec_dict)))
920 for varname in self.varnames:
921 if varname not in spec_dict:
922 self.html_print('Could not find %define for {}'.format(varname))
925 self.html_print("{:<16} {}".format(varname, spec_dict[varname]))
926 self.html_print("{:<16} {}".format('url', self.repository.url()))
927 if self.options.verbose:
928 self.html_print("{:<16} {}".format('main specfile:', self.main_specname()))
929 self.html_print("{:<16} {}".format('specfiles:', self.all_specnames()))
930 self.html_print_end()
933 ##############################
935 self.init_module_dir()
936 self.revert_module_dir()
937 self.update_module_dir()
938 spec_dict = self.spec_dict()
939 self.show_dict(spec_dict)
942 tag_name = self.tag_name(spec_dict)
945 tag_name = self.check_tag(tag_name, need_it=True)
947 if self.options.verbose:
948 print('Getting diff')
949 diff_output = self.repository.diff_with_tag(tag_name)
951 if self.options.list:
955 thename = self.friendly_name()
957 if self.options.www and diff_output:
958 self.html_store_title("Diffs in module {} ({}) : {} chars"\
959 .format(thename, self.last_tag(spec_dict), len(diff_output)))
961 self.html_store_raw('<p> < (left) {} </p>'.format(tag_name))
962 self.html_store_raw('<p> > (right) {} </p>'.format(thename))
963 self.html_store_pre(diff_output)
964 elif not self.options.www:
965 print('x'*30, 'module', thename)
966 print('x'*20, '<', tag_name)
967 print('x'*20, '>', thename)
970 ##############################
971 # store and restitute html fragments
973 def html_href(url,text):
974 return '<a href="{}">{}</a>'.format(url, text)
977 def html_anchor(url,text):
978 return '<a name="{}">{}</a>'.format(url,text)
981 def html_quote(text):
982 return text.replace('&', '&').replace('<', '<').replace('>', '>')
984 # only the fake error module has multiple titles
985 def html_store_title(self, title):
986 if not hasattr(self,'titles'):
988 self.titles.append(title)
990 def html_store_raw(self, html):
991 if not hasattr(self,'body'):
995 def html_store_pre(self, text):
996 if not hasattr(self,'body'):
998 self.body += '<pre>{}</pre>'.format(self.html_quote(text))
1000 def html_print(self, txt):
1001 if not self.options.www:
1004 if not hasattr(self, 'in_list') or not self.in_list:
1005 self.html_store_raw('<ul>')
1007 self.html_store_raw('<li>{}</li>'.format(txt))
1009 def html_print_end(self):
1010 if self.options.www:
1011 self.html_store_raw('</ul>')
1014 def html_dump_header(title):
1015 nowdate = time.strftime("%Y-%m-%d")
1016 nowtime = time.strftime("%H:%M (%Z)")
1017 print("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
1018 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
1021 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1022 <style type="text/css">
1023 body {{ font-family:georgia, serif; }}
1024 h1 {{font-size: large; }}
1025 p.title {{font-size: x-large; }}
1026 span.error {{text-weight:bold; color: red; }}
1030 <p class='title'> {} - status on {} at {}</p>
1032 """.format(title, title, nowdate, nowtime))
1035 def html_dump_middle():
1039 def html_dump_footer():
1040 print("</body></html")
1042 def html_dump_toc(self):
1043 if hasattr(self,'titles'):
1044 for title in self.titles:
1045 print('<li>', self.html_href('#'+self.friendly_name(),title), '</li>')
1047 def html_dump_body(self):
1048 if hasattr(self,'titles'):
1049 for title in self.titles:
1050 print('<hr /><h1>', self.html_anchor(self.friendly_name(),title), '</h1>')
1051 if hasattr(self,'body'):
1053 print('<p class="top">', self.html_href('#','Back to top'), '</p>')
1057 class Build(Module):
1059 def __get_modules(self, tagfile):
1060 self.init_module_dir()
1063 tagfile = os.path.join(self.module_dir, tagfile)
1064 for line in open(tagfile):
1066 name, url = line.split(':=')
1067 name, git_path = name.rsplit('-', 1)
1068 modules[name] = (git_path.strip(), url.strip())
1073 def get_modules(self, tagfile):
1074 modules = self.__get_modules(tagfile)
1075 for module in modules:
1076 module_type = tag_or_branch = ""
1078 path_type, url = modules[module]
1079 if path_type == "GITPATH":
1080 module_spec = os.path.split(url)[-1].replace(".git","")
1081 name, tag_or_branch, module_type = self.parse_module_spec(module_spec)
1083 tag_or_branch = os.path.split(url)[-1].strip()
1084 if url.find('/tags/') >= 0:
1086 elif url.find('/branches/') >= 0:
1087 module_type = "branch"
1089 modules[module] = {"module_type" : module_type,
1090 "path_type": path_type,
1091 "tag_or_branch": tag_or_branch,
1097 def modules_diff(first, second):
1100 for module in first:
1101 if module not in second:
1102 print("=== module {} missing in right-hand side ===".format(module))
1104 if first[module]['tag_or_branch'] != second[module]['tag_or_branch']:
1105 diff[module] = (first[module]['tag_or_branch'], second[module]['tag_or_branch'])
1107 first_set = set(first.keys())
1108 second_set = set(second.keys())
1110 new_modules = list(second_set - first_set)
1111 removed_modules = list(first_set - second_set)
1113 return diff, new_modules, removed_modules
1115 def release_changelog(options, buildtag_old, buildtag_new):
1117 # the command line expects new old, so we treat the tagfiles in the same order
1118 nb_tags = len(options.distrotags)
1120 tagfile_new = tagfile_old = options.distrotags[0]
1122 tagfile_new, tagfile_old = options.distrotags
1124 print("ERROR: provide one or two tagfile name (eg. onelab-k32-tags.mk)")
1125 print("two tagfiles can be mentioned when a tagfile has been renamed")
1129 print("------------------------------ Computing Changelog from")
1130 print("buildtag_old", buildtag_old, "tagfile_old", tagfile_old)
1131 print("buildtag_new", buildtag_new, "tagfile_new", tagfile_new)
1137 print('= build tag {} to {} ='.format(buildtag_old, buildtag_new))
1138 print('== distro {} ({} to {}) =='.format(tagfile_new, buildtag_old, buildtag_new))
1140 build = Build("build@{}".format(buildtag_old), options)
1141 build.init_module_dir()
1142 first = build.get_modules(tagfile_old)
1144 print(' * from', buildtag_old, build.repository.gitweb())
1146 build = Build("build@{}".format(buildtag_new), options)
1147 build.init_module_dir()
1148 second = build.get_modules(tagfile_new)
1150 print(' * to', buildtag_new, build.repository.gitweb())
1152 diff, new_modules, removed_modules = modules_diff(first, second)
1155 def get_module(name, tag):
1156 if not tag or tag == "trunk":
1157 return Module("{}".format(module), options)
1159 return Module("{}@{}".format(module, tag), options)
1163 print('=== {} - {} to {} : package {} ==='.format(tagfile_new, buildtag_old, buildtag_new, module))
1165 first, second = diff[module]
1166 m = get_module(module, first)
1167 os.system('rm -rf {}'.format(m.module_dir)) # cleanup module dir
1170 print(' * from', first, m.repository.gitweb())
1172 specfile = m.main_specname()
1173 (tmpfd, tmpfile) = tempfile.mkstemp()
1174 os.system("cp -f /{} {}".format(specfile, tmpfile))
1176 m = get_module(module, second)
1177 # patch for ipfw that, being managed in a separate repo, won't work for now
1180 except Exception as e:
1181 print("""Could not retrieve module {} - skipped
1183 """.format( m.friendly_name(), e))
1185 specfile = m.main_specname()
1187 print(' * to', second, m.repository.gitweb())
1190 os.system("diff -u {} {} | sed -e 's,{},[[previous version]],'"\
1191 .format(tmpfile, specfile, tmpfile))
1196 for module in new_modules:
1197 print('=== {} : new package in build {} ==='.format(tagfile_new, module))
1199 for module in removed_modules:
1200 print('=== {} : removed package from build {} ==='.format(tagfile_new, module))
1203 def adopt_tag(options, args):
1205 for module in options.modules:
1206 modules += module.split()
1207 for module in modules:
1208 modobj=Module(module,options)
1209 for tags_file in args:
1211 print('adopting tag {} for {} in {}'.format(options.tag, module, tags_file))
1212 modobj.patch_tags_file(tags_file, '_unused_', options.tag, fine_grain=False)
1214 Command("git diff {}".format(" ".join(args)), options).run()
1216 ##############################
1219 module_usage="""Usage: %prog [options] module_desc [ .. module_desc ]
1221 module-tools : a set of tools to manage subversion tags and specfile
1222 requires the specfile to either
1223 * define *version* and *taglevel*
1225 * define redirection variables module_version_varname / module_taglevel_varname
1227 by default, the 'master' branch of modules is the target
1228 in this case, just mention the module name as <module_desc>
1230 if you wish to work on another branch,
1231 you can use something like e.g. Mom:2.1 as <module_desc>
1233 release_usage="""Usage: %prog [options] tag1 .. tagn
1234 Extract release notes from the changes in specfiles between several build tags, latest first
1236 release-changelog 4.2-rc25 4.2-rc24 4.2-rc23 4.2-rc22
1237 You can refer to a (build) branch by prepending a colon, like in
1238 release-changelog :4.2 4.2-rc25
1239 You can refer to the build trunk by just mentioning 'trunk', e.g.
1240 release-changelog -t coblitz-tags.mk coblitz-2.01-rc6 trunk
1241 You can use 2 different tagfile names if that was renamed meanwhile
1242 release-changelog -t onelab-tags.mk 5.0-rc29 -t onelab-k32-tags.mk 5.0-rc28
1244 adopt_usage="""Usage: %prog [options] tag-file[s]
1245 With this command you can adopt a specifi tag or branch in your tag files
1246 This should be run in your daily build workdir; no call of git is done
1248 adopt-tag -m "plewww plcapi" -m Monitor onelab*tags.mk
1249 adopt-tag -m sfa -t sfa-1.0-33 *tags.mk
1251 common_usage="""More help:
1252 see http://svn.planet-lab.org/wiki/ModuleTools"""
1255 'list' : "displays a list of available tags or branches",
1256 'version' : "check latest specfile and print out details",
1257 'diff' : "show difference between module (trunk or branch) and latest tag",
1258 'tag' : """increment taglevel in specfile, insert changelog in specfile,
1259 create new tag and and monitor its adoption in build/*-tags.mk""",
1260 'branch' : """create a branch for this module, from the latest tag on the trunk,
1261 and change trunk's version number to reflect the new branch name;
1262 you can specify the new branch name by using module:branch""",
1263 'sync' : """create a tag from the module
1264 this is a last resort option, mostly for repairs""",
1265 'changelog' : """extract changelog between build tags
1266 expected arguments are a list of tags""",
1267 'adopt' : """locally adopt a specific tag""",
1270 silent_modes = ['list']
1271 # 'changelog' is for release-changelog
1272 # 'adopt' is for 'adopt-tag'
1273 regular_modes = set(modes.keys()).difference(set(['changelog','adopt']))
1276 def optparse_list(option, opt, value, parser):
1278 setattr(parser.values,option.dest,getattr(parser.values,option.dest)+value.split())
1280 setattr(parser.values,option.dest,value.split())
1285 # hack - need to check for adopt first as 'adopt-tag' contains tag..
1286 for function in [ 'adopt' ] + list(Main.modes.keys()):
1287 if sys.argv[0].find(function) >= 0:
1291 print("Unsupported command",sys.argv[0])
1292 print("Supported commands:" + " ".join(list(Main.modes.keys())))
1295 usage='undefined usage, mode={}'.format(mode)
1296 if mode in Main.regular_modes:
1297 usage = Main.module_usage
1298 usage += Main.common_usage
1299 usage += "\nmodule-{} : {}".format(mode, Main.modes[mode])
1300 elif mode == 'changelog':
1301 usage = Main.release_usage
1302 usage += Main.common_usage
1303 elif mode == 'adopt':
1304 usage = Main.adopt_usage
1305 usage += Main.common_usage
1307 parser=OptionParser(usage=usage)
1309 # the 'adopt' mode is really special and doesn't share any option
1311 parser.add_option("-m","--module",action="append",dest="modules",default=[],
1312 help="modules, can be used several times or with quotes")
1313 parser.add_option("-t","--tag",action="store", dest="tag", default='master',
1314 help="specify the tag to adopt, default is 'master'")
1315 parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
1316 help="run in verbose mode")
1317 (options, args) = parser.parse_args()
1318 options.workdir='unused'
1319 options.dry_run=False
1320 options.mode='adopt'
1321 if len(args)==0 or len(options.modules)==0:
1324 adopt_tag(options,args)
1327 # the other commands (module-* and release-changelog) share the same skeleton
1328 if mode in [ 'tag', 'branch'] :
1329 parser.add_option("-s","--set-version",action="store",dest="new_version",default=None,
1330 help="set new version and reset taglevel to 0")
1331 parser.add_option("-0","--bypass",action="store_true",dest="bypass",default=False,
1332 help="skip checks on existence of the previous tag")
1334 parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
1335 help="do not update changelog section in specfile when tagging")
1336 parser.add_option("-b","--build-branch", action="store", dest="build_branch", default=None,
1337 help="specify a build branch; used for locating the *tags.mk files where adoption is to take place")
1338 if mode in [ 'tag', 'sync' ] :
1339 parser.add_option("-e","--editor", action="store", dest="editor", default=default_editor(),
1340 help="specify editor")
1342 if mode in ['diff','version'] :
1343 parser.add_option("-W","--www", action="store", dest="www", default=False,
1344 help="export diff in html format, e.g. -W trunk")
1347 parser.add_option("-l","--list", action="store_true", dest="list", default=False,
1348 help="just list modules that exhibit differences")
1350 default_modules_list=os.path.dirname(sys.argv[0])+"/modules.list"
1351 parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
1352 help="run on all modules as found in {}".format(default_modules_list))
1353 parser.add_option("-f","--file",action="store",dest="modules_list",default=None,
1354 help="run on all modules found in specified file")
1355 parser.add_option("-n","--dry-run",action="store_true",dest="dry_run",default=False,
1356 help="dry run - shell commands are only displayed")
1357 parser.add_option("-t","--distrotags",action="callback",callback=Main.optparse_list, dest="distrotags",
1358 default=[], nargs=1,type="string",
1359 help="""specify distro-tags files, e.g. onelab-tags-4.2.mk
1360 -- can be set multiple times, or use quotes""")
1362 parser.add_option("-w","--workdir", action="store", dest="workdir",
1363 default="{}/{}".format(os.getenv("HOME"),"modules"),
1364 help="""name for dedicated working dir - defaults to ~/modules
1365 ** THIS MUST NOT ** be your usual working directory""")
1366 parser.add_option("-F","--fast-checks",action="store_true",dest="fast_checks",default=False,
1367 help="skip safety checks, such as git pulls -- use with care")
1368 parser.add_option("-B","--build-module",action="store",dest="build_module",default=None,
1369 help="specify a build module to owerride the one in the CONFIG")
1371 # default verbosity depending on function - temp
1372 verbose_modes= ['tag', 'sync', 'branch']
1374 if mode not in verbose_modes:
1375 parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False,
1376 help="run in verbose mode")
1378 parser.add_option("-q","--quiet", action="store_false", dest="verbose", default=True,
1379 help="run in quiet (non-verbose) mode")
1380 options, args = parser.parse_args()
1382 if not hasattr(options,'dry_run'):
1383 options.dry_run=False
1384 if not hasattr(options,'www'):
1392 if options.all_modules:
1393 options.modules_list=default_modules_list
1394 if options.modules_list:
1395 args=Command("grep -v '#' {}".format(options.modules_list), options).output_of().split()
1399 Module.init_homedir(options)
1402 if mode in Main.regular_modes:
1403 modules = [ Module(modname, options) for modname in args ]
1404 # hack: create a dummy Module to store errors/warnings
1405 error_module = Module('__errors__',options)
1407 for module in modules:
1408 if len(args)>1 and mode not in Main.silent_modes:
1410 print('========================================', module.friendly_name())
1411 # call the method called do_<mode>
1412 method = Module.__dict__["do_{}".format(mode)]
1415 except Exception as e:
1417 title='<span class="error"> Skipping module {} - failure: {} </span>'\
1418 .format(module.friendly_name(), str(e))
1419 error_module.html_store_title(title)
1422 traceback.print_exc()
1423 print('Skipping module {}: {}'.format(module.name,e))
1427 modetitle="Changes to tag in {}".format(options.www)
1428 elif mode == "version":
1429 modetitle="Latest tags in {}".format(options.www)
1430 modules.append(error_module)
1431 error_module.html_dump_header(modetitle)
1432 for module in modules:
1433 module.html_dump_toc()
1434 Module.html_dump_middle()
1435 for module in modules:
1436 module.html_dump_body()
1437 Module.html_dump_footer()
1439 # if we provide, say a b c d, we want to build (a,b) (b,c) and (c,d)
1440 # remember that the changelog in the twiki comes latest first, so
1441 # we typically have here latest latest-1 latest-2
1442 for (tag_new,tag_old) in zip ( args[:-1], args [1:]):
1443 release_changelog(options, tag_old, tag_new)
1446 ####################
1447 if __name__ == "__main__" :
1450 except KeyboardInterrupt: