fix handling of trailing spaces
[build.git] / module-tag.py
1 #!/usr/bin/env python
2
3 subversion_id = "$Id: TestMain.py 7635 2008-01-04 09:46:06Z thierry $"
4
5 import sys, os, os.path
6 import re
7 import time
8 from glob import glob
9 from optparse import OptionParser
10
11 def prompt (question,default=True):
12     if default:
13         question += " [y]/n ? "
14     else:
15         question += " y/[n] ? "
16     try:
17         answer=raw_input(question)
18         if not answer:
19             return default
20         elif answer[0] in [ 'y','Y']:
21             return True
22         elif answer[0] in [ 'n','N']:
23             return False
24         else:
25             return prompt(question,default)
26     except KeyboardInterrupt:
27         print "Aborted"
28         return False
29     except:
30         raise
31
32 class Command:
33     def __init__ (self,command,options):
34         self.command=command
35         self.options=options
36         self.tmp="/tmp/command-%d"%os.getpid()
37
38     def run (self):
39         if self.options.verbose:
40             print '+',self.command
41             sys.stdout.flush()
42         return os.system(self.command)
43
44     def run_silent (self):
45         if self.options.verbose:
46             print '+',self.command,' .. ',
47             sys.stdout.flush()
48         retcod=os.system(self.command + " &> " + self.tmp)
49         if retcod != 0:
50             print "FAILED ! -- output quoted below "
51             os.system("cat " + self.tmp)
52             print "FAILED ! -- end of quoted output"
53         elif self.options.verbose:
54             print "OK"
55         os.unlink(self.tmp)
56         return retcod
57
58     def run_fatal(self):
59         if self.run_silent() !=0:
60             raise Exception,"Command %s failed"%self.command
61
62     # returns stdout, like bash's $(mycommand)
63     def output_of (self):
64         tmp="/tmp/status-%d"%os.getpid()
65         if self.options.debug:
66             print '+',self.command,' .. ',
67             sys.stdout.flush()
68         os.system(self.command + " &> " + tmp)
69         result=file(tmp).read()
70         os.unlink(tmp)
71         if self.options.debug:
72             print '+',self.command,'Done',
73         return result
74
75 class Svnpath:
76     def __init__(self,path,options):
77         self.path=path
78         self.options=options
79
80     def url_exists (self):
81         if self.options.verbose:
82             print 'Checking url',self.path
83         return os.system("svn list %s &> /dev/null"%self.path) == 0
84
85     def dir_needs_revert (self):
86         command="svn status %s"%self.path
87         return len(Command(command,self.options).output_of()) != 0
88     # turns out it's the same implem.
89     def file_needs_commit (self):
90         command="svn status %s"%self.path
91         return len(Command(command,self.options).output_of()) != 0
92
93 class Module:
94
95     # where to store user's config
96     config_storage="CONFIG"
97     # 
98     configKeys=[ ('svnpath',"Enter your toplevel svnpath (e.g. svn+ssh://thierry@svn.planet-lab.org/svn/)"),
99                  ('username',"Enter your firstname and lastname for changelogs"),
100                  ("email","Enter your email address for changelogs") ]
101     config={}
102
103     svn_magic_line="--This line, and those below, will be ignored--"
104
105     def __init__ (self,name,options):
106         self.name=name
107         self.options=options
108         self.moddir="%s/%s/%s"%(os.getenv("HOME"),options.modules,name)
109         self.trunkdir="%s/trunk"%(self.moddir)
110         # what to parse in a spec file
111         self.varnames = ["name",options.version,options.taglevel]
112         self.varmatcher=re.compile("%define\s+(\S+)\s+(\S*)\s*")
113
114
115     def run (self,command):
116         return Command(command,self.options).run()
117     def run_fatal (self,command):
118         return Command(command,self.options).run_fatal()
119     def run_prompt (self,message,command):
120         if not self.options.verbose:
121             question=message
122         else:
123             question="Want to run " + command
124         if prompt(question,True):
125             self.run(command)            
126
127     @staticmethod
128     def init_homedir (options):
129         topdir="%s/%s"%(os.getenv("HOME"),options.modules)
130         if options.verbose:
131             print 'Checking for',topdir
132         storage="%s/%s"%(topdir,Module.config_storage)
133         if not os.path.isdir (topdir):
134             # prompt for login or whatever svnpath
135             print "Cannot find",topdir,"let's create it"
136             for (key,message) in Module.configKeys:
137                 Module.config[key]=raw_input(message+" : ").strip()
138             Command("svn co -N %s %s"%(Module.config['svnpath'],topdir),options).run_fatal()
139             # store config
140             f=file(storage,"w")
141             for (key,message) in Module.configKeys:
142                 f.write("%s=%s\n"%(key,Module.config[key]))
143             f.close()
144             if options.debug:
145                 print 'Stored',storage
146                 Command("cat %s"%storage,options).run()
147         else:
148             # read config
149             f=open(storage)
150             for line in f.readlines():
151                 (key,value)=re.compile("^(.+)=(.+)$").match(line).groups()
152                 Module.config[key]=value                
153             f.close()
154             if options.debug:
155                 print 'Using config'
156                 for (key,message) in Module.configKeys:
157                     print key,'=',Module.config[key]
158
159     def init_moddir (self):
160         if self.options.verbose:
161             print 'Checking for',self.moddir
162         if not os.path.isdir (self.moddir):
163             self.run_fatal("svn up -N %s"%self.moddir)
164         if not os.path.isdir (self.moddir):
165             print 'Cannot find %s - check module name'%self.moddir
166             sys.exit(1)
167
168     def init_trunkdir (self):
169         if self.options.verbose:
170             print 'Checking for',self.trunkdir
171         if not os.path.isdir (self.trunkdir):
172             self.run_fatal("svn up -N %s"%self.trunkdir)
173
174     def revert_trunkdir (self):
175         if self.options.verbose:
176             print 'Checking whether',self.trunkdir,'needs being reverted'
177         if Svnpath(self.trunkdir,self.options).dir_needs_revert():
178             self.run_fatal("svn revert -R %s"%self.trunkdir)
179
180     def update_trunkdir (self):
181         if self.options.skip_update:
182             return
183         if self.options.verbose:
184             print 'Updating',self.trunkdir
185         self.run_fatal("svn update %s"%self.trunkdir)
186
187     def guess_specname (self):
188         attempt="%s/%s.spec"%(self.trunkdir,self.name)
189         if os.path.isfile (attempt):
190             return attempt
191         else:
192             try:
193                 return glob("%s/*.spec"%self.trunkdir)[0]
194             except:
195                 print 'Cannot guess specfile for module %s'%self.name
196                 sys.exit(1)
197
198     def spec_dict (self):
199         specfile=self.guess_specname()
200         if self.options.verbose:
201             print 'Parsing',specfile,
202         result={}
203         f=open(specfile)
204         for line in f.readlines():
205             if self.varmatcher.match(line):
206                 (var,value)=self.varmatcher.match(line).groups()
207                 if var in self.varnames:
208                     result[var]=value
209         f.close()
210         if self.options.verbose:
211             print 'found',len(result),'keys'
212         return result
213
214     def patch_spec_var (self, patch_dict):
215         specfile=self.guess_specname()
216         newspecfile=specfile+".new"
217         if self.options.verbose:
218             print 'Patching',specfile,'for',patch_dict.keys()
219         spec=open (specfile)
220         new=open(newspecfile,"w")
221
222         for line in spec.readlines():
223             if self.varmatcher.match(line):
224                 (var,value)=self.varmatcher.match(line).groups()
225                 if var in patch_dict.keys():
226                     new.write('%%define %s %s\n'%(var,patch_dict[var]))
227                     continue
228             new.write(line)
229         spec.close()
230         new.close()
231         os.rename(newspecfile,specfile)
232
233     def unignored_lines (self, logfile):
234         result=[]
235         for logline in file(logfile).readlines():
236             if logline.strip() == Module.svn_magic_line:
237                 break
238             result += logline
239         return result
240
241     def insert_changelog (self, logfile, oldtag, newtag):
242         specfile=self.guess_specname()
243         newspecfile=specfile+".new"
244         if self.options.verbose:
245             print 'Inserting changelog from %s into %s'%(logfile,specfile)
246         spec=open (specfile)
247         new=open(newspecfile,"w")
248         for line in spec.readlines():
249             new.write(line)
250             if re.compile('%changelog').match(line):
251                 dateformat="* %a %b %d %Y"
252                 datepart=time.strftime(dateformat)
253                 logpart="%s <%s> - %s %s"%(Module.config['username'],
254                                              Module.config['email'],
255                                              oldtag,newtag)
256                 new.write(datepart+" "+logpart+"\n")
257                 for logline in self.unignored_lines(logfile):
258                     new.write(logline)
259                 new.write("\n")
260         spec.close()
261         new.close()
262         os.rename(newspecfile,specfile)
263             
264     def show_dict (self, spec_dict):
265         if self.options.verbose:
266             for (k,v) in spec_dict.iteritems():
267                 print k,'=',v
268
269     def trunk_url (self):
270         return "%s/%s/trunk"%(Module.config['svnpath'],self.name)
271     def tag_name (self, spec_dict):
272         return "%s-%s-%s"%(spec_dict['name'],spec_dict[self.options.version],spec_dict[self.options.taglevel])
273     def tag_url (self, spec_dict):
274         return "%s/%s/tags/%s"%(Module.config['svnpath'],self.name,self.tag_name(spec_dict))
275
276     # locate specfile, parse it, check it and show values
277     def do_version (self):
278         self.init_moddir()
279         self.init_trunkdir()
280         self.revert_trunkdir()
281         self.update_trunkdir()
282         print '==============================',self.name
283         #for (key,message) in Module.configKeys:
284         #    print key,':',Module.config[key]
285         spec_dict = self.spec_dict()
286         print 'trunk url',self.trunk_url()
287         print 'latest tag url',self.tag_url(spec_dict)
288         print 'specfile:',self.guess_specname()
289         for varname in self.varnames:
290             if not spec_dict.has_key(varname):
291                 print 'Could not find %%define for %s'%varname
292                 return
293             else:
294                 print varname+":",spec_dict[varname]
295
296     init_warning="""WARNING
297 The module-init function has the following limitations
298 * it does not handle changelogs
299 * it does not scan the -tags.mk files to adopt the new tags"""
300     def do_init(self):
301         if self.options.verbose:
302             print Module.init_warning
303             if not prompt('Want to proceed anyway'):
304                 return
305
306         self.init_moddir()
307         self.init_trunkdir()
308         self.revert_trunkdir()
309         self.update_trunkdir()
310         spec_dict = self.spec_dict()
311
312         trunk_url=self.trunk_url()
313         tag_name=self.tag_name(spec_dict)
314         tag_url=self.tag_url(spec_dict)
315         # check the tag does not exist yet
316         if Svnpath(tag_url,self.options).url_exists():
317             print 'Module %s already has a tag %s'%(self.name,tag_name)
318             return
319
320         if self.options.message:
321             svnopt='--message "%s"'%self.options.message
322         else:
323             svnopt='--editor-cmd=%s'%self.options.editor
324         self.run_prompt("Create initial tag",
325                         "svn copy %s %s %s"%(svnopt,trunk_url,tag_url))
326
327     def do_diff (self):
328         self.init_moddir()
329         self.init_trunkdir()
330         self.revert_trunkdir()
331         self.update_trunkdir()
332         spec_dict = self.spec_dict()
333         self.show_dict(spec_dict)
334
335         trunk_url=self.trunk_url()
336         tag_url=self.tag_url(spec_dict)
337         for url in [ trunk_url, tag_url ] :
338             if not Svnpath(url,self.options).url_exists():
339                 print 'Could not find svn URL %s'%url
340                 sys.exit(1)
341
342         self.run("svn diff %s %s"%(tag_url,trunk_url))
343
344     def patch_tags_files (self, tagsfile, oldname, newname):
345         newtagsfile=tagsfile+".new"
346         if self.options.verbose:
347             print 'Replacing %s into %s in %s'%(oldname,newname,tagsfile)
348         tags=open (tagsfile)
349         new=open(newtagsfile,"w")
350         matcher=re.compile("^(.*)%s(.*)"%oldname)
351         for line in tags.readlines():
352             if not matcher.match(line):
353                 new.write(line)
354             else:
355                 (begin,end)=matcher.match(line).groups()
356                 new.write(begin+newname+end+"\n")
357         tags.close()
358         new.close()
359         os.rename(newtagsfile,tagsfile)
360
361     def do_tag (self):
362         self.init_moddir()
363         self.init_trunkdir()
364         self.revert_trunkdir()
365         self.update_trunkdir()
366         spec_dict = self.spec_dict()
367         self.show_dict(spec_dict)
368         
369         # parse specfile, check that the old tag exists and the new one does not
370         trunk_url=self.trunk_url()
371         old_tag_name = self.tag_name(spec_dict)
372         old_tag_url=self.tag_url(spec_dict)
373         # increment taglevel
374         new_taglevel = str ( int (spec_dict[self.options.taglevel]) + 1)
375         spec_dict[self.options.taglevel] = new_taglevel
376         new_tag_name = self.tag_name(spec_dict)
377         new_tag_url=self.tag_url(spec_dict)
378         for url in [ trunk_url, old_tag_url ] :
379             if not Svnpath(url,self.options).url_exists():
380                 print 'Could not find svn URL %s'%url
381                 sys.exit(1)
382         if Svnpath(new_tag_url,self.options).url_exists():
383             print 'New tag\'s svn URL %s already exists ! '%url
384             sys.exit(1)
385
386         # side effect in trunk's specfile
387         self.patch_spec_var({self.options.taglevel:new_taglevel})
388
389         # prepare changelog file 
390         # we use the standard subversion magic string (see svn_magic_line)
391         # so we can provide useful information, such as version numbers and diff
392         # in the same file
393         changelog="/tmp/%s-%d.txt"%(self.name,os.getpid())
394         file(changelog,"w").write("""
395 %s
396 module %s
397 old tag %s
398 new tag %s
399 """%(Module.svn_magic_line,self.name,old_tag_url,new_tag_url))
400
401         if not self.options.verbose or prompt('Want to run diff',True):
402             self.run("(echo 'DIFF========='; svn diff %s %s) >> %s"%(old_tag_url,trunk_url,changelog))
403         # edit it        
404         self.run("%s %s"%(self.options.editor,changelog))
405         # insert changelog in spec
406         if self.options.changelog:
407             self.insert_changelog (changelog,old_tag_name,new_tag_name)
408
409         ## update build
410         build = Module(self.options.build,self.options)
411         build.init_moddir()
412         build.init_trunkdir()
413         build.revert_trunkdir()
414         build.update_trunkdir()
415         
416         for tagsfile in glob(build.trunkdir+"/*-tags.mk"):
417             if prompt("Want to check %s"%tagsfile):
418                 self.patch_tags_files(tagsfile,old_tag_name,new_tag_name)
419
420         paths=""
421         paths += self.trunkdir + " "
422         paths += build.trunkdir + " "
423         self.run_prompt("Check","svn diff " + paths)
424         self.run_prompt("Commit","svn commit --file %s %s"%(changelog,paths))
425         self.run_prompt("Create tag","svn copy --file %s %s %s"%(changelog,trunk_url,new_tag_url))
426
427         if self.options.debug:
428             print 'Preserving',changelog
429         else:
430             os.unlink(changelog)
431             
432 usage="""Usage: %prog options module1 [ .. modulen ]
433 Purpose:
434   manage subversion tags and specfile
435   requires the specfile to define name, version and taglevel
436 Available functions:
437   module-diff : show difference between trunk and latest tag
438   module-tag  : increment taglevel in specfile, insert changelog in specfile,
439                 create new tag and and adopt it in build/*-tags.mk
440   module-init : create initial tag
441   module-version : only check specfile and print out details"""
442
443 def main():
444     all_modules=os.path.dirname(sys.argv[0])+"/modules.list"
445
446     parser=OptionParser(usage=usage,version=subversion_id)
447     parser.add_option("-a","--all",action="store_true",dest="all_modules",default=False,
448                       help="Runs all modules as found in %s"%all_modules)
449     parser.add_option("-e","--editor", action="store", dest="editor", default="emacs",
450                       help="Specify editor")
451     parser.add_option("-m","--message", action="store", dest="message", default=None,
452                       help="Specify log message")
453     parser.add_option("-u","--no-update",action="store_true",dest="skip_update",default=False,
454                       help="Skips svn updates")
455     parser.add_option("-c","--no-changelog", action="store_false", dest="changelog", default=True,
456                       help="Does not update changelog section in specfile when tagging")
457     parser.add_option("-M","--modules", action="store", dest="modules", default="modules",
458                       help="Name for topdir - defaults to modules")
459     parser.add_option("-B","--build", action="store", dest="build", default="build",
460                       help="Set module name for build")
461     parser.add_option("-T","--taglevel",action="store",dest="taglevel",default="taglevel",
462                       help="Specify an alternate spec variable for taglevel")
463     parser.add_option("-V","--version-string",action="store",dest="version",default="version",
464                       help="Specify an alternate spec variable for version")
465     parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
466                       help="Run in verbose mode")
467     parser.add_option("-d","--debug", action="store_true", dest="debug", default=False, 
468                       help="Debug mode - mostly more verbose")
469     (options, args) = parser.parse_args()
470     if options.debug: options.verbose=True
471
472     if len(args) == 0:
473         if options.all_modules:
474             args=Command("grep -v '#' %s"%all_modules,options).output_of().split()
475         else:
476             parser.print_help()
477             sys.exit(1)
478     Module.init_homedir(options)
479     for modname in args:
480         module=Module(modname,options)
481         if sys.argv[0].find("diff") >= 0:
482             module.do_diff()
483         elif sys.argv[0].find("tag") >= 0:
484             module.do_tag()
485         elif sys.argv[0].find("init") >= 0:
486             module.do_init()
487         elif sys.argv[0].find("version") >= 0:
488             module.do_version()
489         else:
490             print "Unsupported command",sys.argv[0]
491             parser.print_help()
492             sys.exit(1)
493
494 # basically, we exit if anything goes wrong
495 if __name__ == "__main__" :
496     main()