0.3
[opensuse:osc.git] / osc / core.py
1 #!/usr/bin/python
2
3 # Copyright (C) 2006 Peter Poeml.  All rights reserved.
4 # This program is free software; it may be used, copied, modified
5 # and distributed under the terms of the GNU General Public Licence,
6 # either version 2, or (at your option) any later version.
7
8 __version__ = '0.3'
9
10 import os
11 import sys
12 import urllib2
13 from urlparse import urlunsplit
14 import cElementTree as ET
15 from cStringIO import StringIO
16
17
18 from xml.dom.ext.reader import Sax2
19 from xml.dom.ext import PrettyPrint
20
21 netloc = 'api.opensuse.org'
22 scheme = 'http'
23
24 BUFSIZE = 1024*1024
25 store = '.osc'
26 exclude_stuff = [store, '.svn', 'CVS']
27
28
29 class File:
30     """represent a file, including its metadata"""
31     def __init__(self, name, md5, size, mtime):
32         self.name = name
33         self.md5 = md5
34         self.size = size
35         self.mtime = mtime
36     def __str__(self):
37         return self.name
38
39
40 class Package:
41     """represent a package (its directory) and read/keep/write its metadata"""
42     def __init__(self, workingdir):
43         self.dir = workingdir
44         self.storedir = os.path.join(self.dir, store)
45
46         check_store_version(self.dir)
47
48         self.prjname = store_read_project(self.dir)
49         self.name = store_read_package(self.dir)
50
51         files_tree = read_filemeta(self.dir)
52         files_tree_root = files_tree.getroot()
53
54         self.rev = files_tree_root.get('rev')
55
56         self.filenamelist = []
57         self.filelist = []
58         for node in files_tree_root.findall('entry'):
59             f = File(node.get('name'), 
60                      node.get('md5'), 
61                      node.get('size'), 
62                      node.get('mtime'))
63             self.filelist.append(f)
64             self.filenamelist.append(f.name)
65
66         self.to_be_deleted = read_tobedeleted(self.dir)
67
68         self.todo = []
69         self.todo_send = []
70         self.todo_delete = []
71
72         # gather unversioned files (the ones not listed in _meta)
73         self.filenamelist_unvers = []
74         for i in os.listdir(self.dir):
75             if i in exclude_stuff:
76                 continue
77             if not i in self.filenamelist:
78                 self.filenamelist_unvers.append(i) 
79
80     def addfile(self, n):
81         st = os.stat(os.path.join(self.dir, n))
82         f = File(n, dgst(os.path.join(self.dir, n)), st[6], st[8])
83         self.filelist.append(f)
84         self.filenamelist.append(n)
85         self.filenamelist_unvers.remove(n) 
86         copy_file(os.path.join(self.dir, n), os.path.join(self.storedir, n))
87         
88     def delfile(self, n):
89         os.unlink(os.path.join(self.dir, n))
90         os.unlink(os.path.join(self.storedir, n))
91
92     def put_on_deletelist(self, n):
93         if n not in self.to_be_deleted:
94             self.to_be_deleted.append(n)
95
96     def write_deletelist(self):
97         fname = os.path.join(self.storedir, '_to_be_deleted')
98         f = open(fname, 'w')
99         f.write('\n'.join(self.to_be_deleted))
100         f.write('\n')
101         f.close()
102
103     def updatefile(self, n):
104         filename = os.path.join(self.dir, n)
105         storefilename = os.path.join(self.storedir, n)
106         mtime = int(self.findfilebyname(n).mtime)
107
108         get_source_file(self.prjname, self.name, n, targetfilename=filename)
109         os.utime(filename, (-1, mtime))
110
111         copy_file(filename, storefilename)
112         os.utime(storefilename, (-1, mtime))
113
114     def update_filesmeta(self):
115         meta = ''.join(show_files_meta(self.prjname, self.name))
116         f = open(os.path.join(self.storedir, '_files'), 'w')
117         f.write(meta)
118         f.close()
119         
120     def update_pacmeta(self):
121         meta = ''.join(show_package_meta(self.prjname, self.name))
122         f = open(os.path.join(self.storedir, '_meta'), 'w')
123         f.write(meta)
124         f.close()
125
126     def findfilebyname(self, n):
127         for i in self.filelist:
128             if i.name == n:
129                 return i
130
131     def status(self, n):
132         """
133         status can be:
134
135          file  storefile  file present  STATUS
136         exists  exists      in _files
137
138           x       x            -        'A'
139           x       x            x        'M', if digest differs, else ' '
140           x       -            -        '?'
141           x       -            x        'D' and listed in _to_be_deleted
142           -       x            x        '!'
143           -       x            -        'D' (when file in working copy is already deleted)
144           -       -            x        'F' (new in repo, but not yet in working copy)
145           -       -            -        NOT DEFINED
146
147         """
148
149         known_by_meta = False
150         exists = False
151         exists_in_store = False
152         if n in self.filenamelist:
153             known_by_meta = True
154         if os.path.exists(os.path.join(self.dir, n)):
155             exists = True
156         if os.path.exists(os.path.join(self.storedir, n)):
157             exists_in_store = True
158
159
160         if exists and not exists_in_store and known_by_meta:
161             state = 'D'
162         elif n in self.to_be_deleted:
163             state = 'D'
164         elif exists and exists_in_store and known_by_meta:
165             #print self.findfilebyname(n)
166             if dgst(os.path.join(self.dir, n)) != self.findfilebyname(n).md5:
167                 state = 'M'
168             else:
169                 state = ' '
170         elif exists and not exists_in_store and not known_by_meta:
171             state = '?'
172         elif exists and exists_in_store and not known_by_meta:
173             state = 'A'
174         elif not exists and exists_in_store and known_by_meta:
175             state = '!'
176         elif not exists and not exists_in_store and known_by_meta:
177             state = 'F'
178         elif not exists and exists_in_store and not known_by_meta:
179             state = 'D'
180         elif not exists and not exists_in_store and not known_by_meta:
181             print '%s: not exists and not exists_in_store and not nown_by_meta' % n
182             print 'this code path should never be reached!'
183             sys.exit(1)
184         
185         return state
186
187
188     def merge(self, otherpac):
189         self.todo += otherpac.todo
190
191     def __str__(self):
192         r = """
193 name: %s
194 prjname: %s
195 workingdir: %s
196 localfilelist: %s
197 rev: %s
198 'todo' files: %s
199 """ % (self.name, 
200         self.prjname, 
201         self.dir, 
202         '\n               '.join(self.filenamelist), 
203         self.rev, 
204         self.todo)
205
206         return r
207
208
209         
210 def findpacs(files):
211     pacs = []
212     for f in files:
213         if f in exclude_stuff:
214             break
215
216         p = filedir_to_pac(f)
217         known = None
218         for i in pacs:
219             if i.name == p.name:
220                 known = i
221                 break
222         if known:
223             i.merge(p)
224         else:
225             pacs.append(p)
226     return pacs
227         
228
229 def read_filemeta(dir):
230     return ET.parse(os.path.join(dir, store, '_files'))
231
232
233 def read_tobedeleted(dir):
234     r = []
235     fname = os.path.join(dir, store, '_to_be_deleted')
236
237     if os.path.exists(fname):
238
239         for i in open(fname, 'r').readlines():
240             r.append(i.strip())
241
242     return r
243
244
245 def parseargs():
246         if len(sys.argv) > 2:
247             args = sys.argv[2:]
248         else:
249             args = [ os.curdir ]
250         return args
251
252
253 def filedir_to_pac(f):
254
255     if os.path.isdir(f):
256         wd = f
257         p = Package(wd)
258
259     elif os.path.isfile(f):
260         wd = os.path.dirname(f)
261         if wd == '':
262             wd = os.curdir
263         p = Package(wd)
264         p.todo = [ os.path.basename(f) ]
265
266     else:
267         wd = os.path.dirname(f)
268         if wd == '':
269             wd = os.curdir
270         p = Package(wd)
271         p.todo = [ os.path.basename(f) ]
272         
273
274     #else:
275     #    print 
276     #    print 'error: %s is neither a valid file or directory' % f
277     #    sys.exit(1)
278
279     return p
280
281
282 def statfrmt(statusletter, filename):
283     return '%s    %s' % (statusletter, filename)
284
285
286 def makeurl(l):
287     """given a list of path compoments, construct a complete URL"""
288     return urlunsplit((scheme, netloc, '/'.join(l), '', ''))               
289
290
291 def copy_file(src, dst):
292     # fixme: preserve mtime by default?
293     s = open(src)
294     d = open(dst, 'w')
295     while 1:
296         buf = s.read(BUFSIZE)
297         if not buf: break
298         d.write(buf)
299     s.close()
300     d.close()
301
302
303 def readauth():
304     """look for the credentials. If there aren't any, ask and store them"""
305
306     #
307     # try .netrc first
308     #
309
310     # the needed entry in .netrc looks like this:
311     # machine api.opensuse.org login your_login password your_pass
312     # but it is not able for credentials containing spaces
313     import netrc
314     global username, password
315
316     try:
317         info = netrc.netrc()
318         username, account, password = info.authenticators(netloc)
319         return username, password
320
321     except (IOError, TypeError):
322         pass
323
324     #
325     # try .oscrc next
326     #
327     import ConfigParser
328     conffile = os.path.expanduser('~/.oscrc')
329     if os.path.exists(conffile):
330         config = ConfigParser.ConfigParser()
331         config.read(conffile)
332         username = config.get(netloc, 'user')
333         password = config.get(netloc, 'pass')
334         return username, password
335
336     #
337     # create .oscrc
338     #
339     import getpass
340     print >>sys.stderr, \
341 """your user account / password are not configured yet.
342 You will be asked for them below, and they will be stored in
343 %s for later use.
344 """ % conffile
345
346     username = raw_input('Username: ')
347     password = getpass.getpass()
348
349     fd = open(conffile, 'w')
350     os.chmod(conffile, 0600)
351     print >>fd, '[%s]\nuser: %s\npass: %s' % (netloc, username, password)
352     fd.close()
353         
354     return username, password
355         
356
357
358 def init_basicauth():
359
360     username, password = readauth()
361
362     passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
363     # this creates a password manager
364     passmgr.add_password(None, netloc, username, password)
365     # because we have put None at the start it will always
366     # use this username/password combination for  urls
367     # for which `netloc` is a super-url
368
369     authhandler = urllib2.HTTPBasicAuthHandler(passmgr)
370     # create the AuthHandler
371
372     opener = urllib2.build_opener(authhandler)
373
374     urllib2.install_opener(opener)
375     # All calls to urllib2.urlopen will now use our handler
376     # Make sure not to include the protocol in with the URL, or
377     # HTTPPasswordMgrWithDefaultRealm will be very confused.
378     # You must (of course) use it when fetching the page though.
379
380
381 def init_package_dir(project, package, dir):
382     if not os.path.isdir(store):
383         os.mkdir(store)
384     os.chdir(store)
385     f = open('_project', 'w')
386     f.write(project + '\n')
387     f.close
388     f = open('_package', 'w')
389     f.write(package + '\n')
390     f.close
391
392     f = open('_files', 'w')
393     f.write(''.join(show_files_meta(project, package)))
394     f.close()
395
396     f = open('_osclib_version', 'w')
397     f.write(__version__ + '\n')
398     f.close()
399
400     return
401
402
403 def check_store_version(dir):
404     versionfile = os.path.join(dir, store, '_osclib_version')
405     try:
406         v = open(versionfile).read().strip()
407     except:
408         v = ''
409
410     if v == '':
411         print 'error: "%s" is not an osc working copy' % dir
412         sys.exit(1)
413
414     if v != __version__:
415         if v == '0.2':
416             # 0.2 is fine, no migration needed
417             f = open(versionfile, 'w')
418             f.write(__version__ + '\n')
419             f.close()
420             return 
421         print 
422         print 'the osc metadata of your working copy "%s"' % dir
423         print 'has the wrong version (%s), should be %s' % (v, __version__)
424         print 'please do a fresh checkout'
425         print 
426         sys.exit(1)
427     
428
429 def meta_get_packagelist(prj):
430
431     u = makeurl(['source', prj, '_meta'])
432     f = urllib2.urlopen(u)
433
434     tree = ET.parse(f)
435     root = tree.getroot()
436
437     r = []
438     for node in root.findall('package'):
439         r.append(node.get('name'))
440     return r
441
442
443 def meta_get_filelist(prj, package):
444
445     u = makeurl(['source', prj, package])
446     f = urllib2.urlopen(u)
447     tree = ET.parse(f)
448
449     r = []
450     for node in tree.getroot():
451         r.append(node.get('name'))
452     return r
453
454
455 def localmeta_addfile(filename):
456
457     if filename in localmeta_get_filelist():
458         return
459
460     reader = Sax2.Reader()
461     f = open(os.path.join(store, '_files')).read()
462     doc = reader.fromString(f)
463
464     new = doc.createElement('entry')
465     #new.setAttribute('filetype', 'source')
466     new.setAttribute('name', filename)
467     doc.documentElement.appendChild(new)
468
469     o = open(os.path.join(store, '_files'), 'w')
470     PrettyPrint(doc, stream=o)
471     o.close()
472
473     
474 def localmeta_removefile(filename):
475
476     reader = Sax2.Reader()
477     f = open(os.path.join(store, '_files')).read()
478     doc = reader.fromString(f)
479
480     for i in doc.getElementsByTagName('entry'):
481         if i.getAttribute('name') == filename:
482             i.parentNode.removeChild(i)
483
484     o = open(os.path.join(store, '_files'), 'w')
485     PrettyPrint(doc, stream=o)
486     o.close()
487     
488
489 def localmeta_get_filelist():
490
491     tree = ET.parse(os.path.join(store, '_files'))
492     root = tree.getroot()
493
494     r = []
495     for node in root.findall('entry'):
496         r.append(node.get('name'))
497     return r
498
499
500 def get_slash_source():
501     u = makeurl(['source'])
502     tree = ET.parse(urllib2.urlopen(u))
503
504     r = []
505     for node in tree.getroot():
506         r.append(node.get('name'))
507     r.sort()
508     return r
509
510
511 def show_project_meta(prj):
512     f = urllib2.urlopen(makeurl(['source', prj, '_meta']))
513     return f.readlines()
514
515
516 def show_package_meta(prj, pac):
517     f = urllib2.urlopen(makeurl(['source', prj, pac, '_meta']))
518     return f.readlines()
519
520
521 def show_files_meta(prj, pac):
522     f = urllib2.urlopen(makeurl(['source', prj, pac]))
523     return f.readlines()
524
525
526 def read_meta_from_spec(specfile):
527     """read Name, Summary and %description from spec file"""
528     in_descr = False
529     descr = []
530
531     if not os.path.isfile(specfile):
532         print 'file \'%s\' is not a readable file' % specfile
533         return None
534
535     for line in open(specfile, 'r'):
536         if line.startswith('Name:'):
537             name = line.split(':')[1].strip()
538         if line.startswith('Summary:'):
539             summary = line.split(':')[1].strip()
540         if line.startswith('%description'):
541             in_descr = True
542             continue
543         if in_descr and line.startswith('%'):
544             break
545         if in_descr:
546             descr.append(line)
547     
548     return name, summary, descr
549
550
551 def get_user_id(user):
552     u = makeurl(['person', user])
553     f = urllib2.urlopen(u)
554     return f.readlines()
555
556
557 def get_source_file(prj, package, filename, targetfilename=None):
558     u = makeurl(['source', prj, package, filename])
559     #print 'checking out', u
560     f = urllib2.urlopen(u)
561
562     o = open(targetfilename or filename, 'w')
563     while 1:
564         buf = f.read(BUFSIZE)
565         if not buf: break
566         o.write(buf)
567     o.close()
568
569
570 def dgst(file):
571
572     #if not os.path.exists(file):
573         #return None
574
575     import md5
576     s = md5.new()
577     f = open(file, 'r')
578     while 1:
579         buf = f.read(BUFSIZE)
580         if not buf: break
581         s.update(buf)
582     return s.hexdigest()
583
584
585 def get_source_file_diff_upstream(prj, package, filename):
586     url = makeurl(['source', prj, package, filename])
587     f = urllib2.urlopen(url)
588
589     localfile = open(filename, 'r')
590
591     import difflib
592     #print url
593     d = difflib.unified_diff(f.readlines(), localfile.readlines(), fromfile = url, tofile = filename)
594
595     localfile.close()
596
597     return ''.join(d)
598
599
600 def get_source_file_diff(dir, filename, rev):
601     import difflib
602
603     file1 = os.path.join(dir, filename)
604     file2 = os.path.join(dir, store, filename)
605
606     f1 = open(file1, 'r')
607     f2 = open(file2, 'r')
608
609     d = difflib.unified_diff(\
610         f1.readlines(), \
611         f2.readlines(), \
612         fromfile = '%s     (revision %s)' % (filename, rev), \
613         tofile = '%s     (working copy)' % filename)
614
615     f1.close()
616     f2.close()
617
618     return ''.join(d)
619
620
621 def put_source_file(prj, package, filename):
622     import othermethods
623     
624     sys.stdout.write('.')
625     u = makeurl(['source', prj, package, os.path.basename(filename)])
626     othermethods.putfile(u, filename, username, password)
627
628
629 def del_source_file(prj, package, filename):
630     import othermethods
631     
632     u = makeurl(['source', prj, package, filename])
633     othermethods.delfile(u, filename, username, password)
634
635     wcfilename = os.path.join(store, filename)
636     if os.path.exists(filename): os.unlink(filename)
637     if os.path.exists(wcfilename): os.unlink(wcfilename)
638
639
640 def make_dir(project, package):
641     #print "creating directory '%s'" % project
642     print statfrmt('A', project)
643     if not os.path.exists(project):
644         os.mkdir(project)
645         os.mkdir(os.path.join(project, store))
646
647         f = open(os.path.join(project, store, '_project'), 'w')
648         f.write(project + '\n')
649         f.close()
650
651     #print "creating directory '%s/%s'" % (project, package)
652     print statfrmt('A', '%s/%s' % (project, package))    
653     if not os.path.exists(os.path.join(project, package)):
654         os.mkdir(os.path.join(project, package))
655         os.mkdir(os.path.join(project, package, store))
656
657     return(os.path.join(project, package))
658
659
660 def checkout_package(project, package):
661     olddir = os.getcwd()
662
663     os.chdir(make_dir(project, package))
664     for filename in meta_get_filelist(project, package):
665         get_source_file(project, package, filename)
666         copy_file(filename, os.path.join(store, filename))
667         print 'A   ', os.path.join(project, package, filename)
668
669     init_package_dir(project, package, store)
670
671     os.chdir(olddir)
672
673
674 def get_platforms():
675     f = urllib2.urlopen(makeurl(['platform']))
676     tree = ET.parse(f)
677     r = []
678     for node in tree.getroot():
679         r.append(node.get('name'))
680     r.sort()
681     return r
682
683
684 def get_platforms_of_project(prj):
685     f = show_project_meta(prj)
686     tree = ET.parse(StringIO(''.join(f)))
687
688     r = []
689     for node in tree.findall('repository'):
690         r.append(node.get('name'))
691     return r
692
693
694 def show_results_meta(prj, package, platform):
695     u = makeurl(['result', prj, platform, package, 'result'])
696     f = urllib2.urlopen(u)
697     return f.readlines()
698
699
700 def get_results(prj, package, platform):
701     #print '----------------------------------------'
702
703     r = []
704     #result_line_templ = '%(prj)-15s %(pac)-15s %(rep)-15s %(arch)-10s %(status)s'
705     result_line_templ = '%(rep)-15s %(arch)-10s %(status)s'
706
707     f = show_results_meta(prj, package, platform)
708     tree = ET.parse(StringIO(''.join(f)))
709
710     root = tree.getroot()
711
712     rmap = {}
713     rmap['prj'] = root.get('project')
714     rmap['pac'] = root.get('package')
715     rmap['rep'] = root.get('repository')
716
717     for node in root.findall('archresult'):
718         rmap['arch'] = node.get('arch')
719
720         statusnode =  node.find('status')
721         rmap['status'] = statusnode.get('code')
722
723         if rmap['status'] == 'expansion error':
724             rmap['status'] += ': ' + statusnode.find('summary').text
725
726         if rmap['status'] == 'failed':
727             rmap['status'] += ': %s://%s' % (scheme, netloc) + \
728                 '/result/%(prj)s/%(rep)s/%(pac)s/%(arch)s/log' % rmap
729
730         r.append(result_line_templ % rmap)
731     return r
732
733
734 def get_log(prj, package, platform, arch):
735     u = makeurl(['result', prj, platform, package, arch, 'log'])
736     f = urllib2.urlopen(u)
737     return f.readlines()
738
739
740 def get_history(prj, package):
741     # http://api.opensuse.org/rpm/Apache/factory/i586/apache2/history ?
742     # http://api.opensuse.org/package/Apache/apache2/history ?
743     u = makeurl(['package', prj, package, 'history'])
744     print u
745     f = urllib2.urlopen(u)
746     return f.readlines()
747
748
749 def store_read_project(dir):
750     p = open(os.path.join(dir, store, '_project')).readlines()[0].strip()
751     return p
752
753
754 def store_read_package(dir):
755     p = open(os.path.join(dir, store, '_package')).readlines()[0].strip()
756     return p
757
758