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