Putting names at the top of files is is not recommended. Collective wisdom for
[opensuse:osc.git] / osc / core.py
1 #!/usr/bin/python
2
3 # vim: sw=4 et
4
5 # Copyright (C) 2006 Novell Inc.  All rights reserved.
6 # This program is free software; it may be used, copied, modified
7 # and distributed under the terms of the GNU General Public Licence,
8 # either version 2, or (at your option) any later version.
9
10 __version__ = '0.113'
11 # __store_version__ is to be incremented when the format of the working copy
12 # "store" changes in an incompatible way. Please add any needed migration
13 # functionality to check_store_version().
14 __store_version__ = '1.0'
15
16 import os
17 import sys
18 import urllib2
19 from urllib import pathname2url, quote_plus, urlencode
20 from urlparse import urlsplit, urlunsplit
21 from cStringIO import StringIO
22 import shutil
23 import oscerr
24 import conf
25 import subprocess
26 try:
27     from xml.etree import cElementTree as ET
28 except ImportError:
29     import cElementTree as ET
30
31
32
33 BUFSIZE = 1024*1024
34 store = '.osc'
35 exclude_stuff = [store, 'CVS', '*~', '.*', '_linkerror']
36
37
38 new_project_templ = """\
39 <project name="%(name)s">
40
41   <title>Short title of NewProject</title>
42
43   <description>This project aims at providing some foo and bar.
44
45 It also does some weird stuff.
46 </description>
47
48   <person role="maintainer" userid="%(user)s" />
49   <person role="bugowner" userid="%(user)s" />
50 <!-- remove this block to publish your packages on the mirrors -->
51   <publish>
52     <disable />
53   </publish>
54   <build>
55     <enable />
56   </build>
57   <debuginfo>
58     <disable />
59   </debuginfo>
60
61 <!-- remove this comment to enable one or more build targets
62
63   <repository name="openSUSE_Factory">
64     <path project="openSUSE:Factory" repository="standard" />
65     <arch>x86_64</arch>
66     <arch>i586</arch>
67   </repository>
68   <repository name="openSUSE_11.1">
69     <path project="openSUSE:11.1" repository="standard"/>
70     <arch>x86_64</arch>
71     <arch>i586</arch>
72   </repository>
73   <repository name="openSUSE_11.0">
74     <path project="openSUSE:11.0" repository="standard"/>
75     <arch>x86_64</arch>
76     <arch>i586</arch>
77   </repository>
78   <repository name="openSUSE_10.3">
79     <path project="openSUSE:10.3" repository="standard"/>
80     <arch>x86_64</arch>
81     <arch>i586</arch>
82   </repository>
83   <repository name="Fedora_9">
84     <path project="Fedora:9" repository="standard" />
85     <arch>x86_64</arch>
86     <arch>i586</arch>
87   </repository>
88   <repository name="SLE_10">
89     <path project="SUSE:SLE-10:SDK" repository="standard" />
90     <arch>x86_64</arch>
91     <arch>i586</arch>
92   </repository>
93 -->
94  
95 </project>
96 """
97
98 new_package_templ = """\
99 <package name="%(name)s">
100
101   <title>Title of New Package</title>
102
103   <description>
104 LONG DESCRIPTION 
105 GOES 
106 HERE
107   </description>
108
109   <person role="maintainer" userid="%(user)s"/>
110   <person role="bugowner" userid="%(user)s"/>
111   <url>PUT_UPSTREAM_URL_HERE</url>
112
113 <!-- 
114   use one of the examples below to disable building of this package 
115   on a certain architecture, in a certain repository, 
116   or a combination thereof:
117   
118   <disable arch="x86_64"/>
119   <disable repository="SUSE_SLE-10"/>
120   <disable repository="SUSE_SLE-10" arch="x86_64"/>
121
122   Possible sections where you can use the tags above:
123   <build>
124   </build>
125   <debuginfo>
126   </debuginfo>
127   <publish>
128   </publish>
129   <useforbuild>
130   </useforbuild>
131   
132   Please have a look at: 
133   http://en.opensuse.org/Restricted_Formats
134   Packages containing formats listed there are NOT allowed to 
135   be packaged in the openSUSE Buildservice and will be deleted!
136
137 -->
138
139 </package>
140 """
141
142 new_user_template = """\
143 <person>
144   <login>%(user)s</login>
145   <email>PUT_EMAIL_ADDRESS_HERE</email>
146   <realname>PUT_REAL_NAME_HERE</realname>
147   <watchlist>
148     <project name="home:%(user)s"/>
149   </watchlist>
150 </person>
151 """
152
153 info_templ = """\
154 Project name: %s
155 Package name: %s
156 Path: %s
157 API URL: %s
158 Source URL: %s
159 srcmd5: %s
160 Revision: %s
161 Link info: %s
162 """
163
164 new_pattern_template = """\
165 <!-- See http://svn.opensuse.org/svn/zypp/trunk/libzypp/zypp/parser/yum/schema/patterns.rng -->
166
167 <pattern>
168 </pattern>
169 """
170
171 buildstatus_symbols = {'succeeded':       '.',
172                        'disabled':        ' ',
173                        'expansion error': 'E',
174                        'failed':          'F',
175                        'broken':          'B',
176                        'blocked':         'b',
177                        'building':        '%',
178                        'finished':        'f',
179                        'scheduled':       's',
180 }
181
182
183 class File:
184     """represent a file, including its metadata"""
185     def __init__(self, name, md5, size, mtime):
186         self.name = name
187         self.md5 = md5
188         self.size = size
189         self.mtime = mtime
190     def __str__(self):
191         return self.name
192
193
194 class Linkinfo:
195     """linkinfo metadata (which is part of the xml representing a directory
196     """
197     def __init__(self):
198         """creates an empty linkinfo instance"""
199         self.project = None
200         self.package = None
201         self.xsrcmd5 = None
202         self.lsrcmd5 = None
203         self.srcmd5 = None
204         self.error = None
205
206     def read(self, linkinfo_node):
207         """read in the linkinfo metadata from the <linkinfo> element passed as
208         elementtree node.
209         If the passed element is None, the method does nothing.
210         """
211         if linkinfo_node == None:
212             return
213         self.project = linkinfo_node.get('project')
214         self.package = linkinfo_node.get('package')
215         self.xsrcmd5 = linkinfo_node.get('xsrcmd5')
216         self.lsrcmd5 = linkinfo_node.get('lsrcmd5')
217         self.srcmd5  = linkinfo_node.get('srcmd5')
218         self.error   = linkinfo_node.get('error')
219
220     def islink(self):
221         """returns True if the linkinfo is not empty, otherwise False"""
222         if self.xsrcmd5 or self.lsrcmd5:
223             return True
224         return False
225
226     def isexpanded(self):
227         """returns True if the package is an expanded link"""
228         if self.lsrcmd5 and not self.xsrcmd5:
229             return True
230         return False
231
232     def haserror(self):
233         """returns True if the link is in error state (could not be applied)"""
234         if self.error:
235             return True
236         return False
237
238     def __str__(self):
239         """return an informatory string representation"""
240         if self.islink() and not self.isexpanded():
241             return 'project %s, package %s, xsrcmd5 %s' \
242                     % (self.project, self.package, self.xsrcmd5)
243         elif self.islink() and self.isexpanded():
244             if self.haserror():
245                 return 'broken link to project %s, package %s, srcmd5 %s, lsrcmd5 %s: %s' \
246                         % (self.project, self.package, self.srcmd5, self.lsrcmd5, self.error)
247             else:
248                 return 'expanded link to project %s, package %s, srcmd5 %s, lsrcmd5 %s' \
249                         % (self.project, self.package, self.srcmd5, self.lsrcmd5)
250         else:
251             return 'None'
252
253
254 class Project:
255     """represent a project directory, holding packages"""
256     def __init__(self, dir, getPackageList=True):
257         import fnmatch
258         self.dir = dir
259         self.absdir = os.path.abspath(dir)
260
261         self.name = store_read_project(self.dir)
262         self.apiurl = store_read_apiurl(self.dir)
263
264         if getPackageList:
265             self.pacs_available = meta_get_packagelist(self.apiurl, self.name)
266         else:
267             self.pacs_available = []
268         
269         if conf.config['do_package_tracking']:
270             self.pac_root = self.read_packages().getroot()
271             self.pacs_have = [ pac.get('name') for pac in self.pac_root.findall('package') ]
272             self.pacs_excluded = [ i for i in os.listdir(self.dir) 
273                                    for j in exclude_stuff 
274                                    if fnmatch.fnmatch(i, j) ]
275             self.pacs_unvers = [ i for i in os.listdir(self.dir) if i not in self.pacs_have and i not in self.pacs_excluded ]
276             # store all broken packages (e.g. packages which where removed by a non-osc cmd)
277             # in the self.pacs_broken list
278             self.pacs_broken = []
279             for p in self.pacs_have:
280                 if not os.path.isdir(os.path.join(self.absdir, p)):
281                     # all states will be replaced with the '!'-state
282                     # (except it is already marked as deleted ('D'-state))
283                     self.pacs_broken.append(p)
284         else:
285             self.pacs_have = [ i for i in os.listdir(self.dir) if i in self.pacs_available ]
286
287         self.pacs_missing = [ i for i in self.pacs_available if i not in self.pacs_have ]
288
289     def checkout_missing_pacs(self):
290         for pac in self.pacs_missing:
291
292             if conf.config['do_package_tracking'] and pac in self.pacs_unvers:
293                 # pac is not under version control but a local file/dir exists
294                 msg = 'can\'t add package \'%s\': Object already exists' % pac
295                 raise oscerr.PackageExists(self.name, pac, msg)
296             else:
297                 print 'checking out new package %s' % pac
298                 checkout_package(self.apiurl, self.name, pac, \
299                                  pathname=getTransActPath(os.path.join(self.dir, pac)), \
300                                  prj_obj=self, prj_dir=self.dir)
301
302     def set_state(self, pac, state):
303         node = self.get_package_node(pac)
304         if node == None:
305             self.new_package_entry(pac, state)
306         else:
307             node.attrib['state'] = state
308
309     def get_package_node(self, pac):
310         for node in self.pac_root.findall('package'):
311             if pac == node.get('name'):
312                 return node
313         return None
314
315     def del_package_node(self, pac):
316         for node in self.pac_root.findall('package'):
317             if pac == node.get('name'):
318                 self.pac_root.remove(node)
319
320     def get_state(self, pac):
321         node = self.get_package_node(pac)
322         if node != None:
323             return node.get('state')
324         else:
325             return None
326
327     def new_package_entry(self, name, state):
328         ET.SubElement(self.pac_root, 'package', name=name, state=state)
329
330     def read_packages(self):
331         if os.path.isfile(os.path.join(self.absdir, store, '_packages')):
332             return ET.parse(os.path.join(self.absdir, store, '_packages'))
333         else:
334             # scan project for existing packages and migrate them
335             cur_pacs = []
336             for data in os.listdir(self.dir):
337                 pac_dir = os.path.join(self.absdir, data)
338                 # we cannot use self.pacs_available because we cannot guarantee that the package list
339                 # was fetched from the server
340                 if data in meta_get_packagelist(self.apiurl, self.name) and is_package_dir(pac_dir) \
341                    and Package(pac_dir).name == data:
342                     cur_pacs.append(ET.Element('package', name=data, state=' '))
343             store_write_initial_packages(self.absdir, self.name, cur_pacs)
344             return ET.parse(os.path.join(self.absdir, store, '_packages'))
345
346     def write_packages(self):
347         # TODO: should we only modify the existing file instead of overwriting?
348         ET.ElementTree(self.pac_root).write(os.path.join(self.absdir, store, '_packages'))
349
350     def addPackage(self, pac):
351         import fnmatch
352         for i in exclude_stuff:
353             if fnmatch.fnmatch(pac, i):
354                 return False
355         state = self.get_state(pac)
356         if state == None or state == 'D':
357             self.new_package_entry(pac, 'A')
358             self.write_packages()
359             # sometimes the new pac doesn't exist in the list because
360             # it would take too much time to update all data structs regularly
361             if pac in self.pacs_unvers:
362                 self.pacs_unvers.remove(pac)
363             return True
364         else:
365             print 'package \'%s\' is already under version control' % pac
366             return False
367
368     def delPackage(self, pac, force = False):
369         state = self.get_state(pac.name)
370         can_delete = True
371         if state == ' ' or state == 'D':
372             del_files = []
373             for file in pac.filenamelist + pac.filenamelist_unvers:
374                 filestate = pac.status(file)
375                 if filestate == 'M' or filestate == 'C' or \
376                    filestate == 'A' or filestate == '?':
377                     can_delete = False
378                 else:    
379                     del_files.append(file)
380             if can_delete or force:
381                 for file in del_files:
382                     pac.delete_localfile(file)
383                     if pac.status(file) != '?':
384                         pac.delete_storefile(file)
385                         # this is not really necessary
386                         pac.put_on_deletelist(file)
387                         print statfrmt('D', getTransActPath(os.path.join(pac.dir, file)))
388                 print statfrmt('D', getTransActPath(os.path.join(pac.dir, os.pardir, pac.name)))
389                 pac.write_deletelist()
390                 self.set_state(pac.name, 'D')
391                 self.write_packages()
392             else:
393                 print 'package \'%s\' has local modifications (see osc st for details)' % pac.name
394         elif state == 'A':
395             if force:
396                 delete_dir(pac.absdir)
397                 self.del_package_node(pac.name)
398                 self.write_packages()
399                 print statfrmt('D', pac.name)
400             else:
401                 print 'package \'%s\' has local modifications (see osc st for details)' % pac.name
402         elif state == None:
403             print 'package is not under version control'
404         else:
405             print 'unsupported state'
406
407     def update(self, pacs = (), expand_link=False, unexpand_link=False):
408         if len(pacs):
409             for pac in pacs:
410                 Package(os.path.join(self.dir, pac)).update()
411         else:
412             # we need to make sure that the _packages file will be written (even if an exception
413             # occurs)
414             try:
415                 # update complete project
416                 # packages which no longer exists upstream
417                 upstream_del = [ pac for pac in self.pacs_have if not pac in self.pacs_available and self.get_state(pac) != 'A']
418
419                 for pac in upstream_del:
420                     p = Package(os.path.join(self.dir, pac))
421                     self.delPackage(p, force = True)
422                     delete_storedir(p.storedir)
423                     try:
424                         os.rmdir(pac)
425                     except:
426                         pass
427                     self.pac_root.remove(self.get_package_node(p.name))
428                     self.pacs_have.remove(pac)
429
430                 for pac in self.pacs_have:
431                     state = self.get_state(pac)
432                     if pac in self.pacs_broken:
433                         if self.get_state(pac) != 'A':
434                             checkout_package(self.apiurl, self.name, pac,
435                                              pathname=getTransActPath(os.path.join(self.dir, pac)), prj_obj=self, prj_dir=self.dir)
436                     elif state == ' ':
437                         # do a simple update
438                         p = Package(os.path.join(self.dir, pac))
439                         rev = None
440                         if expand_link and p.islink() and not p.isexpanded():
441                             print 'Expanding to rev', p.linkinfo.xsrcmd5
442                             rev = p.linkinfo.xsrcmd5
443                         elif unexpand_link and p.islink() and p.isexpanded():
444                             print 'Unexpanding to rev', p.linkinfo.lsrcmd5
445                             rev = p.linkinfo.lsrcmd5
446                         elif p.islink() and p.isexpanded():
447                             rev = show_upstream_xsrcmd5(p.apiurl,
448                                                         p.prjname, p.name)
449                         print 'Updating %s' % p.name
450                         p.update(rev)
451                     elif state == 'D':
452                         # TODO: Package::update has to fixed to behave like svn does
453                         if pac in self.pacs_broken:
454                             checkout_package(self.apiurl, self.name, pac,
455                                              pathname=getTransActPath(os.path.join(self.dir, pac)), prj_obj=self, prj_dir=self.dir)
456                         else:
457                             Package(os.path.join(self.dir, pac)).update()
458                     elif state == 'A' and pac in self.pacs_available:
459                         # file/dir called pac already exists and is under version control
460                         msg = 'can\'t add package \'%s\': Object already exists' % pac
461                         raise oscerr.PackageExists(self.name, pac, msg)
462                     elif state == 'A':
463                         # do nothing
464                         pass
465                     else:
466                         print 'unexpected state.. package \'%s\'' % pac
467
468                 self.checkout_missing_pacs()
469             finally:
470                 self.write_packages()
471
472     def commit(self, pacs = (), msg = '', files = {}):
473         if len(pacs):
474             try:
475                 for pac in pacs:
476                     todo = []
477                     if files.has_key(pac):
478                         todo = files[pac]
479                     state = self.get_state(pac)
480                     if state == 'A':
481                         self.commitNewPackage(pac, msg, todo)
482                     elif state == 'D':
483                         self.commitDelPackage(pac)
484                     elif state == ' ':
485                         # display the correct dir when sending the changes
486                         if os.path.samefile(os.path.join(self.dir, pac), os.getcwd()):
487                             p = Package('.')
488                         else:
489                             p = Package(os.path.join(self.dir, pac))
490                         p.todo = todo
491                         p.commit(msg)
492                     elif pac in self.pacs_unvers and not is_package_dir(os.path.join(self.dir, pac)):
493                         print 'osc: \'%s\' is not under version control' % pac
494                     elif pac in self.pacs_broken:
495                         print 'osc: \'%s\' package not found' % pac
496                     elif state == None:
497                         self.commitExtPackage(pac, msg, todo)
498             finally:
499                 self.write_packages()
500         else:
501             # if we have packages marked as '!' we cannot commit
502             for pac in self.pacs_broken:
503                 if self.get_state(pac) != 'D':
504                     msg = 'commit failed: package \'%s\' is missing' % pac
505                     raise oscerr.PackageMissing(self.name, pac, msg)
506             try:
507                 for pac in self.pacs_have:
508                     state = self.get_state(pac)
509                     if state == ' ':
510                         # do a simple commit
511                         Package(os.path.join(self.dir, pac)).commit(msg)
512                     elif state == 'D':
513                         self.commitDelPackage(pac)
514                     elif state == 'A':
515                         self.commitNewPackage(pac, msg)
516             finally:
517                 self.write_packages()
518
519     def commitNewPackage(self, pac, msg = '', files = []):
520         """creates and commits a new package if it does not exist on the server"""
521         if pac in self.pacs_available:
522             print 'package \'%s\' already exists' % pac
523         else:
524             user = conf.get_apiurl_usr(self.apiurl)
525             edit_meta(metatype='pkg',
526                       path_args=(quote_plus(self.name), quote_plus(pac)),
527                       template_args=({
528                               'name': pac,
529                               'user': user}),
530                       apiurl=self.apiurl)
531             # display the correct dir when sending the changes
532             olddir = os.getcwd()
533             if os.path.samefile(os.path.join(self.dir, pac), os.curdir):
534                 os.chdir(os.pardir)
535                 p = Package(pac)
536             else:
537                 p = Package(os.path.join(self.dir, pac))
538             p.todo = files
539             print statfrmt('Sending', os.path.normpath(p.dir))
540             p.commit(msg)
541             self.set_state(pac, ' ')
542             os.chdir(olddir)
543
544     def commitDelPackage(self, pac):
545         """deletes a package on the server and in the working copy"""
546         try:
547             # display the correct dir when sending the changes
548             if os.path.samefile(os.path.join(self.dir, pac), os.curdir):
549                 pac_dir = pac
550             else:
551                 pac_dir = os.path.join(self.dir, pac)
552             p = Package(os.path.join(self.dir, pac))
553             #print statfrmt('Deleting', os.path.normpath(os.path.join(p.dir, os.pardir, pac)))
554             delete_storedir(p.storedir)
555             try:
556                 os.rmdir(p.dir)
557             except:
558                 pass
559         except OSError:
560             pac_dir = os.path.join(self.dir, pac)
561         #print statfrmt('Deleting', getTransActPath(os.path.join(self.dir, pac)))
562         print statfrmt('Deleting', getTransActPath(pac_dir))
563         delete_package(self.apiurl, self.name, pac)
564         self.del_package_node(pac)
565
566     def commitExtPackage(self, pac, msg, files = []):
567         """commits a package from an external project"""
568         if os.path.samefile(os.path.join(self.dir, pac), os.getcwd()):
569             pac_path = '.'
570         else:
571             pac_path = os.path.join(self.dir, pac)
572  
573         project = store_read_project(pac_path)
574         package = store_read_package(pac_path)
575         apiurl = store_read_apiurl(pac_path)
576         if meta_exists(metatype='pkg',
577                        path_args=(quote_plus(project), quote_plus(package)),
578                        template_args=None,
579                        create_new=False, apiurl=apiurl):
580             p = Package(pac_path)
581             p.todo = files
582             p.commit(msg)
583         else:
584             user = conf.get_apiurl_usr(self.apiurl)
585             edit_meta(metatype='pkg',
586                       path_args=(quote_plus(project), quote_plus(package)),
587                       template_args=({
588                               'name': pac,
589                               'user': user}),
590                               apiurl=apiurl)
591             p = Package(pac_path)
592             p.todo = files
593             p.commit(msg)
594
595     def __str__(self):
596         r = []
597         r.append('*****************************************************')
598         r.append('Project %s (dir=%s, absdir=%s)' % (self.name, self.dir, self.absdir))
599         r.append('have pacs:\n%s' % ', '.join(self.pacs_have))
600         r.append('missing pacs:\n%s' % ', '.join(self.pacs_missing))
601         r.append('*****************************************************')
602         return '\n'.join(r)
603
604
605
606 class Package:
607     """represent a package (its directory) and read/keep/write its metadata"""
608     def __init__(self, workingdir):
609         self.dir = workingdir
610         self.absdir = os.path.abspath(self.dir)
611         self.storedir = os.path.join(self.absdir, store)
612
613         check_store_version(self.dir)
614
615         self.prjname = store_read_project(self.dir)
616         self.name = store_read_package(self.dir)
617         self.apiurl = store_read_apiurl(self.dir)
618
619         self.update_datastructs()
620
621         self.todo = []
622         self.todo_send = []
623         self.todo_delete = []
624
625     def info(self):
626         source_url = makeurl(self.apiurl, ['source', self.prjname, self.name]) 
627         r = info_templ % (self.prjname, self.name, self.absdir, self.apiurl, source_url, self.srcmd5, self.rev, self.linkinfo)
628         return r
629
630     def addfile(self, n):
631         st = os.stat(os.path.join(self.dir, n))
632         f = File(n, None, st.st_size, st.st_mtime)
633         self.filelist.append(f)
634         self.filenamelist.append(n)
635         self.filenamelist_unvers.remove(n) 
636         shutil.copy2(os.path.join(self.dir, n), os.path.join(self.storedir, n))
637         
638     def delete_storefile(self, n):
639         try: os.unlink(os.path.join(self.storedir, n))
640         except: pass
641
642     def delete_localfile(self, n):
643         try: os.unlink(os.path.join(self.dir, n))
644         except: pass
645
646     def put_on_deletelist(self, n):
647         if n not in self.to_be_deleted:
648             self.to_be_deleted.append(n)
649
650     def put_on_conflictlist(self, n):
651         if n not in self.in_conflict:
652             self.in_conflict.append(n)
653
654     def clear_from_conflictlist(self, n):
655         """delete an entry from the file, and remove the file if it would be empty"""
656         if n in self.in_conflict:
657
658             filename = os.path.join(self.dir, n)
659             storefilename = os.path.join(self.storedir, n)
660             myfilename = os.path.join(self.dir, n + '.mine')
661             upfilename = os.path.join(self.dir, n + '.r' + self.rev)
662
663             try:
664                 os.unlink(myfilename)
665                 # the working copy may be updated, so the .r* ending may be obsolete...
666                 # then we don't care
667                 os.unlink(upfilename)
668             except: 
669                 pass
670
671             self.in_conflict.remove(n)
672
673             self.write_conflictlist()
674
675     def write_deletelist(self):
676         if len(self.to_be_deleted) == 0:
677             try:
678                 os.unlink(os.path.join(self.storedir, '_to_be_deleted'))
679             except:
680                 pass
681         else:
682             fname = os.path.join(self.storedir, '_to_be_deleted')
683             f = open(fname, 'w')
684             f.write('\n'.join(self.to_be_deleted))
685             f.write('\n')
686             f.close()
687
688     def delete_source_file(self, n):
689         """delete local a source file"""
690         self.delete_localfile(n)
691         self.delete_storefile(n)
692
693     def delete_remote_source_file(self, n):
694         """delete a remote source file (e.g. from the server)"""
695         query = 'rev=upload'
696         u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
697         http_DELETE(u)
698  
699     def put_source_file(self, n):
700         
701         # escaping '+' in the URL path (note: not in the URL query string) is 
702         # only a workaround for ruby on rails, which swallows it otherwise
703         query = 'rev=upload'
704         u = makeurl(self.apiurl, ['source', self.prjname, self.name, pathname2url(n)], query=query)
705         http_PUT(u, file = os.path.join(self.dir, n))
706
707         shutil.copy2(os.path.join(self.dir, n), os.path.join(self.storedir, n))
708
709     def commit(self, msg=''):
710         # commit only if the upstream revision is the same as the working copy's
711         if self.islink() and self.isexpanded():
712             upstream_rev = show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name)
713         else:
714             upstream_rev = show_upstream_rev(self.apiurl, self.prjname, self.name)
715         if self.rev != upstream_rev:
716             raise oscerr.WorkingCopyOutdated((self.absdir, self.rev, upstream_rev))
717
718         if not self.todo:
719             self.todo = self.filenamelist_unvers + self.filenamelist
720       
721         pathn = getTransActPath(self.dir)
722
723         for filename in self.todo:
724             st = self.status(filename)
725             if st == 'A' or st == 'M':
726                 self.todo_send.append(filename)
727                 print statfrmt('Sending', os.path.join(pathn, filename))
728             elif st == 'D':
729                 self.todo_delete.append(filename)
730                 print statfrmt('Deleting',  os.path.join(pathn, filename))
731
732         if not self.todo_send and not self.todo_delete:
733             print 'nothing to do for package %s' % self.name
734             return 1
735
736         if self.islink() and self.isexpanded():
737             # resolve the link into the upload revision
738             u = makeurl(self.apiurl, ['source', self.prjname, self.name], 
739                         query='cmd=copy&rev=upload&expand=1')
740             f = http_POST(u)
741
742         print 'Transmitting file data ', 
743         for filename in self.todo_delete:
744             # do not touch local files on commit --
745             # delete remotely instead
746             self.delete_remote_source_file(filename)
747             self.to_be_deleted.remove(filename)
748         for filename in self.todo_send:
749             sys.stdout.write('.')
750             sys.stdout.flush()
751             self.put_source_file(filename)
752
753         # all source files are committed - now comes the log
754         query = { 'cmd'    : 'commit',
755                   'rev'    : 'upload',
756                   'user'   : conf.get_apiurl_usr(self.apiurl),
757                   'comment': msg }
758         if self.islink() and self.isexpanded():
759             query['keeplink'] = '1'
760         u = makeurl(self.apiurl, ['source', self.prjname, self.name], query=query)
761
762         f = http_POST(u)
763         root = ET.parse(f).getroot()
764         self.rev = int(root.get('rev'))
765         print
766         print 'Committed revision %s.' % self.rev
767
768         if self.islink() and self.isexpanded():
769             self.update_local_filesmeta(revision=show_upstream_xsrcmd5(self.apiurl, self.prjname, self.name))
770         else:
771             self.update_local_filesmeta()
772         self.update_datastructs()
773         self.write_deletelist()
774
775     def write_conflictlist(self):
776         if len(self.in_conflict) == 0:
777             os.unlink(os.path.join(self.storedir, '_in_conflict'))
778         else:
779             fname = os.path.join(self.storedir, '_in_conflict')
780             f = open(fname, 'w')
781             f.write('\n'.join(self.in_conflict))
782             f.write('\n')
783             f.close()
784
785     def updatefile(self, n, revision):
786         filename = os.path.join(self.dir, n)
787         storefilename = os.path.join(self.storedir, n)
788         mtime = self.findfilebyname(n).mtime
789
790         get_source_file(self.apiurl, self.prjname, self.name, n, targetfilename=filename, revision=revision)
791         os.utime(filename, (-1, mtime))
792
793         shutil.copy2(filename, storefilename)
794
795     def mergefile(self, n):
796         filename = os.path.join(self.dir, n)
797         storefilename = os.path.join(self.storedir, n)
798         myfilename = os.path.join(self.dir, n + '.mine')
799         upfilename = os.path.join(self.dir, n + '.r' + self.rev)
800         os.rename(filename, myfilename)
801
802         mtime = self.findfilebyname(n).mtime
803         get_source_file(self.apiurl, self.prjname, self.name, n, 
804                         revision=self.rev, targetfilename=upfilename)
805         os.utime(upfilename, (-1, mtime))
806
807         if binary_file(myfilename) or binary_file(upfilename):
808                 # don't try merging
809                 shutil.copy2(upfilename, filename)
810                 shutil.copy2(upfilename, storefilename)
811                 self.in_conflict.append(n)
812                 self.write_conflictlist()
813                 return 'C'
814         else:
815             # try merging
816             # diff3 OPTIONS... MINE OLDER YOURS
817             merge_cmd = 'diff3 -m -E %s %s %s > %s' % (myfilename, storefilename, upfilename, filename)
818             # we would rather use the subprocess module, but it is not availablebefore 2.4
819             ret = subprocess.call(merge_cmd, shell=True) / 256
820             
821             #   "An exit status of 0 means `diff3' was successful, 1 means some
822             #   conflicts were found, and 2 means trouble."
823             if ret == 0:
824                 # merge was successful... clean up
825                 shutil.copy2(upfilename, storefilename)
826                 os.unlink(upfilename)
827                 os.unlink(myfilename)
828                 return 'G'
829             elif ret == 1:
830                 # unsuccessful merge
831                 shutil.copy2(upfilename, storefilename)
832                 self.in_conflict.append(n)
833                 self.write_conflictlist()
834                 return 'C'
835             else:
836                 print >>sys.stderr, '\ndiff3 got in trouble... exit code:', ret
837                 print >>sys.stderr, 'the command line was:'
838                 print >>sys.stderr, merge_cmd
839                 sys.exit(1)
840
841
842
843     def update_local_filesmeta(self, revision=None):
844         """
845         Update the local _files file in the store.
846         It is replaced with the version pulled from upstream.
847         """
848         meta = ''.join(show_files_meta(self.apiurl, self.prjname, self.name, revision))
849         f = open(os.path.join(self.storedir, '_files'), 'w')
850         f.write(meta)
851         f.close()
852
853     def update_datastructs(self):
854         """
855         Update the internal data structures if the local _files
856         file has changed (e.g. update_local_filesmeta() has been
857         called).
858         """
859         import fnmatch
860         files_tree = read_filemeta(self.dir)
861         files_tree_root = files_tree.getroot()
862
863         self.rev = files_tree_root.get('rev')
864         self.srcmd5 = files_tree_root.get('srcmd5')
865
866         self.linkinfo = Linkinfo()
867         self.linkinfo.read(files_tree_root.find('linkinfo'))
868
869         self.filenamelist = []
870         self.filelist = []
871         for node in files_tree_root.findall('entry'):
872             try: 
873                 f = File(node.get('name'), 
874                          node.get('md5'), 
875                          int(node.get('size')), 
876                          int(node.get('mtime')))
877             except: 
878                 # okay, a very old version of _files, which didn't contain any metadata yet... 
879                 f = File(node.get('name'), '', 0, 0)
880             self.filelist.append(f)
881             self.filenamelist.append(f.name)
882
883         self.to_be_deleted = read_tobedeleted(self.dir)
884         self.in_conflict = read_inconflict(self.dir)
885
886         # gather unversioned files, but ignore some stuff
887         self.excluded = [ i for i in os.listdir(self.dir) 
888                           for j in exclude_stuff 
889                           if fnmatch.fnmatch(i, j) ]
890         self.filenamelist_unvers = [ i for i in os.listdir(self.dir)
891                                      if i not in self.excluded
892                                      if i not in self.filenamelist ]
893
894     def islink(self):
895         """tells us if the package is a link (has 'linkinfo').  
896         A package with linkinfo is a package which links to another package.
897         Returns True if the package is a link, otherwise False."""
898         return self.linkinfo.islink()
899
900     def isexpanded(self):
901         """tells us if the package is a link which is expanded.  
902         Returns True if the package is expanded, otherwise False."""
903         return self.linkinfo.isexpanded()
904
905     def update_local_pacmeta(self):
906         """
907         Update the local _meta file in the store.
908         It is replaced with the version pulled from upstream.
909         """
910         meta = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
911         f = open(os.path.join(self.storedir, '_meta'), 'w')
912         f.write(meta)
913         f.close()
914
915     def findfilebyname(self, n):
916         for i in self.filelist:
917             if i.name == n:
918                 return i
919
920     def status(self, n):
921         """
922         status can be:
923
924          file  storefile  file present  STATUS
925         exists  exists      in _files
926
927           x       x            -        'A'
928           x       x            x        ' ' if digest differs: 'M'
929                                             and if in conflicts file: 'C'
930           x       -            -        '?'
931           x       -            x        'D' and listed in _to_be_deleted
932           -       x            x        '!'
933           -       x            -        'D' (when file in working copy is already deleted)
934           -       -            x        'F' (new in repo, but not yet in working copy)
935           -       -            -        NOT DEFINED
936
937         """
938
939         known_by_meta = False
940         exists = False
941         exists_in_store = False
942         if n in self.filenamelist:
943             known_by_meta = True
944         if os.path.exists(os.path.join(self.absdir, n)):
945             exists = True
946         if os.path.exists(os.path.join(self.storedir, n)):
947             exists_in_store = True
948
949
950         if exists and not exists_in_store and known_by_meta:
951             state = 'D'
952         elif n in self.to_be_deleted:
953             state = 'D'
954         elif n in self.in_conflict:
955             state = 'C'
956         elif exists and exists_in_store and known_by_meta:
957             #print self.findfilebyname(n)
958             if dgst(os.path.join(self.absdir, n)) != self.findfilebyname(n).md5:
959                 state = 'M'
960             else:
961                 state = ' '
962         elif exists and not exists_in_store and not known_by_meta:
963             state = '?'
964         elif exists and exists_in_store and not known_by_meta:
965             state = 'A'
966         elif not exists and exists_in_store and known_by_meta:
967             state = '!'
968         elif not exists and not exists_in_store and known_by_meta:
969             state = 'F'
970         elif not exists and exists_in_store and not known_by_meta:
971             state = 'D'
972         elif not exists and not exists_in_store and not known_by_meta:
973             # this case shouldn't happen (except there was a typo in the filename etc.)
974             raise IOError('osc: \'%s\' is not under version control' % n)
975         
976         return state
977
978     def comparePac(self, cmp_pac):
979         """
980         This method compares the local filelist with
981         the filelist of the passed package to see which files
982         were added, removed and changed.
983         """
984         
985         changed_files = []
986         added_files = []
987         removed_files = []
988
989         for file in self.filenamelist+self.filenamelist_unvers:
990             state = self.status(file)
991             if state == 'A' and (not file in cmp_pac.filenamelist):
992                 added_files.append(file)
993             elif file in cmp_pac.filenamelist and state == 'D':
994                 removed_files.append(file)
995             elif state == ' ' and not file in cmp_pac.filenamelist:
996                 added_files.append(file)
997             elif file in cmp_pac.filenamelist and state != 'A' and state != '?':
998                 if dgst(os.path.join(self.absdir, file)) != cmp_pac.findfilebyname(file).md5:
999                     changed_files.append(file)
1000         for file in cmp_pac.filenamelist:
1001             if not file in self.filenamelist:
1002                 removed_files.append(file)
1003         removed_files = set(removed_files)
1004
1005         return changed_files, added_files, removed_files
1006
1007     def merge(self, otherpac):
1008         self.todo += otherpac.todo
1009
1010     def __str__(self):
1011         r = """
1012 name: %s
1013 prjname: %s
1014 workingdir: %s
1015 localfilelist: %s
1016 linkinfo: %s
1017 rev: %s
1018 'todo' files: %s
1019 """ % (self.name, 
1020         self.prjname, 
1021         self.dir, 
1022         '\n               '.join(self.filenamelist), 
1023         self.linkinfo, 
1024         self.rev, 
1025         self.todo)
1026
1027         return r
1028
1029
1030     def read_meta_from_spec(self, spec = None):
1031         import glob
1032         if spec:
1033             specfile = spec
1034         else:
1035             # scan for spec files
1036             speclist = glob.glob(os.path.join(self.dir, '*.spec'))
1037             if len(speclist) == 1:
1038                 specfile = speclist[0]
1039             elif len(speclist) > 1:
1040                 print 'the following specfiles were found:'
1041                 for file in speclist:
1042                     print file
1043                 print 'please specify one with --specfile'
1044                 sys.exit(1)
1045             else:
1046                 print 'no specfile was found - please specify one ' \
1047                       'with --specfile'
1048                 sys.exit(1)     
1049
1050         data = read_meta_from_spec(specfile, 'Summary', 'Url', '%description')
1051         self.summary = data['Summary']
1052         self.url = data['Url']
1053         self.descr = data['%description']
1054
1055
1056     def update_package_meta(self):
1057         """
1058         for the updatepacmetafromspec subcommand
1059         """
1060
1061         import tempfile
1062         (fd, filename) = tempfile.mkstemp(prefix = 'osc_editmeta.', suffix = '.xml', dir = '/tmp')
1063
1064         m = ''.join(show_package_meta(self.apiurl, self.prjname, self.name))
1065
1066         f = os.fdopen(fd, 'w')
1067         f.write(m)
1068         f.close()
1069
1070         tree = ET.parse(filename)
1071         tree.find('title').text = self.summary
1072         tree.find('description').text = ''.join(self.descr)
1073         url = tree.find('url')
1074         if url == None:
1075             url = ET.SubElement(tree.getroot(), 'url')
1076         url.text = self.url
1077         tree.write(filename)
1078
1079         print '*' * 36, 'old', '*' * 36
1080         print m
1081         print '*' * 36, 'new', '*' * 36
1082         tree.write(sys.stdout)
1083         print '*' * 72
1084
1085         # FIXME: for testing...
1086         # open the new description in $EDITOR instead?
1087         repl = raw_input('Write? (y/N) ')
1088         if repl == 'y':
1089             print 'Sending meta data...', 
1090             u = makeurl(self.apiurl, ['source', self.prjname, self.name, '_meta'])
1091             http_PUT(u, file=filename)
1092             print 'Done.'
1093         else:
1094             print 'discarding', filename
1095
1096         os.unlink(filename)
1097
1098     def update(self, rev = None):
1099         # save filelist and (modified) status before replacing the meta file
1100         saved_filenames = self.filenamelist
1101         saved_modifiedfiles = [ f for f in self.filenamelist if self.status(f) == 'M' ]
1102
1103         oldp = self
1104         self.update_local_filesmeta(rev)
1105         self = Package(self.dir)
1106
1107         # which files do no longer exist upstream?
1108         disappeared = [ f for f in saved_filenames if f not in self.filenamelist ]
1109
1110         pathn = getTransActPath(self.dir)
1111
1112         for filename in saved_filenames:
1113             if filename in disappeared:
1114                 print statfrmt('D', os.path.join(pathn, filename))
1115                 # keep file if it has local modifications
1116                 if oldp.status(filename) == ' ':
1117                     self.delete_localfile(filename)
1118                 self.delete_storefile(filename)
1119                 continue
1120
1121         for filename in self.filenamelist:
1122
1123             state = self.status(filename)
1124             if state == 'M' and self.findfilebyname(filename).md5 == oldp.findfilebyname(filename).md5:
1125                 # no merge necessary... local file is changed, but upstream isn't
1126                 pass
1127             elif state == 'M' and filename in saved_modifiedfiles:
1128                 status_after_merge = self.mergefile(filename)
1129                 print statfrmt(status_after_merge, os.path.join(pathn, filename))
1130             elif state == 'M':
1131                 self.updatefile(filename, rev)
1132                 print statfrmt('U', os.path.join(pathn, filename))
1133             elif state == '!':
1134                 self.updatefile(filename, rev)
1135                 print 'Restored \'%s\'' % os.path.join(pathn, filename)
1136             elif state == 'F':
1137                 self.updatefile(filename, rev)
1138                 print statfrmt('A', os.path.join(pathn, filename))
1139             elif state == 'D' and self.findfilebyname(filename).md5 != oldp.findfilebyname(filename).md5:
1140                 self.updatefile(filename, rev)
1141                 self.delete_storefile(filename)
1142                 print statfrmt('U', os.path.join(pathn, filename))
1143             elif state == ' ':
1144                 pass
1145
1146
1147         self.update_local_pacmeta()
1148
1149         #print ljust(p.name, 45), 'At revision %s.' % p.rev
1150         print 'At revision %s.' % self.rev
1151  
1152
1153 class RequestState:
1154     """for objects to represent the "state" of a request"""
1155     def __init__(self, name=None, who=None, when=None, comment=None):
1156         self.name = name
1157         self.who  = who
1158         self.when = when
1159         self.comment = comment
1160
1161
1162 class SubmitReq:
1163     """represent a submit request and holds its metadata
1164        it has methods to read in metadata from xml,
1165        different views, ..."""
1166     def __init__(self):
1167         self.reqid       = None
1168         self.state       = RequestState()
1169         self.who         = None
1170         self.when        = None
1171         self.last_author = None
1172         self.src_project = None
1173         self.src_package = None
1174         self.src_md5     = None
1175         self.dst_project = None
1176         self.dst_package = None
1177         self.descr       = None
1178         self.statehistory = []
1179
1180     def read(self, root):
1181         self.reqid = int(root.get('id'))
1182
1183         n = root.find('submit').find('source')
1184         self.src_project = n.get('project')
1185         self.src_package = n.get('package')
1186         try: self.src_md5 = n.get('rev')
1187         except: pass
1188
1189         n = root.find('submit').find('target')
1190         self.dst_project = n.get('project')
1191         self.dst_package = n.get('package')
1192
1193         # read the state
1194         n = root.find('state')
1195         self.state.name, self.state.who, self.state.when \
1196                 = n.get('name'), n.get('who'), n.get('when')
1197         try:
1198             self.state.comment = n.find('comment').text.strip()
1199         except:
1200             self.state.comment = None
1201
1202         # read the state history
1203         for h in root.findall('history'):
1204             s = RequestState()
1205             s.name = h.get('name')
1206             s.who  = h.get('who')
1207             s.when = h.get('when')
1208             try:
1209                 s.comment = h.find('comment').text.strip()
1210             except:
1211                 s.comment = None
1212             self.statehistory.append(s)
1213         self.statehistory.reverse()
1214
1215         # read a description, if it exists
1216         try:
1217             n = root.find('description').text
1218             self.descr = n
1219         except:
1220             pass
1221
1222
1223     def list_view(self):
1224         dst = "%s/%s" % (self.dst_project, self.dst_package)
1225         if self.src_package == self.dst_package:
1226             dst = self.dst_project
1227
1228         desc = ""
1229         if self.descr:
1230             desc = "\n                 %s" % (repr(self.descr))
1231
1232         return '%6d  %-7s %-12s %-50s  ->  %-20s   %s' % \
1233             (self.reqid, 
1234              self.state.name, "(%s)" % self.state.who,
1235              "%s/%s" % (self.src_project, self.src_package),
1236              dst, desc)
1237
1238     def __cmp__(self, other):
1239         return cmp(self.reqid, other.reqid)
1240
1241     def __str__(self):
1242         s = """\
1243 Request to submit (sri%d): 
1244
1245     %s/%s  ->  %s/%s
1246
1247 Source revision MD5:
1248     %s
1249
1250 Message:
1251     %s
1252
1253 State:   %-10s   %s %s
1254 Comment: %s
1255 """            % (self.reqid,
1256                self.src_project, 
1257                self.src_package, 
1258                self.dst_project, 
1259                self.dst_package, 
1260                self.src_md5 or 'not given',
1261                self.descr,
1262                self.state.name, 
1263                self.state.when, self.state.who, self.state.comment)
1264
1265         if len(self.statehistory):
1266             histitems = [ '%-10s   %s %s' \
1267                     % (i.name, i.when, i.who) \
1268                     for i in self.statehistory ]
1269             s += 'History: ' + '\n         '.join(histitems)
1270
1271         s += '\n'
1272         return s
1273
1274
1275 def shorttime(t):
1276     """format time as Apr 02 18:19
1277     or                Apr 02  2005
1278     depending on whether it is in the current year
1279     """
1280     import time
1281
1282     if time.localtime()[0] == time.localtime(t)[0]:
1283         # same year
1284         return time.strftime('%b %d %H:%M',time.localtime(t))
1285     else:
1286         return time.strftime('%b %d  %Y',time.localtime(t))
1287
1288
1289 def is_project_dir(d):
1290     return os.path.exists(os.path.join(d, store, '_project')) and not \
1291            os.path.exists(os.path.join(d, store, '_package'))
1292
1293         
1294 def is_package_dir(d):
1295     return os.path.exists(os.path.join(d, store, '_project')) and \
1296            os.path.exists(os.path.join(d, store, '_package'))
1297
1298         
1299 def slash_split(l):
1300     """Split command line arguments like 'foo/bar' into 'foo' 'bar'.
1301     This is handy to allow copy/paste a project/package combination in this form.
1302
1303     Trailing slashes are removed before the split, because the split would 
1304     otherwise give an additional empty string.
1305     """
1306     r = []
1307     for i in l:
1308         i = i.rstrip('/')
1309         r += i.split('/')
1310     return r
1311
1312
1313 def findpacs(files):
1314     """collect Package objects belonging to the given files
1315     and make sure each Package is returned only once"""
1316     pacs = []
1317     for f in files:
1318         p = filedir_to_pac(f)
1319         known = None
1320         for i in pacs:
1321             if i.name == p.name:
1322                 known = i
1323                 break
1324         if known:
1325             i.merge(p)
1326         else:
1327             pacs.append(p)
1328     return pacs
1329         
1330
1331 def read_filemeta(dir):
1332     try:
1333         r = ET.parse(os.path.join(dir, store, '_files'))
1334     except SyntaxError, e:
1335         raise oscerr.NoWorkingCopy('\'%s\' is not a valid working copy.\n'
1336                                    'When parsing .osc/_files, the following error was encountered:\n'
1337                                    '%s' % (dir, e))
1338     return r
1339
1340
1341 def read_tobedeleted(dir):
1342     r = []
1343     fname = os.path.join(dir, store, '_to_be_deleted')
1344
1345     if os.path.exists(fname):
1346         r = [ line.strip() for line in open(fname) ]
1347
1348     return r
1349
1350
1351 def read_inconflict(dir):
1352     r = []
1353     fname = os.path.join(dir, store, '_in_conflict')
1354
1355     if os.path.exists(fname):
1356         r = [ line.strip() for line in open(fname) ]
1357
1358     return r
1359
1360
1361 def parseargs(list_of_args):
1362     """Convenience method osc's commandline argument parsing.
1363     
1364     If called with an empty tuple (or list), return a list containing the current directory.
1365     Otherwise, return a list of the arguments."""
1366     if list_of_args:
1367         return list(list_of_args)
1368     else:
1369         return [os.curdir]
1370
1371
1372 def filedir_to_pac(f):
1373     """Takes a working copy path, or a path to a file inside a working copy, 
1374     and returns a Package object instance
1375     
1376     If the argument was a filename, add it onto the "todo" list of the Package """
1377
1378     if os.path.isdir(f):
1379         wd = f
1380         p = Package(wd)
1381
1382     else:
1383         wd = os.path.dirname(f)
1384         if wd == '':
1385             wd = os.curdir
1386         p = Package(wd)
1387         p.todo = [ os.path.basename(f) ]
1388
1389     return p
1390
1391
1392 def statfrmt(statusletter, filename):
1393     return '%s    %s' % (statusletter, filename)
1394
1395
1396 def pathjoin(a, *p):
1397     """Join two or more pathname components, inserting '/' as needed. Cut leading ./"""
1398     path = os.path.join(a, *p)
1399     if path.startswith('./'):
1400         path = path[2:]
1401     return path
1402
1403
1404 def makeurl(baseurl, l, query=[]):
1405     """Given a list of path compoments, construct a complete URL.
1406
1407     Optional parameters for a query string can be given as a list, as a
1408     dictionary, or as an already assembled string.
1409     In case of a dictionary, the parameters will be urlencoded by this
1410     function. In case of a list not -- this is to be backwards compatible.
1411     """
1412
1413     #print 'makeurl:', baseurl, l, query
1414
1415     if type(query) == type(list()):
1416         query = '&'.join(query)
1417     elif type(query) == type(dict()):
1418         query = urlencode(query)
1419
1420     scheme, netloc = urlsplit(baseurl)[0:2]
1421     return urlunsplit((scheme, netloc, '/'.join(l), query, ''))               
1422
1423
1424 def http_request(method, url, headers={}, data=None, file=None):
1425     """wrapper around urllib2.urlopen for error handling,
1426     and to support additional (PUT, DELETE) methods"""
1427
1428     filefd = None
1429
1430     if conf.config['http_debug']:
1431         print 
1432         print
1433         print '--', method, url
1434
1435     if method == 'POST' and not file and not data:
1436         # adding data to an urllib2 request transforms it into a POST
1437         data = ''
1438         
1439     req = urllib2.Request(url)
1440
1441     api_host_options=conf.get_apiurl_api_host_options(url)
1442
1443     for header, value in api_host_options['http_headers']:
1444         req.add_header(header, value)
1445
1446     req.get_method = lambda: method
1447
1448     # POST requests are application/x-www-form-urlencoded per default
1449     # since we change the request into PUT, we also need to adjust the content type header
1450     if method == 'PUT':
1451         req.add_header('Content-Type', 'application/octet-stream')
1452
1453     if type(headers) == type({}):
1454         for i in headers.keys():
1455             print headers[i]
1456             req.add_header(i, headers[i])
1457
1458     if file and not data:
1459         size = os.path.getsize(file)
1460         if size < 1024*512:
1461             data = open(file).read()
1462         else:
1463             import mmap
1464             filefd = open(file, 'r')
1465             try:
1466                 data = mmap.mmap(filefd.fileno(), os.path.getsize(file),
1467                                 mmap.MAP_SHARED, mmap.PROT_READ)
1468             except EnvironmentError, e:
1469                 if e.errno == 19:
1470                     sys.exit('\n\n%s\nThe file \'%s\' could not be memory mapped. It is ' \
1471                              '\non a filesystem which does not support this.' % (e, file))
1472                 else:
1473                     raise
1474
1475     if conf.config['debug']: print method, url
1476
1477     try:
1478         fd = urllib2.urlopen(req, data=data)
1479     finally:
1480         if hasattr(conf.cookiejar, 'save'):
1481             conf.cookiejar.save(ignore_discard=True)
1482
1483     if filefd: filefd.close()
1484
1485     return fd
1486
1487
1488 def http_GET(*args, **kwargs):    return http_request('GET', *args, **kwargs)
1489 def http_POST(*args, **kwargs):   return http_request('POST', *args, **kwargs)
1490 def http_PUT(*args, **kwargs):    return http_request('PUT', *args, **kwargs)
1491 def http_DELETE(*args, **kwargs): return http_request('DELETE', *args, **kwargs)
1492
1493
1494 def init_project_dir(apiurl, dir, project):
1495     if not os.path.exists(dir):
1496         os.mkdir(dir)
1497     if not os.path.exists(os.path.join(dir, store)):
1498         os.mkdir(os.path.join(dir, store))
1499
1500     store_write_project(dir, project)
1501     store_write_apiurl(dir, apiurl)
1502     if conf.config['do_package_tracking']:
1503         store_write_initial_packages(dir, project, [])
1504
1505 def init_package_dir(apiurl, project, package, dir, revision=None, files=True):
1506     if not os.path.isdir(store):
1507         os.mkdir(store)
1508     os.chdir(store)
1509     f = open('_project', 'w')
1510     f.write(project + '\n')
1511     f.close
1512     f = open('_package', 'w')
1513     f.write(package + '\n')
1514     f.close
1515
1516     if files:
1517         f = open('_files', 'w')
1518         f.write(''.join(show_files_meta(apiurl, project, package, revision)))
1519         f.close()
1520     else:
1521         # create dummy
1522         ET.ElementTree(element=ET.Element('directory')).write('_files')
1523
1524     f = open('_osclib_version', 'w')
1525     f.write(__store_version__ + '\n')
1526     f.close()
1527
1528     store_write_apiurl(os.path.pardir, apiurl)
1529
1530     os.chdir(os.pardir)
1531     return
1532
1533
1534 def check_store_version(dir):
1535     versionfile = os.path.join(dir, store, '_osclib_version')
1536     try:
1537         v = open(versionfile).read().strip()
1538     except:
1539         v = ''
1540
1541     if v == '':
1542         msg = 'Error: "%s" is not an osc working copy.' % os.path.abspath(dir)
1543         raise oscerr.NoWorkingCopy(msg)
1544
1545     if v != __store_version__:
1546         if v in ['0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '0.95', '0.96', '0.97', '0.98', '0.99']:
1547             # version is fine, no migration needed
1548             f = open(versionfile, 'w')
1549             f.write(__store_version__ + '\n')
1550             f.close()
1551             return 
1552         msg = 'The osc metadata of your working copy "%s"' % dir
1553         msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__)
1554         msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.'
1555         raise oscerr.WorkingCopyWrongVersion, msg
1556     
1557
1558 def meta_get_packagelist(apiurl, prj):
1559
1560     u = makeurl(apiurl, ['source', prj])
1561     f = http_GET(u)
1562     root = ET.parse(f).getroot()
1563     return [ node.get('name') for node in root.findall('entry') ]
1564
1565
1566 def meta_get_filelist(apiurl, prj, package, verbose=False):
1567     """return a list of file names,
1568     or a list File() instances if verbose=True"""
1569
1570     u = makeurl(apiurl, ['source', prj, package])
1571     f = http_GET(u)
1572     root = ET.parse(f).getroot()
1573
1574     if not verbose:
1575         return [ node.get('name') for node in root.findall('entry') ]
1576
1577     else:
1578         l = []
1579         rev = int(root.get('rev'))
1580         for node in root.findall('entry'):
1581             f = File(node.get('name'), 
1582                      node.get('md5'), 
1583                      int(node.get('size')), 
1584                      int(node.get('mtime')))
1585             f.rev = rev
1586             l.append(f)
1587         return l
1588
1589
1590 def meta_get_project_list(apiurl):
1591     u = makeurl(apiurl, ['source'])
1592     f = http_GET(u)
1593     root = ET.parse(f).getroot()
1594     return sorted([ node.get('name') for node in root ])
1595
1596
1597 def show_project_meta(apiurl, prj):
1598     url = makeurl(apiurl, ['source', prj, '_meta'])
1599     f = http_GET(url)
1600     return f.readlines()
1601
1602
1603 def show_project_conf(apiurl, prj):
1604     url = makeurl(apiurl, ['source', prj, '_config'])
1605     f = http_GET(url)
1606     return f.readlines()
1607
1608
1609 def show_package_meta(apiurl, prj, pac):
1610     url = makeurl(apiurl, ['source', prj, pac, '_meta'])
1611     try:
1612         f = http_GET(url)
1613         return f.readlines()
1614     except urllib2.HTTPError, e:
1615         e.osc_msg = 'Error getting meta for project \'%s\' package \'%s\'' % (prj, pac)
1616         raise
1617
1618
1619 def show_develproject(apiurl, prj, pac):
1620     m = show_package_meta(apiurl, prj, pac)
1621     try:
1622         return ET.parse(StringIO(''.join(m))).getroot().find('devel').get('project')
1623     except:
1624         return None
1625
1626
1627 def show_pattern_metalist(apiurl, prj):
1628     url = makeurl(apiurl, ['source', prj, '_pattern'])
1629     try:
1630         f = http_GET(url)
1631         tree = ET.parse(f)
1632     except urllib2.HTTPError, e:
1633         e.osc_msg = 'show_pattern_metalist: Error getting pattern list for project \'%s\'' % prj
1634         raise
1635     r = [ node.get('name') for node in tree.getroot() ]
1636     r.sort()
1637     return r
1638
1639
1640 def show_pattern_meta(apiurl, prj, pattern):
1641     url = makeurl(apiurl, ['source', prj, '_pattern', pattern])
1642     try:
1643         f = http_GET(url)
1644         return f.readlines()
1645     except urllib2.HTTPError, e:
1646         e.osc_msg = 'show_pattern_meta: Error getting pattern \'%s\' for project \'%s\'' % (pattern, prj)
1647         raise
1648
1649
1650 class metafile:
1651     """metafile that can be manipulated and is stored back after manipulation."""
1652     def __init__(self, url, input, change_is_required=False, file_ext='.xml'):
1653         import tempfile
1654
1655         self.url = url
1656         self.change_is_required = change_is_required
1657
1658         (fd, self.filename) = tempfile.mkstemp(prefix = 'osc_metafile.', suffix = file_ext, dir = '/tmp')
1659
1660         f = os.fdopen(fd, 'w')
1661         f.write(''.join(input))
1662         f.close()
1663
1664         self.hash_orig = dgst(self.filename)
1665
1666     def sync(self):
1667         hash = dgst(self.filename)
1668         if self.change_is_required == True and hash == self.hash_orig:
1669             print 'File unchanged. Not saving.'
1670             os.unlink(self.filename)
1671             return True
1672
1673         print 'Sending meta data...'
1674         # don't do any exception handling... it's up to the caller what to do in case
1675         # of an exception
1676         http_PUT(self.url, file=self.filename)
1677         os.unlink(self.filename)
1678         print 'Done.'
1679         return True
1680     
1681
1682 # different types of metadata
1683 metatypes = { 'prj':     { 'path': 'source/%s/_meta',
1684                            'template': new_project_templ,
1685                            'file_ext': '.xml'
1686                          },
1687               'pkg':     { 'path'     : 'source/%s/%s/_meta',
1688                            'template': new_package_templ,
1689                            'file_ext': '.xml'
1690                          },
1691               'prjconf': { 'path': 'source/%s/_config',
1692                            'template': '',
1693                            'file_ext': '.txt'
1694                          },
1695               'user':    { 'path': 'person/%s',
1696                            'template': new_user_template,
1697                            'file_ext': '.xml'
1698                          },
1699               'pattern': { 'path': 'source/%s/_pattern/%s',
1700                            'template': new_pattern_template,
1701                            'file_ext': '.xml'
1702                          },
1703             }
1704
1705 def meta_exists(metatype,
1706                 path_args=None,
1707                 template_args=None,
1708                 create_new=True,
1709                 apiurl=None):
1710
1711     if not apiurl:
1712         apiurl = conf.config['apiurl']
1713     url = make_meta_url(metatype, path_args, apiurl)
1714     try:
1715         data = http_GET(url).readlines()
1716     except urllib2.HTTPError, e:
1717         if e.code == 404 and create_new:
1718             data = metatypes[metatype]['template']
1719             if template_args:
1720                 data = data % template_args
1721         else:
1722             raise e
1723     return data
1724
1725 def make_meta_url(metatype, path_args=None, apiurl=None):
1726     if not apiurl:
1727         apiurl = conf.config['apiurl']
1728     if metatype not in metatypes.keys():
1729         raise AttributeError('make_meta_url(): Unknown meta type \'%s\'' % metatype)
1730     path = metatypes[metatype]['path']
1731
1732     if path_args:
1733         path = path % path_args
1734
1735     return makeurl(apiurl, [path])
1736
1737
1738 def edit_meta(metatype, 
1739               path_args=None, 
1740               data=None, 
1741               template_args=None, 
1742               edit=False,
1743               change_is_required=False,
1744               apiurl=None):
1745
1746     if not apiurl:
1747         apiurl = conf.config['apiurl']
1748     if not data:
1749         data = meta_exists(metatype,
1750                            path_args,
1751                            template_args,
1752                            create_new=True,
1753                            apiurl=apiurl)
1754
1755     if edit:
1756         change_is_required = True
1757
1758     url = make_meta_url(metatype, path_args, apiurl)
1759     f=metafile(url, data, change_is_required, metatypes[metatype]['file_ext'])
1760
1761     if edit:
1762         editor = os.getenv('EDITOR', default='vim')
1763         while 1:
1764             subprocess.call('%s %s' % (editor, f.filename), shell=True)
1765             if change_is_required == True:
1766                 try:
1767                     f.sync()
1768                 except urllib2.HTTPError, e:
1769                     error_help = "%d" % e.code
1770                     if e.headers.get('X-Opensuse-Errorcode'):
1771                         error_help = "%s (%d)" % (e.headers.get('X-Opensuse-Errorcode'), e.code)
1772
1773                     print >>sys.stderr, 'BuildService API error:', error_help
1774                     # examine the error - we can't raise an exception because we might want
1775                     # to try again
1776                     data = e.read()
1777                     if '<summary>' in data:
1778                         print >>sys.stderr, data.split('<summary>')[1].split('</summary>')[0]
1779                     input = raw_input('Try again? ([y/N]): ')
1780                     if input != 'y' and input != 'Y':
1781                         break
1782                 else:
1783                     break
1784             else:
1785                 f.sync()
1786                 break
1787     else:
1788         f.sync()
1789
1790
1791 def show_files_meta(apiurl, prj, pac, revision=None):
1792     query = None
1793     if revision:
1794         query = { 'rev': revision }
1795     f = http_GET(makeurl(apiurl, ['source', prj, pac], query=query))
1796     return f.readlines()
1797
1798
1799 def show_upstream_srcmd5(apiurl, prj, pac):
1800     m = show_files_meta(apiurl, prj, pac)
1801     return ET.parse(StringIO(''.join(m))).getroot().get('srcmd5')
1802
1803
1804 def show_upstream_xsrcmd5(apiurl, prj, pac):
1805     m = show_files_meta(apiurl, prj, pac)
1806     try:
1807         # only source link packages have a <linkinfo> element.
1808         li_node = ET.parse(StringIO(''.join(m))).getroot().find('linkinfo')
1809     except:
1810         return None
1811
1812     li = Linkinfo()
1813     li.read(li_node)
1814
1815     if li.haserror():
1816         raise oscerr.LinkExpandError, li.error
1817     else:
1818         return li.xsrcmd5
1819
1820
1821 def show_upstream_rev(apiurl, prj, pac):
1822     m = show_files_meta(apiurl, prj, pac)
1823     return ET.parse(StringIO(''.join(m))).getroot().get('rev')
1824
1825
1826 def read_meta_from_spec(specfile, *args):
1827     import codecs, locale, re
1828     """
1829     Read tags and sections from spec file. To read out
1830     a tag the passed argument mustn't end with a colon. To
1831     read out a section the passed argument must start with
1832     a '%'.
1833     This method returns a dictionary which contains the
1834     requested data.
1835     """
1836
1837     if not os.path.isfile(specfile):
1838         raise IOError('\'%s\' is not a regular file' % specfile)
1839
1840     try:
1841         lines = codecs.open(specfile, 'r', locale.getpreferredencoding()).readlines()
1842     except UnicodeDecodeError:
1843         lines = open(specfile).readlines()
1844
1845     tags = []
1846     sections = []
1847     spec_data = {}
1848
1849     for itm in args:
1850         if itm.startswith('%'):
1851             sections.append(itm)
1852         else:
1853             tags.append(itm)
1854
1855     tag_pat = '(?P<tag>^%s)\s*:\s*(?P<val>.*)'
1856     for tag in tags:
1857         m = re.compile(tag_pat % tag, re.I | re.M).search(''.join(lines))
1858         if m and m.group('val'):
1859             spec_data[tag] = m.group('val').strip()
1860         else:
1861             print >>sys.stderr, 'error - tag \'%s\' does not exist' % tag
1862             sys.exit(1)
1863
1864     section_pat = '^%s\s*?$'
1865     for section in sections:
1866         m = re.compile(section_pat % section, re.I | re.M).search(''.join(lines))
1867         if m:
1868             start = lines.index(m.group()+'\n') + 1
1869         else:
1870             print >>sys.stderr, 'error - section \'%s\' does not exist' % section
1871             sys.exit(1)
1872         data = []
1873         for line in lines[start:]:
1874             if line.startswith('%'):
1875                 break
1876             data.append(line)
1877         spec_data[section] = data
1878
1879     return spec_data
1880
1881
1882 def edit_message(footer=''):
1883     delim = '--This line, and those below, will be ignored--\n\n' + footer
1884     import tempfile
1885     (fd, filename) = tempfile.mkstemp(prefix = 'osc-commitmsg', suffix = '.diff', dir = '/tmp')
1886     f = os.fdopen(fd, 'w')
1887     f.write('\n')
1888     f.write(delim)
1889     f.close()
1890     hash_orig = dgst(filename)
1891
1892     editor = os.getenv('EDITOR', default='vim')
1893     while 1:
1894         subprocess.call('%s %s' % (editor, filename), shell=True)
1895         hash = dgst(filename)
1896
1897         if hash != hash_orig:
1898             msg = open(filename).read()
1899             os.unlink(filename)
1900             return msg.split(delim)[0].rstrip()
1901         else:
1902             input = raw_input('Log message unchanged or not specified\n'
1903                               'a)bort, c)ontinue, e)dit: ')
1904             if input in 'aA':
1905                 os.unlink(filename)
1906                 raise oscerr.UserAbort
1907             elif input in 'cC':
1908                 os.unlink(filename)
1909                 return ''
1910             elif input in 'eE':
1911                 pass
1912
1913
1914 def create_submit_request(apiurl, 
1915                          src_project, src_package, 
1916                          dst_project, dst_package,
1917                          message, orev=None):
1918
1919     import cgi
1920
1921     r = SubmitReq()
1922     r.src_project = src_project
1923     r.src_package = src_package
1924     r.src_md5     = orev or show_upstream_srcmd5(apiurl, src_project, src_package)
1925     r.dst_project = dst_project
1926     r.dst_package = dst_package
1927     r.descr = cgi.escape(message or '')
1928
1929     xml = """\
1930 <request type="submit">
1931     <submit>
1932         <source project="%s" package="%s" rev="%s"/>
1933         <target project="%s" package="%s" />
1934     </submit>
1935     <state name="new"/>
1936     <description>%s</description>
1937 </request>
1938 """ % (r.src_project, 
1939        r.src_package,
1940        r.src_md5,
1941        r.dst_project, 
1942        r.dst_package,
1943        r.descr)
1944
1945     u = makeurl(apiurl, ['request'], query='cmd=create')
1946     f = http_POST(u, data=xml)
1947
1948     root = ET.parse(f).getroot()
1949     return root.get('id')
1950
1951
1952 def get_submit_request(apiurl, reqid):
1953     u = makeurl(apiurl, ['request', reqid])
1954     f = http_GET(u)
1955     root = ET.parse(f).getroot()
1956
1957     r = SubmitReq()
1958     r.read(root)
1959     return r
1960
1961
1962 def change_submit_request_state(apiurl, reqid, newstate, message=''):
1963     u = makeurl(apiurl, 
1964                 ['request', reqid], 
1965                 query={'cmd': 'changestate', 'newstate': newstate})
1966     f = http_POST(u, data=message)
1967     return f.read()
1968
1969
1970 def get_submit_request_list(apiurl, project, package, req_state=('new')):
1971     match = 'submit/target/@project=\'%s\'' % quote_plus(project)
1972     if package:
1973         match += '%20and%20' + 'submit/target/@package=\'%s\'' % quote_plus(package)
1974
1975     u = makeurl(apiurl, ['search', 'request'], ['match=%s' % match])
1976     f = http_GET(u)
1977     collection = ET.parse(f).getroot()
1978
1979     requests = []
1980     for root in collection.findall('request'):
1981         r = SubmitReq()
1982         r.read(root)
1983         if (r.state.name in req_state) or not len(req_state):
1984             requests.append(r)
1985
1986     return requests
1987
1988
1989 def get_submit_request_log(apiurl, reqid):
1990     r = get_submit_request(conf.config['apiurl'], reqid)
1991     data = []
1992     frmt = '-' * 76 + '\n%s | %s | %s\n\n%s'
1993     # the description of the submitrequest is used for the initial log entry
1994     # otherwise its comment attribute would contain None
1995     if len(r.statehistory) >= 1:
1996         r.statehistory[-1].comment = r.descr
1997     else:
1998         r.state.comment = r.descr
1999     for state in [ r.state ] + r.statehistory:
2000         s = frmt % (state.name, state.who, state.when, str(state.comment))
2001         data.append(s)
2002     return data
2003
2004
2005 def get_user_meta(apiurl, user):
2006     u = makeurl(apiurl, ['person', quote_plus(user)])
2007     try:
2008         f = http_GET(u)
2009         return ''.join(f.readlines())
2010     except urllib2.HTTPError:
2011         print 'user \'%s\' not found' % user
2012         return None
2013
2014
2015 def get_user_data(apiurl, user, *tags):
2016     """get specified tags from the user meta"""
2017     meta = get_user_meta(apiurl, user)
2018     data = []
2019     if meta != None:
2020         root = ET.fromstring(meta)
2021         for tag in tags:
2022             try:
2023                 if root.find(tag).text != None:
2024                     data.append(root.find(tag).text)
2025                 else:
2026                     # tag is empty
2027                     data.append('-')
2028             except AttributeError:
2029                 # this part is reached if the tags tuple contains an invalid tag
2030                 print 'The xml file for user \'%s\' seems to be broken' % user
2031                 return None
2032         return data
2033     else:
2034         return None
2035
2036
2037 def get_source_file(apiurl, prj, package, filename, targetfilename=None, revision = None):
2038     query = None
2039     if revision:
2040         query = { 'rev': revision }
2041
2042     u = makeurl(apiurl, ['source', prj, package, pathname2url(filename)], query=query)
2043     # print 'url: %s' % u
2044     f = http_GET(u)
2045
2046     o = open(targetfilename or filename, 'w')
2047     while 1:
2048         buf = f.read(BUFSIZE)
2049         if not buf: break
2050         o.write(buf)
2051     o.close()
2052     f.close()
2053
2054
2055 def get_binary_file(apiurl, prj, repo, arch, 
2056                     filename, 
2057                     package = None,
2058                     target_filename = None, 
2059                     target_mtime = None,
2060                     progress_meter = False):
2061
2062     target_filename = target_filename or filename
2063
2064     where = package or '_repository'
2065     u = makeurl(apiurl, ['build', prj, repo, arch, where, filename])
2066
2067     if progress_meter:
2068         sys.stdout.write("Downloading %s [  0%%]" % filename)
2069         sys.stdout.flush()
2070
2071     f = http_GET(u)
2072     binsize = int(f.headers['content-length'])
2073
2074     import tempfile
2075     (fd, tmpfilename) = tempfile.mkstemp(prefix = filename + '.', suffix = '.osc', dir = '/tmp')
2076     os.chmod(tmpfilename, 0644)
2077
2078     try:
2079         o = os.fdopen(fd, 'w')
2080
2081         downloaded = 0
2082         while 1:
2083             #buf = f.read(BUFSIZE)
2084             buf = f.read(16384)
2085             if not buf: break
2086             o.write(buf)
2087             downloaded += len(buf)
2088             if progress_meter:
2089                 completion = str(int((float(downloaded)/binsize)*100))
2090                 sys.stdout.write('%s%*s%%]' % ('\b'*5, 3, completion))
2091                 sys.stdout.flush()
2092         o.close()
2093
2094         if progress_meter:
2095             sys.stdout.write('\n')
2096
2097         shutil.move(tmpfilename, target_filename)
2098         if target_mtime:
2099             os.utime(target_filename, (-1, target_mtime))
2100
2101     # make sure that the temp file is cleaned up when we are interrupted 
2102     finally:
2103         try: os.unlink(tmpfilename)
2104         except: pass
2105
2106
2107 def dgst(file):
2108
2109     #if not os.path.exists(file):
2110         #return None
2111
2112     try:
2113         import hashlib
2114         md5 = hashlib
2115     except ImportError:
2116         import md5
2117         md5 = md5
2118     s = md5.md5()
2119     f = open(file, 'r')
2120     while 1:
2121         buf = f.read(BUFSIZE)
2122         if not buf: break
2123         s.update(buf)
2124     return s.hexdigest()
2125     f.close()
2126
2127
2128 def binary(s):
2129     """return true if a string is binary data using diff's heuristic"""
2130     if s and '\0' in s[:4096]:
2131         return True
2132     return False
2133
2134
2135 def binary_file(fn):
2136     """read 4096 bytes from a file named fn, and call binary() on the data"""
2137     return binary(open(fn, 'r').read(4096))
2138
2139
2140 def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, origfilename = None):
2141     """
2142     This methods diffs oldfilename against filename (so filename will
2143     be shown as the new file).
2144     The variable origfilename is used if filename and oldfilename differ
2145     in their names (for instance if a tempfile is used for filename etc.)
2146     """
2147
2148     import difflib
2149
2150     if not oldfilename:
2151         oldfilename = filename
2152
2153     if not olddir:
2154         olddir = os.path.join(dir, store)
2155
2156     if not origfilename:
2157         origfilename = filename
2158
2159     file1 = os.path.join(olddir, oldfilename)   # old/stored original
2160     file2 = os.path.join(dir, filename)         # working copy
2161
2162     f1 = open(file1, 'r')
2163     s1 = f1.read()
2164     f1.close()
2165
2166     f2 = open(file2, 'r')
2167     s2 = f2.read()
2168     f2.close()
2169
2170     if binary(s1) or binary (s2):
2171         d = ['Binary file %s has changed\n' % origfilename]
2172
2173     else:
2174         d = difflib.unified_diff(\
2175             s1.splitlines(1), \
2176             s2.splitlines(1), \
2177             fromfile = '%s     (revision %s)' % (origfilename, rev), \
2178             tofile = '%s     (working copy)' % origfilename)
2179
2180         # if file doesn't end with newline, we need to append one in the diff result
2181         d = list(d)
2182         for i, line in enumerate(d):
2183             if not line.endswith('\n'):
2184                 d[i] += '\n\\ No newline at end of file'
2185                 if i+1 != len(d):
2186                     d[i] += '\n'
2187
2188     return ''.join(d)
2189
2190 def make_diff(wc, revision):
2191     import tempfile
2192     changed_files = []
2193     added_files = []
2194     removed_files = []
2195     cmp_pac = None
2196     diff_hdr = 'Index: %s\n'
2197     diff_hdr += '===================================================================\n'
2198     diff = []
2199     olddir = os.getcwd()
2200     if not revision:
2201         # normal diff
2202         if wc.todo:
2203             for file in wc.todo:
2204                 if file in wc.filenamelist+wc.filenamelist_unvers:
2205                     state = wc.status(file)
2206                     if state == 'A':
2207                         added_files.append(file)
2208                     elif state == 'D':
2209                         removed_files.append(file)
2210                     elif state == 'M' or state == 'C':
2211                         changed_files.append(file)
2212                 else:
2213                     diff.append('osc: \'%s\' is not under version control' % file)
2214         else:
2215             for file in wc.filenamelist+wc.filenamelist_unvers:
2216                 state = wc.status(file)
2217                 if state == 'M' or state == 'C':
2218                     changed_files.append(file)
2219                 elif state == 'A':
2220                     added_files.append(file)
2221                 elif state == 'D':
2222                     removed_files.append(file)
2223     else:
2224         tmpdir  = tempfile.mkdtemp(revision, wc.name, '/tmp')
2225         os.chdir(tmpdir)
2226         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, revision)
2227         cmp_pac = Package(tmpdir)
2228         if wc.todo:
2229             for file in wc.todo:
2230                 if file in cmp_pac.filenamelist:
2231                     if file in wc.filenamelist:
2232                         changed_files.append(file)
2233                     else:
2234                         diff.append('osc: \'%s\' is not under version control' % file)
2235                 else:
2236                     diff.append('osc: unable to find \'%s\' in revision %s' % (file, cmp_pac.rev))
2237         else:
2238             changed_files, added_files, removed_files = wc.comparePac(cmp_pac)
2239
2240     for file in changed_files:
2241         diff.append(diff_hdr % file)
2242         if cmp_pac == None:
2243             diff.append(get_source_file_diff(wc.absdir, file, wc.rev))
2244         else:
2245             cmp_pac.updatefile(file, revision)
2246             diff.append(get_source_file_diff(wc.absdir, file, revision, file,
2247                                              cmp_pac.absdir, file))
2248     (fd, tmpfile) = tempfile.mkstemp(dir='/tmp')
2249     for file in added_files:
2250         diff.append(diff_hdr % file)
2251         if cmp_pac == None:
2252             diff.append(get_source_file_diff(wc.absdir, file, wc.rev, os.path.basename(tmpfile),
2253                                              os.path.dirname(tmpfile), file))
2254         else:
2255             diff.append(get_source_file_diff(wc.absdir, file, revision, os.path.basename(tmpfile),
2256                                              os.path.dirname(tmpfile), file))
2257
2258     # FIXME: this is ugly but it cannot be avoided atm
2259     #        if a file is deleted via "osc rm file" we should keep the storefile.
2260     tmp_pac = None
2261     if cmp_pac == None and removed_files:
2262         tmpdir  = tempfile.mkdtemp(dir='/tmp')
2263         os.chdir(tmpdir)
2264         init_package_dir(wc.apiurl, wc.prjname, wc.name, tmpdir, wc.rev)
2265         tmp_pac = Package(tmpdir)
2266         os.chdir(olddir)
2267
2268     for file in removed_files:
2269         diff.append(diff_hdr % file)
2270         if cmp_pac == None:
2271             tmp_pac.updatefile(file, tmp_pac.rev)
2272             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
2273                                              wc.rev, file, tmp_pac.storedir, file))
2274         else:
2275             cmp_pac.updatefile(file, revision)
2276             diff.append(get_source_file_diff(os.path.dirname(tmpfile), os.path.basename(tmpfile),
2277                                              revision, file, cmp_pac.storedir, file))
2278
2279     os.chdir(olddir)
2280     if cmp_pac != None:
2281         delete_tmpdir(cmp_pac.absdir)
2282     if tmp_pac != None:
2283         delete_tmpdir(tmp_pac.absdir)
2284     return diff
2285
2286
2287 def server_diff(apiurl,
2288                 old_project, old_package, old_revision,
2289                 new_project, new_package, new_revision, unified=False):
2290
2291     query = {'cmd': 'diff', 'expand': '1'}
2292     if old_project:
2293         query['oproject'] = old_project
2294     if old_package:
2295         query['opackage'] = old_package
2296     if old_revision:
2297         query['orev'] = old_revision
2298     if new_revision:
2299         query['rev'] = new_revision
2300     if unified:
2301         query['unified'] = 1
2302
2303     u = makeurl(apiurl, ['source', new_project, new_package], query=query)
2304
2305     f = http_POST(u)
2306     return f.read()
2307
2308
2309 def make_dir(apiurl, project, package, pathname=None, prj_dir=None):
2310     """
2311     creates the plain directory structure for a package dir.
2312     The 'apiurl' parameter is needed for the project dir initialization.
2313     The 'project' and 'package' parameters specify the name of the
2314     project and the package. The optional 'pathname' parameter is used
2315     for printing out the message that a new dir was created (default: 'prj_dir/package').
2316     The optional 'prj_dir' parameter specifies the path to the project dir (default: 'project').
2317     """
2318     prj_dir = prj_dir or project
2319     pathname = pathname or getTransActPath(os.path.join(prj_dir, package))
2320     if not os.path.exists(prj_dir):
2321         print statfrmt('A', prj_dir)
2322         init_project_dir(apiurl, prj_dir, project)
2323     if not os.path.exists(os.path.join(prj_dir, package)):
2324         print statfrmt('A', pathname)
2325         os.mkdir(os.path.join(prj_dir, package))
2326         os.mkdir(os.path.join(prj_dir, package, store))
2327
2328     return(os.path.join(prj_dir, package))
2329
2330
2331 def checkout_package(apiurl, project, package, 
2332                      revision=None, pathname=None, prj_obj=None,
2333                      expand_link=False, prj_dir=None):
2334
2335     try:
2336         # the project we're in might be deleted.
2337         # that'll throw an error then.
2338         olddir = os.getcwd()
2339     except:
2340         olddir = os.environ.get("PWD")
2341
2342     if not prj_dir:
2343         prj_dir = olddir
2344  
2345     if not pathname:
2346         pathname = getTransActPath(os.path.join(prj_dir, package))
2347
2348     # before we create directories and stuff, check if the package actually
2349     # exists
2350     show_package_meta(apiurl, project, package)
2351  
2352     if expand_link:
2353         # try to read from the linkinfo
2354         x = show_upstream_xsrcmd5(apiurl, project, package)
2355         if x:
2356             # it is a link - thus, we use the xsrcmd5 as the revision to be
2357             # checked out
2358             revision = x
2359     os.chdir(make_dir(apiurl, project, package, pathname, prj_dir))
2360     init_package_dir(apiurl, project, package, store, revision)
2361     os.chdir(os.pardir)
2362     p = Package(package)
2363
2364     for filename in p.filenamelist:
2365         p.updatefile(filename, revision)
2366         #print 'A   ', os.path.join(project, package, filename)
2367         print statfrmt('A', os.path.join(pathname, filename))
2368     if conf.config['do_package_tracking']:
2369         # check if we can re-use an existing project object
2370         if prj_obj == None:
2371             prj_obj = Project(os.getcwd())
2372         prj_obj.set_state(p.name, ' ')
2373         prj_obj.write_packages()
2374     os.chdir(olddir)
2375
2376
2377 def replace_pkg_meta(pkgmeta, new_name, new_prj, keep_maintainers = False, dst_userid = None):
2378     """
2379     update pkgmeta with new new_name and new_prj and set calling user as the
2380     only maintainer (unless keep_maintainers is set)
2381     """
2382     root = ET.fromstring(''.join(pkgmeta))
2383     root.set('name', new_name)
2384     root.set('project', new_prj)
2385     if not keep_maintainers:
2386         for person in root.findall('person'):
2387             root.remove(person)
2388         dst_userid = dst_userid or conf.config['user']
2389         ET.SubElement(root, 'person',
2390                       userid = dst_userid, role = 'maintainer')
2391     return ET.tostring(root)
2392
2393 def link_pac(src_project, src_package, dst_project, dst_package, rev=''):
2394     """
2395     create a linked package
2396      - "src" is the original package
2397      - "dst" is the "link" package that we are creating here
2398     """
2399
2400     src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
2401     src_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
2402
2403     edit_meta('pkg',
2404               path_args=(dst_project, dst_package), 
2405               data=src_meta)
2406
2407     # create the _link file
2408     # but first, make sure not to overwrite an existing one
2409     if '_link' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
2410         print >>sys.stderr
2411         print >>sys.stderr, '_link file already exists...! Aborting'
2412         sys.exit(1)
2413     
2414     if rev:
2415         rev = 'rev="%s"' % rev
2416     else:
2417         rev = ''
2418         
2419     print 'Creating _link...',
2420     link_template = """\
2421 <link project="%s" package="%s" %s>
2422 <patches>
2423   <!-- <apply name="patch" /> -->
2424   <!-- <topadd>%%define build_with_feature_x 1</topadd> -->
2425 </patches>
2426 </link>
2427 """ % (src_project, src_package, rev)
2428
2429     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_link'])
2430     http_PUT(u, data=link_template)
2431     print 'Done.'
2432
2433 def aggregate_pac(src_project, src_package, dst_project, dst_package):
2434     """
2435     aggregate package
2436      - "src" is the original package
2437      - "dst" is the "aggregate" package that we are creating here
2438     """
2439
2440     src_meta = show_package_meta(conf.config['apiurl'], src_project, src_package)
2441     src_meta = replace_pkg_meta(src_meta, dst_package, dst_project)
2442
2443     edit_meta('pkg',
2444               path_args=(dst_project, dst_package), 
2445               data=src_meta)
2446
2447     # create the _aggregate file
2448     # but first, make sure not to overwrite an existing one
2449     if '_aggregate' in meta_get_filelist(conf.config['apiurl'], dst_project, dst_package):
2450         print >>sys.stderr
2451         print >>sys.stderr, '_aggregate file already exists...! Aborting'
2452         sys.exit(1)
2453
2454     print 'Creating _aggregate...',
2455     aggregate_template = """\
2456 <aggregatelist>
2457   <aggregate project="%s">
2458     <package>%s</package>
2459   </aggregate>
2460 </aggregatelist>
2461 """ % (src_project, src_package)
2462
2463     u = makeurl(conf.config['apiurl'], ['source', dst_project, dst_package, '_aggregate'])
2464     http_PUT(u, data=aggregate_template)
2465     print 'Done.'
2466
2467 def branch_pkg(apiurl, src_project, src_package, nodevelproject=False, rev=None):
2468     """
2469     Branch a package (via API call)
2470     """
2471     query = { 'cmd': 'branch' }
2472     if nodevelproject:
2473         query['ignoredevel'] = '1'
2474     if rev:
2475         query['rev'] = rev
2476     u = makeurl(conf.config['apiurl'], 
2477                 ['source', src_project, src_package], 
2478                 query=query)
2479     f = http_POST(u)
2480     r = f.read()
2481     #print r
2482     r = r.split('targetproject">')[1]
2483     r = r.split('</data>')[0]
2484     return r
2485
2486
2487
2488 def copy_pac(src_apiurl, src_project, src_package, 
2489              dst_apiurl, dst_project, dst_package,
2490              client_side_copy = False,
2491              keep_maintainers = False):
2492     """
2493     Create a copy of a package.
2494
2495     Copying can be done by downloading the files from one package and commit
2496     them into the other by uploading them (client-side copy) --
2497     or by the server, in a single api call.
2498
2499     """
2500
2501     src_meta = show_package_meta(src_apiurl, src_project, src_package)
2502     dst_userid = conf.get_apiurl_usr(dst_apiurl)
2503     src_meta = replace_pkg_meta(src_meta, dst_package, dst_project, keep_maintainers, dst_userid)
2504
2505     print 'Sending meta data...'
2506     u = makeurl(dst_apiurl, ['source', dst_project, dst_package, '_meta'])
2507     http_PUT(u, data=src_meta)
2508
2509     print 'Copying files...'
2510     if not client_side_copy:
2511         query = {'cmd': 'copy', 'oproject': src_project, 'opackage': src_package }
2512         u = makeurl(dst_apiurl, ['source', dst_project, dst_package], query=query)
2513         f = http_POST(u)
2514         return f.read()
2515
2516     else:
2517         # copy one file after the other
2518         import tempfile
2519         tmpdir = tempfile.mkdtemp(prefix='osc_copypac', dir='/tmp')
2520         os.chdir(tmpdir)
2521         for n in meta_get_filelist(src_apiurl, src_project, src_package):
2522             print '  ', n
2523             get_source_file(src_apiurl, src_project, src_package, n, targetfilename=n)
2524             u = makeurl(dst_apiurl, ['source', dst_project, dst_package, pathname2url(n)])
2525             http_PUT(u, file = n)
2526             os.unlink(n)
2527         os.rmdir(tmpdir)
2528         return 'Done.'
2529
2530
2531 def delete_package(apiurl, prj, pac):
2532     u = makeurl(apiurl, ['source', prj, pac])
2533     http_DELETE(u)
2534
2535
2536 def delete_project(apiurl, prj):
2537     u = makeurl(apiurl, ['source', prj])
2538     http_DELETE(u)
2539
2540
2541 def get_platforms(apiurl):
2542     f = http_GET(makeurl(apiurl, ['platform']))
2543     tree = ET.parse(f)
2544     r = [ node.get('name') for node in tree.getroot() ]
2545     r.sort()
2546     return r
2547
2548
2549 def get_platforms_of_project(apiurl, prj):
2550     f = show_project_meta(apiurl, prj)
2551     tree = ET.parse(StringIO(''.join(f)))
2552
2553     r = [ node.get('name') for node in tree.findall('repository')]
2554     return r
2555
2556
2557 def get_repos_of_project(apiurl, prj):
2558     f = show_project_meta(apiurl, prj)
2559     tree = ET.parse(StringIO(''.join(f)))
2560
2561     repo_line_templ = '%-15s %-10s'
2562     for node in tree.findall('repository'):
2563         for node2 in node.findall('arch'):
2564             yield repo_line_templ % (node.get('name'), node2.text)
2565
2566
2567 def get_binarylist(apiurl, prj, repo, arch, package=None, verbose=False):
2568     what = package or '_repository'
2569     u = makeurl(apiurl, ['build', prj, repo, arch, what])
2570     f = http_GET(u)
2571     tree = ET.parse(f)
2572     if not verbose:
2573         return [ node.get('filename') for node in tree.findall('binary')]
2574     else:
2575         l = []
2576         for node in tree.findall('binary'):
2577             f = File(node.get('filename'), 
2578                      None, 
2579                      int(node.get('size')), 
2580                      int(node.get('mtime')))
2581             l.append(f)
2582         return l
2583
2584
2585 def get_binarylist_published(apiurl, prj, repo, arch):
2586     u = makeurl(apiurl, ['published', prj, repo, arch])
2587     f = http_GET(u)
2588     tree = ET.parse(f)
2589     r = [ node.get('name') for node in tree.findall('entry')]
2590     return r
2591
2592
2593 def show_results_meta(apiurl, prj, package=None, lastbuild=None):
2594     query = {}
2595     if package:
2596         query['package'] = package
2597     if lastbuild:
2598         query['lastbuild'] = 1
2599     u = makeurl(apiurl, ['build', prj, '_result'], query=query)
2600     f = http_GET(u)
2601     return f.readlines()
2602
2603
2604 def show_prj_results_meta(apiurl, prj):
2605     u = makeurl(apiurl, ['build', prj, '_result'])
2606     f = http_GET(u)
2607     return f.readlines()
2608
2609
2610 def get_results(apiurl, prj, package, lastbuild=None):
2611     r = []
2612     result_line_templ = '%(rep)-15s %(arch)-10s %(status)s'
2613
2614     f = show_results_meta(apiurl, prj, package=package, lastbuild=lastbuild)
2615     tree = ET.parse(StringIO(''.join(f)))
2616     root = tree.getroot()
2617
2618     for node in root.findall('result'):
2619         rmap = {}
2620         rmap['prj'] = prj
2621         rmap['pac'] = package
2622         rmap['rep'] = node.get('repository')
2623         rmap['arch'] = node.get('arch')
2624
2625         statusnode =  node.find('status')
2626         try:
2627             rmap['status'] = statusnode.get('code')
2628         except:
2629             # code can be missing when package is too new:
2630             return {}
2631
2632         if rmap['status'] in ['expansion error', 'broken']:
2633             rmap['status'] += ': ' + statusnode.find('details').text
2634
2635         if rmap['status'] == 'failed':
2636             rmap['status'] += ': %s' % apiurl + \
2637                 '/build/%(prj)s/%(rep)s/%(arch)s/%(pac)s/_log' % rmap
2638
2639         r.append(result_line_templ % rmap)
2640     return r
2641
2642 def get_prj_results(apiurl, prj, hide_legend=False, csv=False, status_filter=None, name_filter=None):
2643     #print '----------------------------------------'
2644
2645     r = []
2646     #result_line_templ = '%(prj)-15s %(pac)-15s %(rep)-15s %(arch)-10s %(status)s'
2647     result_line_templ = '%(rep)-15s %(arch)-10s %(status)s'
2648
2649     f = show_prj_results_meta(apiurl, prj)
2650     tree = ET.parse(StringIO(''.join(f)))
2651     root = tree.getroot()
2652
2653     pacs = []
2654     # sequence of (repo,arch) tuples
2655     targets = []
2656     # {package: {(repo,arch): status}}
2657     status = {}
2658     if not root.find('result'):
2659         return []
2660     for node in root.find('result'):
2661         pacs.append(node.get('package'))
2662     pacs.sort()
2663     for node in root.findall('result'):
2664         tg = (node.get('repository'), node.get('arch'))
2665         targets.append(tg)
2666         for pacnode in node.findall('status'):
2667             pac = pacnode.get('package')
2668             if pac not in status:
2669                 status[pac] = {}
2670             status[pac][tg] = pacnode.get('code')
2671     targets.sort()
2672
2673     # filter option
2674     if status_filter or name_filter:
2675
2676         pacs_to_show = []
2677         targets_to_show = []
2678
2679         #filtering for Package Status 
2680         if status_filter:
2681             if status_filter in buildstatus_symbols.values():
2682                 for txt, sym in buildstatus_symbols.items():
2683                     if sym == status_filter:
2684                         filt_txt = txt
2685                 for pkg in status.keys():
2686                     for repo in status[pkg].keys():
2687                         if status[pkg][repo] == filt_txt:
2688                             if not name_filter:
2689                                 pacs_to_show.append(pkg)
2690                                 targets_to_show.append(repo)
2691                             elif name_filter in pkg:
2692                                 pacs_to_show.append(pkg)
2693
2694         #filtering for Package Name 
2695         elif name_filter:
2696             for pkg in pacs:
2697                 if name_filter in pkg:
2698                     pacs_to_show.append(pkg)
2699
2700         pacs = [ i for i in pacs if i in pacs_to_show ]
2701         if len(targets_to_show):
2702             targets = [ i for i in targets if i in targets_to_show ]
2703
2704     # csv output
2705     if csv:
2706         # TODO: option to disable the table header
2707         row = ['_'] + ['/'.join(tg) for tg in targets]
2708         r.append(';'.join(row))
2709         for pac in pacs:
2710             row = [pac] + [status[pac][tg] for tg in targets]
2711             r.append(';'.join(row))
2712         return r
2713
2714     # human readable output
2715     max_pacs = 40
2716     for startpac in range(0, len(pacs), max_pacs):
2717         offset = 0
2718         for pac in pacs[startpac:startpac+max_pacs]:
2719             r.append(' |' * offset + ' ' + pac)
2720             offset += 1
2721
2722         for tg in targets:
2723             line = []
2724             line.append(' ')
2725             for pac in pacs[startpac:startpac+max_pacs]:
2726                 st = ''
2727                 if not status.has_key(pac) or not status[pac].has_key(tg):
2728                     # for newly added packages, status may be missing
2729                     st = '?'
2730                 else:
2731                     try:
2732                         st = buildstatus_symbols[status[pac][tg]]
2733                     except:
2734                         print 'osc: warn: unknown status \'%s\'...' % status[pac][tg]
2735                         print 'please edit osc/core.py, and extend the buildstatus_symbols dictionary.'
2736                         st = '?'
2737                         buildstatus_symbols[status[pac][tg]] = '?'
2738                 line.append(st)
2739                 line.append(' ')
2740             line.append(' %s %s' % tg)
2741             line = ''.join(line)
2742
2743             r.append(line)
2744
2745         r.append('')
2746
2747     if not hide_legend and len(pacs):
2748         r.append(' Legend:')
2749         for i, j in buildstatus_symbols.items():
2750             r.append('  %s %s' % (j, i))
2751
2752     return r
2753
2754
2755 def print_buildlog(apiurl, prj, package, platform, arch, offset = 0):
2756     """prints out the buildlog on stdout"""
2757     try:
2758         while True:
2759             u = makeurl(apiurl, ['build', prj, platform, arch, package, '_log?nostream=1&start=%s' % offset])
2760             f = http_GET(u)
2761             start_offset = offset
2762             while True:
2763                 log_chunk = f.read(8192)
2764                 offset += len(log_chunk)
2765                 print log_chunk,
2766                 if not len(log_chunk):
2767                     break
2768             if start_offset == offset:
2769                 break
2770
2771     except urllib2.HTTPError, e:
2772         print >>sys.stderr, 'Can\'t get logfile'
2773         print >>sys.stderr, e
2774     except KeyboardInterrupt:
2775         pass
2776
2777 def get_buildinfo(apiurl, prj, package, platform, arch, specfile=None, addlist=None):
2778     query = []
2779     if addlist:
2780         for i in addlist:
2781             query.append('add=%s' % quote_plus(i))
2782
2783     u = makeurl(apiurl, ['build', prj, platform, arch, package, '_buildinfo'], query=query)
2784
2785     if specfile:
2786         f = http_POST(u, data=specfile)
2787     else:
2788         f = http_GET(u)
2789     return f.read()
2790
2791
2792 def get_buildconfig(apiurl, prj, package, platform, arch):
2793     u = makeurl(apiurl, ['build', prj, platform, '_buildconfig'])
2794     f = http_GET(u)
2795     return f.read()
2796
2797
2798 def get_buildhistory(apiurl, prj, package, platform, arch):
2799     import time
2800     u = makeurl(apiurl, ['build', prj, platform, arch, package, '_history'])
2801     f = http_GET(u)
2802     root = ET.parse(f).getroot()
2803
2804     r = []
2805     for node in root.findall('entry'):
2806         rev = int(node.get('rev'))
2807         srcmd5 = node.get('srcmd5') 
2808         versrel = node.get('versrel') 
2809         bcnt = int(node.get('bcnt'))
2810         t = time.localtime(int(node.get('time')))
2811         t = time.strftime('%Y-%m-%d %H:%M:%S', t)
2812
2813         r.append('%s   %s %6d    %s.%d' % (t, srcmd5, rev, versrel, bcnt))
2814
2815     r.insert(0, 'time                  srcmd5                              rev   vers-rel.bcnt')
2816
2817     return r
2818
2819 def print_jobhistory(apiurl, prj, current_package, platform, arch):
2820     import time
2821     if current_package:
2822         u = makeurl(apiurl, ['build', prj, platform, arch, '_jobhistory'], "package=%s" % (current_package))
2823     else:
2824         u = makeurl(apiurl, ['build', prj, platform, arch, '_jobhistory'])
2825     f = http_GET(u)
2826     root = ET.parse(f).getroot()
2827
2828     print "time                 package                                            reason           code              build time"
2829     for node in root.findall('jobhist'):
2830         package = node.get('package')
2831         reason = node.get('reason')
2832         if not reason:
2833             reason = "unknown"
2834         bcnt = node.get('bcnt')
2835         code = node.get('code')
2836         rev = int(node.get('rev'))
2837         srcmd5 = node.get('srcmd5') 
2838         rt = int(node.get('readytime'))
2839         readyt = time.localtime(rt)
2840         readyt = time.strftime('%Y-%m-%d %H:%M:%S', readyt)
2841         st = int(node.get('starttime'))
2842         et = int(node.get('endtime'))
2843         endtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(et))
2844         waitstart = time.strftime('%H:%M:%S', time.gmtime(st-rt))
2845         waittm = time.gmtime(et-st)
2846         if waittm.tm_hour:
2847             waitbuild = "%2dh %2dm %2ds" % (waittm.tm_hour, waittm.tm_min, waittm.tm_sec)
2848         else:
2849             waitbuild = "    %2dm %2ds" % (waittm.tm_min, waittm.tm_sec)
2850
2851         print '%s  %-50s %-16s %-16s %s' % (endtime, package[0:49], reason[0:15], code[0:15], waitbuild)
2852
2853
2854 def get_commitlog(apiurl, prj, package, revision):
2855     import time, locale
2856     u = makeurl(apiurl, ['source', prj, package, '_history'])
2857     f = http_GET(u)
2858     root = ET.parse(f).getroot()
2859
2860     r = []
2861     revisions = root.findall('revision')
2862     revisions.reverse()
2863     for node in revisions:
2864         try:
2865             rev = int(node.get('rev'))
2866             #vrev = int(node.get('vrev')) # what is the meaning of vrev?
2867             if revision and rev != int(revision):
2868                 continue
2869         except ValueError:
2870             # this part should _never_ be reached but...
2871             return [ 'an unexpected error occured - please file a bug' ]
2872         srcmd5 = node.find('srcmd5').text
2873         version = node.find('version').text
2874         user = node.find('user').text
2875         try:
2876             comment = node.find('comment').text.encode(locale.getpreferredencoding(), 'replace')
2877         except:
2878             comment = '<no message>'
2879         t = time.localtime(int(node.find('time').text))
2880         t = time.strftime('%Y-%m-%d %H:%M:%S', t)
2881
2882         s = '-' * 76 + \
2883             '\nr%s | %s | %s | %s | %s\n' % (rev, user, t, srcmd5, version) + \
2884             '\n' + comment
2885         r.append(s)
2886
2887     r.append('-' * 76)
2888     return r
2889
2890
2891 def rebuild(apiurl, prj, package, repo, arch, code=None):
2892     query = { 'cmd': 'rebuild' }
2893     if package:
2894         query['package'] = package
2895     if repo:
2896         query['repository'] = repo
2897     if arch:
2898         query['arch'] = arch
2899     if code:
2900         query['code'] = code
2901
2902     u = makeurl(apiurl, ['build', prj], query=query)
2903     try:
2904         f = http_POST(u)
2905     except urllib2.HTTPError, e:
2906         e.osc_msg = 'could not trigger rebuild for project \'%s\' package \'%s\'' % (prj, package)
2907         raise
2908
2909     root = ET.parse(f).getroot()
2910     return root.get('code')
2911
2912
2913 def store_read_project(dir):
2914     try:
2915         p = open(os.path.join(dir, store, '_project')).readlines()[0].strip()
2916     except IOError:
2917         raise oscerr.NoWorkingCopy('Error: \'%s\' is not an osc project dir ' \
2918                             'or working copy.' % os.path.abspath(dir))
2919     return p
2920
2921
2922 def store_read_package(dir):
2923     try:
2924         p = open(os.path.join(dir, store, '_package')).readlines()[0].strip()
2925     except IOError:
2926         raise oscerr.NoWorkingCopy('error: \'%s\' is not an osc working copy' \
2927                 % os.path.abspath(dir))
2928     return p
2929
2930 def store_read_apiurl(dir):
2931     fname = os.path.join(dir, store, '_apiurl')
2932     try:
2933         apiurl = open(fname).readlines()[0].strip()
2934     except:
2935         apiurl = conf.config['apiurl']
2936         #store_write_apiurl(dir, apiurl)
2937     return apiurl
2938
2939 def store_write_project(dir, project):
2940     fname = os.path.join(dir, store, '_project')
2941     open(fname, 'w').write(project + '\n')
2942
2943 def store_write_apiurl(dir, apiurl):
2944     fname = os.path.join(dir, store, '_apiurl')
2945     open(fname, 'w').write(apiurl + '\n')
2946
2947 def store_write_initial_packages(dir, project, subelements):
2948     fname = os.path.join(dir, store, '_packages')
2949     root = ET.Element('project', name=project)
2950     for elem in subelements:
2951         root.append(elem)
2952     ET.ElementTree(root).write(fname)
2953
2954 def get_osc_version():
2955     return __version__
2956
2957
2958 def abortbuild(apiurl, project, package=None, arch=None, repo=None):
2959     query = { 'cmd': 'abortbuild' }
2960     if package:
2961         query['package'] = package
2962     if arch:
2963         query['arch'] = arch
2964     if repo:
2965         query['repository'] = repo
2966     u = makeurl(apiurl, ['build', project], query)
2967     try:
2968         f = http_POST(u)
2969     except urllib2.HTTPError, e:
2970         e.osc_msg = 'abortion failed for project %s' % project
2971         if package:
2972             e.osc_msg += ' package %s' % package
2973         if arch:
2974             e.osc_msg += ' arch %s' % arch
2975         if repo:
2976             e.osc_msg += ' repo %s' % repo
2977         raise
2978
2979     root = ET.parse(f).getroot()
2980     return root.get('code')
2981
2982
2983 def wipebinaries(apiurl, project, package=None, arch=None, repo=None, code=None):
2984     query = { 'cmd': 'wipe' }
2985     if package:
2986         query['package'] = package
2987     if arch:
2988         query['arch'] = arch
2989     if repo:
2990         query['repository'] = repo
2991     if code:
2992         query['code'] = code
2993
2994     u = makeurl(apiurl, ['build', project], query)
2995     try:
2996         f = http_POST(u)
2997     except urllib2.HTTPError, e:
2998         e.osc_msg = 'wipe binary rpms failed for project %s' % project
2999         if package:
3000             e.osc_msg += ' package %s' % package
3001         if arch:
3002             e.osc_msg += ' arch %s' % arch
3003         if repo:
3004             e.osc_msg += ' repository %s' % repo
3005         if code:
3006             e.osc_msg += ' code=%s' % code
3007         raise