- work around ruby on rails issue, which swallows '+' signs in filenames in PUT
[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.6'
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 import shutil
17
18
19 from xml.dom.ext.reader import Sax2
20 from xml.dom.ext import PrettyPrint
21
22 netloc = 'api.opensuse.org'
23 scheme = 'http'
24
25 BUFSIZE = 1024*1024
26 store = '.osc'
27 exclude_stuff = [store, '.svn', 'CVS']
28
29
30 new_project_templ = """\
31 <project name="%s">
32   <title>Short title of NewProject</title>
33   <description>This project aims at providing some foo and bar.
34
35 It also does some weird stuff.
36 </description>
37   <person role="maintainer" userid="%s" />
38 </project>
39                         """
40
41 new_package_templ = """\
42 <package name="%s">
43   <title>Title of NewPackage</title>
44   <description>DESCIPTION</description>
45   <person role="maintainer" userid="%s"/>
46 </package>
47 """
48
49
50 class File:
51     """represent a file, including its metadata"""
52     def __init__(self, name, md5, size, mtime):
53         self.name = name
54         self.md5 = md5
55         self.size = size
56         self.mtime = mtime
57     def __str__(self):
58         return self.name
59
60
61 class Project:
62     """represent a project directory, holding packages"""
63     def __init__(self, dir):
64         self.dir = dir
65         self.absdir = os.path.abspath(dir)
66
67         self.name = store_read_project(self.dir)
68
69         self.pacs_available = meta_get_packagelist(self.name)
70
71         self.pacs_have = []
72         for i in os.listdir(self.dir):
73             if i in self.pacs_available: 
74                 self.pacs_have.append(i)
75
76         self.pacs_missing = []
77         for i in self.pacs_available:
78             if i not in self.pacs_have:
79                 self.pacs_missing.append(i)
80
81     def checkout_missing_pacs(self):
82         for pac in self.pacs_missing:
83             print 'checking out new package %s' % pac
84             olddir = os.getcwd()
85             os.chdir(os.pardir)
86             checkout_package(self.name, pac)
87             os.chdir(olddir)
88
89
90     def __str__(self):
91         r = []
92         r.append('*****************************************************')
93         r.append('Project %s (dir=%s, absdir=%s)' % (self.name, self.dir, self.absdir))
94         r.append('have pacs:\n%s' % ', '.join(self.pacs_have))
95         r.append('missing pacs:\n%s' % ', '.join(self.pacs_missing))
96         r.append('*****************************************************')
97         return '\n'.join(r)
98
99
100
101 class Package:
102     """represent a package (its directory) and read/keep/write its metadata"""
103     def __init__(self, workingdir):
104         self.dir = workingdir
105         self.absdir = os.path.abspath(self.dir)
106         self.storedir = os.path.join(self.dir, store)
107
108         check_store_version(self.dir)
109
110         self.prjname = store_read_project(self.dir)
111         self.name = store_read_package(self.dir)
112
113         files_tree = read_filemeta(self.dir)
114         files_tree_root = files_tree.getroot()
115
116         self.rev = files_tree_root.get('rev')
117
118         self.filenamelist = []
119         self.filelist = []
120         for node in files_tree_root.findall('entry'):
121             try: 
122                 f = File(node.get('name'), 
123                          node.get('md5'), 
124                          int(node.get('size')), 
125                          int(node.get('mtime')))
126             except: 
127                 # okay, a very old version of _files, which didn't contain any metadata yet... 
128                 f = File(node.get('name'), '', 0, 0)
129             self.filelist.append(f)
130             self.filenamelist.append(f.name)
131
132         self.to_be_deleted = read_tobedeleted(self.dir)
133         self.in_conflict = read_inconflict(self.dir)
134
135         self.todo = []
136         self.todo_send = []
137         self.todo_delete = []
138
139         # gather unversioned files (the ones not listed in _meta)
140         self.filenamelist_unvers = []
141         for i in os.listdir(self.dir):
142             if i in exclude_stuff:
143                 continue
144             if not i in self.filenamelist:
145                 self.filenamelist_unvers.append(i) 
146
147     def addfile(self, n):
148         st = os.stat(os.path.join(self.dir, n))
149         f = File(n, None, st[6], st[8])
150         self.filelist.append(f)
151         self.filenamelist.append(n)
152         self.filenamelist_unvers.remove(n) 
153         shutil.copy2(os.path.join(self.dir, n), os.path.join(self.storedir, n))
154         
155     def delete_localfile(self, n):
156         try: os.unlink(os.path.join(self.dir, n))
157         except: pass
158         try: os.unlink(os.path.join(self.storedir, n))
159         except: pass
160
161     def put_on_deletelist(self, n):
162         if n not in self.to_be_deleted:
163             self.to_be_deleted.append(n)
164
165     def put_on_conflictlist(self, n):
166         if n not in self.in_conflict:
167             self.in_conflict.append(n)
168
169     def clear_from_conflictlist(self, n):
170         """delete an entry from the file, and remove the file if it would be empty"""
171         if n in self.in_conflict:
172
173             filename = os.path.join(self.dir, n)
174             storefilename = os.path.join(self.storedir, n)
175             myfilename = os.path.join(self.dir, n + '.mine')
176             upfilename = os.path.join(self.dir, n + '.r' + self.rev)
177
178             try: os.unlink(myfilename)
179             except: pass
180             os.rename(upfilename, storefilename)
181
182             self.in_conflict.remove(n)
183
184             self.write_conflictlist()
185
186     def write_deletelist(self):
187         if len(self.to_be_deleted) == 0:
188             try:
189                 os.unlink(os.path.join(self.storedir, '_to_be_deleted'))
190             except:
191                 pass
192         else:
193             fname = os.path.join(self.storedir, '_to_be_deleted')
194             f = open(fname, 'w')
195             f.write('\n'.join(self.to_be_deleted))
196             f.write('\n')
197             f.close()
198
199     def delete_source_file(self, n):
200         import othermethods
201         
202         u = makeurl(['source', self.prjname, self.name, n])
203         othermethods.delfile(u, n, username, password)
204
205         self.delete_localfile(n)
206
207     def put_source_file(self, n):
208         import othermethods
209         
210         # escaping '+' in the URL path (note: not in the URL query string) is 
211         # only a workaround for ruby on rails, which swallows it otherwise
212         u = makeurl(['source', self.prjname, self.name, n.replace('+', '%2B')])
213         othermethods.putfile(u, os.path.join(self.dir, n), username, password)
214
215         shutil.copy2(os.path.join(self.dir, n), os.path.join(self.storedir, n))
216
217     def write_conflictlist(self):
218         if len(self.in_conflict) == 0:
219             os.unlink(os.path.join(self.storedir, '_in_conflict'))
220         else:
221             fname = os.path.join(self.storedir, '_in_conflict')
222             f = open(fname, 'w')
223             f.write('\n'.join(self.in_conflict))
224             f.write('\n')
225             f.close()
226
227     def updatefile(self, n):
228         filename = os.path.join(self.dir, n)
229         storefilename = os.path.join(self.storedir, n)
230         mtime = self.findfilebyname(n).mtime
231
232         get_source_file(self.prjname, self.name, n, targetfilename=filename)
233         os.utime(filename, (-1, mtime))
234
235         shutil.copy2(filename, storefilename)
236
237     def mergefile(self, n):
238         filename = os.path.join(self.dir, n)
239         storefilename = os.path.join(self.storedir, n)
240         myfilename = os.path.join(self.dir, n + '.mine')
241         upfilename = os.path.join(self.dir, n + '.r' + self.rev)
242         os.rename(filename, myfilename)
243
244         get_source_file(self.prjname, self.name, n, targetfilename=upfilename)
245
246         ret = os.system('cd %s; diff3 -m -E %s %s %s > %s' \
247             % (self.dir, myfilename, storefilename, upfilename, filename))
248         if ret == 0:
249             # merge was successful... clean up
250             os.rename(upfilename, filename)
251             shutil.copy2(filename, storefilename)
252             os.unlink(myfilename)
253             return 'G'
254         else:
255             # unsuccessful merge
256             self.in_conflict.append(n)
257             self.write_conflictlist()
258             return 'C'
259
260
261
262     def update_filesmeta(self):
263         meta = ''.join(show_files_meta(self.prjname, self.name))
264         f = open(os.path.join(self.storedir, '_files'), 'w')
265         f.write(meta)
266         f.close()
267         
268     def update_pacmeta(self):
269         meta = ''.join(show_package_meta(self.prjname, self.name))
270         f = open(os.path.join(self.storedir, '_meta'), 'w')
271         f.write(meta)
272         f.close()
273
274     def findfilebyname(self, n):
275         for i in self.filelist:
276             if i.name == n:
277                 return i
278
279     def status(self, n):
280         """
281         status can be:
282
283          file  storefile  file present  STATUS
284         exists  exists      in _files
285
286           x       x            -        'A'
287           x       x            x        ' ' if digest differs: 'M'
288                                             and if in conflicts file: 'C'
289           x       -            -        '?'
290           x       -            x        'D' and listed in _to_be_deleted
291           -       x            x        '!'
292           -       x            -        'D' (when file in working copy is already deleted)
293           -       -            x        'F' (new in repo, but not yet in working copy)
294           -       -            -        NOT DEFINED
295
296         """
297
298         known_by_meta = False
299         exists = False
300         exists_in_store = False
301         if n in self.filenamelist:
302             known_by_meta = True
303         if os.path.exists(os.path.join(self.dir, n)):
304             exists = True
305         if os.path.exists(os.path.join(self.storedir, n)):
306             exists_in_store = True
307
308
309         if exists and not exists_in_store and known_by_meta:
310             state = 'D'
311         elif n in self.to_be_deleted:
312             state = 'D'
313         elif n in self.in_conflict:
314             state = 'C'
315         elif exists and exists_in_store and known_by_meta:
316             #print self.findfilebyname(n)
317             if dgst(os.path.join(self.dir, n)) != self.findfilebyname(n).md5:
318                 state = 'M'
319             else:
320                 state = ' '
321         elif exists and not exists_in_store and not known_by_meta:
322             state = '?'
323         elif exists and exists_in_store and not known_by_meta:
324             state = 'A'
325         elif not exists and exists_in_store and known_by_meta:
326             state = '!'
327         elif not exists and not exists_in_store and known_by_meta:
328             state = 'F'
329         elif not exists and exists_in_store and not known_by_meta:
330             state = 'D'
331         elif not exists and not exists_in_store and not known_by_meta:
332             print '%s: not exists and not exists_in_store and not nown_by_meta' % n
333             print 'this code path should never be reached!'
334             sys.exit(1)
335         
336         return state
337
338
339     def merge(self, otherpac):
340         self.todo += otherpac.todo
341
342     def __str__(self):
343         r = """
344 name: %s
345 prjname: %s
346 workingdir: %s
347 localfilelist: %s
348 rev: %s
349 'todo' files: %s
350 """ % (self.name, 
351         self.prjname, 
352         self.dir, 
353         '\n               '.join(self.filenamelist), 
354         self.rev, 
355         self.todo)
356
357         return r
358
359
360     def read_meta_from_spec(self):
361         specfile = os.path.join(self.dir, self.name + '.spec')
362         name, summary, descr = read_meta_from_spec(specfile)
363
364         if name != self.name:
365             print 'name from spec does not match name of package... this is probably a problem'
366             sys.exit(1)
367         self.summary = summary
368         self.descr = descr
369
370
371     def update_pac_meta(self):
372         import othermethods
373         import tempfile
374
375         (f, filename) = tempfile.mkstemp(prefix = 'osc_editmeta.', suffix = '.xml', dir = '/tmp')
376
377         try:
378             m = show_package_meta(self.prjname, self.name)
379         except urllib2.HTTPError, e:
380             if e.code == 404:
381                 print 'package does not exist yet... creating it'
382                 m = new_package_templ % (pac, username)
383             else:
384                 print 'error getting package meta for project \'%s\' package \'%s\':' % (prj, pac)
385                 print e
386                 sys.exit(1)
387
388         f = open(filename, 'w')
389         f.write(''.join(m))
390         f.close()
391
392         tree = ET.parse(filename)
393         tree.find('title').text = self.summary
394         tree.find('description').text = ''.join(self.descr)
395         tree.write(filename)
396
397         # FIXME: escape stuff for xml
398         print '*' * 36, 'old', '*' * 36
399         print ''.join(m)
400         print '*' * 36, 'new', '*' * 36
401         tree.write(sys.stdout)
402         print '*' * 72
403
404         # FIXME: for testing...
405         # open the new description in $EDITOR instead?
406         repl = raw_input('Write? (y/N) ')
407         if repl == 'y':
408             print 'Sending meta data...', 
409             u = makeurl(['source', self.prjname, self.name, '_meta'])
410             othermethods.putfile(u, filename, username, password)
411             print 'Done.'
412         else:
413             print 'discarding', filename
414
415         os.unlink(filename)
416
417
418
419
420 def is_project_dir(d):
421     if os.path.exists(os.path.join(d, store, '_project')) and not \
422        os.path.exists(os.path.join(d, store, '_package')):
423         return True
424     else:
425         return False
426
427         
428 def findpacs(files):
429     pacs = []
430     for f in files:
431         if f in exclude_stuff:
432             break
433
434         p = filedir_to_pac(f)
435         known = None
436         for i in pacs:
437             if i.name == p.name:
438                 known = i
439                 break
440         if known:
441             i.merge(p)
442         else:
443             pacs.append(p)
444     return pacs
445         
446
447 def read_filemeta(dir):
448     return ET.parse(os.path.join(dir, store, '_files'))
449
450
451 def read_tobedeleted(dir):
452     r = []
453     fname = os.path.join(dir, store, '_to_be_deleted')
454
455     if os.path.exists(fname):
456
457         for i in open(fname, 'r').readlines():
458             r.append(i.strip())
459
460     return r
461
462
463 def read_inconflict(dir):
464     r = []
465     fname = os.path.join(dir, store, '_in_conflict')
466
467     if os.path.exists(fname):
468
469         for i in open(fname, 'r').readlines():
470             r.append(i.strip())
471
472     return r
473
474
475 def parseargs(list_of_args):
476         if list_of_args:
477             return list_of_args
478         else:
479             return [ os.curdir ]
480
481
482 def filedir_to_pac(f):
483
484     if os.path.isdir(f):
485         wd = f
486         p = Package(wd)
487
488     elif os.path.isfile(f):
489         wd = os.path.dirname(f)
490         if wd == '':
491             wd = os.curdir
492         p = Package(wd)
493         p.todo = [ os.path.basename(f) ]
494
495     else:
496         wd = os.path.dirname(f)
497         if wd == '':
498             wd = os.curdir
499         p = Package(wd)
500         p.todo = [ os.path.basename(f) ]
501         
502
503     #else:
504     #    print 
505     #    print 'error: %s is neither a valid file or directory' % f
506     #    sys.exit(1)
507
508     return p
509
510
511 def statfrmt(statusletter, filename):
512     return '%s    %s' % (statusletter, filename)
513
514
515 def makeurl(l):
516     """given a list of path compoments, construct a complete URL"""
517     return urlunsplit((scheme, netloc, '/'.join(l), '', ''))               
518
519
520 def readauth():
521     """look for the credentials. If there aren't any, ask and store them"""
522
523     #
524     # try .netrc first
525     #
526
527     # the needed entry in .netrc looks like this:
528     # machine api.opensuse.org login your_login password your_pass
529     # but it is not able for credentials containing spaces
530     import netrc
531     global username, password
532
533     try:
534         info = netrc.netrc()
535         username, account, password = info.authenticators(netloc)
536         return username, password
537
538     except (IOError, TypeError):
539         pass
540
541     #
542     # try .oscrc next
543     #
544     import ConfigParser
545     conffile = os.path.expanduser('~/.oscrc')
546     if os.path.exists(conffile):
547         config = ConfigParser.ConfigParser()
548         config.read(conffile)
549         username = config.get(netloc, 'user')
550         password = config.get(netloc, 'pass')
551         return username, password
552
553     #
554     # create .oscrc
555     #
556     import getpass
557     print >>sys.stderr, \
558 """your user account / password are not configured yet.
559 You will be asked for them below, and they will be stored in
560 %s for later use.
561 """ % conffile
562
563     username = raw_input('Username: ')
564     password = getpass.getpass()
565
566     fd = open(conffile, 'w')
567     os.chmod(conffile, 0600)
568     print >>fd, '[%s]\nuser: %s\npass: %s' % (netloc, username, password)
569     fd.close()
570         
571     return username, password
572         
573
574
575 def init_basicauth():
576
577     username, password = readauth()
578
579     passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
580     # this creates a password manager
581     passmgr.add_password(None, netloc, username, password)
582     # because we have put None at the start it will always
583     # use this username/password combination for  urls
584     # for which `netloc` is a super-url
585
586     authhandler = urllib2.HTTPBasicAuthHandler(passmgr)
587     # create the AuthHandler
588
589     opener = urllib2.build_opener(authhandler)
590     opener.addheaders = [('User-agent', 'osc/%s' % __version__)]
591
592     urllib2.install_opener(opener)
593     # All calls to urllib2.urlopen will now use our handler
594     # Make sure not to include the protocol in with the URL, or
595     # HTTPPasswordMgrWithDefaultRealm will be very confused.
596     # You must (of course) use it when fetching the page though.
597
598
599 def init_package_dir(project, package, dir):
600     if not os.path.isdir(store):
601         os.mkdir(store)
602     os.chdir(store)
603     f = open('_project', 'w')
604     f.write(project + '\n')
605     f.close
606     f = open('_package', 'w')
607     f.write(package + '\n')
608     f.close
609
610     f = open('_files', 'w')
611     f.write(''.join(show_files_meta(project, package)))
612     f.close()
613
614     f = open('_osclib_version', 'w')
615     f.write(__version__ + '\n')
616     f.close()
617
618     os.chdir(os.pardir)
619     return
620
621
622 def check_store_version(dir):
623     versionfile = os.path.join(dir, store, '_osclib_version')
624     try:
625         v = open(versionfile).read().strip()
626     except:
627         v = ''
628
629     if v == '':
630         print 'error: "%s" is not an osc working copy' % dir
631         sys.exit(1)
632
633     if v != __version__:
634         if v in ['0.2', '0.3', '0.4', '0.5']:
635             # version is fine, no migration needed
636             f = open(versionfile, 'w')
637             f.write(__version__ + '\n')
638             f.close()
639             return 
640         print 
641         print 'the osc metadata of your working copy "%s"' % dir
642         print 'has the wrong version (%s), should be %s' % (v, __version__)
643         print 'please do a fresh checkout'
644         print 
645         sys.exit(1)
646     
647
648 def meta_get_packagelist(prj):
649
650     u = makeurl(['source', prj, '_meta'])
651
652     try:
653         f = urllib2.urlopen(u)
654     except urllib2.HTTPError, e:
655         if e.code == 404:
656             print 'project \'%s\' does not exist' % prj
657             sys.exit(1)
658         else:
659             print e
660             print 'url: \'%s\'' % u
661             sys.exit(1)
662
663     tree = ET.parse(f)
664     root = tree.getroot()
665
666     r = []
667     for node in root.findall('package'):
668         r.append(node.get('name'))
669     return r
670
671
672 def meta_get_filelist(prj, package):
673
674     u = makeurl(['source', prj, package])
675     f = urllib2.urlopen(u)
676     tree = ET.parse(f)
677
678     r = []
679     for node in tree.getroot():
680         r.append(node.get('name'))
681     return r
682
683
684 def localmeta_addfile(filename):
685
686     if filename in localmeta_get_filelist():
687         return
688
689     reader = Sax2.Reader()
690     f = open(os.path.join(store, '_files')).read()
691     doc = reader.fromString(f)
692
693     new = doc.createElement('entry')
694     #new.setAttribute('filetype', 'source')
695     new.setAttribute('name', filename)
696     doc.documentElement.appendChild(new)
697
698     o = open(os.path.join(store, '_files'), 'w')
699     PrettyPrint(doc, stream=o)
700     o.close()
701
702     
703 def localmeta_removefile(filename):
704
705     reader = Sax2.Reader()
706     f = open(os.path.join(store, '_files')).read()
707     doc = reader.fromString(f)
708
709     for i in doc.getElementsByTagName('entry'):
710         if i.getAttribute('name') == filename:
711             i.parentNode.removeChild(i)
712
713     o = open(os.path.join(store, '_files'), 'w')
714     PrettyPrint(doc, stream=o)
715     o.close()
716     
717
718 def localmeta_get_filelist():
719
720     tree = ET.parse(os.path.join(store, '_files'))
721     root = tree.getroot()
722
723     r = []
724     for node in root.findall('entry'):
725         r.append(node.get('name'))
726     return r
727
728
729 def get_slash_source():
730     u = makeurl(['source'])
731     tree = ET.parse(urllib2.urlopen(u))
732
733     r = []
734     for node in tree.getroot():
735         r.append(node.get('name'))
736     r.sort()
737     return r
738
739
740 def show_project_meta(prj):
741     f = urllib2.urlopen(makeurl(['source', prj, '_meta']))
742     return f.readlines()
743
744
745 def show_package_meta(prj, pac):
746     f = urllib2.urlopen(makeurl(['source', prj, pac, '_meta']))
747     return f.readlines()
748
749
750 def edit_meta(prj, pac):
751     import othermethods
752     import tempfile
753
754     (f, filename) = tempfile.mkstemp(prefix = 'osc_editmeta.', suffix = '.xml', dir = '/tmp')
755
756     if pac:
757         # package meta
758         u = makeurl(['source', prj, pac, '_meta'])
759         try:
760             m = show_package_meta(prj, pac)
761         except urllib2.HTTPError, e:
762             if e.code == 404:
763                 m = new_package_templ % (pac, username)
764             else:
765                 print 'error getting package meta for project \'%s\' package \'%s\':' % (prj, pac)
766                 print e
767                 sys.exit(1)
768
769     else:
770         # project meta
771         u = makeurl(['source', prj, '_meta'])
772         try:
773             m = show_project_meta(prj)
774         except urllib2.HTTPError, e:
775             if e.code == 404:
776                 m = new_project_templ % (prj, username)
777             else:
778                 print 'error getting package meta for project \'%s\':' % prj
779                 print e
780                 sys.exit(1)
781
782     f = open(filename, 'w')
783     f.write(''.join(m))
784     f.close()
785
786     timestamp = os.stat(filename).st_mtime
787
788     editor = os.getenv('EDITOR', default='vim')
789     os.system('%s %s' % (editor, filename))
790
791     if os.stat(filename).st_mtime == timestamp:
792         print 'File unchanged. Not saving.'
793         os.unlink(filename)
794
795     else:
796         print 'Sending meta data...', 
797         othermethods.putfile(u, filename, username, password)
798         os.unlink(filename)
799         print 'Done.'
800
801
802 def show_files_meta(prj, pac):
803     f = urllib2.urlopen(makeurl(['source', prj, pac]))
804     return f.readlines()
805
806
807 def show_upstream_rev(prj, pac):
808     m = show_files_meta(prj, pac)
809     return ET.parse(StringIO(''.join(m))).getroot().get('rev')
810
811
812 def read_meta_from_spec(specfile):
813     """read Name, Summary and %description from spec file"""
814     in_descr = False
815     descr = []
816
817     if not os.path.isfile(specfile):
818         print 'file \'%s\' is not a readable file' % specfile
819         return None
820
821     for line in open(specfile, 'r'):
822         if line.startswith('Name:'):
823             name = line.split(':')[1].strip()
824         if line.startswith('Summary:'):
825             summary = line.split(':')[1].strip()
826         if line.startswith('%description'):
827             in_descr = True
828             continue
829         if in_descr and line.startswith('%'):
830             break
831         if in_descr:
832             descr.append(line)
833     
834     return name, summary, descr
835
836
837 def get_user_id(user):
838     u = makeurl(['person', user.replace(' ', '+')])
839     try:
840         f = urllib2.urlopen(u)
841         return ''.join(f.readlines())
842     except urllib2.HTTPError:
843         print 'user \'%s\' not found' % user
844         return None
845
846
847 def get_source_file(prj, package, filename, targetfilename=None):
848     u = makeurl(['source', prj, package, filename])
849     #print 'checking out', u
850     f = urllib2.urlopen(u)
851
852     o = open(targetfilename or filename, 'w')
853     while 1:
854         buf = f.read(BUFSIZE)
855         if not buf: break
856         o.write(buf)
857     o.close()
858
859
860 def dgst(file):
861
862     #if not os.path.exists(file):
863         #return None
864
865     import md5
866     s = md5.new()
867     f = open(file, 'r')
868     while 1:
869         buf = f.read(BUFSIZE)
870         if not buf: break
871         s.update(buf)
872     return s.hexdigest()
873
874
875 def get_source_file_diff_upstream(prj, package, filename):
876     url = makeurl(['source', prj, package, filename])
877     f = urllib2.urlopen(url)
878
879     localfile = open(filename, 'r')
880
881     import difflib
882     #print url
883     d = difflib.unified_diff(f.readlines(), localfile.readlines(), fromfile = url, tofile = filename)
884
885     localfile.close()
886
887     return ''.join(d)
888
889
890 def get_source_file_diff(dir, filename, rev):
891     import difflib
892
893     file1 = os.path.join(dir, store, filename)  # stored original
894     file2 = os.path.join(dir, filename)         # working copy
895
896     f1 = open(file1, 'r')
897     f2 = open(file2, 'r')
898
899     d = difflib.unified_diff(\
900         f1.readlines(), \
901         f2.readlines(), \
902         fromfile = '%s     (revision %s)' % (filename, rev), \
903         tofile = '%s     (working copy)' % filename)
904
905     f1.close()
906     f2.close()
907
908     return ''.join(d)
909
910
911 def make_dir(project, package):
912     #print "creating directory '%s'" % project
913     if not os.path.exists(project):
914         print statfrmt('A', project)
915         os.mkdir(project)
916         os.mkdir(os.path.join(project, store))
917
918         f = open(os.path.join(project, store, '_project'), 'w')
919         f.write(project + '\n')
920         f.close()
921
922     #print "creating directory '%s/%s'" % (project, package)
923     if not os.path.exists(os.path.join(project, package)):
924         print statfrmt('A', '%s/%s' % (project, package))    
925         os.mkdir(os.path.join(project, package))
926         os.mkdir(os.path.join(project, package, store))
927
928     return(os.path.join(project, package))
929
930
931 def checkout_package(project, package):
932     olddir = os.getcwd()
933
934     os.chdir(make_dir(project, package))
935     init_package_dir(project, package, store)
936     p = Package(os.curdir)
937
938     for filename in p.filenamelist:
939         p.updatefile(filename)
940         print 'A   ', os.path.join(project, package, filename)
941
942     os.chdir(olddir)
943
944
945 def get_platforms():
946     f = urllib2.urlopen(makeurl(['platform']))
947     tree = ET.parse(f)
948     r = []
949     for node in tree.getroot():
950         r.append(node.get('name'))
951     r.sort()
952     return r
953
954
955 def get_platforms_of_project(prj):
956     f = show_project_meta(prj)
957     tree = ET.parse(StringIO(''.join(f)))
958
959     r = []
960     for node in tree.findall('repository'):
961         r.append(node.get('name'))
962     return r
963
964
965 def show_results_meta(prj, package, platform):
966     u = makeurl(['result', prj, platform, package, 'result'])
967     f = urllib2.urlopen(u)
968     return f.readlines()
969
970
971 def get_results(prj, package, platform):
972     #print '----------------------------------------'
973
974     r = []
975     #result_line_templ = '%(prj)-15s %(pac)-15s %(rep)-15s %(arch)-10s %(status)s'
976     result_line_templ = '%(rep)-15s %(arch)-10s %(status)s'
977
978     f = show_results_meta(prj, package, platform)
979     tree = ET.parse(StringIO(''.join(f)))
980
981     root = tree.getroot()
982
983     rmap = {}
984     rmap['prj'] = root.get('project')
985     rmap['pac'] = root.get('package')
986     rmap['rep'] = root.get('repository')
987
988     for node in root.findall('archresult'):
989         rmap['arch'] = node.get('arch')
990
991         statusnode =  node.find('status')
992         rmap['status'] = statusnode.get('code')
993
994         if rmap['status'] in ['expansion error', 'broken']:
995             rmap['status'] += ': ' + statusnode.find('summary').text
996
997         if rmap['status'] == 'failed':
998             rmap['status'] += ': %s://%s' % (scheme, netloc) + \
999                 '/result/%(prj)s/%(rep)s/%(pac)s/%(arch)s/log' % rmap
1000
1001         r.append(result_line_templ % rmap)
1002     return r
1003
1004
1005 def get_log(prj, package, platform, arch):
1006     u = makeurl(['result', prj, platform, package, arch, 'log'])
1007     f = urllib2.urlopen(u)
1008     return f.readlines()
1009
1010
1011 def get_history(prj, package):
1012     # http://api.opensuse.org/rpm/Apache/factory/i586/apache2/history ?
1013     # http://api.opensuse.org/package/Apache/apache2/history ?
1014     u = makeurl(['package', prj, package, 'history'])
1015     print u
1016     f = urllib2.urlopen(u)
1017     return f.readlines()
1018
1019
1020 def store_read_project(dir):
1021     p = open(os.path.join(dir, store, '_project')).readlines()[0].strip()
1022     return p
1023
1024
1025 def store_read_package(dir):
1026     p = open(os.path.join(dir, store, '_package')).readlines()[0].strip()
1027     return p
1028
1029 def get_osc_version():
1030     return __version__
1031